Compare commits

...

87 Commits

Author SHA1 Message Date
ZhanGSKen
9ece6778b7 <mymessagemanager>APK 15.3.3 release Publish. 2025-08-28 21:13:38 +08:00
ZhanGSKen
e13c8e7af0 <mymessagemanager>Start New Stage Version. 2025-08-28 16:03:47 +08:00
ZhanGSKen
a4988b5b68 联系人查询发送窗口添加拼音查询功能。 2025-08-28 16:02:41 +08:00
ZhanGSKen
5507126f6b <mymessagemanager>APK 15.3.2 release Publish. 2025-08-23 13:40:21 +08:00
ZhanGSKen
d381c29452 短信发送窗口UI优化 2025-08-23 13:38:20 +08:00
ZhanGSKen
945eadb617 <mymessagemanager>APK 15.3.1 release Publish. 2025-07-03 13:50:15 +08:00
ZhanGSKen
c5bffc5eef <mymessagemanager>APK 15.3.0 release Publish. 2025-07-03 13:47:44 +08:00
ZhanGSKen
88597fe407 介于UI界面有调整,版本号升级以区分。 2025-07-03 13:28:17 +08:00
ZhanGSKen
53f985533a Merge branch 'mymessagemanager' into appbase 2025-07-03 13:00:51 +08:00
ZhanGSKen
a3950f13ad 应用介绍页链接更新 2025-07-03 12:56:57 +08:00
ZhanGSKen
c878e9dc02 菜单功能BugFix 2025-07-03 12:53:33 +08:00
ZhanGSKen
f2f7cab330 应用菜单排列调整 2025-07-03 12:38:09 +08:00
ZhanGSKen
c4e88e9593 编译配置初始化 2025-06-24 19:18:23 +08:00
ZhanGSKen
08d9d92ae4 <autoinstaller>APK 15.2.2 release Publish. 2025-06-24 09:54:47 +08:00
ZhanGSKen
74841c08dc 编译参数修复 2025-06-24 09:52:39 +08:00
ZhanGSKen
945bacb825 <autoinstaller>APK 15.2.1 release Publish. 2025-06-24 09:49:39 +08:00
ZhanGSKen
0e464495fd 编译调试 2025-06-24 09:45:47 +08:00
ZhanGSKen
e8682ce410 Merge remote-tracking branch 'origin/androidxdemo' into appbase 2025-06-19 20:56:32 +08:00
ZhanGSKen
2e4003dae0 编译调试 2025-06-19 20:52:02 +08:00
ZhanGSKen
198b0975ce <libaes>Library Release 15.9.1 2025-06-19 20:42:45 +08:00
ZhanGSKen
24a578a9d2 <aes>APK 15.9.1 release Publish. 2025-06-19 20:42:26 +08:00
ZhanGSKen
46de24447f 编译调试 2025-06-19 20:40:41 +08:00
ZhanGSKen
1320984829 修改项目关键设置说明 2025-06-09 11:17:21 +08:00
ZhanGSKen
abf1e5ba42 添加 WinBoLL 应用签名密钥配置说明。 2025-06-09 11:07:59 +08:00
ZhanGSKen
1cd2f88038 标记重点与选项 2025-06-09 10:49:23 +08:00
ZhanGSKen
3f6e583d68 添加应用签名密钥配置说明。 2025-06-09 10:47:38 +08:00
ZhanGSKen
271456bfcd <appbase>APK 15.8.8 release Publish. 2025-06-09 09:38:21 +08:00
ZhanGSKen
ee5458d82c 忽略GitHub工作流启动标记与执行 2025-06-09 09:37:15 +08:00
ZhanGSKen
3a83367f71 与NumTable模块建立时的变更源码合并,并且修复AES模块编译参数。 2025-06-09 01:56:31 +08:00
ZhanGSKen
74b9350a6a <aes>APK 15.9.0 release Publish. 2025-06-09 01:44:30 +08:00
ZhanGSKen
d2858f23f7 准备与NumTable模块项目合并。先提升AES模块版本号,掌控模块源码版本控制优先权。 2025-06-09 01:42:49 +08:00
ZhanGSKen
40a5b9c339 <numtable>APK 15.1.0 release Publish. 2025-06-08 21:21:12 +08:00
ZhanGSKen
fd79113572 设置NumTable版本号 2025-06-08 21:19:54 +08:00
ZhanGSKen
9b911b583c <numtable>APK 15.0.0 release Publish. 2025-06-08 21:16:53 +08:00
ZhanGSKen
37817c3e8c <numtable>Start New Stage Version. 2025-06-08 21:14:54 +08:00
ZhanGSKen
0b5402f5f3 设置应用图标方案 2025-06-08 21:10:47 +08:00
ZhanGSKen
bea22e3853 添加NumTable项目 2025-06-08 20:19:58 +08:00
ZhanGSKen
7e2ad0c01d 精简代码 2025-06-05 17:00:28 +08:00
ZhanGSKen
476ce02fc8 Json数据适配吻合 2025-06-05 15:06:55 +08:00
ZhanGSKen
bc697279ad 编译参数修复 2025-06-05 12:03:14 +08:00
ZhanGSKen
dee01f1179 接口初步对接,正在调试数据处理方法... 2025-06-05 11:59:36 +08:00
ZhanGSKen
a500decc7a 登录数据加密解密成功 2025-06-05 09:52:04 +08:00
ZhanGSKen
5099d00050 <appbase>APK 15.8.7 release Publish. 2025-06-04 15:04:39 +08:00
ZhanGSKen
515d14e896 编译测试 2025-06-04 15:04:01 +08:00
ZhanGSKen
f630e27ed8 编译参数修复 2025-06-04 15:03:06 +08:00
ZhanGSKen
cd7ed01216 添加Logon框和RSA加密模块。 2025-06-04 14:58:01 +08:00
ZhanGSKen
bb24bbfbd1 <appbase>APK 15.8.6 release Publish. 2025-06-04 13:17:34 +08:00
ZhanGSKen
2ba2f88510 添加调试模块 2025-06-04 13:16:51 +08:00
ZhanGSKen
db3a3644a8 <appbase>APK 15.8.5 release Publish. 2025-06-04 12:14:48 +08:00
ZhanGSKen
556bfa7024 添加主机识别 2025-06-04 12:11:19 +08:00
ZhanGSKen
4842a1ec30 添加云宝云测试接口 2025-06-04 11:21:26 +08:00
ZhanGSKen
b0388a2972 Merge remote-tracking branch 'origin/powerbell' into appbase 2025-06-03 20:17:33 +08:00
ZhanGSKen
bd5a1f18ce Merge remote-tracking branch 'origin/ollama' into appbase 2025-06-03 20:17:27 +08:00
ZhanGSKen
99798b4816 Merge branch 'androidxdemo' into appbase 2025-06-03 20:11:30 +08:00
ZhanGSKen
f93b6047a8 Merge remote-tracking branch 'origin/androiddemo' into appbase 2025-06-03 20:07:55 +08:00
ZhanGSKen
daa3f858a0 Merge remote-tracking branch 'origin/aes' into appbase 2025-06-03 20:07:49 +08:00
ZhanGSKen
3fded32426 Merge remote-tracking branch 'origin/apputils' into appbase 2025-06-03 20:07:42 +08:00
ZhanGSKen
8f85006040 Merge branch 'androiddemo' into appbase 2025-06-03 20:05:45 +08:00
ZhanGSKen
e28b0bd75e <libaes>Library Release 15.8.1 2025-06-03 19:17:10 +08:00
ZhanGSKen
af1d6d3439 <aes>APK 15.8.1 release Publish. 2025-06-03 19:16:41 +08:00
ZhanGSKen
470d1ffa1f 编译测试 2025-06-03 19:13:16 +08:00
ZhanGSKen
49ae869df1 Merge remote-tracking branch 'origin/aes' into appbase 2025-06-03 17:54:27 +08:00
ZhanGSKen
77e98bafe4 <libapputils>Library Release 15.8.4 2025-06-03 15:05:54 +08:00
ZhanGSKen
ff14d0c0c3 <apputils>APK 15.8.4 release Publish. 2025-06-03 15:05:42 +08:00
ZhanGSKen
950be3a182 <libapputils>Library Release 15.8.3 2025-06-03 14:35:23 +08:00
ZhanGSKen
1f20fca9be <apputils>APK 15.8.3 release Publish. 2025-06-03 14:35:00 +08:00
ZhanGSKen
8d29d11078 Merge branch 'apputils' into m2 2025-06-03 13:50:51 +08:00
ZhanGSKen
7534881f50 <libappbase>Library Release 15.8.4 2025-06-03 13:40:13 +08:00
ZhanGSKen
ef992dcd7c <appbase>APK 15.8.4 release Publish. 2025-06-03 13:40:01 +08:00
ZhanGSKen
71c1baa4ba <libappbase>Library Release 15.8.3 2025-06-03 13:26:16 +08:00
ZhanGSKen
8d1872a893 应用介绍页,完成论坛与Git仓库连接跳转。 2025-06-01 20:50:03 +08:00
ZhanGSKen
9a0ee889ba 更新类库 2025-06-01 16:04:21 +08:00
ZhanGSKen
c40066ca4d 更新类库 2025-06-01 16:03:16 +08:00
ZhanGSKen
5348d1ef6d <libapputils>Library Release 15.8.2 2025-06-01 16:01:39 +08:00
ZhanGSKen
063c997bbb <apputils>APK 15.8.2 release Publish. 2025-06-01 16:01:19 +08:00
ZhanGSKen
1376ca7ebb Merge remote-tracking branch 'origin/appbase' into apputils 2025-06-01 15:59:59 +08:00
ZhanGSKen
92e271b569 更新类库 2025-06-01 15:58:12 +08:00
ZhanGSKen
a5083cc52f 更新类库 2025-06-01 15:43:48 +08:00
ZhanGSKen
6cce9c4d3f <powerbell>APK 15.4.1 release Publish. 2025-05-29 09:43:37 +08:00
ZhanGSKen
df18c34976 <powerbell>APK 15.4.0 release Publish. 2025-05-29 09:43:12 +08:00
ZhanGSKen
22ca83b5b7 版本号调整 2025-05-29 09:38:25 +08:00
ZhanGSKen
98233ce148 编译测试 2025-05-29 09:31:28 +08:00
ZhanGSKen
b61c63c426 <ollama>Start New Stage Version. 2025-03-28 05:09:23 +08:00
ZhanGSKen
f02dc215ca <ollama>APK 15.0.0 release Publish. 2025-03-28 05:09:21 +08:00
ZhanGSKen
1c27d0ccdc Ollama 问答模块完成。 2025-03-28 05:04:15 +08:00
ZhanGSKen
803745d12e 添加简单http访问功能 2025-03-27 20:45:57 +08:00
ZhanGSKen
a66be9cd37 添加WinBoll Ollama 访问项目。 2025-03-27 19:39:54 +08:00
112 changed files with 3920 additions and 359 deletions

View File

@@ -113,10 +113,10 @@ if [[ $? -eq 0 ]]; then
# 如果Git已经提交了所有代码就执行标签和应用发布操作
# 预先询问是否添加工作流标签
echo "Add Github Workflows Tag? (yes/No)"
result=$(askAddWorkflowsTag)
nAskAddWorkflowsTag=$?
echo $result
#echo "Add Github Workflows Tag? (yes/No)"
#result=$(askAddWorkflowsTag)
#nAskAddWorkflowsTag=$?
#echo $result
# 发布应用
echo "Publishing WinBoLL APK ..."
@@ -138,17 +138,17 @@ if [[ $? -eq 0 ]]; then
fi
# 添加 GitHub 工作流标签
if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
#if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
# 如果用户选择添加工作流标签
result=$(addWorkflowsTag $1)
if [[ $? -eq 0 ]]; then
echo $result
#result=$(addWorkflowsTag $1)
#if [[ $? -eq 0 ]]; then
# echo $result
# 工作流标签添加成功
else
echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
exit 1 # addWorkflowsTag 异常
fi
fi
#else
#echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
#exit 1 # addWorkflowsTag 异常
#fi
#fi
## 清理更新描述文件内容
echo "" > $1/app_update_description.txt

View File

