Compare commits

...

283 Commits

Author SHA1 Message Date
ZhanGSKen
9768103741 添加WebPageSources项目 2025-09-10 02:09:47 +08:00
ZhanGSKen
787b8f0d77 Merge remote-tracking branch 'origin/timestamp' into appbase 2025-09-10 02:02:52 +08:00
ZhanGSKen
1d58126fd8 Merge remote-tracking branch 'origin/powerbell' into appbase 2025-09-10 02:02:42 +08:00
ZhanGSKen
7b5a3d2d71 Merge remote-tracking branch 'origin/mymessagemanager' into appbase 2025-09-10 02:02:24 +08:00
ZhanGSKen
a988a9d4f6 Merge remote-tracking branch 'origin/midiplayer' into appbase 2025-09-10 02:01:57 +08:00
ZhanGSKen
04a67e666b Merge remote-tracking branch 'origin/apputils' into appbase 2025-09-10 02:01:15 +08:00
ZhanGSKen
348edc8aaf Merge remote-tracking branch 'gitee/timestamp' into appbase 2025-09-10 01:58:15 +08:00
ZhanGSKen
5970ae33c8 Merge remote-tracking branch 'gitee/powerbell' into appbase 2025-09-10 01:58:08 +08:00
ZhanGSKen
91a44f48ef Merge remote-tracking branch 'gitee/numtable' into appbase 2025-09-10 01:58:01 +08:00
ZhanGSKen
c66e9a090b Merge remote-tracking branch 'gitee/mymessagemanager' into appbase 2025-09-10 01:57:54 +08:00
ZhanGSKen
7a14b55247 Merge remote-tracking branch 'gitee/contacts' into appbase 2025-09-10 01:57:47 +08:00
ZhanGSKen
fab68f16c8 Merge remote-tracking branch 'gitee/autoinstaller' into appbase 2025-09-10 01:57:40 +08:00
ZhanGSKen
3a97c6135f Merge remote-tracking branch 'gitee/apputils' into appbase 2025-09-10 01:57:35 +08:00
ZhanGSKen
cdad017d8c Merge remote-tracking branch 'gitee/androidxdemo' into appbase 2025-09-10 01:57:24 +08:00
ZhanGSKen
bf84382963 Merge remote-tracking branch 'gitee/androiddemo' into appbase 2025-09-10 01:57:17 +08:00
ZhanGSKen
34601fc5b1 Merge remote-tracking branch 'gitee/aes' into appbase 2025-09-10 01:57:11 +08:00
ZhanGSKen
a40dbcfb61 <mymessagemanager>APK 15.3.8 release Publish. 2025-09-06 01:57:20 +08:00
ZhanGSKen
4d344b299b 修改联系人查询发送的窗口,设置输入框号码完全匹配某个联系人时,才显示号码对应的联系人名称。 2025-09-06 01:52:37 +08:00
ZhanGSKen
37b0867d34 20250906_012327_326 2025-09-06 01:23:42 +08:00
ZhanGSKen
cdfbb082d2 <powerbell>APK 15.4.12 release Publish. 2025-09-03 20:59:53 +08:00
ZhanGSKen
7e476894a7 电量记录表里添加换行显示功能。 2025-09-03 20:54:48 +08:00
ZhanGSKen
0e8ae2e020 修复MidiPlayer项目文件夹命名错误问题。 2025-09-02 21:06:49 +08:00
ZhanGSKen
48623a2805 更新说明书 2025-09-01 23:23:18 +08:00
ZhanGSKen
b505156211 <mymessagemanager>APK 15.3.7 release Publish. 2025-09-01 08:07:48 +08:00
ZhanGSKen
91b30fb576 <mymessagemanager>Start New Stage Version. 2025-09-01 08:07:13 +08:00
ZhanGSKen
ab3ac72d54 RegexPPiUtils 入选 APPUtils 类库。 2025-09-01 08:04:36 +08:00
ZhanGSKen
73285c8779 <libapputils>Library Release 15.8.6 2025-09-01 07:56:35 +08:00
ZhanGSKen
fa338ec8c7 <apputils>APK 15.8.6 release Publish. 2025-09-01 07:56:11 +08:00
ZhanGSKen
7a3a1f4bcd 添加正则表达式前置预防针工具类 2025-09-01 07:51:20 +08:00
ZhanGSKen
ea65810e7d <libaes>Library Release 15.9.3 2025-08-31 23:40:22 +08:00
ZhanGSKen
ad991e3da2 <libapputils>Library Release 15.8.5 2025-08-31 23:39:23 +08:00
ZhanGSKen
7e263447c8 <libappbase>Library Release 15.9.5 2025-08-31 23:37:44 +08:00
ZhanGSKen
80201e8370 <powerbell>APK 15.4.11 release Publish. 2025-08-31 06:21:48 +08:00
ZhanGSKen
ea0473606a 更新类库 2025-08-31 06:17:41 +08:00
ZhanGSKen
870e9a94fb <mymessagemanager>APK 15.3.6 release Publish. 2025-08-31 06:13:45 +08:00
ZhanGSKen
2421ecb943 更新类库 2025-08-31 06:12:49 +08:00
ZhanGSKen
687fff7216 <mymessagemanager>Start New Stage Version. 2025-08-31 06:10:17 +08:00
ZhanGSKen
50d4cd830b <contacts>APK 15.3.16 release Publish. 2025-08-31 06:05:42 +08:00
ZhanGSKen
2079822c00 更新类库 2025-08-31 06:04:54 +08:00
ZhanGSKen
297c76f328 <contacts>Start New Stage Version. 2025-08-31 06:02:02 +08:00
ZhanGSKen
43b18ee662 <contacts>Start New Stage Version. 2025-08-31 06:01:23 +08:00
ZhanGSKen
3ec3a4cfc2 <androidxdemo>APK 15.1.1 release Publish. 2025-08-31 05:42:50 +08:00
ZhanGSKen
2c10a9f38c 设置初始版本号 2025-08-31 05:42:19 +08:00
ZhanGSKen
e62888636e <androidxdemo>APK 15.0.0 release Publish. 2025-08-31 05:39:39 +08:00
ZhanGSKen
364980dd02 更新类库 2025-08-31 05:38:28 +08:00
ZhanGSKen
0e155e4f3a <androidxdemo>Start New Stage Version. 2025-08-31 05:29:49 +08:00
ZhanGSKen
89febba5a9 <androidxdemo>Start New Stage Version. 2025-08-31 05:29:33 +08:00
ZhanGSKen
653330f8e1 <androidxdemo>Start New Stage Version. 2025-08-31 05:26:36 +08:00
ZhanGSKen
386c73effc <androidxdemo>Start New Stage Version. 2025-08-31 05:19:41 +08:00
ZhanGSKen
b385583c5a <androiddemo>Start New Stage Version. 2025-08-31 05:11:29 +08:00
ZhanGSKen
e7a9be2f56 <androiddemo>APK 15.0.0 release Publish. 2025-08-31 05:11:26 +08:00
ZhanGSKen
b27f7b0080 更新类库 2025-08-31 05:06:06 +08:00
ZhanGSKen
0a440419ff <libaes>Library Release 15.9.3 2025-08-31 05:01:04 +08:00
ZhanGSKen
9e189ed5ac <aes>APK 15.9.3 release Publish. 2025-08-31 05:00:43 +08:00
ZhanGSKen
0fb6aadc72 更新类库版本 2025-08-31 04:55:30 +08:00
ZhanGSKen
46f3315b02 Merge remote-tracking branch 'gitee/appbase' into aes 2025-08-31 04:54:49 +08:00
ZhanGSKen
c0ff228845 <libapputils>Library Release 15.8.5 2025-08-31 04:53:26 +08:00
ZhanGSKen
a0fe8f17a8 <apputils>APK 15.8.5 release Publish. 2025-08-31 04:53:04 +08:00
ZhanGSKen
16bd40fc59 Merge remote-tracking branch 'gitee/appbase' into apputils 2025-08-31 04:51:48 +08:00
ZhanGSKen
ffaf683c54 <libapputils>Library Release 15.8.4 2025-08-31 04:46:19 +08:00
ZhanGSKen
26f5f8d3db 编译参数修复 2025-08-31 04:45:24 +08:00
ZhanGSKen
917e25cdc8 更新基础类库版本 2025-08-31 04:41:42 +08:00
ZhanGSKen
707bed52c7 <libappbase>Library Release 15.9.5 2025-08-31 04:33:11 +08:00
ZhanGSKen
3795cf8631 <mymessagemanager>APK 15.3.5 release Publish. 2025-08-31 01:10:33 +08:00
ZhanGSKen
b374f3117a 更新联系人查询与短信发送框,在搜索到空数据时的显示逻辑。 2025-08-30 21:15:01 +08:00
ZhanGSKen
d581cd9842 <contacts>APK 15.3.15 release Publish. 2025-08-30 15:04:01 +08:00
ZhanGSKen
cef50d087d 联系人搜索框添加拼音首字母简单搜索功能,如“周星星”输入“zxx”即可搜索到。 2025-08-30 15:02:37 +08:00
ZhanGSKen
6d9adc124e <mymessagemanager>APK 15.3.4 release Publish. 2025-08-30 11:42:36 +08:00
ZhanGSKen
52f738b45b 修正选项开关提示 2025-08-30 11:40:55 +08:00
ZhanGSKen
9ece6778b7 <mymessagemanager>APK 15.3.3 release Publish. 2025-08-28 21:13:38 +08:00
ZhanGSKen
b7f8b76ace <libapputils>Library Release 15.8.4 2025-08-28 21:11:05 +08:00
ZhanGSKen
326e5fa68e Merge remote-tracking branch 'studio/aes' into aes 2025-08-28 21:08:37 +08:00
ZhanGSKen
e9c8f9029e <appbase>APK 15.9.1 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
c00bfa1292 <appbase>APK 15.9.0 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
11ee4dcf27 提升版本 2025-08-28 21:04:59 +08:00
ZhanGSKen
8974e24dce <appbase>Start New Stage Version. 2025-08-28 21:04:59 +08:00
ZhanGSKen
89142e379c 编译测试 2025-08-28 21:04:59 +08:00
ZhanGSKen
dabc671c27 <contacts>APK 15.3.11 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
ed849e92d1 区分防御层数量级差异,区分量级给出相应提示。 2025-08-28 21:04:59 +08:00
ZhanGSKen
42d2522927 <contacts>APK 15.3.10 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
6a52b2a8c3 添加应用效果提示 2025-08-28 21:04:59 +08:00
ZhanGSKen
02ed5bd5d1 <contacts>APK 15.3.9 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
b685665d0c 命名重构 2025-08-28 21:04:59 +08:00
ZhanGSKen
1b030a2855 联系人号码添加复制功能 2025-08-28 21:04:59 +08:00
ZhanGSKen
dd577f1765 通话记录号码添加复制功能 2025-08-28 21:04:59 +08:00
ZhanGSKen
0f8350600d <contacts>APK 15.3.8 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
1777ebb8dc 添加清空 BoBullToon 数据功能,更新默认 BoBullToon 数据地址。 2025-08-28 21:04:59 +08:00
ZhanGSKen
2b99f707e7 <contacts>APK 15.3.7 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
068c94e749 规则编辑列表显示优化 2025-08-28 21:04:59 +08:00
ZhanGSKen
0b3bc7e296 <contacts>APK 15.3.6 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
6ef747bcf8 <contacts/>Start New Stage Version. 2025-08-28 21:04:59 +08:00
ZhanGSKen
a8e843c388 更新应用描述 2025-08-28 21:04:59 +08:00
ZhanGSKen
d3fd593cb0 更新应用介绍页 2025-08-28 21:04:59 +08:00
ZhanGSKen
3aec176b8b 更新按钮文字描述 2025-08-28 21:04:59 +08:00
ZhanGSKen
5960f76238 添加应用使用方法提示 2025-08-28 21:04:59 +08:00
ZhanGSKen
1a118da827 更新说明书 2025-08-28 21:04:59 +08:00
ZhanGSKen
f73cad6f3e <contacts>APK 15.3.5 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
7130ecf023 <contacts>APK 15.3.4 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
953c8f08cd 检验拨不通号码群排在查询通讯录联系人前面 2025-08-28 21:04:59 +08:00
ZhanGSKen
87b7557f72 <contacts>APK 15.3.3 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
77f5a156f8 编译调试 2025-08-28 21:04:59 +08:00
ZhanGSKen
b34ea40536 <mymessagemanager>APK 15.3.1 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
d202a3443d <mymessagemanager>APK 15.3.0 release Publish. 2025-08-28 21:04:59 +08:00
ZhanGSKen
8c532c885f 介于UI界面有调整,版本号升级以区分。 2025-08-28 21:04:59 +08:00
ZhanGSKen
5fc4cb5f74 应用介绍页链接更新 2025-08-28 21:04:59 +08:00
ZhanGSKen
2a590a99fb 菜单功能BugFix 2025-08-28 21:04:59 +08:00
ZhanGSKen
c6ad707ca2 应用菜单排列调整 2025-08-28 21:04:59 +08:00
ZhanGSKen
ee13a43fb6 <libaes>Library Release 15.9.2 2025-08-28 21:04:18 +08:00
ZhanGSKen
5fbe1d8f71 <libaes>Library Release 15.9.2 2025-08-28 20:57:31 +08:00
ZhanGSKen
38fe941a8b <libappbase>Library Release 15.9.5 2025-08-28 20:56:48 +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
04df902b6b <contacts>APK 15.3.14 release Publish. 2025-08-26 01:37:58 +08:00
ZhanGSKen
33c71ea868 添加手机端BoBullToon数据文件夹查找模块。 2025-08-25 20:47:49 +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
ba861d910e <contacts>APK 15.3.13 release Publish. 2025-08-20 20:19:22 +08:00
ZhanGSKen
f5d9aafe43 更新默认BoBullToon数据源URL 2025-08-20 20:17:53 +08:00
ZhanGSKen
e80d7e7b03 <appbase>APK 15.9.5 release Publish. 2025-08-18 03:56:26 +08:00
ZhanGSKen
6e0a833fde <appbase>APK 15.9.4 release Publish. 2025-08-17 21:30:42 +08:00
ZhanGSKen
c596ee5fa4 <appbase>APK 15.9.3 release Publish. 2025-08-17 21:10:44 +08:00
ZhanGSKen
5e99f1278e <appbase>APK 15.9.2 release Publish. 2025-08-17 19:23:49 +08:00
ZhanGSKen
b7158d1ebd 添加项目关键配置说明 2025-08-15 21:39:22 +08:00
ZhanGSKen
9b51250ebf Merge remote-tracking branch 'origin/androidxdemo' into appbase 2025-08-15 20:53:02 +08:00
ZhanGSKen
eb61eb7306 <powerbell>APK 15.4.10 release Publish. 2025-08-13 02:46:25 +08:00
ZhanGSKen
31ad66685c 修复信息提示错误问题。 2025-08-13 02:45:05 +08:00
ZhanGSKen
beb561ad6a <powerbell>APK 15.4.9 release Publish. 2025-08-13 02:37:14 +08:00
ZhanGSKen
8869265d60 优化电量清理活动窗口UI显示。 2025-08-13 02:35:36 +08:00
ZhanGSKen
2739627aff <timestamp>APK 15.1.3 release Publish. 2025-07-28 11:36:51 +08:00
ZhanGSKen
58e0be9cf4 添加开机启动功能。 2025-07-28 11:35:30 +08:00
ZhanGSKen
9e9402f84e <contacts>APK 15.3.12 release Publish. 2025-07-27 15:39:49 +08:00
ZhanGSKen
ec18330022 窗口回显时刷新到最新数据。 2025-07-27 15:38:16 +08:00
ZhanGSKen
8bb80ef575 <timestamp>APK 15.1.2 release Publish. 2025-07-24 15:25:52 +08:00
ZhanGSKen
c1e6e32809 在主界面添加时间戳截取按钮 2025-07-24 15:22:18 +08:00
ZhanGSKen
3e7722e2c0 <numtable>APK 15.1.1 release Publish. 2025-07-24 10:16:27 +08:00
ZhanGSKen
a1707e73b5 添加页面方向指示图标创建功能 2025-07-24 10:12:30 +08:00
ZhanGSKen
9dcbaa0d75 添加桌面快捷方式创建功能 2025-07-24 09:00:19 +08:00
ZhanGSKen
23920a7ff1 Merge remote-tracking branch 'origin/aes' into appbase 2025-07-24 08:25:21 +08:00
ZhanGSKen
17c373c490 <appbase>APK 15.9.1 release Publish. 2025-07-17 11:39:14 +08:00
ZhanGSKen
5f7c94b349 <appbase>APK 15.9.0 release Publish. 2025-07-17 11:24:17 +08:00
ZhanGSKen
c2b739d345 提升版本 2025-07-17 11:23:30 +08:00
ZhanGSKen
67a05cd457 <appbase>Start New Stage Version. 2025-07-17 11:21:04 +08:00
ZhanGSKen
554ab758bf 编译测试 2025-07-17 11:17:58 +08:00
ZhanGSKen
20e118cd34 Merge remote-tracking branch 'origin/contacts' into appbase 2025-07-17 11:15:51 +08:00
ZhanGSKen
f370ae8ffb <contacts>APK 15.3.11 release Publish. 2025-07-17 09:57:24 +08:00
ZhanGSKen
c92c874ea1 区分防御层数量级差异,区分量级给出相应提示。 2025-07-17 09:54:47 +08:00
ZhanGSKen
90a6116c0a <contacts>APK 15.3.10 release Publish. 2025-07-17 04:06:42 +08:00
ZhanGSKen
45208ecbb1 添加应用效果提示 2025-07-17 04:05:01 +08:00
ZhanGSKen
c28d655fe3 <contacts>APK 15.3.9 release Publish. 2025-07-06 16:18:59 +08:00
ZhanGSKen
4b5905f74e 命名重构 2025-07-06 16:17:09 +08:00
ZhanGSKen
6bd01780ec 联系人号码添加复制功能 2025-07-06 16:14:16 +08:00
ZhanGSKen
a6699262f8 通话记录号码添加复制功能 2025-07-06 16:02:45 +08:00
ZhanGSKen
07b5e66875 <powerbell>APK 15.4.8 release Publish. 2025-07-06 12:53:58 +08:00
ZhanGSKen
91f5cf9051 更新应用介绍页 2025-07-06 12:51:50 +08:00
ZhanGSKen
ea2d38defc <contacts>APK 15.3.8 release Publish. 2025-07-05 12:43:15 +08:00
ZhanGSKen
e430b7abe4 添加清空 BoBullToon 数据功能,更新默认 BoBullToon 数据地址。 2025-07-05 12:41:14 +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
0e3b9dc760 20250629_120423_103 2025-06-29 12:04:31 +08:00
ZhanGSKen
6c8b0dcfa5 <contacts>APK 15.3.7 release Publish. 2025-06-28 19:57:09 +08:00
ZhanGSKen
7de8a4f084 规则编辑列表显示优化 2025-06-28 19:54:58 +08:00
ZhanGSKen
219c6614be <contacts>APK 15.3.6 release Publish. 2025-06-28 13:20:38 +08:00
ZhanGSKen
0f5bb020b9 <contacts/>Start New Stage Version. 2025-06-28 13:17:42 +08:00
ZhanGSKen
7794ff80ec 更新应用描述 2025-06-28 13:16:18 +08:00
ZhanGSKen
7463ad3352 更新应用介绍页 2025-06-28 13:08:24 +08:00
ZhanGSKen
69187e3ed0 更新类库 2025-06-28 13:04:03 +08:00
ZhanGSKen
753032efed <libaes>Library Release 15.9.2 2025-06-28 12:59:55 +08:00
ZhanGSKen
2b4c43c9af <aes>APK 15.9.2 release Publish. 2025-06-28 12:59:30 +08:00
ZhanGSKen
711c98d556 应用介绍页更新 2025-06-28 12:42:52 +08:00
ZhanGSKen
202205588a 更新按钮文字描述 2025-06-28 12:21:55 +08:00
ZhanGSKen
42c4978b44 添加应用使用方法提示 2025-06-28 12:11:26 +08:00
ZhanGSKen
1a2b7b862d Merge remote-tracking branch 'origin/appbase' into contacts 2025-06-28 12:04:27 +08:00
ZhanGSKen
8730f434dd 更新说明书 2025-06-28 01:18:20 +08:00
ZhanGSKen
eb253b374f 更新说明书 2025-06-28 01:03:45 +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
f8944490f8 <powerbell>APK 15.4.7 release Publish. 2025-06-23 20:07:50 +08:00
ZhanGSKen
733af004f6 编译参数修复 2025-06-23 20:07:12 +08:00
ZhanGSKen
c03568e1f5 <powerbell>APK 15.4.6 release Publish. 2025-06-23 20:04:59 +08:00
ZhanGSKen
a0575a5e8b 调整像素拾取窗口、背景图片设置窗口与主窗口的切换逻辑。 2025-06-23 20:03:32 +08:00
ZhanGSKen
0e57ce679e <powerbell>APK 15.4.5 release Publish. 2025-06-23 14:49:46 +08:00
ZhanGSKen
f9211a8eb4 优化背景像素拾取UI 2025-06-23 14:48:24 +08:00
ZhanGSKen
4c31ff9b54 调整窗口切换模式 2025-06-23 14:29:59 +08:00
ZhanGSKen
8cf610962e <powerbell>APK 15.4.4 release Publish. 2025-06-22 16:21:12 +08:00
ZhanGSKen
3071d186ec 添加图片像素拾取并可以设置像素为图片背景 2025-06-22 16:19:24 +08:00
ZhanGSKen
df10306059 <powerbell>APK 15.4.3 release Publish. 2025-06-19 21:16:18 +08:00
ZhanGSKen
ccdb9c5abd UI美化,应用视图布局调整。 2025-06-19 21:14:22 +08:00
ZhanGSKen
f27209ab87 Merge remote-tracking branch 'origin/appbase' into powerbell 2025-06-19 20:58:26 +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
2a819e94e4 <powerbell>APK 15.4.2 release Publish. 2025-06-19 10:22:12 +08:00
ZhanGSKen
6635358ec5 设置图片临时剪裁路径保存在Pictures。 2025-06-19 10:19:10 +08:00
ZhanGSKen
ac1c008035 <contacts>APK 15.3.5 release Publish. 2025-06-14 18:43:41 +08:00
ZhanGSKen
b124487cb1 Merge remote-tracking branch 'origin/appbase' into contacts 2025-06-14 17:59:39 +08:00
ZhanGSKen
9621d35f79 <contacts>APK 15.3.4 release Publish. 2025-06-14 17:58:55 +08:00
ZhanGSKen
17de0832a6 检验拨不通号码群排在查询通讯录联系人前面 2025-06-14 17:49:16 +08:00
ZhanGSKen
f53b222b7f <webpagesources>APK 15.0.6 release Publish. 2025-06-13 10:04:49 +08:00
ZhanGSKen
0c0cde8406 编译参数冲突修复 2025-06-13 09:41:59 +08:00
ZhanGSKen
46967065c0 修复dev网站证书问题,添加证书配置。 2025-06-13 09:37:45 +08:00
ZhanGSKen
8edbff5ac1 <webpagesources>APK 15.0.5 release Publish. 2025-06-12 02:46:36 +08:00
ZhanGSKen
434f8a8549 精简信息 2025-06-12 02:44:57 +08:00
ZhanGSKen
c04be60b13 修复外部应用传入view action时的处理方法Bug 2025-06-12 02:43:51 +08:00
ZhanGSKen
641098f8fb <webpagesources>APK 15.0.4 release Publish. 2025-06-11 13:59:15 +08:00
ZhanGSKen
dba54ac4b2 编译测试 2025-06-11 13:57:48 +08:00
ZhanGSKen
c6cd779889 <webpagesources>APK 15.0.3 release Publish. 2025-06-11 13:46:01 +08:00
ZhanGSKen
dfb1692a04 <webpagesources>APK 15.0.2 release Publish. 2025-06-11 13:42:18 +08:00
ZhanGSKen
c83c8f66b3 <webpagesources>APK 15.0.1 release Publish. 2025-06-11 05:46:14 +08:00
ZhanGSKen
cd7b5f38bf <webpagesources>APK 15.0.0 release Publish. 2025-06-11 05:44:31 +08:00
ZhanGSKen
0c2e73b82e <webpagesources>Start New Stage Version. 2025-06-11 05:26:10 +08:00
ZhanGSKen
7b1838ff8e 添加Log窗口调用 2025-06-11 05:15:25 +08:00
ZhanGSKen
73ff3d1726 移除LogView.添加消息状态栏。 2025-06-11 05:11:56 +08:00
ZhanGSKen
a69572e216 清理冗余代码 2025-06-11 04:09:09 +08:00
ZhanGSKen
fa79c3f807 请豆包优化BaseWebView的WebSettings部分后,再修复点击网页链接无反应的Bug。 2025-06-11 03:56:10 +08:00
ZhanGSKen
fde4b275f7 设置默认浏览器类型接口 2025-06-10 15:16:58 +08:00
ZhanGSKen
d66d9373ff 继承https://archives-git.winboll.cc/git/repositories_old/repositories_Bck20250428/webpagesource.git源码 2025-06-10 14:42:17 +08:00
ZhanGSKen
f32ed94e4e 添加 WinBoLL 浏览器 2025-06-10 12:35:45 +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
89dac91cc6 <contacts>APK 15.3.3 release Publish. 2025-06-04 00:08:54 +08:00
ZhanGSKen
3809c1bcab 编译调试 2025-06-04 00:04:20 +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
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
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
298 changed files with 13330 additions and 1423 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,12 @@
# 本项目要实际运用需要注意以下几个步骤:
# 在项目根目录下:
## 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并取消相应项目模块的注释。
## . 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties
## . 项目 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
#Sun Aug 31 23:40:17 HKT 2025
stageCount=4
libraryProject=libaes
baseVersion=15.8
publishVersion=15.8.0
buildCount=2
baseBetaVersion=15.8.1
baseVersion=15.9
publishVersion=15.9.3
buildCount=0
baseBetaVersion=15.9.4

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=3&extra=page%3D1");
appInfo.setAppAPKName("AES");
appInfo.setAppAPKFolderName("AES");
//appInfo.setIsAddDebugTools(false);