@@ -114,9 +114,11 @@
# 本项目要实际运用需要注意以下几个步骤:
# 在项目根目录下:
## 1. 项目模块编译环境设置(必须)settings.gradle-demo 要复制为 settings.gradle并取消相应项目模块的注释。
## 2. 项目 Android SDK 编译环境设置(可选)local.properties-demo 要复制为 local.properties并按需要设置 Android SDK 目录。
## 3. 类库型模块编译环境设置(可选)winboll.properties-demo 要复制为 winboll.properties并按需要设置 WinBoLL Maven 库登录用户信息
## . 项目模块编译环境设置(必须)settings.gradle-demo 要复制为 settings.gradle并取消相应项目模块的注释。
## . 项目 Android SDK 编译环境设置(可选)local.properties-demo 要复制为 local.properties并按需要设置 Android SDK 目录。
## . 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
## ☆. 类库型模块编译环境设置(可选)winboll.properties-demo 要复制为 winboll.properties并按需要设置 WinBoLL Maven 库登录用户信息。
# ☆类库型项目编译方法

View File

@@ -29,7 +29,7 @@ android {
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.8"
versionName "15.9"
if(true) {
versionName = genVersionName("${versionName}")
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 01 08:26:46 GMT 2025
stageCount=1
#Thu Jun 19 20:42:40 HKT 2025
stageCount=2
libraryProject=libaes
baseVersion=15.8
publishVersion=15.8.0
buildCount=2
baseBetaVersion=15.8.1
baseVersion=15.9
publishVersion=15.9.1
buildCount=0
baseBetaVersion=15.9.2

View File

@@ -83,7 +83,7 @@ public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=AES");
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=2&fromuid=1");
appInfo.setAppAPKName("AES");
appInfo.setAppAPKFolderName("AES");
//appInfo.setIsAddDebugTools(false);

View File

@@ -67,6 +67,6 @@ dependencies {
// https://mvnrepository.com/artifact/com.android.support/recyclerview-v7
api 'com.android.support:recyclerview-v7:28.0.0'
api 'cc.winboll.studio:libapputils:15.8.1'
api 'cc.winboll.studio:libappbase:15.8.1'
api 'cc.winboll.studio:libapputils:15.8.2'
api 'cc.winboll.studio:libappbase:15.8.2'
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon May 19 21:45:28 GMT 2025
#Sun Jun 01 08:02:46 GMT 2025
stageCount=0
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=25
buildCount=27
baseBetaVersion=15.0.1

View File

@@ -68,6 +68,6 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
api 'cc.winboll.studio:libaes:15.8.0'
api 'cc.winboll.studio:libapputils:15.8.1'
api 'cc.winboll.studio:libappbase:15.8.1'
api 'cc.winboll.studio:libapputils:15.8.2'
api 'cc.winboll.studio:libappbase:15.8.2'
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon May 19 21:43:40 GMT 2025
#Thu Jun 19 12:49:47 GMT 2025
stageCount=0
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=22
buildCount=26
baseBetaVersion=15.0.1

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 03 13:25:46 HKT 2025
stageCount=4
#Mon Jun 09 09:38:19 HKT 2025
stageCount=9
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.3
publishVersion=15.8.8
buildCount=0
baseBetaVersion=15.8.4
baseBetaVersion=15.8.9

View File

@@ -9,7 +9,8 @@
android:label="@string/app_name"
android:theme="@style/MyAPPBaseTheme"
android:resizeableActivity="true"
android:process=":App">
android:process=":App"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
@@ -39,7 +40,8 @@
android:resizeableActivity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
<activity android:name=".activities.New2Activity"
<activity
android:name=".activities.New2Activity"
android:label="New2Activity"
android:exported="true"
android:resizeableActivity="true"
@@ -74,7 +76,8 @@
<service android:name=".services.AssistantService"/>
<receiver android:name="cc.winboll.studio.appbase.receivers.MainReceiver"
<receiver
android:name="cc.winboll.studio.appbase.receivers.MainReceiver"
android:exported="true">
<intent-filter>
@@ -105,7 +108,8 @@
</receiver>
<receiver android:name=".receivers.APPNewsWidgetClickListener"
<receiver
android:name=".receivers.APPNewsWidgetClickListener"
android:exported="true">
<intent-filter>
@@ -122,7 +126,6 @@
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>

View File

@@ -62,6 +62,11 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.item_yun) {
GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(this, cc.winboll.studio.libappbase.activities.YunActivity.class);
} else if(item.getItemId() == R.id.item_logon) {
GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(this, cc.winboll.studio.libappbase.activities.LogonActivity.class);
}
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item);
}

View File

@@ -5,6 +5,14 @@
android:id="@+id/item_home"
android:title="HOME"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_yun"
android:title="YUN"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_logon"
android:title="Logon"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_log"
android:title="LOG"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 允许访问 winboll.cc 及其子域名(原配置) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">winboll.cc</domain>
</domain-config>
<!-- **新增:允许访问 IP 地址 10.8.0.250** -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.8.0.250</domain> <!-- 不包含子域名 -->
</domain-config>
</network-security-config>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 13 11:18:09 HKT 2025
stageCount=2
#Tue Jun 03 15:05:48 HKT 2025
stageCount=5
libraryProject=libapputils
baseVersion=15.8
publishVersion=15.8.1
publishVersion=15.8.4
buildCount=0
baseBetaVersion=15.8.2
baseBetaVersion=15.8.5

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun May 04 05:32:00 GMT 2025
stageCount=1
#Tue Jun 24 09:54:47 HKT 2025
stageCount=3
libraryProject=
baseVersion=15.2
publishVersion=15.2.0
buildCount=74
baseBetaVersion=15.2.1
publishVersion=15.2.2
buildCount=0
baseBetaVersion=15.2.3

View File

@@ -42,23 +42,24 @@ android {
}
}
compileOptions {
/*compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}*/
}
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api project(':libjc')
api 'cc.winboll.studio:libaes:15.9.1'
api 'cc.winboll.studio:libapputils:15.8.4'
api 'cc.winboll.studio:libappbase:15.8.4'
// https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on
//implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.69'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.69'
api project(':libjc')
api 'androidx.appcompat:appcompat:1.0.0'
api 'com.google.android.material:material:1.0.0'
api 'cc.winboll.studio:libapputils:9.1.0'
api 'cc.winboll.studio:libappbase:1.0.3'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jan 10 22:03:57 GMT 2025
#Tue Jun 24 11:17:30 GMT 2025
stageCount=0
libraryProject=libjc
baseVersion=1.0
publishVersion=1.0.0
buildCount=133
buildCount=135
baseBetaVersion=1.0.1

View File

@@ -15,10 +15,10 @@ import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import cc.winboll.studio.jc.R;
import cc.winboll.studio.libapputils.log.LogUtils;
import cc.winboll.studio.libjc.JAR_RUNNING_MODE;
import cc.winboll.studio.libjc.JCMainThread;
import cc.winboll.studio.libjc.net.JCSocketClient;
import cc.winboll.studio.libjc.util.LogUtils;
import cc.winboll.studio.libjc.Main;
final public class MainActivity extends Activity implements JCMainThread.OnMessageListener {
@@ -77,7 +77,7 @@ final public class MainActivity extends Activity implements JCMainThread.OnMessa
// 启动主线程
_JCMainThread = JCMainThread.getInstance(getPackageName());
_JCMainThread.setOnLogListener(this);
_JCMainThread.setRunningMode(JAR_RUNNING_MODE.JC);
//_JCMainThread.setRunningMode(Main.JAR_RUNNING_MODE.JC);
_JCMainThread.start();
// 设置 WinBoll 应用 UI 类型

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 01 08:26:46 GMT 2025
stageCount=1
#Thu Jun 19 20:42:26 HKT 2025
stageCount=2
libraryProject=libaes
baseVersion=15.8
publishVersion=15.8.0
buildCount=2
baseBetaVersion=15.8.1
baseVersion=15.9
publishVersion=15.9.1
buildCount=0
baseBetaVersion=15.9.2

View File

@@ -115,7 +115,8 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
mszCurrentAppPackageName = mszAppAPKName + "_" + mszAppVersionName + ".apk";
mszHomePage = mszWinBoLLServerHost + "/studio/details.php?app=" + mszAppAPKFolderName;
mszHomePage = mAPPInfo.getAppHomePage();
//mszHomePage = mszWinBoLLServerHost + "/studio/details.php?app=" + mszAppAPKFolderName;
if (mAPPInfo.getAppGitAPPBranch().equals("")) {
mszGitea = "https://gitea.winboll.cc/" + mAPPInfo.getAppGitOwner() + "/" + mszAppGitName;
} else {

View File

@@ -22,4 +22,9 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
api 'com.google.code.gson:gson:2.10.1'
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 03 13:25:46 HKT 2025
stageCount=4
#Mon Jun 09 09:38:19 HKT 2025
stageCount=9
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.3
publishVersion=15.8.8
buildCount=0
baseBetaVersion=15.8.4
baseBetaVersion=15.8.9

View File

@@ -103,6 +103,10 @@
</receiver>
<activity android:name="cc.winboll.studio.libappbase.activities.YunActivity"/>
<activity android:name="cc.winboll.studio.libappbase.activities.LogonActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,150 @@
package cc.winboll.studio.libappbase.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.RadioButton;
import cc.winboll.studio.libappbase.BuildConfig;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.models.UserInfoModel;
import cc.winboll.studio.libappbase.utils.RSAUtils;
import cc.winboll.studio.libappbase.utils.YunUtils;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 13:29
* @Describe 用户登录框
*/
public class LogonActivity extends Activity implements IWinBoLLActivity {
public static final String TAG = "LogonActivity";
public static final String DEBUG_HOST = "http://10.8.0.250:456";
public static final String YUN_HOST = "https://yun.winboll.cc";
String mHost = "";
RadioButton mrbYunHost;
RadioButton mrbDebugHost;
LogView mLogView;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_logon);
mLogView = findViewById(R.id.logview);
mLogView.start();
mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST;
if (BuildConfig.DEBUG) {
mrbYunHost = findViewById(R.id.rb_yunhost);
mrbDebugHost = findViewById(R.id.rb_debughost);
mrbYunHost.setChecked(!BuildConfig.DEBUG);
mrbDebugHost.setChecked(BuildConfig.DEBUG);
} else {
findViewById(R.id.ll_hostbar).setVisibility(View.GONE);
}
}
public void onSwitchHost(View view) {
if (view.getId() == R.id.rb_yunhost) {
mrbDebugHost.setChecked(false);
mHost = YUN_HOST;
} else if (view.getId() == R.id.rb_debughost) {
mrbYunHost.setChecked(false);
mHost = DEBUG_HOST;
}
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
public void onTestLogin(View view) {
LogUtils.d(TAG, "onTestLogin");
final YunUtils yunUtils = YunUtils.getInstance(this);
UserInfoModel userInfoModel = new UserInfoModel();
userInfoModel.setUsername("jian");
userInfoModel.setPassword("kkiio");
userInfoModel.setToken("aaa111");
yunUtils.login(mHost, userInfoModel);
}
public void onTestRSA(View view) {
LogUtils.d(TAG, "onTestRSA");
RSAUtils utils = RSAUtils.getInstance(this);
try {
// 测试 1首次生成密钥对
LogUtils.d(TAG, "==== 首次生成密钥对 ====");
if (utils.keysExist()) {
LogUtils.d(TAG, "密钥对已生成");
} else {
utils.generateAndSaveKeys();
LogUtils.d(TAG, "密钥对生成成功。");
}
// 测试 2获取密钥对自动读取已生成的文件
KeyPair keyPair = utils.getOrGenerateKeys();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 打印密钥信息
LogUtils.d(TAG, "\n==== 密钥信息 ====");
LogUtils.d(TAG, "公钥算法:" + publicKey.getAlgorithm());
LogUtils.d(TAG, "公钥编码长度:" + publicKey.getEncoded().length + "字节");
LogUtils.d(TAG, "私钥算法:" + privateKey.getAlgorithm());
LogUtils.d(TAG, "私钥编码长度:" + privateKey.getEncoded().length + "字节");
// 测试 3重复调用时检查是否复用文件
LogUtils.d(TAG, "\n==== 二次调用 ====");
KeyPair reusedPair = utils.getOrGenerateKeys();
LogUtils.d(TAG, "是否为同一公钥:" + (publicKey.equals(reusedPair.getPublic()))); // true单例引用
LogUtils.d(TAG, "操作完成");
String testMessage = "Hello, RSA Encryption!";
// 1. 获取或生成密钥对
PublicKey publicKeyReused = reusedPair.getPublic();
PrivateKey privateKeyReused = reusedPair.getPrivate();
// 2. 公钥加密
byte[] encryptedData = utils.encryptWithPublicKey(testMessage, publicKeyReused);
LogUtils.d(TAG, "加密后数据(字节长度):" + encryptedData.length);
// 3. 私钥解密
String decryptedMessage = utils.decryptWithPrivateKey(encryptedData, privateKeyReused);
LogUtils.d(TAG, "解密结果: " + decryptedMessage);
// 4. 验证解密是否成功
if (testMessage.equals(decryptedMessage)) {
LogUtils.d(TAG, "加密解密测试通过!");
} else {
LogUtils.d(TAG, "测试失败:内容不一致");
}
} catch (Exception e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
}

View File

@@ -0,0 +1,126 @@
package cc.winboll.studio.libappbase.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import cc.winboll.studio.libappbase.BuildConfig;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import android.widget.RadioButton;
import cc.winboll.studio.libappbase.LogView;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 11:06
* @Describe 云宝云
*/
public class YunActivity extends Activity implements IWinBoLLActivity {
public static final String TAG = "YunActivity";
public static final String DEBUG_HOST = "http://10.8.0.250:456";
public static final String YUN_HOST = "https://yun.winboll.cc";
String mHost = "";
RadioButton mrbYunHost;
RadioButton mrbDebugHost;
LogView mLogView;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_yun);
mLogView = findViewById(R.id.logview);
mLogView.start();
mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST;
if (BuildConfig.DEBUG) {
mrbYunHost = findViewById(R.id.rb_yunhost);
mrbDebugHost = findViewById(R.id.rb_debughost);
mrbYunHost.setChecked(!BuildConfig.DEBUG);
mrbDebugHost.setChecked(BuildConfig.DEBUG);
} else {
findViewById(R.id.ll_hostbar).setVisibility(View.GONE);
}
}
public void onSwitchHost(View view) {
if (view.getId() == R.id.rb_yunhost) {
mrbDebugHost.setChecked(false);
mHost = YUN_HOST;
} else if (view.getId() == R.id.rb_debughost) {
mrbYunHost.setChecked(false);
mHost = DEBUG_HOST;
}
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
public void onTestYun(View view) {
LogUtils.d(TAG, "onTestYun");
(new Thread(new Runnable(){
@Override
public void run() {
testYun();
}
})).start();
}
void testYun() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(mHost + "/backups/")
.build();
Response response = null;
try {
response = client.newCall(request).execute();
if (response.isSuccessful()) {
String responseBody = "";
if (response.body() != null) {
responseBody = response.body().string();
}
// 正则匹配:任意主机名 -> Test OK主机名部分匹配非空字符
boolean isMatch = responseBody.matches(".+? -> Test OK");
if (isMatch) {
LogUtils.d(TAG, responseBody);
} else {
LogUtils.d(TAG, "响应内容不匹配,内容:" + responseBody);
}
} else {
LogUtils.d(TAG, "请求失败,状态码:" + response.code());
}
} catch (IOException e) {
LogUtils.d(TAG, "读取响应体失败:" + e.getMessage());
} catch (Exception e) {
LogUtils.d(TAG, "异常:" + e.getMessage());
e.printStackTrace(); // Java 7 需显式打印堆栈
} finally {
// 手动关闭 ResponseJava 7 不支持 try-with-resources
if (response != null && response.body() != null) {
response.body().close();
}
}
}
}

View File

@@ -0,0 +1,53 @@
package cc.winboll.studio.libappbase.models;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/05 11:26
*/
public class ResponseData {
public static final String STATUS_SUCCESS = "success";
public static final String STATUS_ERROR = "error";
private String status;
private String message;
private UserInfoModel data;
public ResponseData() {
this.status = "";
this.message = "";
this.data = new UserInfoModel();
}
public ResponseData(String status, String message, UserInfoModel data) {
this.status = status;
this.message = message;
this.data = data;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setData(UserInfoModel data) {
this.data = data;
}
public UserInfoModel getData() {
return data;
}
}

View File

@@ -0,0 +1,92 @@
package cc.winboll.studio.libappbase.models;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 19:14
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
public class UserInfoModel extends BaseBean {
public static final String TAG = "UserInfoModel";
String username;
String password;
String token;
public UserInfoModel() {
this.username = "";
this.password = "";
this.token = "";
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
@Override
public String getName() {
return UserInfoModel.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name("username").value(getUsername());
jsonWriter.name("password").value(getPassword());
jsonWriter.name("token").value(getToken());
}
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
if (name.equals("username")) {
setUsername(jsonReader.nextString());
} else if (name.equals("password")) {
setPassword(jsonReader.nextString());
} else if (name.equals("token")) {
setToken(jsonReader.nextString());
} else {
return false;
}
}
return true;
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return this;
}
}

View File

@@ -0,0 +1,128 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 20:15
* @Describe 文件操作类
*/
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileUtils {
public static final String TAG = "FileUtils";
/**
* 读取文件为字节数组Java 7 语法)
*/
public static byte[] readFileToByteArray(String filePath) {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(filePath);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
// 手动关闭流Java 7 不支持 try-with-resources
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 写入字节数组到文件Java 7 语法)
*/
public static boolean writeByteArrayToFile(byte[] data, String filePath) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(filePath);
fos.write(data);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 原字符串读写方法(适配 Java 7
public static String readFileToString(String filePath) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(filePath));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append(System.getProperty("line.separator"));
}
// 去除最后一个换行符(可选)
if (content.length() > 0) {
content.deleteCharAt(content.length() - 1);
}
return content.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static boolean writeStringToFile(String content, String filePath, boolean append) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(filePath, append));
writer.write(content);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

View File

@@ -0,0 +1,222 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 13:36
* @Describe RSA加密工具
*/
import android.content.Context;
import android.util.Base64;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Objects;
import javax.crypto.Cipher;
public class RSAUtils {
private static final String TAG = "RSAUtils";
private static final int KEY_SIZE = 2048;
private static final String KEY_ALGORITHM = "RSA";
private static final String PUBLIC_KEY_FILE = "public.key";
private static final String PRIVATE_KEY_FILE = "private.key";
private static final String CIPHER_ALGORITHM = KEY_ALGORITHM + "/ECB/PKCS1Padding"; // 保留原加密方式
private final String keyPath;
private static volatile RSAUtils INSTANCE;
/**
* 构造方法:初始化密钥存储路径(内部存储)
*/
private RSAUtils(Context context) {
keyPath = context.getFilesDir() + File.separator + "keys" + File.separator; // 修正路径格式
}
/**
* 获取单例实例
*/
public static synchronized RSAUtils getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = new RSAUtils(context);
}
return INSTANCE;
}
/**
* 检查密钥文件是否存在
*/
public boolean keysExist() {
File publicKeyFile = new File(keyPath + PUBLIC_KEY_FILE);
File privateKeyFile = new File(keyPath + PRIVATE_KEY_FILE);
return publicKeyFile.exists() && privateKeyFile.exists();
}
/**
* 生成密钥对并保存到文件
*/
public void generateAndSaveKeys() throws Exception {
LogUtils.d(TAG, "开始生成 RSA 密钥对2048位");
KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
generator.initialize(KEY_SIZE);
KeyPair keyPair = generator.generateKeyPair();
saveKey(PUBLIC_KEY_FILE, keyPair.getPublic().getEncoded());
saveKey(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded());
LogUtils.d(TAG, "密钥对生成并保存成功");
}
/**
* 获取或生成密钥对(线程安全)
*/
public KeyPair getOrGenerateKeys() throws Exception {
if (!keysExist()) {
synchronized (RSAUtils.class) { // 双重检查锁,避免多线程重复生成
if (!keysExist()) {
generateAndSaveKeys();
}
}
}
return readKeysFromFile();
}
/**
* 从文件读取密钥对
*/
private KeyPair readKeysFromFile() throws Exception {
LogUtils.d(TAG, "读取密钥对文件");
try {
byte[] publicKeyBytes = readFileToBytes(keyPath + PUBLIC_KEY_FILE);
byte[] privateKeyBytes = readFileToBytes(keyPath + PRIVATE_KEY_FILE);
X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes);
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicKey = factory.generatePublic(publicSpec);
PrivateKey privateKey = factory.generatePrivate(privateSpec);
return new KeyPair(publicKey, privateKey);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
LogUtils.e(TAG, "密钥文件读取失败:" + e.getMessage());
throw new Exception("密钥文件损坏或格式错误", e);
}
}
/**
* 保存密钥到文件(通用方法)
*/
private void saveKey(String fileName, byte[] keyBytes) throws IOException {
Objects.requireNonNull(keyBytes, "密钥字节数据不可为空");
File dir = new File(keyPath);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("创建密钥目录失败:" + keyPath);
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(keyPath + fileName);
fos.write(keyBytes);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
}
}
}
}
/**
* 读取文件为字节数组Java 7 兼容)
*/
private byte[] readFileToBytes(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists() || file.isDirectory()) {
throw new IOException("文件不存在或为目录:" + filePath);
}
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
int bytesRead = fis.read(data);
if (bytesRead != data.length) {
throw new IOException("文件读取不完整");
}
return data;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
}
}
}
}
/**
* 公钥加密(带参数校验)
*/
public byte[] encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception {
Objects.requireNonNull(plainText, "明文不可为空");
Objects.requireNonNull(publicKey, "公钥不可为空");
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 检查数据长度是否超过 RSA 限制2048位密钥最大明文为 214字节PKCS1Padding
int maxPlainTextSize = cipher.getBlockSize() - 11; // PKCS1Padding 固定填充长度
if (plainText.getBytes("UTF-8").length > maxPlainTextSize) {
throw new IllegalArgumentException("明文过长,最大支持 " + maxPlainTextSize + " 字节");
}
return cipher.doFinal(plainText.getBytes("UTF-8"));
}
/**
* 私钥解密(带参数校验)
*/
public String decryptWithPrivateKey(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Objects.requireNonNull(encryptedData, "密文不可为空");
Objects.requireNonNull(privateKey, "私钥不可为空");
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(encryptedData);
return new String(decryptedBytes, "UTF-8");
}
/**
* 将 HTTP 传输的 Base64 字符串还原为加密字节数组Java 7 兼容)
* @param httpString Base64 字符串(非 null
* @return 加密字节数组
* @throws IllegalArgumentException 解码失败时抛出
*/
public byte[] httpStringToEncryptBytes(String httpString) {
Objects.requireNonNull(httpString, "HTTP 字符串不可为空");
// 计算缺失的填充符数量Java 7 不支持 repeat(),手动拼接)
int pad = httpString.length() % 4;
StringBuilder paddedString = new StringBuilder(httpString);
if (pad != 0) {
for (int i = 0; i < pad; i++) {
paddedString.append('='); // 补全 '='
}
}
// 使用 Base64 解码Android 原生 Base64 类兼容 Java 7
return Base64.decode(paddedString.toString(), Base64.URL_SAFE);
}
}

View File