View File

@@ -0,0 +1 @@

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.5'
api 'cc.winboll.studio:libappbase:15.9.5'
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon May 19 21:45:28 GMT 2025
stageCount=0
#Sun Aug 31 05:11:26 CST 2025
stageCount=1
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=25
buildCount=0
baseBetaVersion=15.0.1

View File

@@ -0,0 +1 @@

View File

@@ -29,7 +29,7 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.0"
versionName "15.1"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -67,7 +67,7 @@ dependencies {
//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.1'
api 'cc.winboll.studio:libappbase:15.8.1'
api 'cc.winboll.studio:libaes:15.9.3'
api 'cc.winboll.studio:libapputils:15.8.5'
api 'cc.winboll.studio:libappbase:15.9.5'
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon May 19 21:43:40 GMT 2025
stageCount=0
#Sun Aug 31 05:42:50 CST 2025
stageCount=2
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=22
baseBetaVersion=15.0.1
baseVersion=15.1
publishVersion=15.1.1
buildCount=0
baseBetaVersion=15.1.2

View File

@@ -30,7 +30,7 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 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
#Tue Jun 03 13:40:08 HKT 2025
stageCount=5
#Sun Aug 31 23:37:38 HKT 2025
stageCount=6
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.4
baseVersion=15.9
publishVersion=15.9.5
buildCount=0
baseBetaVersion=15.8.5
baseBetaVersion=15.9.6

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 Jun 03 15:05:42 HKT 2025
stageCount=5
#Mon Sep 01 07:56:33 HKT 2025
stageCount=7
libraryProject=libapputils
baseVersion=15.8
publishVersion=15.8.4
publishVersion=15.8.6
buildCount=0
baseBetaVersion=15.8.5
baseBetaVersion=15.8.7

35
autoinstaller/README.md Normal file
View File

@@ -0,0 +1,35 @@
# AutoInstaller
#### 介绍
APK 文件监控,监控指定文件路径的 APK 文件。
在 APK 文件改变后自动调用系统安装命令安装,或者调用[应用信息查看器]打开。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh autoinstaller
#### 使用说明
#### 参与贡献
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

@@ -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

@@ -3,7 +3,7 @@
https://github.com/aJIEw/PhoneCallApp.git
#### 介绍
通讯录与拨号
这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。

View File

@@ -45,9 +45,9 @@ 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.3'
api 'cc.winboll.studio:libapputils:15.8.5'
api 'cc.winboll.studio:libappbase:15.9.5'
// 权限请求框架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 13:02:18 HKT 2025
stageCount=3
#Sun Aug 31 06:05:42 CST 2025
stageCount=17
libraryProject=
baseVersion=15.3
publishVersion=15.3.2
publishVersion=15.3.16
buildCount=0
baseBetaVersion=15.3.3
baseBetaVersion=15.3.17

View File

@@ -1,5 +1,10 @@
package cc.winboll.studio.contacts;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 主窗口
*/
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
@@ -8,6 +13,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.telecom.TelecomManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@@ -41,7 +47,7 @@ import java.util.List;
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
public static final String TAG = "MainActivity";
public static final String TAG = "MainActivity";
public static final int REQUEST_HOME_ACTIVITY = 0;
public static final int REQUEST_ABOUT_ACTIVITY = 1;
@@ -55,13 +61,10 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
MainServiceBean mMainServiceBean;
private TabLayout tabLayout;
private ViewPager viewPager;
private List<View> views; //用来存放放进ViewPager里面的布局
//实例化存储imageView导航原点的集合
private List<View> views;
ImageView[] imageViews;
//MyPagerAdapter adapter;//适配器
MyPagerAdapter pagerAdapter;
LinearLayout linearLayout;//下标所在在LinearLayout布局里
int currentPoint = 0;//当前被选中中页面的下标
LinearLayout linearLayout;
int currentPoint = 0;
private TelephonyManager telephonyManager;
private MyPhoneStateListener phoneStateListener;
@@ -70,30 +73,6 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
private static final int DIALER_REQUEST_CODE = 1;
// @Override
// public Activity getActivity() {
// return this;
// }
// @Override
// public APPInfo getAppInfo() {
// String szBranchName = "contacts";
//
// APPInfo appInfo = AboutActivityFactory.buildDefaultAPPInfo();
// appInfo.setAppName("Contacts");
// appInfo.setAppIcon(cc.winboll.studio.libapputils.R.drawable.ic_winboll);
// appInfo.setAppDescription("Contacts Description");
// appInfo.setAppGitName("APP");
// appInfo.setAppGitOwner("Studio");
// appInfo.setAppGitAPPBranch(szBranchName);
// appInfo.setAppGitAPPSubProjectFolder(szBranchName);
// appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=Contacts");
// appInfo.setAppAPKName("Contacts");
// appInfo.setAppAPKFolderName("Contacts");
// return appInfo;
// return null;
// }
@Override
public Activity getActivity() {
@@ -107,89 +86,62 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
@Override
protected void onCreate(Bundle savedInstanceState) {
// 接收并处理 Intent 数据,函数 Intent 处理接收就直接返回
//if (prosessIntents(getIntent())) return;
// 以下正常创建主窗口
super.onCreate(savedInstanceState);
_MainActivity = this;
setContentView(R.layout.activity_main);
// 初始化工具栏
mToolbar = findViewById(R.id.activitymainToolbar1);
// 初始化工具栏仅加载基础UI
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
setSupportActionBar(mToolbar);
// if (isEnableDisplayHomeAsUp()) {
// // 显示后退按钮
// getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// }
getSupportActionBar().setSubtitle(TAG);
tabLayout = findViewById(R.id.tabLayout);
viewPager = findViewById(R.id.viewPager);
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
viewPager = (ViewPager) findViewById(R.id.viewPager);
// 创建Fragment列表和标题列表
fragmentList = new ArrayList<>();
tabTitleList = new ArrayList<>();
// 创建Fragment列表(仅实例化,不加载数据)
fragmentList = new ArrayList<Fragment>();
tabTitleList = new ArrayList<String>();
fragmentList.add(CallLogFragment.newInstance(0));
fragmentList.add(ContactsFragment.newInstance(1));
fragmentList.add(ContactsFragment.newInstance(1)); // 延迟加载联系人数据
fragmentList.add(LogFragment.newInstance(2));
tabTitleList.add("通话记录");
tabTitleList.add("联系人");
tabTitleList.add("应用日志");
// 设置ViewPager适配器
// 设置ViewPager适配器
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
viewPager.setAdapter(adapter);
// 关键:关闭预加载,仅当前页初始化
viewPager.setOffscreenPageLimit(0);
// 关联TabLayout和ViewPager
tabLayout.setupWithViewPager(viewPager);
// initData();
// initView();
// //initPoint();//调用初始化导航原点的方法
// viewPager.addOnPageChangeListener(this);//滑动事件
//ViewPager viewPager = findViewById(R.id.activitymainViewPager1);
//MyPagerAdapter pagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
//viewPager.setAdapter(pagerAdapter);
//TabLayout tabLayout = findViewById(R.id.activitymainTabLayout1);
//tabLayout.setupWithViewPager(viewPager);
// mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
// if (mMainServiceBean == null) {
// mMainServiceBean = new MainServiceBean();
// }
// cbMainService = findViewById(R.id.activitymainCheckBox1);
// cbMainService.setChecked(mMainServiceBean.isEnable());
// cbMainService.setOnClickListener(new View.OnClickListener(){
// @Override
// public void onClick(View view) {
// if (cbMainService.isChecked()) {
// MainService.startMainService(MainActivity.this);
// } else {
// MainService.stopMainService(MainActivity.this);
// }
// }
// });
// 初始化服务状态(延迟启动非核心服务)
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
if (mMainServiceBean == null) {
mMainServiceBean = new MainServiceBean();
MainServiceBean.saveBean(this, mMainServiceBean);
}
if (mMainServiceBean.isEnable()) {
MainService.startMainService(this);
// 延迟1秒启动服务避免阻塞启动
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MainService.startMainService(MainActivity.this);
}
}, 1000);
}
// 初始化TelephonyManager和PhoneStateListener
// 初始化电话状态监听(基础功能保留)
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
phoneStateListener = new MyPhoneStateListener();
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
// ViewPager适配器
// ViewPager适配器Java 7语法
private class MyPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
@@ -226,91 +178,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
_MainActivity.startActivity(intent);
}
//初始化view即显示的图片
// void initView() {
// viewPager = findViewById(R.id.activitymainViewPager1);
// pagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
// viewPager.setAdapter(pagerAdapter);
// //adapter = new MyPagerAdapter(views);
// //viewPager = findViewById(R.id.activitymainViewPager1);
// //viewPager.setAdapter(adapter);
// //linearLayout = findViewById(R.id.activitymainLinearLayout1);
// //initPoint();//初始化页面下方的点
// viewPager.setOnPageChangeListener(this);
//
// }
//初始化所要显示的布局
// void initData() {
// LayoutInflater inflater = LayoutInflater.from(getActivity());
// View view1 = inflater.inflate(R.layout.fragment_call_log, viewPager, false);
// View view2 = inflater.inflate(R.layout.fragment_contacts, viewPager, false);
// View view3 = inflater.inflate(R.layout.fragment_log, viewPager, false);
//
// views = new ArrayList<>();
// views.add(view1);
// views.add(view2);
// views.add(view3);
// }
// void initPoint() {
// imageViews = new ImageView[5];//实例化5个图片
// for (int i = 0; i < linearLayout.getChildCount(); i++) {
// imageViews[i] = (ImageView) linearLayout.getChildAt(i);
// imageViews[i].setImageResource(R.drawable.ic_launcher);
// imageViews[i].setOnClickListener(this);//点击导航点,即可跳转
// imageViews[i].setTag(i);//重复利用实例化的对象
// }
// currentPoint = 0;//默认第一个坐标
// imageViews[currentPoint].setImageResource(R.drawable.ic_launcher);
// }
//OnPageChangeListener接口要实现的三个方法
/* onPageScrollStateChanged(int state)
此方法是在状态改变的时候调用其中state这个参数有三种状态
SCROLL_STATE_DRAGGING1表示用户手指“按在屏幕上并且开始拖动”的状态
手指按下但是还没有拖动的时候还不是这个状态只有按下并且手指开始拖动后log才打出。
SCROLL_STATE_IDLE0滑动动画做完的状态。
SCROLL_STATE_SETTLING2在“手指离开屏幕”的状态。*/
// OnPageChangeListener接口实现
@Override
public void onPageScrollStateChanged(int state) {
public void onPageScrollStateChanged(int state) {}
}
/* onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
当页面在滑动的时候会调用此方法,在滑动被停止之前,此方法回一直得到调用。其中三个参数的含义分别为:
position :当前页面即你点击滑动的页面从A滑B则是A页面的position。
positionOffset:当前页面偏移的百分比
positionOffsetPixels:当前页面偏移的像素位置*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
}
/* onPageSelected(int position)
此方法是页面滑动完后得到调用position是你当前选中的页面的Position位置编号
(从A滑动到B就是B的position)*/
public void onPageSelected(int position) {
// ImageView preView = imageViews[currentPoint];
// preView.setImageResource(R.drawable.ic_launcher);
// ImageView currView = imageViews[position];
// currView.setImageResource(R.drawable.ic_launcher);
// currentPoint = position;
}
//小圆点点击事件
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
//通过getTag(),可以判断是哪个控件
// int i = (Integer) v.getTag();
// viewPager.setCurrentItem(i);//直接跳转到某一个页面的情况
}
public void onPageSelected(int position) {}
@Override
public void onClick(View v) {}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
//setSubTitle("");
}
private class MyPhoneStateListener extends PhoneStateListener {
@@ -336,109 +219,31 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
LogUtils.d(TAG, "onDestroy() SOS");
}
//
// 处理传入的 Intent 数据
//
// boolean prosessIntents(Intent intent) {
// if (intent == null
// || intent.getAction() == null
// || intent.getAction().equals(""))
// return false;
//
// if (intent.getAction().equals(StringToQrCodeView.ACTION_UNITTEST_QRCODE)) {
// try {
// WinBoLLActivity clazzActivity = UnitTestActivity.class.newInstance();
// String tag = clazzActivity.getTag();
// LogUtils.d(TAG, "String tag = clazzActivity.getTag(); tag " + tag);
// Intent subIntent = new Intent(this, UnitTestActivity.class);
// subIntent.setAction(intent.getAction());
// File file = new File(getCacheDir(), UUID.randomUUID().toString());
// //取出文件uri
// Uri uri = intent.getData();
// if (uri == null) {
// uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
// }
// //获取文件真实地址
// String szSrcPath = UriUtils.getFileFromUri(getApplication(), uri);
// if (TextUtils.isEmpty(szSrcPath)) {
// return false;
// }
//
// Files.copy(Paths.get(szSrcPath), Paths.get(file.getPath()));
// //startWinBoLLActivity(subIntent, tag);
// WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, subIntent, UnitTestActivity.class);
// } catch (IllegalAccessException | InstantiationException | IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// // 函数处理异常返回失败
// return false;
// }
// } else {
// LogUtils.d(TAG, "prosessIntents|" + intent.getAction() + "|yet");
// return false;
// }
// return true;
// }
// @Override
// public String getTag() {
// return TAG;
// }
// @Override
// public void onBackPressed() {
// exit();
// }
//
// void exit() {
// YesNoAlertDialog.OnDialogResultListener listener = new YesNoAlertDialog.OnDialogResultListener(){
//
// @Override
// public void onYes() {
// WinBoLLActivityManager.getInstance(getApplicationContext()).finishAll();
// }
//
// @Override
// public void onNo() {
// }
// };
// YesNoAlertDialog.show(this, "[ " + getString(R.string.app_name) + " ]", "Exit(Yes/No).\nIs close all activity?", listener);
// }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
//WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, CallActivity.class);
}
// } else
// if (item.getItemId() == R.id.item_exit) {
// exit();
// return true;
// }
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
}
/**
* Android M 及以上检查是否是系统默认电话应用
* 检查是否是系统默认电话应用
*/
public boolean isDefaultPhoneCallApp() {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
if (manger != null && manger.getDefaultDialerPackage() != null) {
return manger.getDefaultDialerPackage().equals(getPackageName());
@@ -452,35 +257,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
if (manager == null) return false;
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
Integer.MAX_VALUE)) {
Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
return true;
}
}
return false;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// switch (resultCode) {
// case REQUEST_HOME_ACTIVITY : {
// LogUtils.d(TAG, "REQUEST_HOME_ACTIVITY");
// break;
// }
// case REQUEST_ABOUT_ACTIVITY : {
// LogUtils.d(TAG, "REQUEST_ABOUT_ACTIVITY");
// break;
// }
// default : {
// super.onActivityResult(requestCode, resultCode, data);
// }
// }
if (requestCode == DIALER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show();
}
}
}
}