@@ -0,0 +1,281 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/04 17:21
* @Describe 应用登录与接口工具
*/
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.ResponseData;
import cc.winboll.studio.libappbase.models.UserInfoModel;
import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.UnsupportedEncodingException;
public class YunUtils {
public static final String TAG = "YunUtils";
// 私有静态实例,类加载时创建
private static volatile YunUtils INSTANCE;
Context mContext;
UserInfoModel mUserInfoModel;
String token = "";
String mDataFolderPath = "";
String mUserInfoModelPath = "";
private static final int CONNECT_TIMEOUT = 15; // 连接超时时间(秒)
private static final int READ_TIMEOUT = 20; // 读取超时时间(秒)
private static volatile YunUtils instance;
private OkHttpClient okHttpClient;
private Handler mainHandler; // 主线程 Handler
// 私有构造方法,防止外部实例化
private YunUtils(Context context) {
LogUtils.d(TAG, "YunUtils");
mContext = context;
mDataFolderPath = mContext.getExternalFilesDir(TAG).toString();
File fTest = new File(mDataFolderPath);
if (!fTest.exists()) {
fTest.mkdirs();
}
mUserInfoModelPath = mDataFolderPath + File.separator + "UserInfoModel.rsajson";
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.build();
mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程 Looper
}
// 公共静态方法,返回唯一实例
public static synchronized YunUtils getInstance(Context context) {
LogUtils.d(TAG, "getInstance");
if (INSTANCE == null) {
INSTANCE = new YunUtils(context);
}
return INSTANCE;
}
public void checkLoginStatus() {
String token = getLocalToken();
LogUtils.d(TAG, String.format("checkLoginStatus token is %s", token));
}
String getLocalToken() {
UserInfoModel userInfoModel = loadUserInfoModel();
return (userInfoModel == null) ?"": userInfoModel.getToken();
}
public void login(String host, UserInfoModel userInfoModel) {
LogUtils.d(TAG, "login");
// 发送 POST 请求
String apiUrl = host + "/login/index.php";
// 序列化对象为JSON
Gson gson = new Gson();
String jsonData = gson.toJson(userInfoModel); // 自动生成标准JSON
//String jsonData = userInfoModel.toString();
LogUtils.d(TAG, "要发送的数据 : " + jsonData);
sendPostRequest(apiUrl, jsonData, new OnResponseListener() {
// 成功回调(主线程)
@Override
public void onSuccess(String responseBody) {
LogUtils.d(TAG, "onSuccess");
LogUtils.d(TAG, String.format("responseBody %s", responseBody));
Gson gson = new Gson();
ResponseData result = gson.fromJson(responseBody, ResponseData.class); // 转为 Result 实例
if(result.getStatus().equals(ResponseData.STATUS_SUCCESS)) {
UserInfoModel userInfoModel = result.getData();
if (userInfoModel != null) {
LogUtils.d(TAG, "收到网站 UserInfoModel");
String token = userInfoModel.getToken();
saveLocalToken(token);
checkLoginStatus();
}
} else if(result.getStatus().equals(ResponseData.STATUS_ERROR)) {
try {
String decodedMessage = URLDecoder.decode(result.getMessage(), "UTF-8");
LogUtils.d(TAG, "服务器返回信息: " + decodedMessage);
} catch (UnsupportedEncodingException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
}
// 失败回调(主线程)
@Override
public void onFailure(String errorMsg) {
LogUtils.d(TAG, errorMsg);
// 处理错误
}
});
}
public void saveLocalToken(String token) {
UserInfoModel userInfoModel = new UserInfoModel();
userInfoModel.setToken(token);
saveUserInfoModel(userInfoModel);
}
UserInfoModel loadUserInfoModel() {
LogUtils.d(TAG, "loadUserInfoModel");
if (new File(mUserInfoModelPath).exists()) {
try {
// 加载加密后的模型数据
byte[] encryptedData = FileUtils.readFileToByteArray(mUserInfoModelPath);
// 加载 RSA 工具
RSAUtils utils = RSAUtils.getInstance(mContext);
KeyPair keyPair = utils.getOrGenerateKeys();
//PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 私钥解密模型数据
String szInfo = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
LogUtils.d(TAG, String.format("szInfo %s", szInfo));
mUserInfoModel = UserInfoModel.parseStringToBean(szInfo, UserInfoModel.class);
if (mUserInfoModel == null) {
LogUtils.d(TAG, "模型数据解析为空数据。");
}
LogUtils.d(TAG, "UserInfoModel 解密加载结束。");
} catch (Exception e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
} else {
LogUtils.d(TAG, "云服务登录信息不存在。");
mUserInfoModel = null;
}
return mUserInfoModel;
}
void saveUserInfoModel(UserInfoModel userInfoModel) {
LogUtils.d(TAG, "saveUserInfoModel");
try {
String szInfo = userInfoModel.toString();
LogUtils.d(TAG, "原始数据: " + szInfo);
RSAUtils utils = RSAUtils.getInstance(mContext);
KeyPair keyPair = utils.getOrGenerateKeys();
PublicKey publicKey = keyPair.getPublic();
// 公钥加密(传入字节数组,避免中间字符串转换)
byte[] encryptedData = utils.encryptWithPublicKey(szInfo, publicKey);
// 保存加密字节数组到文件(直接操作字节,无需转字符串)
FileUtils.writeByteArrayToFile(encryptedData, mUserInfoModelPath);
LogUtils.d(TAG, "加密数据已保存");
// 测试解密(仅调试用)
String szInfo2 = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
LogUtils.d(TAG, "解密结果: " + szInfo2);
mUserInfoModel = UserInfoModel.parseStringToBean(szInfo2, UserInfoModel.class);
if (mUserInfoModel == null) {
LogUtils.d(TAG, "模型解析失败");
}
} catch (Exception e) {
LogUtils.d(TAG, "加密/解密失败: " + e.getMessage());
}
}
// 发送 POST 请求JSON 数据)
public void sendPostRequest(String url, String data, OnResponseListener listener) {
RequestBody requestBody = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"), // 关键头信息
data.getBytes(StandardCharsets.UTF_8)
);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Content-Type", "application/json") // 显式添加头
.build();
executeRequest(request, listener);
}
// 发送 GET 请求
public void sendGetRequest(String url, OnResponseListener listener) {
Request request = new Request.Builder()
.url(url)
.get()
.build();
executeRequest(request, listener);
}
// 执行请求(子线程处理)
private void executeRequest(final Request request, final OnResponseListener listener) {
okHttpClient.newCall(request).enqueue(new Callback() {
// 响应成功(子线程)
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
if (!response.isSuccessful()) {
postFailure(listener, "响应码错误:" + response.code());
return;
}
String responseBody = response.body().string();
postSuccess(listener, responseBody);
} catch (Exception e) {
postFailure(listener, "解析失败:" + e.getMessage());
}
}
// 响应失败(子线程)
@Override
public void onFailure(Call call, IOException e) {
postFailure(listener, "网络失败:" + e.getMessage());
}
// 主线程回调(使用 Handler
private void postSuccess(final OnResponseListener listener, final String msg) {
mainHandler.post(new Runnable() {
@Override
public void run() {
listener.onSuccess(msg);
}
});
}
private void postFailure(final OnResponseListener listener, final String msg) {
mainHandler.post(new Runnable() {
@Override
public void run() {
listener.onFailure(msg);
}
});
}
});
}
public interface OnResponseListener {
/**
* 成功响应(主线程回调)
* @param responseBody 响应体字符串
*/
void onSuccess(String responseBody);
/**
* 失败回调(包含错误信息)
* @param errorMsg 错误描述
*/
void onFailure(String errorMsg);
}
}

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:padding="10dp"
android:id="@+id/ll_hostbar">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10.8.0.250:456"
android:id="@+id/rb_debughost"
android:onClick="onSwitchHost"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yun.winboll.cc"
android:id="@+id/rb_yunhost"
android:onClick="onSwitchHost"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test RSA"
android:onClick="onTestRSA"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Login"
android:onClick="onTestLogin"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:padding="10dp"
android:id="@+id/ll_hostbar">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10.8.0.250:456"
android:id="@+id/rb_debughost"
android:onClick="onSwitchHost"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yun.winboll.cc"
android:id="@+id/rb_yunhost"
android:onClick="onSwitchHost"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestYun"
android:onClick="onTestYun"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>

View File

@@ -21,7 +21,7 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'cc.winboll.studio:libappbase:15.8.0'
api 'cc.winboll.studio:libappbase:15.8.2'
// 二维码类库
api 'com.google.zxing:core:3.4.1'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 13 11:17:53 HKT 2025
stageCount=2
#Tue Jun 03 15:05:42 HKT 2025
stageCount=5
libraryProject=libapputils
baseVersion=15.8
publishVersion=15.8.1
publishVersion=15.8.4
buildCount=0
baseBetaVersion=15.8.2
baseBetaVersion=15.8.5

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jan 10 22:03:57 GMT 2025
#Tue Jun 24 11:17:30 GMT 2025
stageCount=0
libraryProject=libjc
baseVersion=1.0
publishVersion=1.0.0
buildCount=133
buildCount=135
baseBetaVersion=1.0.1

View File

@@ -21,7 +21,7 @@ public class Main {
public final static int JAR_RUNNING_MODE_JCNDK_DEBUG = 4;
public final static int JAR_RUNNING_MODE_JC = 5;
public final static int JAR_RUNNING_MODE_JC_DEBUG = 6;
public enum JAR_RUNNING_MODE {
public static enum JAR_RUNNING_MODE {
UNKNOWN(JAR_RUNNING_MODE_UNKNOWN),
CONSOLE(JAR_RUNNING_MODE_CONSOLE),
CONSOLE_DEBUG(JAR_RUNNING_MODE_CONSOLE_DEBUG),

View File

@@ -29,7 +29,7 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.2"
versionName "15.3"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -45,15 +45,17 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'cc.winboll.studio:libaes:15.8.0'
api 'cc.winboll.studio:libapputils:15.8.1'
api 'cc.winboll.studio:libappbase:15.8.1'
api 'cc.winboll.studio:libaes:15.9.2'
api 'cc.winboll.studio:libapputils:15.8.4'
api 'cc.winboll.studio:libappbase:15.8.4'
api 'io.github.medyo:android-about-page:2.0.0'
api 'com.github.getActivity:ToastUtils:10.5'
api 'com.jcraft:jsch:0.1.55'
api 'org.jsoup:jsoup:1.13.1'
api 'com.squareup.okhttp3:okhttp:4.4.1'
api 'com.belerweb:pinyin4j:2.5.1'
// 权限请求框架https://github.com/getActivity/XXPermissions
api 'com.github.getActivity:XXPermissions:18.63'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 20 20:39:06 HKT 2025
stageCount=6
#Thu Aug 28 21:13:38 HKT 2025
stageCount=4
libraryProject=
baseVersion=15.2
publishVersion=15.2.5
baseVersion=15.3
publishVersion=15.3.3
buildCount=0
baseBetaVersion=15.2.6
baseBetaVersion=15.3.4

View File

@@ -17,7 +17,7 @@ import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import cc.winboll.studio.mymessagemanager.App;
import cc.winboll.studio.mymessagemanager.R;
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
public class AboutActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "AboutActivity";
@@ -79,11 +79,11 @@ public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("APP");
appInfo.setAppGitName("APPBase");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=MyMessageManager");
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=5&extra=page%3D1");
appInfo.setAppAPKName("MyMessageManager");
appInfo.setAppAPKFolderName("MyMessageManager");
return new AboutView(mContext, appInfo);

View File

@@ -103,6 +103,9 @@ abstract public class BaseActivity extends AppCompatActivity {
} else if (R.id.item_defaulttheme == item.getItemId()) {
AESThemeUtil.saveThemeStyleID(this, R.style.MyAppTheme);
recreate();
} else if (R.id.item_defaulttheme == item.getItemId()) {
AESThemeUtil.saveThemeStyleID(this, R.style.MyAppTheme);
recreate();
}
//ToastUtils.show("nThemeStyleID " + Integer.toString(nThemeStyleID));

View File

@@ -35,6 +35,8 @@ public class ComposeSMSActivity extends BaseActivity {
String mszSMSBody;
String mszScheme;
String mszPhoneTo;
TextView mtvTOName;
EditText metTONameSearch;
EditText metTO;
EditText metSMSBody;
SimpleAdapter mSimpleAdapter;
@@ -59,9 +61,9 @@ public class ComposeSMSActivity extends BaseActivity {
// 初始化视图
initView();
// 设置适配器
initAdapter();
initAdapter(null);
// 设置搜索到的匹配位置
setListViewPrePosition();
setListViewPrePositionByPhone();
}
//
@@ -76,8 +78,25 @@ public class ComposeSMSActivity extends BaseActivity {
setActionBar(mToolbar);
// 初始化联系人栏目框
mtvTOName = findViewById(R.id.activitycomposesmsTextView2);
mrlContracts = findViewById(R.id.activitycomposesmsRelativeLayout1);
//mrlContracts.setBackground(drawableFrame);
metTONameSearch = findViewById(R.id.activitycomposesmsEditText2);
metTONameSearch.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
metTO.setText("");
setListViewPrePositionByName();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 初始化联系人列表
mlvContracts = findViewById(R.id.activitycomposesmsListView1);
@@ -88,7 +107,10 @@ public class ComposeSMSActivity extends BaseActivity {
metTO.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
setListViewPrePosition();
mtvTOName.setText("");
//重新加载数据
initAdapter(null);
setListViewPrePositionByPhone();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -96,6 +118,7 @@ public class ComposeSMSActivity extends BaseActivity {
@Override
public void afterTextChanged(Editable s) {
}
});
@@ -136,18 +159,34 @@ public class ComposeSMSActivity extends BaseActivity {
}
//
// 设置搜索到的匹配位置
// 设置号码搜索到的匹配位置
//
void setListViewPrePosition() {
int nPrePosition = getContractsDataPrePosition(metTO.getText().toString());
void setListViewPrePositionByPhone() {
int nPrePosition = getContractsDataPrePositionByPhone(metTO.getText().toString());
PhoneUtil phoneUtils = new PhoneUtil(this);
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
mlvContracts.setSelected(false);
mlvContracts.setSelection(nPrePosition);
}
//
// 设置名称搜索到的匹配位置
//
void setListViewPrePositionByName() {
PhoneUtil phoneUtils = new PhoneUtil(this);
List<PhoneBean> newPhoneData = phoneUtils.getPhonesByName(metTONameSearch.getText().toString());
// 重新绑定数据
initAdapter(newPhoneData);
mlvContracts.setSelected(false);
}
//
// 返回搜索到的匹配位置
//
int getContractsDataPrePosition(String szPhone) {
int getContractsDataPrePositionByPhone(String szPhone) {
for (int i = 0; i < mListPhoneBeanContracts.size(); i++) {
if (mListPhoneBeanContracts.get(i).getTelPhone().compareTo(szPhone) > -1) {
return i;
@@ -157,15 +196,65 @@ public class ComposeSMSActivity extends BaseActivity {
return 0;
}
//
// 返回搜索到的匹配位置
//
int getContractsDataPrePositionByName(String szName) {
for (int i = 0; i < mListPhoneBeanContracts.size(); i++) {
if (mListPhoneBeanContracts.get(i).getName().startsWith(szName)) {
return i;
}
}
return 0;
}
//
// 初始化适配器
//
void initAdapter() {
/*void initAdapter() {
// 初始化联系人数据适配器
mAdapterData = new ArrayList<>();
// 读取联系人数据
final PhoneUtil phoneUtils = new PhoneUtil(this);
mListPhoneBeanContracts = phoneUtils.getPhoneList();
// 映射联系人数据给适配器数据对象
for (int i = 0;i < mListPhoneBeanContracts.size();i++) {
Map<String,Object> map =new HashMap<>();
map.put(MAP_NAME, mListPhoneBeanContracts.get(i).getName());
map.put(MAP_PHONE, mListPhoneBeanContracts.get(i).getTelPhone());
mAdapterData.add(map);
}
// 绑定适配器与数据
mSimpleAdapter = new SimpleAdapter(ComposeSMSActivity.this, mAdapterData, R.layout.listview_contracts
, new String[]{MAP_NAME, MAP_PHONE}
, new int[]{R.id.listviewcontractsTextView1, R.id.listviewcontractsTextView2});
mSimpleAdapter.setDropDownViewResource(R.layout.listview_contracts);
mlvContracts.setAdapter(mSimpleAdapter);
mlvContracts.setOnItemClickListener(new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
metTO.setText(mAdapterData.get(position).get(MAP_PHONE).toString());
mListPhoneBeanContracts = phoneUtils.getPhoneList();
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
}
});
}*/
void initAdapter(List<PhoneBean> initData) {
// 初始化联系人数据适配器
mAdapterData = new ArrayList<>();
// 读取联系人数据
PhoneUtil phoneUtils = new PhoneUtil(this);
mListPhoneBeanContracts = phoneUtils.getPhoneList();
mAdapterData = new ArrayList<>();
final PhoneUtil phoneUtils = new PhoneUtil(this);
if (initData != null) {
mListPhoneBeanContracts = initData;
} else {
// 读取联系人数据
mListPhoneBeanContracts = phoneUtils.getPhoneList();
}
// 映射联系人数据给适配器数据对象
for (int i = 0;i < mListPhoneBeanContracts.size();i++) {
Map<String,Object> map =new HashMap<>();
@@ -184,7 +273,7 @@ public class ComposeSMSActivity extends BaseActivity {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
metTO.setText(mAdapterData.get(position).get(MAP_PHONE).toString());
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
}
});
}

View File

@@ -128,7 +128,7 @@ public class MainActivity extends BaseActivity {
mToolbar = findViewById(R.id.activitymainASupportToolbar1);
mToolbar.setSubtitle(getString(R.string.activity_name_main));
setSupportActionBar(mToolbar);
boolean isEnableService = mAppConfigUtil.mAppConfigBean.isEnableService();
msvEnableService = findViewById(R.id.activitymainSwitchView1);
msvEnableService.setChecked(isEnableService);
@@ -269,17 +269,9 @@ public class MainActivity extends BaseActivity {
public boolean onCreateOptionsMenu(Menu menu) {
//return super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar_main, menu);
/*ThemeUtil.BaseTheme baseTheme = ThemeUtil.getTheme(mAppConfigUtil.mAppConfigBean.getAppThemeID());
if (baseTheme == ThemeUtil.BaseTheme.DEFAULT) {
menu.findItem(R.id.app_defaulttheme).setChecked(true);
} else if (baseTheme == ThemeUtil.BaseTheme.SKY) {
menu.findItem(R.id.app_skytheme).setChecked(true);
} else if (baseTheme == ThemeUtil.BaseTheme.GOLDEN) {
menu.findItem(R.id.app_goldentheme).setChecked(true);
}*/
return super.onCreateOptionsMenu(menu);
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar_main2, menu);
return true;
}
public static void reloadSMS() {
@@ -306,7 +298,7 @@ public class MainActivity extends BaseActivity {
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else if (nItemId == R.id.app_log) {
//App.getWinBoLLActivityManager().startLogActivity(this);
App.getWinBoLLActivityManager().startLogActivity(this);
} else if (nItemId == R.id.app_unittest) {
Intent i = new Intent(MainActivity.this, UnitTestActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -324,7 +316,7 @@ public class MainActivity extends BaseActivity {
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
return super.onOptionsItemSelected(item);
}

View File

@@ -4,11 +4,16 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -16,19 +21,17 @@ import android.widget.Toolbar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.mymessagemanager.R;
import cc.winboll.studio.mymessagemanager.activitys.SMSActivity;
import cc.winboll.studio.mymessagemanager.adapters.SMSArrayAdapter;
import cc.winboll.studio.mymessagemanager.utils.AddressUtils;
import cc.winboll.studio.mymessagemanager.utils.SMSUtil;
import cc.winboll.studio.mymessagemanager.utils.ViewUtil;
import cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView;
import cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView;
import java.lang.ref.WeakReference;
public class SMSActivity extends BaseActivity {
public static String TAG = "SMSActivity";
public static final String ACTION_NOTIFY_SMS_CHANGED = "cc.winboll.studio.mymessagemanager.activitys.SMSActivity.ACTION_NOTIFY_SMS_CHANGED";
public static final String EXTRA_PHONE = "Phone";
final static int MSG_SET_FOCUS = 0;
@@ -36,10 +39,11 @@ public class SMSActivity extends BaseActivity {
Toolbar mToolbar;
String mszPhoneTo;
SMSArrayAdapter mSMSArrayAdapter;
ScrollView mScrollView;
BottomPositionFixedScrollView mScrollView1;
EditText metSMSBody;
SMSActivityBroadcastReceiver mSMSActivityBroadcastReceiver;
Handler mSetFocusHandler;
private boolean isImeVisible = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -47,45 +51,90 @@ public class SMSActivity extends BaseActivity {
setContentView(R.layout.activity_sms);
initView();
mSetFocusHandler = new MyHandler(SMSActivity.this);
scrollScrollView();
setupImeStatusListener();
// 每隔一定时间设置输入框获得焦点
//
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {}
Message message = mSetFocusHandler.obtainMessage(MSG_SET_FOCUS);
mSetFocusHandler.sendMessage(message);
}
}}.start();
// 新增监听窗口加载完成触发mScrollView1滚动到底部
setupScrollToBottomAfterWindowLoaded();
}
//
// 设置输入框获得焦点的类
//
static class MyHandler extends Handler {
WeakReference<SMSActivity> mActivity;
MyHandler(SMSActivity activity) {
mActivity = new WeakReference<SMSActivity>(activity);
}
public void handleMessage(Message msg) {
SMSActivity theActivity = mActivity.get();
switch (msg.what) {
case MSG_SET_FOCUS:
theActivity.metSMSBody.setFocusable(true);
theActivity.metSMSBody.requestFocus();
break;
default:
break;
}
super.handleMessage(msg);
}
}
// 新增窗口加载完成后让mScrollView1滚动到底部
private void setupScrollToBottomAfterWindowLoaded() {
final View rootView = findViewById(android.R.id.content);
// 监听根布局绘制完成(窗口加载完成的标志)
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 滚动到底部
mScrollView1.post(new Runnable() {
@Override
public void run() {
mScrollView1.fullScroll(ScrollView.FOCUS_DOWN);
}
});
// 移除监听,避免重复触发
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
}
private void setupImeStatusListener() {
final View rootView = findViewById(android.R.id.content);
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int rootViewHeight = rootView.getHeight();
int screenHeight = getResources().getDisplayMetrics().heightPixels;
int imeThreshold = dp2px(200);
boolean currentImeVisible = (screenHeight - rootViewHeight) > imeThreshold;
if (currentImeVisible != isImeVisible) {
isImeVisible = currentImeVisible;
setupScrollView1Height();
if (!isImeVisible) {
metSMSBody.clearFocus();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
setupImeStatusListener();
}
});
}
private int dp2px(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
/*static class MyHandler extends Handler {
WeakReference<SMSActivity> mActivity;
MyHandler(SMSActivity activity) {
mActivity = new WeakReference<SMSActivity>(activity);
}
public void handleMessage(Message msg) {
SMSActivity theActivity = mActivity.get();
switch (msg.what) {
case MSG_SET_FOCUS:
theActivity.metSMSBody.setFocusable(true);
theActivity.metSMSBody.requestFocus();
theActivity.setupScrollView1Height();
break;
default:
break;
}
super.handleMessage(msg);
}
}*/
@Override
protected void onDestroy() {
@@ -94,135 +143,130 @@ public class SMSActivity extends BaseActivity {
}
void initView() {
// 发送端空号码退出
mszPhoneTo = getIntent().getStringExtra(EXTRA_PHONE);
if (mszPhoneTo == null || mszPhoneTo.trim().equals("")) {
finish();
}
// 初始化标题栏
mToolbar = findViewById(R.id.activitysmsASupportToolbar1);
mToolbar = (Toolbar) findViewById(R.id.activitysmsASupportToolbar1);
mToolbar.setSubtitle(getString(R.string.activity_name_smsinphone) + " < Phone : " + AddressUtils.getFormattedAddress(mszPhoneTo) + " >");
setActionBar(mToolbar);
// 初始化滚动窗口
mScrollView = findViewById(R.id.activitysmsinphoneScrollView1);
mScrollView1 = (BottomPositionFixedScrollView) findViewById(R.id.activitysmsScrollView1);
// 初始化发送消息框
//Drawable drawableFrame = AppCompatResources.getDrawable(this, R.drawable.bg_frame);
metSMSBody = findViewById(R.id.viewsmssendpart1EditText1);
//metSMSBody.setBackground(drawableFrame);
metSMSBody = (EditText) findViewById(R.id.viewsmssendpart1EditText1);
metSMSBody.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setupScrollView1Height();
}
});
metSMSBody.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
setupScrollView1Height();
}
});
// 初始化发送拉动控件
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.viewsmssendpart1AOHPCTCSeekBar1);
final AOHPCTCSeekBar aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.viewsmssendpart1AOHPCTCSeekBar1);
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.ic_message));
aOHPCTCSeekBar.setThumbOffset(20);
aOHPCTCSeekBar.setOnOHPCListener(
new AOHPCTCSeekBar.OnOHPCListener(){
@Override
public void onOHPCommit() {
//Toast.makeText(getApplication(), "Send", Toast.LENGTH_SHORT).show();
sendSMS();
}
});
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
@Override
public void onOHPCommit() {
sendSMS();
}
});
// 初始化提示框
TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.viewsmssendpart1TextView1);
TextView tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.viewsmssendpart1TextView1);
tvAOHPCTCSeekBarMSG.setText(R.string.msg_100sendmsg);
mlvSMS = (SMSListViewForScrollView) findViewById(R.id.activitysmsinphoneListView1);
// 准备数据
mlvSMS = (SMSListViewForScrollView) findViewById(R.id.activitysmsSMSListViewForScrollView1);
mSMSArrayAdapter = new SMSArrayAdapter(SMSActivity.this, mszPhoneTo);
mlvSMS.setAdapter(mSMSArrayAdapter);
// 设置短信列表滚动到底部就取消已发送的通知消息
//
mlvSMS.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
// 滑动到了底部
mSMSArrayAdapter.cancelMessageNotification();
}
}
});
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
mSMSArrayAdapter.cancelMessageNotification();
}
}
});
mSMSActivityBroadcastReceiver = new SMSActivityBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_NOTIFY_SMS_CHANGED);
IntentFilter intentFilter = new IntentFilter(ACTION_NOTIFY_SMS_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mSMSActivityBroadcastReceiver, intentFilter);
/*SMSView mSMSView = findViewById(R.id.viewsmssendSMSView1);
mSMSView.setSMSType(SMSView.SMSType.SEND);*/
}
//
// 更新信息列表
//
private void setupScrollView1Height() {
mScrollView1.postDelayed(new Runnable() {
@Override
public void run() {
final ScrollView scrollView2 = (ScrollView) findViewById(R.id.activitysmsScrollView2);
final BottomPositionFixedScrollView scrollView1 = (BottomPositionFixedScrollView) findViewById(R.id.activitysmsScrollView1);
final View includeView = findViewById(R.id.activitysmsinclude1);
scrollView2.post(new Runnable() {
@Override
public void run() {
int scrollView2Height = scrollView2.getHeight();
int includeHeight = includeView.getHeight();
int targetHeight = Math.max(scrollView2Height - includeHeight, 0);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) scrollView1.getLayoutParams();
params.height = targetHeight;
scrollView1.setLayoutParams(params);
}
});
}
}, 100);
}
public void updateSMSView() {
mSMSArrayAdapter.reLoadSMSList(SMSActivity.this, mszPhoneTo);
mSMSArrayAdapter.notifyDataSetChanged();
}
//
// 滚动消息文本框
//
void scrollScrollView() {
ViewUtil.scrollScrollView(mScrollView);
ViewUtil.scrollScrollView(mScrollView1);
}
//
// 发送短信
//
void sendSMS() {
// 空消息不发送
String szSMSBody = metSMSBody.getText().toString();
if (szSMSBody.equals("")) {
Toast.makeText(getApplication(), "没有消息内容可发送。", Toast.LENGTH_SHORT).show();
return;
}
// 发送短信
if (SMSUtil.sendMessageByInterface2(this, mszPhoneTo, szSMSBody)) {
metSMSBody.setText("");
new Handler().postDelayed(new Runnable(){
@Override
public void run() {
updateSMSView();
ViewUtil.scrollScrollView(mScrollView);
}
}, 1000);
metSMSBody.clearFocus();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
updateSMSView();
ViewUtil.scrollScrollView(mScrollView1);
}
}, 1000);
}
}
class SMSActivityBroadcastReceiver extends BroadcastReceiver {
public SMSActivityBroadcastReceiver() {
//LogUtils.d(TAG, "SMSActivityBroadcastReceiver()");
}
public SMSActivityBroadcastReceiver() {}
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_NOTIFY_SMS_CHANGED :
//Toast.makeText(context, "ACTION_NOTIFY_SMS_CHANGED", Toast.LENGTH_SHORT).show();
updateSMSView();
ViewUtil.scrollScrollView(mScrollView);
//LogUtils.d(TAG, "ACTION_NOTIFY_SMS_CHANGED");
break;
default:
throw new IllegalStateException("Unexpected value: " + intent.getAction());
if (ACTION_NOTIFY_SMS_CHANGED.equals(intent.getAction())) {
updateSMSView();
ViewUtil.scrollScrollView(mScrollView1);
} else {
throw new IllegalStateException("Unexpected value: " + intent.getAction());
}
}
}
}