View File

@@ -79,12 +79,12 @@ public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity
APPInfo appInfo = new APPInfo();
appInfo.setAppName("Contacts");
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
appInfo.setAppDescription("通讯录与拨号");
appInfo.setAppGitName("APP");
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
appInfo.setAppGitName("APPBase");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=Contacts");
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=4&extra=page%3D1");
appInfo.setAppAPKName("Contacts");
appInfo.setAppAPKFolderName("Contacts");
return new AboutView(mContext, appInfo);

View File

@@ -198,6 +198,9 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
settingsModel.setDunTotalCount(Integer.parseInt(etDunTotalCount.getText().toString()));
settingsModel.setDunResumeSecondCount(Integer.parseInt(etDunResumeSecondCount.getText().toString()));
settingsModel.setDunResumeCount(Integer.parseInt(etDunResumeCount.getText().toString()));
// 应用效果提示
ToastUtils.show((settingsModel.getDunTotalCount() == 1)?"电话骚扰防御力几乎为0。":String.format("以下设置将在连拨%d次后接通电话。", settingsModel.getDunTotalCount()));
}
settingsModel.setIsEnableDun(isEnableDun);
Rules.getInstance(this).saveDun();
@@ -207,6 +210,7 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount()));
etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount()));
etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount()));
}
void updateStreamVolumeTextView() {
@@ -243,6 +247,9 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
Rules.getInstance(this).resetDefaultBoBullToonURL();
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
etBoBullToonURL.setText(Rules.getInstance(this).getBoBullToonURL());
final TomCat tomCat = TomCat.getInstance(this);
tomCat.cleanBoBullToon();
}
public void onDownloadBoBullToon(View view) {
@@ -330,4 +337,8 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
public void onAbout(View view) {
App.getWinBoLLActivityManager().startWinBoLLActivity(this, AboutActivity.class);
}
public void onLogView(View view) {
App.getWinBoLLActivityManager().startLogActivity(this);
}
}

View File

@@ -5,13 +5,18 @@ package cc.winboll.studio.contacts.adapters;
* @Date 2025/02/26 13:09:32
* @Describe CallLogAdapter
*/
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
@@ -35,6 +40,10 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
this.mContactUtils = ContactUtils.getInstance(mContext);
this.callLogList = callLogList;
}
public void relaodContacts() {
this.mContactUtils.relaodContacts();
}
@NonNull
@Override
@@ -47,6 +56,38 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
final CallLogModel callLog = callLogList.get(position);
holder.phoneNumber.setText(callLog.getPhoneNumber() + "" + mContactUtils.getContactsName(callLog.getPhoneNumber()));
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View p1) {
// 弹出复制菜单
PopupMenu menu = new PopupMenu(mContext, holder.phoneNumber);
//加载菜单资源
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
//设置点击事件的响应
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
int nItemId = menuItem.getItemId();
if (nItemId == R.id.item_calllog_phonenumber_copy) {
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
// Creates a new text clip to put on the clipboard
ClipData clip = ClipData.newPlainText("simple text", callLog.getPhoneNumber());
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
}
return true;
}
});
//一定要调用show()来显示弹出式菜单
menu.show();
return true;
}
});
holder.callStatus.setText(callLog.getCallStatus());
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
holder.callDate.setText(dateFormat.format(callLog.getCallDate()));

View File

@@ -5,19 +5,25 @@ package cc.winboll.studio.contacts.adapters;
* @Date 2025/02/26 13:35:44
* @Describe ContactAdapter
*/
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.beans.ContactModel;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import com.hjq.toast.ToastUtils;
import java.util.List;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
@@ -26,8 +32,10 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
private static final int REQUEST_CALL_PHONE = 1;
private List<ContactModel> contactList;
Context mContext;
public ContactAdapter(List<ContactModel> contactList) {
public ContactAdapter(Context context, List<ContactModel> contactList) {
mContext = context;
this.contactList = contactList;
}
@@ -41,6 +49,37 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
@Override
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
final ContactModel contact = contactList.get(position);
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View p1) {
// 弹出复制菜单
PopupMenu menu = new PopupMenu(mContext, holder.llPhoneNumberMain);
//加载菜单资源
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
//设置点击事件的响应
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
int nItemId = menuItem.getItemId();
if (nItemId == R.id.item_contact_phonenumber_copy) {
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
// Creates a new text clip to put on the clipboard
ClipData clip = ClipData.newPlainText("simple text", contact.getNumber());
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
}
return true;
}
});
//一定要调用show()来显示弹出式菜单
menu.show();
return true;
}
});
holder.contactName.setText(contact.getName());
holder.contactNumber.setText(contact.getNumber());
@@ -69,12 +108,14 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
}
public class ContactViewHolder extends RecyclerView.ViewHolder {
LinearLayout llPhoneNumberMain;
TextView contactName;
TextView contactNumber;
AOHPCTCSeekBar dialAOHPCTCSeekBar;
public ContactViewHolder(@NonNull View itemView) {
super(itemView);
llPhoneNumberMain = itemView.findViewById(R.id.itemcontactLinearLayout1);
contactName = itemView.findViewById(R.id.contact_name);
contactNumber = itemView.findViewById(R.id.contact_number);
dialAOHPCTCSeekBar = itemView.findViewById(R.id.aohpctcseekbar_dial);

View File

@@ -7,6 +7,7 @@ package cc.winboll.studio.contacts.adapters;
*/
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@@ -20,6 +21,7 @@ import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.views.LeftScrollView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
@@ -60,6 +62,10 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
final SimpleViewHolder simpleViewHolder = (SimpleViewHolder) holder;
String szView = model.getRuleText().trim().equals("") ?"[NULL]": model.getRuleText();
simpleViewHolder.tvRuleText.setText(szView);
simpleViewHolder.checkBoxAllow.setChecked(model.isAllowConnection());
simpleViewHolder.checkBoxAllow.setEnabled(false);
simpleViewHolder.checkBoxEnable.setChecked(model.isEnable());
simpleViewHolder.checkBoxEnable.setEnabled(false);
simpleViewHolder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener(){
@Override
@@ -215,16 +221,22 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
private final LeftScrollView scrollView;
private final TextView tvRuleText;
CheckBox checkBoxAllow;
CheckBox checkBoxEnable;
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
super(itemView);
scrollView = itemView.findViewById(R.id.scrollView);
//tvRuleText = itemView.findViewById(R.id.ruletext_tv);
tvRuleText = new TextView(itemView.getContext());
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
tvRuleText = viewContent.findViewById(R.id.ruletext_tv);
checkBoxAllow = viewContent.findViewById(R.id.checkbox_allow);
checkBoxEnable = viewContent.findViewById(R.id.checkbox_enable);
//tvRuleText = new TextView(itemView.getContext());
scrollView.setContentWidth(parent.getWidth());
//scrollView.setContentWidth(600);
scrollView.addContentLayout(tvRuleText);
scrollView.addContentLayout(viewContent);
}
}
@@ -243,5 +255,9 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
buttonConfirm = itemView.findViewById(R.id.button_confirm);
}
}
private void setCheckBoxTouchListener(CheckBox checkBox) {
}
}

View File

@@ -1,9 +1,9 @@
package cc.winboll.studio.contacts.beans;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/02/26 13:37:00
* @Describe ContactModel
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人信息数据模型
*/
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
@@ -18,13 +18,18 @@ public class ContactModel {
private String name;
private String number;
private String pinyin;
// 新增:存储姓名的拼音首字母(如"啊牛"→"an"
private String pinyinFirstLetter;
public ContactModel(String name, String number) {
this.name = name;
this.number = number.replaceAll("\\s", "");
this.pinyin = convertToPinyin(name);
// 初始化时生成拼音首字母
this.pinyinFirstLetter = convertToPinyinFirstLetter(name);
}
// 原方法:转换为全拼(如"啊牛"→"aniu"
private String convertToPinyin(String chinese) {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
@@ -33,22 +38,55 @@ public class ContactModel {
StringBuilder pinyin = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) {
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null) {
pinyin.append(pinyinArray[0]);
if (pinyinArray != null && pinyinArray.length > 0) {
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认首选项)
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyin.append(ch);
pinyin.append(ch); // 非汉字直接拼接(如字母、数字、符号)
}
}
return pinyin.toString();
}
// 新增:转换为拼音首字母(如"啊牛"→"an"
private String convertToPinyinFirstLetter(String chinese) {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
StringBuilder firstLetters = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null && pinyinArray.length > 0) {
// 取拼音的第一个字母(如"a"、"niu"→"a"、"n"
firstLetters.append(pinyinArray[0].charAt(0));
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
// 非汉字可根据需求处理:此处保留原字符(如"李3"→"l3""张A"→"za"
firstLetters.append(ch);
}
}
return firstLetters.toString();
}
// 新增:获取拼音首字母
public String getPinyinFirstLetter() {
return pinyinFirstLetter;
}
// 原有getter方法
public String getName() {
return name;
}

View File

@@ -11,6 +11,7 @@ import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -44,7 +45,7 @@ public class TomCat {
}
return _TomCat;
}
public String getDefaultBobulltoonUrl() {
return mContext.getString(R.string.default_bobulltoon_url);
}
@@ -123,7 +124,7 @@ public class TomCat {
}
// 更新新文件
if(downloadAndExtractZip(zipUrl, destinationFolder)) {
if (downloadAndExtractZip(zipUrl, destinationFolder)) {
LogUtils.d(TAG, "ZIP 文件下载并解压成功。");
return true;
}
@@ -154,12 +155,81 @@ public class TomCat {
File getWorkingFolder() {
return mContext.getExternalFilesDir(TAG);
}
public File getBoBullToonDataFolder() {
File fCheckRoot = getWorkingFolder();
if (fCheckRoot == null || !fCheckRoot.exists()) {
return fCheckRoot;
}
// 递归查找符合条件的文件夹
File targetFolder = findTargetFolder(fCheckRoot);
return targetFolder != null ? targetFolder : fCheckRoot;
}
/**
* 递归查找同时包含LICENSE和README.md文件的文件夹
*/
private File findTargetFolder(File currentFolder) {
// 检查当前文件夹是否符合条件
if (hasRequiredFiles(currentFolder)) {
return currentFolder;
}
// 查找子文件夹Java 7不支持方法引用用匿名内部类过滤
File[] subFolders = currentFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory(); // 仅保留子文件夹
}
});
if (subFolders != null) {
for (File subFolder : subFolders) {
File result = findTargetFolder(subFolder);
if (result != null) {
return result;
}
}
}
return null;
}
/**
* 检查文件夹中是否同时存在LICENSE和README.md文件
*/
private boolean hasRequiredFiles(File folder) {
if (folder == null || !folder.isDirectory()) {
return false;
}
// 检查两个文件是否同时存在且均为文件(非文件夹)
File licenseFile = new File(folder, "LICENSE");
File readmeFile = new File(folder, "README.md");
return licenseFile.exists() && licenseFile.isFile()
&& readmeFile.exists() && readmeFile.isFile();
}
public void cleanBoBullToon() {
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
// 删除旧文件
File fOldFolder = new File(destinationFolder);
if (fOldFolder.exists()) {
deleteFolderRecursive(fOldFolder);
fOldFolder.mkdirs();
}
ToastUtils.show("已清空 BoBullToon 数据!");
LogUtils.d(TAG, "已清空 BoBullToon 数据");
}
public boolean loadPhoneBoBullToon() {
listPhoneBoBullToon.clear();
File fBoBullToon = new File(getWorkingFolder(), "bobulltoon");
File fBoBullToon = getBoBullToonDataFolder();
if (fBoBullToon.exists()) {
LogUtils.d(TAG, String.format("getWorkingFolder() %s", getWorkingFolder()));
LogUtils.d(TAG, String.format("getBoBullToonDataFolder() %s", getWorkingFolder()));
for (File userFolder : fBoBullToon.listFiles()) {
if (userFolder.isDirectory()) {
for (File recordFile : userFolder.listFiles()) {

View File

@@ -145,6 +145,14 @@ public class Rules {
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 检验拨不通号码群
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 查询通讯录是否有该联系人
boolean isPhoneInContacts = ContactUtils.getInstance(mContext).isPhoneInContacts(mContext, phoneNumber);
if (!isDefend) {
@@ -158,14 +166,6 @@ public class Rules {
}
}
// 检验拨不通号码群
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 正则匹配规则名单校验
if (!isDefend) {
for (int i = 0; i < _PhoneConnectRuleModelList.size(); i++) {

View File

@@ -161,4 +161,12 @@ public class CallLogFragment extends Fragment {
_CallLogFragment.triggerUpdate();
}
}
@Override
public void onResume() {
super.onResume();
//ToastUtils.show("onResume");
callLogAdapter.relaodContacts();
readCallLog(); // 窗口回显时更新通话记录
}
}

View File

@@ -1,15 +1,18 @@
package cc.winboll.studio.contacts.fragments;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/02/20 12:57:50
* @Describe 联系人
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人视图
*/
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
@@ -27,24 +30,39 @@ import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.ContactAdapter;
import cc.winboll.studio.contacts.beans.ContactModel;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ContactsFragment extends Fragment {
public static final String TAG = "ContactsFragment";
private static final String ARG_PAGE = "ARG_PAGE";
private int mPage;
private static final int REQUEST_READ_CONTACTS = 1;
private int mPage;
private RecyclerView recyclerView;
private ContactAdapter contactAdapter;
private List<ContactModel> contactList = new ArrayList<>();
private List<ContactModel> originalContactList = new ArrayList<>();
private EditText searchEditText;
private Button btnDial;
private boolean isViewInitialized = false; // 标记视图是否已初始化
// 静态缓存:全局复用联系人数据
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
// 当前页面数据容器
private List<ContactModel> contactList = new ArrayList<ContactModel>();
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
// 异步工具
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private boolean isDataLoaded = false;
public static ContactsFragment newInstance(int page) {
Bundle args = new Bundle();
@@ -65,103 +83,272 @@ public class ContactsFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_contacts, container, false);
// 加载布局(已移除进度条相关代码)
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.contacts_recycler_view);
// 初始化RecyclerView
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
contactAdapter = new ContactAdapter(contactList);
contactList = new ArrayList<ContactModel>();
contactAdapter = new ContactAdapter(getContext(), contactList);
recyclerView.setAdapter(contactAdapter);
// 初始隐藏列表,数据加载后显示
recyclerView.setVisibility(View.GONE);
searchEditText = view.findViewById(R.id.search_edit_text);
searchEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
// 绑定搜索框和拨号按钮
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
btnDial = (Button) view.findViewById(R.id.btn_dial);
// 初始隐藏搜索相关控件,延迟到首次可见时显示
searchEditText.setVisibility(View.GONE);
btnDial.setVisibility(View.GONE);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
filterContacts(s.toString());
}
// 首次可见时初始化资源
@Override
public void onResume() {
super.onResume();
if (!isViewInitialized) {
initSearchAndDial(); // 初始化搜索和拨号功能
checkContactPermission(); // 检查权限并加载数据
isViewInitialized = true;
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 初始化搜索框和拨号按钮
private void initSearchAndDial() {
// 显示搜索相关控件
searchEditText.setVisibility(View.VISIBLE);
btnDial.setVisibility(View.VISIBLE);
// 搜索框防抖监听
searchEditText.addTextChangedListener(new DebounceTextWatcher(300) {
@Override
public void onDebounceTextChanged(String query) {
filterContacts(query);
}
});
// 拨号按钮点击事件
btnDial.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
if (phoneNumber.isEmpty()) {
ToastUtils.show("请输入号码");
return;
}
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
}
// 权限检查
private void checkContactPermission() {
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
} else {
readContacts();
loadContacts();
}
Button btnDial = view.findViewById(R.id.btn_dial);
btnDial.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View p1) {
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
//phoneNumber = "+8616769764848";
ToastUtils.show(phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
}
// 加载联系人(延迟到首次可见时)
private void loadContacts() {
// 若有缓存,直接复用
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE); // 显示列表
isDataLoaded = true;
return;
}
// 无缓存时异步加载
if (!isDataLoaded) {
recyclerView.setVisibility(View.GONE); // 加载中隐藏列表
executor.execute(new Runnable() {
@Override
public void run() {
// 子线程读取联系人
final List<ContactModel> tempList = readContactsInBackground();
// 主线程更新UI
mainHandler.post(new Runnable() {
@Override
public void run() {
// 更新缓存
sCachedOriginalList.clear();
sCachedOriginalList.addAll(tempList);
sCachedFilteredList.clear();
sCachedFilteredList.addAll(tempList);
// 更新当前列表
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
LogUtils.d(TAG, String.format("联系人加载完成,共%d条数据", contactList.size()));
// 数据加载后显示列表
recyclerView.setVisibility(View.VISIBLE);
isDataLoaded = true;
}
});
}
});
}
}
// 子线程读取联系人
private List<ContactModel> readContactsInBackground() {
List<ContactModel> tempList = new ArrayList<ContactModel>();
Cursor cursor = null;
try {
// 查询联系人姓名和号码
cursor = requireContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
},
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
do {
String name = cursor.getString(nameIndex);
String number = cursor.getString(numberIndex).replaceAll("\\s", ""); // 去除空格
tempList.add(new ContactModel(name, number));
} while (cursor.moveToNext());
}
} catch (Exception e) {
LogUtils.d(TAG, "读取联系人失败:" + e);
} finally {
if (cursor != null) {
cursor.close(); // 关闭游标,避免内存泄漏
}
}
return tempList;
}
// 过滤联系人
private void filterContacts(String query) {
contactList.clear();
if (query.isEmpty()) {
contactList.addAll(originalContactList);
sCachedFilteredList.clear();
sCachedFilteredList.addAll(originalContactList);
} else {
String lowerQuery = query.toLowerCase();
for (ContactModel contact : originalContactList) {
// 匹配姓名、全拼、简拼、号码
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
boolean matchNumber = contact.getNumber().contains(lowerQuery);
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
contactList.add(contact);
}
}
sCachedFilteredList.clear();
sCachedFilteredList.addAll(contactList);
}
contactAdapter.notifyDataSetChanged();
// 过滤后确保列表可见
recyclerView.setVisibility(View.VISIBLE);
}
// 权限回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_CONTACTS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
loadContacts(); // 授权后加载联系人
} else {
ToastUtils.show("请授予联系人权限以查看联系人列表");
recyclerView.setVisibility(View.VISIBLE); // 显示空列表
}
}
}
private void readContacts() {
contactList.clear();
originalContactList.clear();
Cursor cursor = requireContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC");
// 防抖TextWatcherJava 7实现
public abstract static class DebounceTextWatcher implements TextWatcher {
private final long debounceDelay;
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable pendingRunnable;
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
ContactModel contact = new ContactModel(name, number);
contactList.add(contact);
originalContactList.add(contact);
}
cursor.close();
contactAdapter.notifyDataSetChanged();
public DebounceTextWatcher(long debounceDelay) {
this.debounceDelay = debounceDelay;
}
}
private void filterContacts(String query) {
contactList.clear();
if (query.isEmpty()) {
contactList.addAll(originalContactList);
} else {
for (ContactModel contact : originalContactList) {
if (contact.getName().toLowerCase().contains(query.toLowerCase()) ||
contact.getPinyin().toLowerCase().contains(query.toLowerCase()) ||
contact.getNumber().toLowerCase().contains(query.toLowerCase())) {
contactList.add(contact);
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// 无需处理
}
@Override
public void onTextChanged(final CharSequence s, int start, int before, int count) {
// 移除之前的延迟任务
if (pendingRunnable != null) {
handler.removeCallbacks(pendingRunnable);
}
// 延迟执行过滤
pendingRunnable = new Runnable() {
@Override
public void run() {
onDebounceTextChanged(s.toString());
}
}
};
handler.postDelayed(pendingRunnable, debounceDelay);
}
@Override
public void afterTextChanged(Editable s) {
// 无需处理
}
// 抽象方法:防抖后的回调
public abstract void onDebounceTextChanged(String query);
}
// 资源释放
@Override
public void onDestroy() {
super.onDestroy();
executor.shutdown(); // 关闭线程池
mainHandler.removeCallbacksAndMessages(null); // 清除未执行任务
}
// Fragment隐藏/显示时的处理
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && isDataLoaded) {
// 复用缓存数据并显示列表
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
}
contactAdapter.notifyDataSetChanged();
}
}