View File

@@ -13,9 +13,9 @@ import cc.winboll.studio.libaes.beans.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivity {
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBollActivity";
public static final String TAG = "WinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;

View File

@@ -5,6 +5,7 @@ package cc.winboll.studio.mymessagemanager.utils;
* @Date 2024/07/19 14:30:57
* @Describe 手机联系人工具类
*/
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
@@ -12,6 +13,12 @@ import android.net.Uri;
import android.provider.ContactsContract;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.mymessagemanager.beans.PhoneBean;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -38,28 +45,137 @@ public class PhoneUtil {
}
// 读取所有联系人
//
public List<PhoneBean> getPhoneList() {
List<PhoneBean> listPhoneBean = new ArrayList<>();
ContentResolver cr = mContext.getContentResolver();
Cursor cursor = cr.query(mUriPhoneContent, new String[]{NUMBER, DISPLAY_NAME}, null, null, null);
while (cursor.moveToNext()) {
PhoneBean phoneBean = new PhoneBean(cursor.getString(1), cursor.getString(0).replaceAll("\\s", ""));
listPhoneBean.add(phoneBean);
if (cursor != null) {
while (cursor.moveToNext()) {
// 去除号码中的空格
String phone = cursor.getString(0).replaceAll("\\s", "");
String name = cursor.getString(1);
PhoneBean phoneBean = new PhoneBean(name, phone);
listPhoneBean.add(phoneBean);
}
cursor.close();
}
cursor.close();
// 按电话号码排序
Collections.sort(listPhoneBean, new Comparator<PhoneBean>() {
@Override
public int compare(PhoneBean o1, PhoneBean o2) {
return o1.getTelPhone().compareTo(o2.getTelPhone());
}
});
@Override
public int compare(PhoneBean o1, PhoneBean o2) {
return o1.getTelPhone().compareTo(o2.getTelPhone());
}
});
return listPhoneBean;
}
/**
* 根据联系人名称查询号码(兼容拼音查询)
* @param keyword 搜索关键词(支持汉字、拼音、拼音首字母)
* @return 匹配的联系人列表(包含姓名和号码)
*/
public List<PhoneBean> getPhonesByName(String keyword) {
List<PhoneBean> result = new ArrayList<>();
if (keyword == null || keyword.trim().isEmpty()) {
return result; // 关键词为空,返回空列表
}
// 获取所有联系人
List<PhoneBean> allContacts = getPhoneList();
// 统一转为小写,忽略大小写
String keywordLower = keyword.trim().toLowerCase();
for (PhoneBean contact : allContacts) {
String name = contact.getName();
if (name == null || name.isEmpty()) {
continue;
}
// 1. 直接匹配姓名(包含关键词)
if (name.toLowerCase().contains(keywordLower)) {
result.add(contact);
continue;
}
// 2. 匹配姓名的全拼(包含关键词)
String namePinyin = getPinyin(name).toLowerCase();
if (namePinyin.contains(keywordLower)) {
result.add(contact);
continue;
}
// 3. 匹配姓名的拼音首字母(包含关键词)
String namePinyinFirstLetter = getPinyinFirstLetter(name).toLowerCase();
if (namePinyinFirstLetter.contains(keywordLower)) {
result.add(contact);
continue;
}
}
return result;
}
/**
* 将汉字转为全拼(不带声调,小写)
* 例如:"张三" → "zhangsan"
*/
private String getPinyin(String chinese) {
StringBuilder pinyin = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE); // 小写
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE); // 不带声调
char[] chars = chinese.toCharArray();
for (char c : chars) {
// 如果是汉字,转换为拼音;否则直接拼接(如字母、数字、符号)
if (Character.toString(c).matches("[\\u4e00-\\u9fa5]")) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认取第一个)
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
LogUtils.e(TAG, "拼音转换失败:" + e.getMessage());
}
} else {
pinyin.append(c);
}
}
return pinyin.toString();
}
/**
* 将汉字转为拼音首字母(小写)
* 例如:"张三" → "zs"
*/
private String getPinyinFirstLetter(String chinese) {
StringBuilder firstLetters = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
char[] chars = chinese.toCharArray();
for (char c : chars) {
if (Character.toString(c).matches("[\\u4e00-\\u9fa5]")) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
// 取拼音首字母(如"zhang" → "z"
firstLetters.append(pinyinArray[0].charAt(0));
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
LogUtils.e(TAG, "拼音首字母转换失败:" + e.getMessage());
}
} else {
// 非汉字直接拼接首字符(如"李3" → "l3"
firstLetters.append(c);
}
}
return firstLetters.toString();
}
public boolean isPhoneInContacts(String szPhone) {
List<PhoneBean> listPhoneDto = getPhoneList();
LogUtils.d(TAG, String.format("isPhoneInContacts(...) listPhoneDto.size() %d", listPhoneDto.size()));
@@ -70,49 +186,56 @@ public class PhoneUtil {
}
return false;
}
public String getNameByPhone(String szPhone) {
if (szPhone == null || szPhone.equals("")) {
return "";
}
List<PhoneBean> listPhoneDto = getPhoneList();
LogUtils.d(TAG, String.format("getNameByPhone(...) listPhoneDto.size() %d", listPhoneDto.size()));
for (int i = 0; i < listPhoneDto.size(); i++) {
if (isTheSamePhoneNumber(listPhoneDto.get(i).getTelPhone(), szPhone)) {
return listPhoneDto.get(i).getName();
}
}
return "";
}
boolean isTheSamePhoneNumber(String szNum1, String szNum2) {
//LogUtils.d(TAG, String.format("szNum1 %s\nszNum2 %s", szNum1, szNum2));
if(szNum1.equals(szNum2)) {
if (szNum1.equals(szNum2)) {
LogUtils.d(TAG, "szNum1.equals(szNum2)");
return true;
}
if(UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum1)) {
if(szNum1.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum2))) {
if (UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum1)) {
if (szNum1.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum2))) {
LogUtils.d(TAG, "szNum1.equals(UnitAreaUtils.genCurrentUnitAreaNumber(szNum2))");
return true;
}
}
if(UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum2)) {
if(szNum2.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum1))) {
if (UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum2)) {
if (szNum2.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum1))) {
LogUtils.d(TAG, "szNum2.equals(UnitAreaUtils.genCurrentUnitAreaNumber(szNum1))");
return true;
}
}
LogUtils.d(TAG, "isTheSamePhoneNumber(...) return false;");
return false;
}
//
// 检验电话号码是否是数字
//
public static boolean isPhoneByDigit(String szPhone) {
if(!RegexPPiUtils.isPPiOK(szPhone)) {
if (!RegexPPiUtils.isPPiOK(szPhone)) {
return false;
}
//String text = "这里是一些任意的文本内容";
String regex = "[+]?\\d+";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(szPhone);
LogUtils.d(TAG, String.format("matcher.matches() : %s", matcher.matches()));
/*if (matcher.matches()) {
System.out.println("文本满足该正则表达式模式");
} else {
System.out.println("文本不满足该正则表达式模式");
}*/
return matcher.matches();
}
}

View File