View File

@@ -1,9 +1,9 @@
package cc.winboll.studio.contacts.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/03/06 21:08:16
* @Describe ContactUtils
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人工具集
*/
import android.content.ContentResolver;
import android.content.Context;

View File

@@ -47,8 +47,8 @@ public class LeftScrollView extends HorizontalScrollView {
init();
}
public void addContentLayout(TextView textView) {
contentLayout.addView(textView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
public void addContentLayout(View viewContent) {
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
}
public void setContentWidth(int contentWidth) {

View File

@@ -269,6 +269,19 @@
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="&lt;&lt;==向左拉动列表项可编辑内容"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
@@ -287,6 +300,12 @@
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LogView"
android:onClick="onLogView"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -15,8 +15,10 @@
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Main"
android:onClick="onTestMain"/>
android:text="Add Demo Rules(While size is 0) and Test"
android:onClick="onTestMain"
android:textSize="10sp"
android:textAllCaps="false"/>
</LinearLayout>
@@ -43,7 +45,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Phone"
android:onClick="onTestPhone"/>
android:onClick="onTestPhone"
android:textAllCaps="false"/>
</LinearLayout>

View File

@@ -9,7 +9,8 @@
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:id="@+id/itemcontactLinearLayout1">
<TextView
android:id="@+id/contact_number"

View File

@@ -23,7 +23,7 @@
android:id="@+id/checkbox_allow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="允许连接"/>
android:text="连接"/>
<CheckBox
android:id="@+id/checkbox_enable"

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView
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="wrap_content"
android:scrollbars="none"
android:id="@+id/scrollView">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 内容区域 -->
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@color/white">
<!-- 这里放置你的列表项内容 -->
<TextView
android:id="@+id/text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"/>
</LinearLayout>
<!-- 操作按钮 -->
<LinearLayout
android:id="@+id/action_layout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="@color/lightgray">
<Button
android:id="@+id/edit_btn"
android:layout_width="80dp"
android:layout_height="match_parent"
android:text="编辑"
android:background="@color/blue" />
<Button
android:id="@+id/delete_btn"
android:layout_width="80dp"
android:layout_height="match_parent"
android:text="删除"
android:background="@color/red" />
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>

View File

@@ -0,0 +1,35 @@
<?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="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text"
android:layout_weight="1.0"
android:id="@+id/ruletext_tv"/>
<CheckBox
android:id="@+id/checkbox_allow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接"
android:clickable="false"
android:focusable="false"/>
<CheckBox
android:id="@+id/checkbox_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="启用"
android:clickable="false"
android:focusable="false"/>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?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:id="@+id/item_calllog_phonenumber_copy"
android:title="Copy"/>
</menu>

View File

@@ -0,0 +1,9 @@
<?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:id="@+id/item_contact_phonenumber_copy"
android:title="Copy"/>
</menu>

View File

@@ -2,6 +2,6 @@
<resources>
<string name="app_name">Contacts</string>
<string name="default_bobulltoon_url">http://10.8.0.12:3000/Studio/BoBullToon/archive/main.zip</string>
<string name="default_bobulltoon_url">https://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip</string>
</resources>

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

@@ -21,8 +21,8 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'cc.winboll.studio:libapputils:15.8.2'
api 'cc.winboll.studio:libappbase:15.8.2'
api 'cc.winboll.studio:libapputils:15.8.5'
api 'cc.winboll.studio:libappbase:15.9.5'
// 吐司类库
api 'com.github.getActivity:ToastUtils:10.5'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 01 08:26:46 GMT 2025
stageCount=1
#Sun Aug 31 05:00:43 CST 2025
stageCount=4
libraryProject=libaes
baseVersion=15.8
publishVersion=15.8.0
buildCount=2
baseBetaVersion=15.8.1
baseVersion=15.9
publishVersion=15.9.3
buildCount=0
baseBetaVersion=15.9.4

View File

@@ -107,7 +107,7 @@ public class AboutView extends LinearLayout {
mszAppDescription = mAPPInfo.getAppDescription();
mnAppIcon = mAPPInfo.getAppIcon();
mszWinBoLLServerHost = GlobalApplication.isDebuging() ? "https://dev.winboll.cc": "https://www.winboll.cc";
mszWinBoLLServerHost = GlobalApplication.isDebuging() ? "https://yun-preivew.winboll.cc": "https://yun.winboll.cc";
try {
mszAppVersionName = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName;
@@ -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:40:01 HKT 2025
stageCount=5
#Mon Aug 18 03:56:26 HKT 2025
stageCount=6
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.4
baseVersion=15.9
publishVersion=15.9.5
buildCount=0
baseBetaVersion=15.8.5
baseBetaVersion=15.9.6

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.2'
api 'cc.winboll.studio:libappbase:15.9.5'
// 二维码类库
api 'com.google.zxing:core:3.4.1'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 03 15:05:42 HKT 2025
stageCount=5
#Mon Sep 01 07:56:11 HKT 2025
stageCount=7
libraryProject=libapputils
baseVersion=15.8
publishVersion=15.8.4
publishVersion=15.8.6
buildCount=0
baseBetaVersion=15.8.5
baseBetaVersion=15.8.7

View File

@@ -0,0 +1,32 @@
package cc.winboll.studio.libapputils.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/09/01 07:49
* @Describe .* 前置预防针
regex pointer preventive injection
简称 RegexPPi
*/
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexPPiUtils {
public static final String TAG = "RegexPPiUtils";
//
// 检验文本是否满足适合正则表达式模式计算
//
public static boolean isPPiOK(String text) {
//String text = "这里是一些任意的文本内容";
String regex = ".*";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
/*if (matcher.matches()) {
System.out.println("文本满足该正则表达式模式");
} else {
System.out.println("文本不满足该正则表达式模式");
}*/
return matcher.matches();
}
}

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),

1
midiplayer/.gitignore vendored Normal file
View File

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

34
midiplayer/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Midi Player
#### 介绍
Midi 音乐播放器
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh miniplayer
#### 使用说明
#### 参与贡献
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

73
midiplayer/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.midiplayer"
minSdkVersion 26
targetSdkVersion 30
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'])
// 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.9.2'
api 'cc.winboll.studio:libapputils:15.8.4'
api 'cc.winboll.studio:libappbase:15.8.4'
}

View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Sep 02 12:52:51 GMT 2025
stageCount=1
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=2
baseBetaVersion=15.0.1

21
midiplayer/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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MidiPlayer +</string>
</resources>

View File

@@ -0,0 +1,50 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.midiplayer">
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MidiPlayerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 支持打开Midi文件 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/midi" />
<data android:scheme="file" />
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>

Binary file not shown.

View File

@@ -0,0 +1,345 @@
package cc.winboll.studio.midiplayer;
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,137 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:56
* @Describe 用于将assets/midi目录下的文件拷贝到内部存储
*/
import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class AssetMidiCopier {
public static final String TAG = "AssetMidiCopier";
private Context mContext;
public AssetMidiCopier(Context context) {
mContext = context;
}
/**
* 拷贝assets/midi目录下所有文件到内部存储的midi文件夹
* @return 拷贝是否成功
*/
public boolean copyMidiFiles() {
AssetManager assetManager = mContext.getAssets();
String[] midiFiles;
try {
// 获取assets/midi目录下的所有文件
midiFiles = assetManager.list("midi");
if (midiFiles == null || midiFiles.length == 0) {
Log.d(TAG, "assets/midi目录下没有文件");
return false;
}
// 获取内部存储的目标目录(/data/data/包名/files/midi
File targetDir = new File(mContext.getFilesDir(), "midi");
if (!targetDir.exists()) {
// 创建目录(包括父目录)
if (!targetDir.mkdirs()) {
Log.e(TAG, "创建目标目录失败: " + targetDir.getAbsolutePath());
return false;
}
}
// 逐个拷贝文件
for (String fileName : midiFiles) {
// 跳过目录,只处理文件
if (fileName.contains("/")) {
continue;
}
// 源文件路径assets/midi/文件名)
String sourcePath = "midi/" + fileName;
// 目标文件路径
File targetFile = new File(targetDir, fileName);
// 如果文件已存在,跳过拷贝
if (targetFile.exists()) {
Log.d(TAG, "文件已存在,跳过: " + fileName);
continue;
}
// 执行拷贝
if (!copySingleFile(assetManager, sourcePath, targetFile)) {
Log.e(TAG, "拷贝文件失败: " + fileName);
return false;
}
}
Log.d(TAG, "所有MIDI文件拷贝完成" + midiFiles.length + "个文件");
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝过程发生错误: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 拷贝单个assets文件到目标路径
*/
private boolean copySingleFile(AssetManager assetManager, String sourcePath, File targetFile) {
InputStream in = null;
FileOutputStream out = null;
try {
// 打开assets中的源文件
in = assetManager.open(sourcePath);
// 创建目标文件输出流
out = new FileOutputStream(targetFile);
// 缓冲区
byte[] buffer = new byte[1024];
int length;
// 读写文件
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
// 刷新输出流,确保数据写入
out.flush();
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝单个文件错误: " + e.getMessage());
return false;
} finally {
// 关闭流
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取内部存储中MIDI文件的目录
*/
public File getMidiTargetDir() {
return new File(mContext.getFilesDir(), "midi");
}
}

View File

@@ -0,0 +1,78 @@
package cc.winboll.studio.midiplayer;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import com.hjq.toast.ToastUtils;
import java.io.File;
public class MainActivity extends WinBoLLActivity {
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_main);
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mLogView = findViewById(R.id.logview);
ToastUtils.show("onCreate");
copyAssetsMidiFiles();
}
public void onOpenMidiPlayer(View view) {
App.getWinBoLLActivityManager().startWinBoLLActivity(this, MidiPlayerActivity.class);
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
// 在需要拷贝的地方调用如Activity的onCreate中
private void copyAssetsMidiFiles() {
// 新建拷贝工具类实例
final AssetMidiCopier copier = new AssetMidiCopier(this);
// 开启子线程执行拷贝(避免主线程阻塞)
new Thread(new Runnable() {
@Override
public void run() {
final boolean success = copier.copyMidiFiles();
// 拷贝结果可通过Handler通知主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
if (success) {
// 拷贝成功,获取目标目录
File midiDir = copier.getMidiTargetDir();
LogUtils.d(TAG, "文件保存路径: " + midiDir.getAbsolutePath());
// 可在这里加载MIDI文件
} else {
// 拷贝失败,提示用户
}
}
});
}
}).start();
}
}

View File

@@ -0,0 +1,420 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:26
* @Describe MIDI文件解析器用于解析MIDI文件并提取轨道信息
*/
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
public class MidiParser {
public static final String TAG = "MidiParser";
private static final Charset US_ASCII = Charset.forName("US-ASCII");
private InputStream mInputStream;
private int mTrackCount; // 轨道数量
private int mTicksPerBeat; // 每拍的ticks数从文件头解析
public MidiParser(File file) throws IOException {
this.mInputStream = new FileInputStream(file);
}
/**
* 解析MIDI文件返回包含轨道和每拍ticks数的结果支持速度控制
*/
public MidiPlayer.MidiParseResult parseWithTicks() throws IOException {
try {
// 1. 验证MIDI文件头MThd
if (!verifyHeader()) {
LogUtils.d(TAG, "不是有效的MIDI文件");
return null;
}
// 2. 读取文件头信息包含每拍ticks数
readHeaderInfo();
// 3. 解析每个轨道包含事件的deltaTicks
MidiPlayer.MidiTrack[] tracks = new MidiPlayer.MidiTrack[mTrackCount];
for (int i = 0; i < mTrackCount; i++) {
tracks[i] = parseTrackWithTicks();
}
// 返回解析结果(轨道数组 + 每拍ticks数
return new MidiPlayer.MidiParseResult(tracks, mTicksPerBeat);
} finally {
if (mInputStream != null) {
mInputStream.close();
}
}
}
/**
* 原始解析方法(兼容旧逻辑)
*/
public MidiTrack[] parse() throws IOException {
try {
if (!verifyHeader()) {
LogUtils.d(TAG, "不是有效的MIDI文件");
return null;
}
readHeaderInfo();
MidiTrack[] tracks = new MidiTrack[mTrackCount];
for (int i = 0; i < mTrackCount; i++) {
tracks[i] = parseTrack();
}
return tracks;
} finally {
if (mInputStream != null) {
mInputStream.close();
}
}
}
/**
* 验证MIDI文件头必须以"MThd"开头)
*/
private boolean verifyHeader() throws IOException {
byte[] header = new byte[4];
int read = mInputStream.read(header);
if (read != 4) {
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
return false;
}
String headerStr = new String(header, US_ASCII);
boolean isValid = "MThd".equals(headerStr);
if (!isValid) {
LogUtils.d(TAG, "无效的文件头标识: " + headerStr
+ " (十六进制: " + bytesToHex(header) + ")");
}
return isValid;
}
/**
* 读取MIDI文件头信息提取每拍ticks数
*/
private void readHeaderInfo() throws IOException {
// 1. 读取头长度4字节标准MIDI固定为6
int headerLength = readInt();
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
// 2. 读取头数据共6字节
byte[] headerData = new byte[6];
int read = mInputStream.read(headerData);
if (read != 6) {
LogUtils.d(TAG, "文件头数据不完整预期6字节实际读取: " + read);
throw new IOException("无效的MIDI文件头数据");
}
// 3. 解析头信息格式类型、轨道数、每拍ticks数
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 0xFF);
mTrackCount = ((headerData[2] & 0xFF) << 8) | (headerData[3] & 0xFF);
mTicksPerBeat = ((headerData[4] & 0xFF) << 8) | (headerData[5] & 0xFF); // 存储每拍ticks数
LogUtils.d(TAG, "MIDI文件格式: " + formatType);
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + mTicksPerBeat);
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
// 4. 处理扩展头
if (headerLength > 6) {
long skipped = mInputStream.skip(headerLength - 6);
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped);
}
}
/**
* 解析单个轨道包含事件的deltaTicks用于速度控制
*/
private MidiPlayer.MidiTrack parseTrackWithTicks() throws IOException {
// 1. 读取轨道头MTrk
byte[] trackHeader = new byte[4];
int headerRead = mInputStream.read(trackHeader);
if (headerRead != 4) {
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
}
// 2. 验证轨道头标识
String headerStr = new String(trackHeader, US_ASCII);
if (!"MTrk".equals(headerStr)) {
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr);
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
}
// 3. 读取轨道长度
int trackLength = readInt();
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
// 4. 读取完整轨道数据
byte[] trackData = new byte[trackLength];
int totalRead = 0;
while (totalRead < trackLength) {
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
if (bytesRead == -1) {
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead);
break;
}
totalRead += bytesRead;
}
// 5. 解析轨道事件包含deltaTicks
List<MidiPlayer.MidiEvent> events = new ArrayList<MidiPlayer.MidiEvent>();
if (totalRead == trackLength) {
parseEventsWithTicks(events, trackData);
} else {
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
}
return new MidiPlayer.MidiTrack(events);
}
/**
* 解析轨道事件提取deltaTicks用于计算播放延迟
*/
private void parseEventsWithTicks(List<MidiPlayer.MidiEvent> events, byte[] trackData) {
int offset = 0;
while (offset < trackData.length) {
// 1. 读取deltaTicks事件间隔单位ticks
long deltaTicks = readVariableLength(trackData, offset);
int deltaSize = getVariableLengthSize(deltaTicks);
offset += deltaSize;
if (offset >= trackData.length) {
break;
}
// 2. 读取事件状态字节
int statusByte = trackData[offset] & 0xFF;
offset++;
// 3. 确定事件数据长度
int dataLength = getEventDataLength(statusByte);
if (offset + dataLength > trackData.length) {
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte));
break;
}
// 4. 提取事件数据(状态字节+数据字节)
byte[] eventData = new byte[1 + dataLength];
eventData[0] = (byte) statusByte;
System.arraycopy(trackData, offset, eventData, 1, dataLength);
offset += dataLength;
// 5. 存储事件包含deltaTicks
events.add(new MidiPlayer.MidiEvent(eventData, (int) deltaTicks));
}
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + events.size());
}
/**
* 原始轨道解析方法(兼容旧逻辑)
*/
private MidiTrack parseTrack() throws IOException {
// 1. 读取轨道头MTrk
byte[] trackHeader = new byte[4];
int headerRead = mInputStream.read(trackHeader);
if (headerRead != 4) {
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
return new MidiTrack();
}
// 2. 验证轨道头标识
String headerStr = new String(trackHeader, US_ASCII);
if (!"MTrk".equals(headerStr)) {
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
return new MidiTrack();
}
// 3. 读取轨道长度
int trackLength = readInt();
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
// 4. 读取完整轨道数据
byte[] trackData = new byte[trackLength];
int totalRead = 0;
while (totalRead < trackLength) {
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
if (bytesRead == -1) {
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead + ",预期: " + trackLength);
break;
}
totalRead += bytesRead;
}
// 5. 解析轨道事件
MidiTrack track = new MidiTrack();
if (totalRead == trackLength) {
parseEvents(track, trackData);
} else {
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
}
return track;
}
/**
* 原始事件解析方法(兼容旧逻辑)
*/
private void parseEvents(MidiTrack track, byte[] trackData) {
int offset = 0;
while (offset < trackData.length) {
// 1. 读取可变长度时间戳MIDI事件时间差
long deltaTime = readVariableLength(trackData, offset);
offset += getVariableLengthSize(deltaTime);
if (offset >= trackData.length) {
break;
}
// 2. 读取事件状态字节
int statusByte = trackData[offset] & 0xFF;
offset++;
// 3. 确定事件数据长度
int dataLength = getEventDataLength(statusByte);
if (offset + dataLength > trackData.length) {
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte)
+ ",剩余字节: " + (trackData.length - offset));
break;
}
// 4. 提取完整事件(状态字节+数据字节)
byte[] event = new byte[1 + dataLength];
event[0] = (byte) statusByte;
System.arraycopy(trackData, offset, event, 1, dataLength);
offset += dataLength;
track.addEvent(event);
}
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + track.getEventCount());
}
/**
* 根据状态字节获取事件数据长度
*/
private int getEventDataLength(int statusByte) {
int eventType = statusByte >> 4;
switch (eventType) {
case 0x8: // 音符关闭
case 0x9: // 音符开启
case 0xA: // 触后
case 0xB: // 控制器
case 0xE: // 弯音
return 2;
case 0xC: // 程序变更
case 0xD: // 通道触后
return 1;
case 0xF: // 系统事件
if (statusByte == 0xFF) { // 元事件
return 1;
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
return 0;
}
default:
return 0;
}
}
/**
* 读取可变长度整数MIDI事件时间差deltaTicks
*/
private long readVariableLength(byte[] data, int offset) {
long value = 0;
int b;
int i = 0;
do {
b = data[offset + i] & 0xFF;
value = (value << 7) | (b & 0x7F);
i++;
} while ((b & 0x80) != 0 && i < 4); // 最多4字节
return value;
}
/**
* 获取可变长度整数的字节数
*/
private int getVariableLengthSize(long value) {
if (value < 0x80) return 1;
if (value < 0x4000) return 2;
if (value < 0x200000) return 3;
return 4;
}
/**
* 读取无符号短整型2字节大端序
*/
private int readUnsignedShort() throws IOException {
int b1 = mInputStream.read() & 0xFF;
int b2 = mInputStream.read() & 0xFF;
return (b1 << 8) | b2;
}
/**
* 读取整型4字节大端序
*/
private int readInt() throws IOException {
int b1 = mInputStream.read() & 0xFF;
int b2 = mInputStream.read() & 0xFF;
int b3 = mInputStream.read() & 0xFF;
int b4 = mInputStream.read() & 0xFF;
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
}
/**
* 字节数组转十六进制字符串(调试用)
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
/**
* 旧轨道类(兼容旧逻辑)
*/
/*public static class MidiTrack {
private List<byte[]> events = new ArrayList<byte[]>();
private boolean isMuted = false;
private int currentIndex = 0;
public void addEvent(byte[] event) {
events.add(event);
}
public int getEventCount() {
return events.size();
}
public void setMute(boolean mute) {
isMuted = mute;
}
public boolean hasNextEvent() {
return currentIndex < events.size();
}
public byte[] nextEvent() {
if (currentIndex < events.size()) {
return events.get(currentIndex++);
}
return null;
}
public void reset() {
currentIndex = 0;
}
}*/
}

View File

@@ -0,0 +1,655 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:13
* @Describe MidiPlayer
*/
import android.content.Context;
import android.media.midi.MidiDevice;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiInputPort;
import android.media.midi.MidiManager;
import android.os.Build;
import android.os.Handler;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import cc.winboll.studio.libappbase.LogUtils;
/**
* MIDI播放器核心类支持加载MIDI文件、连接合成器、控制播放轨道及事件解析
* 需Android 6.0API 23及以上版本
*/
public class MidiPlayer {
public static final String TAG = "MidiPlayer";
// 上下文与系统服务
private final Context mContext;
private MidiManager mMidiManager;
// MIDI设备与端口
private MidiDevice mMidiDevice;
private MidiInputPort mInputPort;
private boolean isSynthConnected = false;
// 线程与Handler
private ExecutorService mExecutor;
private final Handler mHandler = new Handler();
// 播放数据与状态
private MidiTrack[] mTracks;
private boolean isPlaying = false;
private int mCurrentTrack = -1; // -1表示播放所有轨道
private int mTicksPerBeat; // 每拍的ticks数从MIDI文件解析
private long startTime; // 播放开始时间(用于日志)
// 音色库管理
private final SoundFontManager mSoundFontManager;
// 连接回调接口
public interface OnSynthConnectedListener {
void onConnected(boolean success);
}
private OnSynthConnectedListener mConnectionListener;
/**
* MIDI事件类存储事件数据及时间间隔
*/
public static class MidiEvent {
public byte[] data; // 事件指令数据
public int deltaTicks; // 与上一事件的时间间隔ticks
public MidiEvent(byte[] data, int deltaTicks) {
this.data = data;
this.deltaTicks = deltaTicks;
}
}
/**
* MIDI轨道类包含事件列表及播放状态
*/
public static class MidiTrack {
private final List<MidiEvent> events;
private boolean isMuted = false;
private int currentIndex = 0;
public MidiTrack(List<MidiEvent> events) {
this.events = events;
}
public boolean hasNextEvent() {
return currentIndex < events.size();
}
public MidiEvent nextEvent() {
return currentIndex < events.size() ? events.get(currentIndex++) : null;
}
public void reset() {
currentIndex = 0;
}
public void setMute(boolean mute) {
isMuted = mute;
}
public boolean isMuted() {
return isMuted;
}
}
/**
* MIDI解析结果类包含轨道数组与每拍ticks数
*/
public static class MidiParseResult {
public MidiTrack[] tracks;
public int ticksPerBeat;
public MidiParseResult(MidiTrack[] tracks, int ticksPerBeat) {
this.tracks = tracks;
this.ticksPerBeat = ticksPerBeat;
}
}
// 构造方法
public MidiPlayer(Context context) {
mContext = context;
mSoundFontManager = new SoundFontManager(context);
// 初始化MIDI服务需API 23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mMidiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE);
}
}
// 设置合成器连接回调
public void setOnSynthConnectedListener(OnSynthConnectedListener listener) {
mConnectionListener = listener;
}
/**
* 加载MIDI文件并解析为轨道
* @param file MIDI文件
* @return 是否加载成功
*/
public boolean loadMidiFile(File file) {
try {
MidiParser parser = new MidiParser(file);
MidiParseResult result = parser.parseWithTicks();
mTracks = result.tracks;
mTicksPerBeat = result.ticksPerBeat;
return mTracks != null && mTracks.length > 0;
} catch (IOException e) {
LogUtils.d(TAG, "加载MIDI文件失败: " + e.getMessage());
return false;
}
}
/**
* 加载音色库文件
* @param soundFontFile 音色库文件
* @return 是否加载成功
*/
public boolean loadSoundFont(File soundFontFile) {
return mSoundFontManager.loadSoundFont(soundFontFile);
}
/**
* 开始播放MIDI文件
*/
public void start() {
LogUtils.d(TAG, "start()");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LogUtils.d(TAG, "需Android 6.0及以上版本");
notifyConnectionResult(false);
return;
}
if (isPlaying || mTracks == null || mMidiManager == null) {
return;
}
isPlaying = true;
getExecutor().execute(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "开始连接合成器");
isSynthConnected = false;
connectToSynth();
// 等待合成器连接最多3秒
int waitCount = 0;
while (!isSynthConnected && waitCount < 30) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
waitCount++;
}
if (!isSynthConnected || mInputPort == null) {
LogUtils.d(TAG, "合成器连接失败");
isPlaying = false;
notifyConnectionResult(false);
return;
}
notifyConnectionResult(true);
playTracksWithSpeedControl();
}
});
}
/**
* 带速度控制的轨道播放逻辑
*/
private void playTracksWithSpeedControl() {
LogUtils.d(TAG, "开始播放每拍ticks数: " + mTicksPerBeat);
if (mInputPort == null || mTracks == null || mTicksPerBeat <= 0) {
return;
}
startTime = System.currentTimeMillis();
int trackCount = mTracks.length;
long[] trackNextEventTime = new long[trackCount]; // 各轨道下一事件的播放时间ms
// 初始化轨道状态
for (int i = 0; i < trackCount; i++) {
mTracks[i].reset();
trackNextEventTime[i] = 0;
}
// 默认BPM可通过MIDI文件中的tempo事件动态调整
int bpm = 120;
double msPerTick = (60.0 / bpm) * 1000 / mTicksPerBeat;
// 循环播放事件
while (isPlaying) {
// 找到最早需要播放的事件时间
long earliestTime = Long.MAX_VALUE;
for (int i = 0; i < trackCount; i++) {
if ((mCurrentTrack == -1 || mCurrentTrack == i)
&& !mTracks[i].isMuted()
&& mTracks[i].hasNextEvent()) {
earliestTime = Math.min(earliestTime, trackNextEventTime[i]);
}
}
if (earliestTime == Long.MAX_VALUE) {
LogUtils.d(TAG, "所有轨道事件播放完毕");
break;
}
// 计算等待时间并休眠
long currentTime = System.currentTimeMillis() - startTime;
long delay = earliestTime - currentTime;
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// 播放所有到达时间点的事件
for (int i = 0; i < trackCount; i++) {
if ((mCurrentTrack == -1 || mCurrentTrack == i)
&& !mTracks[i].isMuted()
&& mTracks[i].hasNextEvent()
&& trackNextEventTime[i] == earliestTime) {
MidiEvent event = mTracks[i].nextEvent();
if (event != null) {
try {
logMidiEventDetails(i, event);
mInputPort.send(event.data, 0, event.data.length);
// 更新下一事件时间
trackNextEventTime[i] = earliestTime + (long) (event.deltaTicks * msPerTick);
} catch (IOException e) {
LogUtils.d(TAG, "发送事件失败: " + e.getMessage());
}
}
}
}
}
isPlaying = false;
LogUtils.d(TAG, "播放结束");
}
/**
* 解析MIDI事件并输出详细日志
*/
private void logMidiEventDetails(int trackIndex, MidiEvent event) {
// 无效事件增加原始数据日志,便于调试
if (event.data == null || event.data.length < 2) {
String dataStr = (event.data != null) ? Arrays.toString(event.data) : "null";
LogUtils.d(TAG, "轨道[" + trackIndex + "] 无效事件: 数据长度不足 ("
+ (event.data != null ? event.data.length : 0) + "字节),原始数据: " + dataStr);
return;
}
int statusByte = event.data[0] & 0xFF;
int eventType = statusByte & 0xF0;
int channel = (statusByte & 0x0F) + 1; // 通道号1-16
StringBuilder log = new StringBuilder();
log.append("轨道[").append(trackIndex).append("] 事件类型: ");
switch (eventType) {
case 0x90: // 音符开启
if (event.data.length >= 3) {
int pitch = event.data[1] & 0xFF;
int velocity = event.data[2] & 0xFF;
log.append("音符开启 | 通道: ").append(channel)
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
.append(" | 力度: ").append(velocity)
.append(" | 间隔ticks: ").append(event.deltaTicks)
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
} else {
log.append("音符开启 (数据不完整) | 长度: ").append(event.data.length);
}
break;
case 0x80: // 音符关闭
if (event.data.length >= 3) {
int pitch = event.data[1] & 0xFF;
int velocity = event.data[2] & 0xFF;
log.append("音符关闭 | 通道: ").append(channel)
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
.append(" | 力度: ").append(velocity)
.append(" | 间隔ticks: ").append(event.deltaTicks)
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
} else {
log.append("音符关闭 (数据不完整) | 长度: ").append(event.data.length);
}
break;
case 0xB0: // 控制变化
if (event.data.length >= 3) {
int controlNumber = event.data[1] & 0xFF;
int controlValue = event.data[2] & 0xFF;
log.append("控制变化 | 通道: ").append(channel)
.append(" | 控制器: ").append(controlNumber).append(" (").append(getControlName(controlNumber)).append(")")
.append(" | 数值: ").append(controlValue);
} else {
log.append("控制变化 (数据不完整) | 长度: ").append(event.data.length);
}
break;
case 0xC0: // 程序改变(乐器切换)
if (event.data.length >= 2) {
int program = event.data[1] & 0xFF;
// 增加乐器编号有效性校验日志
String instrumentName = getInstrumentName(program);
if (program < 0 || program >= 128) {
instrumentName += " (超出GM标准范围0-127)";
}
log.append("程序改变 | 通道: ").append(channel)
.append(" | 乐器编号: ").append(program).append(" (").append(instrumentName).append(")");
} else {
log.append("程序改变 (数据不完整) | 长度: ").append(event.data.length);
}
break;
case 0xD0: // 通道触后事件
log.append("通道触后 | 通道: ").append(channel)
.append(" | 压力值: ").append(event.data[1] & 0xFF);
break;
case 0xF0: // 系统专属事件(SysEx)
log.append("系统专属事件(SysEx) | 长度: ").append(event.data.length)
.append(" | 首字节: 0x").append(Integer.toHexString(event.data[0] & 0xFF));
break;
default:
log.append("未知类型 (0x").append(Integer.toHexString(eventType)).append(") | 通道: ").append(channel)
.append(",原始数据: ").append(Arrays.toString(event.data));
break;
}
LogUtils.d(TAG, log.toString());
}
/**
* 音高转音符名称如60→C4
*/
private String getNoteName(int pitch) {
if (pitch < 0 || pitch > 127) return "无效音高";
String[] noteNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
int octave = (pitch / 12) - 1; // C4对应60
return noteNames[pitch % 12] + octave;
}
/**
* MIDI控制器编号转名称
*/
private String getControlName(int controlNumber) {
switch (controlNumber) {
case 0: return "Bank Select (MSB)";
case 1: return "Modulation Wheel";
case 7: return "音量控制";
case 10: return "声像控制";
case 11: return "表达控制";
case 64: return "延音踏板";
case 121: return "重置所有控制器";
default: return "未知控制器";
}
}
/**
* 乐器编号转名称GM标准
*/
private String getInstrumentName(int program) {
String[] instruments = {
"钢琴", "明亮钢琴", "电钢琴", "Honky-tonk钢琴", "电钢琴", "羽管键琴",
"击弦古钢琴", "颤音琴", "竖琴", "管风琴", "手风琴", "acoustic贝斯",
"电贝斯(指弹)", "电贝斯(拨片)", "小提琴", "大提琴"
};
if (program < 0 || program >= 128) {
return "无效编号";
}
if (program < instruments.length) {
return instruments[program];
} else {
return "乐器 " + program;
}
}
/**
* 连接MIDI合成器
*/
public void connectToSynth() {
if (mMidiManager == null) return;
closeMidiResources();
searchSynthInDevices();
}
/**
* 搜索并选择可用的MIDI设备
*/
private void searchSynthInDevices() {
MidiDeviceInfo[] devices = mMidiManager.getDevices();
if (devices == null || devices.length == 0) {
LogUtils.d(TAG, "未检测到MIDI设备");
isSynthConnected = false;
return;
}
// 优先选择合成器类型3其次选择有输入端口的设备
MidiDeviceInfo targetDevice = null;
for (MidiDeviceInfo device : devices) {
if (device.getType() == 3) {
targetDevice = device;
break;
}
if (targetDevice == null && device.getInputPortCount() > 0) {
targetDevice = device;
}
}
if (targetDevice != null) {
LogUtils.d(TAG, "尝试打开设备: 设备ID=" + targetDevice.getId() + ",名称=" + targetDevice.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
mMidiManager.openDevice(targetDevice, new MidiManager.OnDeviceOpenedListener() {
@Override
public void onDeviceOpened(MidiDevice device) {
setupMidiDevice(device);
}
}, mHandler);
} else {
LogUtils.d(TAG, "未找到可用设备(无合成器或带输入端口的设备)");
isSynthConnected = false;
}
}
/**
* 初始化MIDI设备并打开输入端口
*/
private void setupMidiDevice(MidiDevice device) {
if (device == null) {
LogUtils.d(TAG, "打开MIDI设备失败: 设备为null");
isSynthConnected = false;
return;
}
mMidiDevice = device;
try {
// 从设备信息中获取输入端口数量
MidiDeviceInfo deviceInfo = device.getInfo();
if (deviceInfo.getInputPortCount() > 0) {
mInputPort = device.openInputPort(0);
isSynthConnected = (mInputPort != null);
LogUtils.d(TAG, isSynthConnected ? "成功打开输入端口端口0" : "输入端口为null打开失败");
} else {
LogUtils.d(TAG, "设备无输入端口无法接收MIDI事件");
isSynthConnected = false;
}
} catch (Exception e) {
LogUtils.d(TAG, "打开端口失败: " + e.getMessage());
isSynthConnected = false;
}
}
/**
* 关闭MIDI设备及端口资源
*/
private void closeMidiResources() {
if (mInputPort != null) {
try {
mInputPort.close();
} catch (IOException e) {
LogUtils.d(TAG, "关闭输入端口失败: " + e.getMessage());
}
mInputPort = null;
}
if (mMidiDevice != null) {
try {
mMidiDevice.close();
} catch (IOException e) {
LogUtils.d(TAG, "关闭MIDI设备失败: " + e.getMessage());
}
mMidiDevice = null;
}
}
/**
* 通知合成器连接结果
*/
private void notifyConnectionResult(final boolean success) {
if (mConnectionListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mConnectionListener.onConnected(success);
}
});
}
}
/**
* 设置指定轨道静音状态
* @param trackIndex 轨道索引
* @param mute 是否静音
*/
public void setTrackMute(int trackIndex, boolean mute) {
if (mTracks == null) {
LogUtils.d(TAG, "设置静音失败未加载MIDI轨道");
return;
}
if (trackIndex >= 0 && trackIndex < mTracks.length) {
mTracks[trackIndex].setMute(mute);
LogUtils.d(TAG, "轨道[" + trackIndex + "] 静音状态: " + (mute ? "已静音" : "正常播放"));
} else {
LogUtils.d(TAG, "设置静音失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + "");
}
}
/**
* 切换当前播放轨道(-1表示所有轨道
* @param trackIndex 轨道索引
*/
public void setCurrentTrack(int trackIndex) {
if (mTracks == null) {
LogUtils.d(TAG, "切换轨道失败未加载MIDI轨道");
return;
}
// 校验轨道索引有效性
if (trackIndex == -1 || (trackIndex >= 0 && trackIndex < mTracks.length)) {
mCurrentTrack = trackIndex;
LogUtils.d(TAG, "切换当前播放轨道: " + (trackIndex == -1 ? "所有轨道" : "轨道[" + trackIndex + "]"));
} else {
LogUtils.d(TAG, "切换轨道失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + "");
}
}
/**
* 获取轨道总数
* @return 轨道数量
*/
public int getTrackCount() {
return mTracks != null ? mTracks.length : 0;
}
/**
* 暂停播放
*/
public void pause() {
boolean wasPlaying = isPlaying;
isPlaying = false;
LogUtils.d(TAG, "播放暂停: " + (wasPlaying ? "已暂停当前播放" : "当前未在播放"));
}
/**
* 停止播放并释放所有资源
*/
public void stop() {
boolean wasPlaying = isPlaying;
isPlaying = false;
isSynthConnected = false;
closeMidiResources();
if (mExecutor != null) {
mExecutor.shutdownNow();
mExecutor = null;
}
LogUtils.d(TAG, "播放停止: " + (wasPlaying ? "已终止播放并释放资源" : "当前未在播放"));
}
/**
* 获取线程池(单例)
*/
private ExecutorService getExecutor() {
if (mExecutor == null || mExecutor.isShutdown() || mExecutor.isTerminated()) {
mExecutor = Executors.newSingleThreadExecutor();
}
return mExecutor;
}
/**
* 检查合成器是否已连接
* @return 连接状态
*/
public boolean isSynthConnected() {
return isSynthConnected;
}
/**
* 获取当前MIDI输入端口
* @return 输入端口
*/
public MidiInputPort getInputPort() {
return mInputPort;
}
}

View File

@@ -0,0 +1,437 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/06/29 10:10
* @Describe Midi 播放窗口
*/
import android.app.Activity;
import android.content.Context;
import android.media.midi.MidiInputPort;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class MidiPlayerActivity extends WinBoLLActivity {
public static final String TAG = "MidiPlayerActivity";
private MidiPlayer mMidiPlayer;
private Button mPlayBtn, mPauseBtn, mStopBtn, mTestBtn;
private ListView mTrackListView;
private ListView mFileListView;
private TrackAdapter mTrackAdapter;
private TextView mFileNameTv;
private List<File> mMidiFileList = new ArrayList<>();
@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_midi_player);
// 初始化控件
mPlayBtn = (Button) findViewById(R.id.btn_play);
mPauseBtn = (Button) findViewById(R.id.btn_pause);
mStopBtn = (Button) findViewById(R.id.btn_stop);
mTestBtn = (Button) findViewById(R.id.btn_test);
mTrackListView = (ListView) findViewById(R.id.lv_tracks);
mFileListView = (ListView) findViewById(R.id.lv_midi_files);
mFileNameTv = (TextView) findViewById(R.id.tv_file_name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
initMidiPlayer();
loadMidiFileList();
initControlButtons();
initTestButton();
} else {
mFileNameTv.setText("当前设备不支持MIDI播放需Android 6.0及以上)");
disableButtons();
}
copyAssetsMidiFiles();
}
// 初始化测试按钮及1分钟测试逻辑
private void initTestButton() {
mTestBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testMidiOutput();
}
});
}
/**
* 1分钟MIDI测试序列包含多样音符组合验证输出功能
*/
private void testMidiOutput() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
ToastUtils.show("测试需要Android 6.0及以上");
return;
}
if (mMidiPlayer == null) {
initMidiPlayer();
}
new Thread(new Runnable() {
@Override
public void run() {
// 确保合成器连接
if (!mMidiPlayer.isSynthConnected()) {
LogUtils.d(TAG, "测试:连接合成器中...");
mMidiPlayer.connectToSynth();
// 最多等待3秒连接
int waitCount = 0;
while (!mMidiPlayer.isSynthConnected() && waitCount < 30) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
waitCount++;
}
}
final MidiInputPort inputPort = mMidiPlayer.getInputPort();
if (inputPort == null) {
LogUtils.d(TAG, "测试失败:无可用输入端口");
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("测试失败:未找到输入端口");
}
});
return;
}
try {
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("开始1分钟测试音符...");
}
});
// 测试序列定义:[音高, 力度, 时长(毫秒)]
int[][] noteSequence = {
// 0-10秒低音区短音符
{48, 64, 200}, {50, 64, 200}, {52, 64, 200}, {53, 64, 200},
{55, 64, 200}, {57, 64, 200}, {59, 64, 200}, {60, 64, 400},
// 10-20秒中音区连音
{60, 72, 300}, {62, 72, 300}, {64, 72, 300}, {65, 72, 300},
{67, 72, 300}, {69, 72, 300}, {71, 72, 300}, {72, 72, 600},
// 20-30秒高音区跳音
{72, 80, 150}, {76, 80, 150}, {79, 80, 150}, {84, 80, 300},
{79, 80, 150}, {76, 80, 150}, {72, 80, 150}, {69, 80, 450},
// 30-40秒和弦组合
{60, 64, 400}, {64, 64, 400}, {67, 64, 400}, // C和弦
{62, 64, 400}, {65, 64, 400}, {69, 64, 400}, // Dm和弦
{64, 64, 400}, {67, 64, 400}, {71, 64, 400}, // Em和弦
// 40-50秒渐强长音
{55, 40, 1000}, {55, 60, 1000}, {55, 80, 1000}, {55, 100, 1000},
// 50-60秒快速音阶
{50, 70, 100}, {52, 70, 100}, {53, 70, 100}, {55, 70, 100},
{57, 70, 100}, {59, 70, 100}, {60, 70, 100}, {62, 70, 100},
{64, 70, 100}, {65, 70, 100}, {67, 70, 100}, {69, 70, 100},
{71, 70, 100}, {72, 90, 500}
};
// 发送测试序列
for (int[] note : noteSequence) {
if (!mMidiPlayer.isSynthConnected()) {
LogUtils.d(TAG, "连接已断开,停止测试");
break;
}
int pitch = note[0];
int velocity = note[1];
int duration = note[2];
// 发送音符开启事件
byte[] noteOn = new byte[]{(byte) 0x90, (byte) pitch, (byte) velocity};
inputPort.send(noteOn, 0, noteOn.length);
//LogUtils.d(TAG, "测试音符:音高=" + pitch + ", 力度=" + velocity + ", 时长=" + duration + "ms");
// 等待音符时长
Thread.sleep(duration);
// 发送音符关闭事件
byte[] noteOff = new byte[]{(byte) 0x80, (byte) pitch, 0};
inputPort.send(noteOff, 0, noteOff.length);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("1分钟测试完成");
}
});
} catch (final Exception e) {
LogUtils.d(TAG, "测试出错:" + e.getMessage());
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("测试出错:" + e.getMessage());
}
});
}
}
}).start();
}
// 拷贝Assets中的MIDI文件到本地
private void copyAssetsMidiFiles() {
final AssetMidiCopier copier = new AssetMidiCopier(this);
new Thread(new Runnable() {
@Override
public void run() {
final boolean success = copier.copyMidiFiles();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (success) {
File midiDir = copier.getMidiTargetDir();
LogUtils.d(TAG, "MIDI文件拷贝成功路径" + midiDir.getAbsolutePath());
// 重新加载文件列表
loadMidiFileList();
} else {
ToastUtils.show("MIDI文件拷贝失败");
}
}
});
}
}).start();
}
// 初始化MIDI播放器
private void initMidiPlayer() {
mMidiPlayer = new MidiPlayer(this);
mMidiPlayer.setOnSynthConnectedListener(new MidiPlayer.OnSynthConnectedListener() {
@Override
public void onConnected(final boolean success) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (success) {
ToastUtils.show("MIDI合成器连接成功");
} else {
ToastUtils.show("未找到MIDI合成器请安装第三方合成器应用");
}
}
});
}
});
}
// 加载本地MIDI文件列表
private void loadMidiFileList() {
File midiDir = new File(getFilesDir(), "midi");
if (!midiDir.exists()) {
midiDir.mkdirs();
mFileNameTv.setText("midi目录已创建等待文件拷贝...");
return;
}
mMidiFileList.clear();
File[] files = midiDir.listFiles();
if (files != null) {
for (File file : files) {
String name = file.getName().toLowerCase();
if (name.endsWith(".mid") || name.endsWith(".midi")) {
mMidiFileList.add(file);
}
}
}
if (mMidiFileList.isEmpty()) {
mFileNameTv.setText("midi目录中无可用文件");
} else {
showFileList();
mFileNameTv.setText("找到" + mMidiFileList.size() + "个MIDI文件");
}
}
// 显示MIDI文件列表
private void showFileList() {
List<String> fileNameList = new ArrayList<>();
for (File file : mMidiFileList) {
fileNameList.add(file.getName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
fileNameList
);
mFileListView.setAdapter(adapter);
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mMidiPlayer.stop();
File selectedFile = mMidiFileList.get(position);
boolean loaded = mMidiPlayer.loadMidiFile(selectedFile);
if (loaded) {
mFileNameTv.setText("当前文件:" + selectedFile.getName());
initTrackList();
ToastUtils.show("已加载:" + selectedFile.getName());
} else {
mFileNameTv.setText("文件加载失败:" + selectedFile.getName());
}
}
});
}
// 初始化轨道列表
private void initTrackList() {
mTrackAdapter = new TrackAdapter(this, mMidiPlayer);
mTrackListView.setAdapter(mTrackAdapter);
}
// 初始化播放控制按钮
private void initControlButtons() {
mPlayBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mMidiPlayer != null) {
mMidiPlayer.start();
}
}
});
mPauseBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mMidiPlayer != null) {
mMidiPlayer.pause();
}
}
});
mStopBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mMidiPlayer != null) {
mMidiPlayer.stop();
}
}
});
}
// 禁用所有按钮(低版本设备)
private void disableButtons() {
mPlayBtn.setEnabled(false);
mPauseBtn.setEnabled(false);
mStopBtn.setEnabled(false);
mTestBtn.setEnabled(false);
}
// 轨道列表适配器
private class TrackAdapter extends android.widget.BaseAdapter {
private Context mContext;
private MidiPlayer mPlayer;
public TrackAdapter(Context context, MidiPlayer player) {
mContext = context;
mPlayer = player;
}
@Override
public int getCount() {
return mPlayer != null ? mPlayer.getTrackCount() : 0;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view = getLayoutInflater().inflate(R.layout.item_track, parent, false);
TextView trackNameTv = (TextView) view.findViewById(R.id.tv_track_name);
final Button muteBtn = (Button) view.findViewById(R.id.btn_mute);
SeekBar volumeSb = (SeekBar) view.findViewById(R.id.sb_volume);
trackNameTv.setText("轨道 " + (position + 1));
muteBtn.setText("静音");
// 静音按钮逻辑
muteBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isMuted = muteBtn.isSelected();
mPlayer.setTrackMute(position, !isMuted);
muteBtn.setSelected(!isMuted);
muteBtn.setText(isMuted ? "静音" : "已静音");
}
});
// 音量条(预留逻辑)
volumeSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 可添加音量控制逻辑如发送MIDI音量事件
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
return view;
}
}
// 日志查看按钮点击事件
public void onLog(View view) {
App.getWinBoLLActivityManager().startLogActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mMidiPlayer != null) {
mMidiPlayer.stop();
}
}
}