@@ -10,9 +10,9 @@ import cc.winboll.studio.mymessagemanager.beans.AppConfigBean;
import android.content.Context;
public class UnitAreaUtils {
public static final String TAG = "UnitAreaUtils";
static UnitAreaUtils _UnitAreaUtils;
Context mContext;
@@ -26,19 +26,25 @@ public class UnitAreaUtils {
}
return _UnitAreaUtils;
}
public boolean isCurrentUnitAreaNumber(String szPhoneNumer) {
String szUnitArea = getUnitArea();
LogUtils.d(TAG, String.format("szPhoneNumer.substring(1,3) %s", szPhoneNumer.substring(1,3)));
return szPhoneNumer.substring(1,3).equals(szUnitArea);
try {
String szPhoneNumerUnitArea = szPhoneNumer.substring(1, 3);
LogUtils.d(TAG, String.format("szPhoneNumerUnitArea %s", szPhoneNumerUnitArea));
return szPhoneNumerUnitArea.equals(szUnitArea);
} catch (StringIndexOutOfBoundsException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
return false;
}
public String genCurrentUnitAreaNumber(String szPhoneNumer) {
String szUnitArea = getUnitArea();
LogUtils.d(TAG, String.format("szUnitArea %s", szUnitArea));
return "+" + szUnitArea + szPhoneNumer;
}
String getUnitArea() {
String szUnitArea = AppConfigUtil.getInstance(mContext).mAppConfigBean.getCountryCode();
LogUtils.d(TAG, String.format("szUnitArea %s", szUnitArea));

View File

@@ -0,0 +1,125 @@
package cc.winboll.studio.mymessagemanager.views;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/23 00:39
* @Describe 多级拉动响应自定义控件
*/
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.widget.ScrollView;
public class BottomPositionFixedScrollView extends ScrollView {
public static final String TAG = "BottomPositionFixedScrollView";
// 记录底部对应的内容绝对位置即底部位置在内容中的y坐标该位置需始终保持在视图底部
private int mBottomContentY = 0;
// 标记是否是首次布局(避免初始加载误触发)
private boolean isFirstLayout = true;
public BottomPositionFixedScrollView(Context context) {
super(context);
init();
}
public BottomPositionFixedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BottomPositionFixedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
// 监听布局变化(高度改变时触发)
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (isFirstLayout) {
isFirstLayout = false;
return;
}
// 布局变化后,恢复底部位置
restoreBottomPosition();
}
});
}
/**
* 重写滚动事件,记录“底部对应的内容绝对位置”
* 即当前视图底部边缘对应的内容y坐标该坐标需始终保持在视图底部
*/
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (getChildCount() == 0) {
mBottomContentY = 0;
return;
}
// 内容总高度
int contentHeight = getChildAt(0).getMeasuredHeight();
// 视图可视高度(自身高度)
int scrollViewHeight = getMeasuredHeight();
// 当前视图底部边缘对应的内容y坐标 = 顶部滚动距离(t) + 可视高度
// (该坐标就是“底部内容的绝对位置”,需始终保持在视图底部)
mBottomContentY = t + scrollViewHeight;
// 避免超过内容总高度(比如内容不足一屏时,底部最多到内容底部)
if (mBottomContentY > contentHeight) {
mBottomContentY = contentHeight;
}
}
/**
* 恢复底部位置:让原记录的“底部内容绝对位置”仍保持在视图底部
*/
private void restoreBottomPosition() {
if (getChildCount() == 0) {
return;
}
// 新的内容总高度
int newContentHeight = getChildAt(0).getMeasuredHeight();
// 新的视图可视高度
int newScrollViewHeight = getMeasuredHeight();
// 目标让原mBottomContentY底部内容绝对位置仍位于视图底部
// 此时需要的顶部滚动距离 = mBottomContentY - 新的可视高度
int targetScrollY = mBottomContentY - newScrollViewHeight;
// 边界修正:
// 1. 不能小于0避免滚动到负数位置
// 2. 不能大于“最大可滚动距离”(内容高度 - 可视高度,避免超出内容范围)
int maxScrollY = Math.max(newContentHeight - newScrollViewHeight, 0);
targetScrollY = Math.max(targetScrollY, 0);
targetScrollY = Math.min(targetScrollY, maxScrollY);
// 滚动到目标位置,保持底部内容位置不变
smoothScrollTo(0, targetScrollY);
}
/**
* 外部手动设置底部内容绝对位置(可选)
*/
public void setBottomContentY(int bottomContentY) {
if (getChildCount() == 0) {
mBottomContentY = bottomContentY;
return;
}
// 限制不超过内容总高度
int contentHeight = getChildAt(0).getMeasuredHeight();
mBottomContentY = Math.min(bottomContentY, contentHeight);
restoreBottomPosition();
}
/**
* 获取当前底部内容绝对位置(可选)
*/
public int getBottomContentY() {
return mBottomContentY;
}
}

View File

@@ -21,25 +21,63 @@
android:layout_height="wrap_content"
android:id="@+id/activitycomposesmsRelativeLayout1">
<TextView
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SMS TO : "
android:id="@+id/activitycomposesmsTextView1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="10dp"
android:layout_centerVertical="true"/>
<EditText
android:layout_toRightOf="@id/activitycomposesmsTextView1"
android:layout_width="wrap_content"
android:inputType="phone"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/activitycomposesmsEditText1"
android:id="@+id/activitycomposesmsLinearLayout1"
android:gravity="center_vertical"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:layout_centerVertical="true"/>
android:layout_marginLeft="10dp"
android:layout_alignParentLeft="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(拼音搜索):"/>
<EditText
android:layout_width="80dp"
android:ems="10"
android:layout_height="wrap_content"
android:id="@+id/activitycomposesmsEditText2"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/activitycomposesmsEditText2"
android:id="@+id/activitycomposesmsTextView2"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_below="@id/activitycomposesmsLinearLayout1"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_alignParentLeft="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(SMS TO) :"
android:id="@+id/activitycomposesmsTextView1"/>
<EditText
android:layout_width="wrap_content"
android:inputType="phone"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/activitycomposesmsEditText1"/>
</LinearLayout>
</RelativeLayout>

View File

@@ -10,36 +10,40 @@
android:layout_height="@dimen/toolbar_height"
android:id="@+id/activitysmsASupportToolbar1"/>
<RelativeLayout
android:orientation="vertical"
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="10dp">
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/activitysmsScrollView2">
<ScrollView
android:layout_alignParentTop="true"
android:layout_above="@+id/activitysmsinclude1"
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/activitysmsinphoneScrollView1"
android:layout_weight="1.0"
android:isScrollContainer="false">
android:layout_height="match_parent"
android:id="@+id/activitysmsLinearLayout1">
<cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView
<cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView
android:layout_width="match_parent"
android:layout_height="520dp"
android:isScrollContainer="false"
android:id="@+id/activitysmsScrollView1">
<cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/activitysmsSMSListViewForScrollView1"/>
</cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView>
<include
layout="@layout/view_smssend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/activitysmsinphoneListView1"/>
android:id="@+id/activitysmsinclude1"/>
</LinearLayout>
</ScrollView>
<include
android:layout_alignParentBottom="true"
layout="@layout/view_smssend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/activitysmsinclude1"/>
</RelativeLayout>
</ScrollView>
</LinearLayout>

View File

@@ -34,7 +34,7 @@
</ScrollView>
<cc.winboll.studio.shared.log.LogView
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"

View File

@@ -7,6 +7,18 @@
android:layout_height="wrap_content"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:id="@+id/viewsmssendpart1TextView1"/>
<cc.winboll.studio.libaes.views.AOHPCTCSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/viewsmssendpart1AOHPCTCSeekBar1"/>
<EditText
android:scrollbars="vertical"
android:maxHeight="150dp"
@@ -17,17 +29,6 @@
android:id="@+id/viewsmssendpart1EditText1"
android:background="@drawable/bg_frame"/>
<cc.winboll.studio.libaes.views.AOHPCTCSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/viewsmssendpart1AOHPCTCSeekBar1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:id="@+id/viewsmssendpart1TextView1"/>
</LinearLayout>

View File

@@ -17,26 +17,4 @@
<item
android:id="@+id/app_smsrule"
android:title="@string/text_smsrule"/>
<item android:title="@string/app_developoptions">
<menu>
<item
android:id="@+id/app_log"
android:title="@string/app_log"/>
<item
android:id="@+id/app_unittest"
android:title="@string/app_unittest"/>
<item
android:id="@+id/app_crashtest"
android:title="@string/app_crashtest"/>
</menu>
</item>
<item
android:id="@+id/app_appsettings"
android:title="@string/text_appsettings"/>
<item
android:id="@+id/app_about"
android:title="@string/app_about"/>
<item
android:id="@+id/app_smsrecycle"
android:title="@string/app_smsrecycle"/>
</menu>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/app_developoptions">
<menu>
<item
android:id="@+id/app_log"
android:title="@string/app_log"/>
<item
android:id="@+id/app_unittest"
android:title="@string/app_unittest"/>
<item
android:id="@+id/app_crashtest"
android:title="@string/app_crashtest"/>
</menu>
</item>
<item
android:id="@+id/app_appsettings"
android:title="@string/text_appsettings"/>
<item
android:id="@+id/app_about"
android:title="@string/app_about"/>
<item
android:id="@+id/app_smsrecycle"
android:title="@string/app_smsrecycle"/>
</menu>

View File

@@ -41,6 +41,6 @@
<string name="text_appsettings">应用设置</string>
<string name="text_ttsplaydelaytimes">TTS播放延迟时间</string>
<string name="msg_newsms">接收到新的消息。</string>
<string name="msg_100sendmsg">&gt;&gt;&gt;图标动到 100% 以发送信息&gt;&gt;&gt;</string>
<string name="msg_100applysettings">&gt;&gt;&gt;图标动到 100% 应用设置&gt;&gt;&gt;</string>
<string name="msg_100sendmsg">&gt;&gt;&gt;拉动到100%可发信息&gt;&gt;&gt;</string>
<string name="msg_100applysettings">&gt;&gt;&gt;拉动到100%应用设置&gt;&gt;&gt;</string>
</resources>

1
numtable/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

34
numtable/README.md Normal file
View File

@@ -0,0 +1,34 @@
# NumTable
#### 介绍
桌面图标多元应用。提供一个数字风格化的桌面标识图标,快捷的桌面标识创建途径。主要应用于桌面繁多时的页面环境辅助识别。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh numtable
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -0,0 +1 @@

73
numtable/build.gradle Normal file
View File

@@ -0,0 +1,73 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
compileSdkVersion 32
buildToolsVersion "32.0.0"
defaultConfig {
applicationId "cc.winboll.studio.numtable"
minSdkVersion 24
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.1"
if(true) {
versionName = genVersionName("${versionName}")
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 吐司类库
api 'com.github.getActivity:ToastUtils:10.5'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// AndroidX 类库
api 'androidx.appcompat:appcompat:1.1.0'
api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
api 'cc.winboll.studio:libaes:15.8.0'
api 'cc.winboll.studio:libapputils:15.8.2'
api 'cc.winboll.studio:libappbase:15.8.2'
}

View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 08 21:21:11 HKT 2025
stageCount=1
libraryProject=
baseVersion=15.1
publishVersion=15.1.0
buildCount=0
baseBetaVersion=15.1.1

21
numtable/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application
tools:replace="android:icon"
android:icon="@drawable/ic_launcher_beta">
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NumTable +</string>
</resources>

View File

@@ -0,0 +1,37 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.numtable">
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:roundIcon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,345 @@
package cc.winboll.studio.numtable;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalApplication;
import com.hjq.toast.ToastUtils;
import com.hjq.toast.style.WhiteToastStyle;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 框架
ToastUtils.init(this);
// 设置 Toast 布局样式
//ToastUtils.setView(R.layout.view_toast);
ToastUtils.setStyle(new WhiteToastStyle());
ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}

View File

@@ -0,0 +1,31 @@
package cc.winboll.studio.numtable;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogView;
import com.hjq.toast.ToastUtils;
public class MainActivity extends AppCompatActivity {
LogView mLogView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mLogView = findViewById(R.id.logview);
ToastUtils.show("onCreate");
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
}

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF3D8A1C"
android:strokeColor="#FFF5DD00"
android:strokeWidth="20.0"
android:strokeLineCap="round"
android:strokeMiterLimit="10"
android:pathData="M47.53 417.75C38.23 258.17 97.09 94.95 156.27 94.16 211.66 98.38 260.9 237.99 329.08 242.47 351.54 241.69 397.94 228.17 449.46 88.59 447.52 262.11 394.97 417.45 338.71 418.74 279.06 412.93 220.17 297.22 157.81 295.26 132.98 294.7 116.37 308.69 47.53 417.75Z"/>
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#FFFBC41E"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:clickable="true">
<item android:drawable="@drawable/ic_launcher_background"/>
<item
android:left="15dp"
android:top="15dp"
android:right="15dp"
android:bottom="15dp"
android:drawable="@drawable/ic_launcher"/>
</layer-list>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:gravity="center_vertical|center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NumTable"
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#FF9800</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">NumTable</string>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>