View File

@@ -0,0 +1,78 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:24
* @Describe Midi轨道类用于存储和管理单条Midi轨道的事件
*/
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class MidiTrack {
public static final String TAG = "MidiTrack";
// 轨道事件列表存储Midi事件字节数组
private List<byte[]> mEvents = new ArrayList<>();
// 事件迭代器(用于播放时遍历事件)
private Iterator<byte[]> mEventIterator;
// 轨道是否静音
private boolean isMuted = false;
public MidiTrack() {
}
/**
* 添加Midi事件到轨道
*/
public void addEvent(byte[] event) {
mEvents.add(event);
}
/**
* 重置轨道播放状态(回到起始位置)
*/
public void reset() {
mEventIterator = mEvents.iterator();
}
/**
* 判断是否有下一个事件
*/
public boolean hasNextEvent() {
// 如果静音返回false不播放事件
if (isMuted) {
return false;
}
// 初始化迭代器(首次使用或重置后)
if (mEventIterator == null) {
reset();
}
return mEventIterator.hasNext();
}
/**
* 获取下一个Midi事件
*/
public byte[] nextEvent() {
if (mEventIterator == null) {
reset();
}
return mEventIterator.next();
}
/**
* 设置轨道静音状态
*/
public void setMute(boolean mute) {
isMuted = mute;
}
/**
* 获取轨道事件数量
*/
public int getEventCount() {
return mEvents.size();
}
}

View File

@@ -0,0 +1,59 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:12
* @Describe SoundFontManager
*/
import android.content.Context;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
// 简化版实际需使用SoundFont解析库如Audiokit、FluidSynth
public class SoundFontManager {
public static final String TAG = "SoundFontManager";
private Context mContext;
private File mSoundFontFile;
public SoundFontManager(Context context) {
mContext = context;
}
// 加载SoundFont文件.sf2格式
public boolean loadSoundFont(File file) {
if (!file.exists() || !file.getName().endsWith(".sf2")) {
LogUtils.d(TAG, "无效的SoundFont文件");
return false;
}
// 复制到应用私有目录(可选)
try {
File dest = new File(mContext.getFilesDir(), "custom_soundfont.sf2");
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
fos.close();
mSoundFontFile = dest;
return true;
} catch (IOException e) {
LogUtils.d(TAG, "复制SoundFont失败: " + e.getMessage());
return false;
}
}
// 获取加载的SoundFont路径供合成器使用
public String getSoundFontPath() {
return mSoundFontFile != null ? mSoundFontFile.getAbsolutePath() : null;
}
}

View File

@@ -0,0 +1,60 @@
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:40
* @Describe WinBoLLActivity
*/
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, String.format("onResume %s", getTag()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
/*if (item.getItemId() == R.id.item_log) {
GlobalApplication.getWinBoLLActivityManager().startLogActivity(this);
return true;
} else if (item.getItemId() == R.id.item_home) {
GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
return true;
}*/
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
GlobalApplication.getWinBoLLActivityManager().add(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
}
}

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,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="#26A69A"
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,57 @@
<?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="MidiPlayer"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OpenMidiPlayer"
android:onClick="onOpenMidiPlayer"/>
</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,91 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="MIDI文件列表"
android:background="@android:color/darker_gray"/>
<ListView
android:id="@+id/lv_midi_files"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/tv_file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="未选择文件"
android:background="@android:color/holo_blue_light"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="轨道控制"
android:background="@android:color/darker_gray"/>
<ListView
android:id="@+id/lv_tracks"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放"/>
<Button
android:id="@+id/btn_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂停"/>
<Button
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试音符"
android:layout_below="@id/btn_stop"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Log"
android:onClick="onLog"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:id="@+id/tv_track_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="轨道 1" />
<Button
android:id="@+id/btn_mute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="静音" />
<Button
android:id="@+id/btn_solo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="独奏" />
<SeekBar
android:id="@+id/sb_volume"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:max="100"
android:progress="80" />
</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

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