1
ollama/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1 @@

73
ollama/build.gradle Normal file
View File

@@ -0,0 +1,73 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
compileSdkVersion 32
buildToolsVersion "32.0.0"
defaultConfig {
applicationId "cc.winboll.studio.ollama"
minSdkVersion 26
targetSdkVersion 29
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.0"
if(true) {
versionName = genVersionName("${versionName}")
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'com.squareup.okhttp3:okhttp:4.4.1'
// 吐司类库
api 'com.github.getActivity:ToastUtils:10.5'
// Android 类库
api 'com.android.support:appcompat-v7:28.0.0' // 包含 AppCompatActivity
// https://mvnrepository.com/artifact/com.android.support/support-compat
api 'com.android.support:support-compat:28.0.0' // 保留原有依赖(可选)
// https://mvnrepository.com/artifact/com.android.support/support-v4
api 'com.android.support:support-v4:28.0.0'
// https://mvnrepository.com/artifact/com.android.support/support-media-compat
api 'com.android.support:support-media-compat:28.0.0'
// https://mvnrepository.com/artifact/com.android.support/support-core-utils
api 'com.android.support:support-core-utils:28.0.0'
// https://mvnrepository.com/artifact/com.android.support/support-core-ui
api 'com.android.support:support-core-ui:28.0.0'
// https://mvnrepository.com/artifact/com.android.support/support-fragment
api 'com.android.support:support-fragment:28.0.0'
// https://mvnrepository.com/artifact/com.android.support/recyclerview-v7
api 'com.android.support:recyclerview-v7:28.0.0'
api 'cc.winboll.studio:libappbase:15.0.9'
api 'cc.winboll.studio:libapputils:15.0.11'
}

8
ollama/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Mar 28 05:09:21 HKT 2025
stageCount=1
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=0
baseBetaVersion=15.0.1

21
ollama/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Ollama +</string>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.ollama">
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,334 @@
package cc.winboll.studio.ollama;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalApplication;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
super.onCreate();
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}

View File

@@ -0,0 +1,88 @@
package cc.winboll.studio.ollama;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
public final static int MSG_APPEND = 0;
private Handler _Handler = new Handler(Looper.getMainLooper());
private TextView mtvMessage;
private EditText metAsk;
private Button mbtSend;
private ScrollView msvMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mtvMessage = (TextView) findViewById(R.id.message_tv);
metAsk = (EditText) findViewById(R.id.ask_et);
mbtSend = (Button) findViewById(R.id.send_bt);
msvMessage = findViewById(R.id.message_sv);
mbtSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendQuestion();
}
});
}
//
// 设置输入框获得焦点的类
//
// static class MyHandler extends Handler {
// WeakReference<MainActivity> mActivity;
// MyHandler(MainActivity activity) {
// mActivity = new WeakReference<MainActivity>(activity);
// }
// public void handleMessage(Message msg) {
// MainActivity theActivity = mActivity.get();
// switch (msg.what) {
// case MSG_APPEND:
// theActivity.mtvMessage.append((String)msg.obj);
// break;
// default:
// break;
// }
// super.handleMessage(msg);
// }
// }
private void sendQuestion() {
final String question = metAsk.getText().toString().trim();
if (!question.equals("")) {
mtvMessage.append("\n\nI " + metAsk.getText().toString() + "\nOllama : ");
metAsk.setText("");
new OllamaClient.SyncAskThread(question, new OllamaClient.OnAnswerCallback() {
@Override
public void onAnswer(final String answer) {
_Handler.post(new Runnable() {
@Override
public void run() {
mtvMessage.append(answer);
msvMessage.post(new Runnable(){
@Override
public void run() {
msvMessage.fullScroll(View.FOCUS_DOWN);
}
});
}
});
}
}).start();
}
}
}

View File

@@ -0,0 +1,223 @@
package cc.winboll.studio.ollama;
/**
* @Author ZhanGSKen@AliYun.Com
* @Date 2025/03/27 19:55:28
* @Describe 简单Http协议访问客户端
*/
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.Source;
import org.json.JSONException;
import org.json.JSONObject;
public class OllamaClient {
public static final String TAG = "OllamaClient";
private static final String API_BASE_URL = "https://ollama-api.winboll.cc";
//private static final String API_BASE_URL = "http://10.8.0.10:11434";
//private static final OkHttpClient client = new OkHttpClient();
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build();
// 1. 生成文本示例
// static void generateText(String prompt, String model) {
// String url = API_BASE_URL + "/api/generate";
// try {
// JSONObject payload = new JSONObject()
// .put("model", model)
// .put("prompt", prompt)
// .put("temperature", 0.7)
// .put("max_tokens", 200);
//
// Request request = new Request.Builder()
// .url(url)
// .post(RequestBody.create(payload.toString(), MediaType.get("application/json")))
// .build();
//
// Response response = client.newCall(request).execute();
// if (response.isSuccessful()) {
// String result = response.body().string();
// String formattedStream = OllamaResponseFormatter.formatStreamingResponse(result);
//
// // 输出示例:
//// [2025-03-27T19:34:29.274955439Z] [llama3.1:8b] It looks like you might have miss
//// [2025-03-27T19:34:30.482553089Z] [llama3.1:8b] pelled the word "Ollama" or perhaps said something that is not a standard word in the English language. However, I'm here to provide information and assistance on various topics, so please let me know what you meant by "Ollama." Was it related to a name, place, movie, game, or something else?
//
// LogUtils.d(TAG, formattedStream);
//// JSONObject json = new JSONObject(result);
//// LogUtils.d(TAG, "生成结果: " + json.getString("response"));
// //System.out.println("生成结果: " + json.getString("response"));
// } else {
// LogUtils.d(TAG, "请求失败: " + response.code());
// //System.out.println("请求失败: " + response.code() + " " + response.message());
// }
// } catch (JSONException|IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// }
// }
// 实时输出流式响应的函数
static void generateTextStream(String prompt, String model, final OnAnswerCallback callback) {
String url = API_BASE_URL + "/api/generate";
try {
JSONObject payload = new JSONObject()
.put("model", model)
.put("prompt", prompt)
.put("temperature", 0.7)
.put("max_tokens", 200)
.put("stream", true); // 启用流式响应
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(payload.toString(), MediaType.get("application/json")))
.build();
LogUtils.d(TAG, "Request request");
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
LogUtils.d(TAG, "请求失败: " + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
LogUtils.d(TAG, "请求失败: " + response.code());
return;
}
try (ResponseBody body = response.body()) {
if (body == null) return;
// 实时流式解析
LogUtils.d(TAG, "实时流式解析");
Source source = body.source();
Buffer buffer = new Buffer();
StringBuilder fullResponse = new StringBuilder();
boolean isDone = false;
while (!isDone && source.read(buffer, 1024) != -1) {
//LogUtils.d(TAG, "!isDone");
String chunk = buffer.readUtf8();
String[] lines = chunk.split("\n");
for (String line : lines) {
LogUtils.d(TAG, line);
if (line.trim().startsWith("{\"model\":")) {
LogUtils.d(TAG, line);
String jsonStr = line;
if (jsonStr.equals("[DONE]")) {
isDone = true;
LogUtils.d(TAG, "流式生成完成");
break;
}
try {
//LogUtils.d(TAG, jsonStr);
JSONObject json = new JSONObject(jsonStr);
//LogUtils.d(TAG, json.toString());
String responseText = json.getString("response");
//LogUtils.d(TAG, responseText);
fullResponse.append(responseText);
// 实时输出
callback.onAnswer(responseText);
LogUtils.d(TAG, "实时响应: " + responseText);
// 处理完成状态
if (json.getBoolean("done")) {
isDone = true;
String doneReason = json.optString("done_reason", "unknown");
LogUtils.d(TAG, "生成完成 (原因: " + doneReason + ")");
LogUtils.d(TAG, "完整回答: " + fullResponse.toString());
}
} catch (JSONException e) {
LogUtils.d(TAG, "JSON解析错误: " + e.getMessage());
}
}
}
}
}
}
});
} catch (JSONException e) {
LogUtils.d(TAG, "JSON格式错误: " + e.getMessage());
}
}
// 2. 获取模型列表示例
static void getModelList() {
String url = API_BASE_URL + "/v1/models";
LogUtils.d(TAG, "url : " + url);
Request request = new Request.Builder().url(url).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
LogUtils.d(TAG, response.body().string());
// JSONArray models = new JSONArray(response.body().string());
// //System.out.println("可用模型列表:");
// LogUtils.d(TAG, "可用模型列表:");
// for (int i = 0; i < models.length(); i++) {
// JSONObject model = models.getJSONObject(i);
// LogUtils.d(TAG, "- " + model.getString("name") + " (" + model.getString("size") + ")");
// //System.out.println("- " + model.getString("name") + " (" + model.getString("size") + ")");
// }
} else {
LogUtils.d(TAG, "获取模型列表失败: " + response.code());
//System.out.println("获取模型列表失败: " + response.code());
}
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
static void unittest(String ask, OnAnswerCallback callback) {
// 获取模型列表
getModelList();
// 生成文本
generateTextStream(ask, "llama3.1:8b", callback);
}
public static class SyncAskThread extends Thread {
private String ask;
private OnAnswerCallback callback;
public SyncAskThread(String ask, OnAnswerCallback callback) {
this.ask = ask;
this.callback = callback;
}
@Override
public void run() {
super.run();
LogUtils.d(TAG, "run() start.");
unittest(ask, callback);
LogUtils.d(TAG, "run() end.");
}
}
public interface OnAnswerCallback {
void onAnswer(String answer);
}
}

View File

@@ -0,0 +1,72 @@
package cc.winboll.studio.ollama;
/**
* @Author ZhanGSKen@AliYun.Com
* @Date 2025/03/28 03:38:50
*/
import org.json.JSONArray;
import org.json.JSONObject;
public class OllamaResponseFormatter {
public static final String TAG = "OllamaResponseFormatter";
// 处理模型列表响应
public static String formatModelList(String jsonResponse) {
try {
JSONObject json = new JSONObject(jsonResponse);
JSONArray models = json.getJSONArray("data");
StringBuilder sb = new StringBuilder();
sb.append("可用模型列表:\n");
for (int i = 0; i < models.length(); i++) {
JSONObject model = models.getJSONObject(i);
String modelId = model.getString("id");
sb.append(String.format("-%d. %s\n", i + 1, modelId));
}
return sb.toString();
} catch (Exception e) {
return "格式解析错误: " + e.getMessage();
}
}
// 处理流式生成文本响应
public static String formatStreamingResponse(String jsonResponse) {
try {
JSONObject json = new JSONObject(jsonResponse);
StringBuilder sb = new StringBuilder();
String responseText = json.getString("response");
boolean isDone = json.getBoolean("done");
// 添加时间戳和模型标识
String timestamp = json.getString("created_at");
String modelName = json.getString("model");
sb.append(String.format("[%s] [%s] ", timestamp, modelName));
// 处理响应内容
if (responseText.isEmpty() && isDone) {
sb.append("生成完成\n");
} else {
sb.append(responseText);
if (isDone) {
String doneReason = json.optString("done_reason", "unknown");
sb.append(String.format(" (完成原因: %s)\n", doneReason));
}
}
return sb.toString();
} catch (Exception e) {
return "格式解析错误: " + e.getMessage();
}
}
// 使用示例
public static void main(String[] args) {
// 模型列表测试
String modelListJson = "{\"object\":\"list\",\"data\":[{...}]}";
System.out.println(formatModelList(modelListJson));
// 流式响应测试
String streamingJson = "{\"model\":\"llama3.1:8b\",\"created_at\":\"2025-03-27T19:34:29.274955439Z\",\"response\":\"It\",\"done\":false}";
System.out.println(formatStreamingResponse(streamingJson));
}
}

Some files were not shown because too many files have changed in this diff Show More