Compare commits

..

107 Commits

Author SHA1 Message Date
ZhanGSKen 26301c68fa <mygallery>APK 15.0.20 release Publish. 2026-08-23 08:47:32 +08:00
BigPickle 9f0d8fbffb 已置顶相册长按菜单新增“置顶”功能,支持再次置顶到最前
PinnedAlbumDbHelper 新增 repinAlbumToFront():将当前相册集
排序号置为1,其余置顶记录整体后移一位;并新增
normalizeSortOrders() 按现有顺序把所有排序号重排为连续的
1..n,修正数据库中可能出现的序号空洞或重复。
AlbumAdapter 中已置顶相册的弹出菜单扩展为「取消置顶」「置顶」
两项,点击「置顶」即重新置顶到最前并刷新列表。
2026-08-23 08:44:07 +08:00
ZhanGSKen c270ef9d1f 更新源码维护人员联系信息。 2026-08-23 08:27:02 +08:00
ZhanGSKen 3b92af52f6 <mygallery>APK 15.0.19 release Publish. 2026-08-23 08:17:44 +08:00
BigPickle 6fb6fac2ba 修复封面剪裁窗口空白的包名残留问题
模块更名为 mygallery 后,activity_crop.xml 中自定义视图
仍引用旧包 cc.winboll.studio.gallery,导致 CropActivity
布局膨胀失败、剪裁窗口显示空白。现统一更正为
cc.winboll.studio.mygallery 包名引用,并同步更新
ACTION_COVER_UPDATED 广播 action 常量。
2026-08-23 08:10:38 +08:00
ZhanGSKen 04cd87842a 更新基础类库引用 2026-08-23 07:49:39 +08:00
ZhanGSKen 1b86e70069 更新应用介绍和资源链接 2026-08-23 07:40:42 +08:00
ZhanGSKen 13f1aa9e1f 更新应用默认显示名称 2026-08-23 07:22:25 +08:00
ZhanGSKen 675fa60670 设置应用图标配置规则,区分beta图标stage图标。 2026-08-23 07:19:10 +08:00
ZhanGSKen 0383f7c72d 更新java源码应用模块文件夹名称gallery为mygallery。 2026-08-23 07:03:06 +08:00
ZhanGSKen 800ba03d0f 更新应用配置模块名称为mygallery 2026-08-23 06:57:53 +08:00
ZhanGSKen 6aa19195f6 更换应用模块gallery为mygallery 2026-08-23 06:53:40 +08:00
ZhanGSKen 3c84c8205a 添加新应用图标资源 2026-08-23 06:49:53 +08:00
ZhanGSKen 0c348db4f7 <gallery>APK 15.0.18 release Publish. 2026-07-20 19:20:48 +08:00
MiMo a9f3c9fa81 feat(gallery): 实现置顶相册排序功能
1. 数据库升级
   - PinnedAlbumDbHelper 版本升级至 2
   - 新增 sort_order 字段存储排序号
   - 自动迁移旧数据并分配排序号

2. 置顶排序逻辑
   - 新置顶相册排序号设为1,其他相册排序号+1
   - 取消置顶时,后续相册排序号自动-1
   - 使用事务确保数据一致性

3. 列表加载与刷新
   - 加载相册时按排序号由小到大排列
   - 刷新列表时保持排序号顺序显示
   - 使用 LinkedHashMap 保持插入顺序

4. 调试日志
   - 置顶操作后输出所有相册排序号
   - 取消置顶时输出原排序号

修改文件:
- PinnedAlbumDbHelper.java
- MainActivity.java
- AlbumAdapter.java
- build.properties
2026-07-20 19:15:59 +08:00
ZhanGSKen ce01ecf138 <gallery>APK 15.0.17 release Publish. 2026-07-19 15:10:50 +08:00
MiMo 9bc9cf3700 Gallery: 主窗口加载性能优化
- loadAlbums移至后台线程(ExecutorService+Handler),主线程不再阻塞
- 消除重复getImagesInFolder调用,MediaStore查询减少50%
- onResume移除scanMediaStore避免重复磁盘扫描
- pinned状态批量加载到Set,SQLite查询从3N次降为1次
- AlbumAdapter移除冗余sortAlbums,直接接收已排序数据
- Cursor改用try-finally确保关闭,移除冗余日志输出
2026-07-19 15:02:22 +08:00
ZhanGSKen e97c5fe050 转换旧有的应用基础类为libappbase的GlobalApplication基础类。 2026-07-19 14:13:22 +08:00
MiMo 14ed5d60c5 更正编译参数为最后发布的基准参数。 2026-07-19 13:54:17 +08:00
MiMo dbd10b88d9 合并手机端开发的分支文件,修复编译参数不同步问题。 2026-07-19 13:51:55 +08:00
MiMo a4acdce1de Gallery: 添加App入口类,调试菜单动态显隐
- 新增App类继承GlobalWinBoLLApplication,参照appbase添加崩溃处理
- Manifest application节点android:name切换为App类
- MainActivity菜单栏Debug Log项根据GlobalWinBoLLApplication.isDebugging()控制可见性
2026-07-19 13:46:50 +08:00
ZhanGSKen 507d614602 <gallery>APK 15.0.16 release Publish. 2026-07-18 15:42:59 +08:00
ZhanGSKen 261ae68193 更换主窗口菜单项action_mi_gallery的图标资源,修改为原有图标的透明背景方式的图标。 2026-07-18 15:35:56 +08:00
MiMo eed003f590 Gallery: 添加小米图标资源,替换菜单图标
- 新增 drawable/ic_mi_gallery2.xml (layer-list 引用 mi.png)
- menu_main action_mi_gallery 图标改为 ic_mi_gallery2
2026-07-18 15:24:19 +08:00
ZhanGSKen 01b2d4a4c3 添加小米旧版logo 2026-07-18 15:12:11 +08:00
ZhanGSKen 394efd3892 更新源码仓库地址 2026-07-18 15:11:46 +08:00
MiMo 184d4eef64 Gallery: 升级依赖版本,配置主题系统参照aes模块
1. 依赖升级
   - libaes 15.15.9 → 15.20.21
   - libappbase 15.15.21 → 15.20.34

2. 主题配置(参照aes模块)
   - AppTheme → GalleryTheme (parent: AESTheme)
   - 新增 values-night/ 支持暗色模式
   - 补充主题颜色定义

3. 编译修复
   - Adapter onBindViewHolder holder 参数加 final(Java 7 要求)
2026-07-18 14:56:28 +08:00
ZhanGSKen 975af960a4 修改编译目标适配的安卓系统版本 2026-07-18 13:44:11 +08:00
ZhanGSKen 2619974da6 恢复原WinBoLL项目,Gallery分支。 2026-07-18 13:37:46 +08:00
ZhanGSKen e136045d1f Merge remote-tracking branch 'winboll_https/projects_keeper_tag' into mywinboll 2026-07-11 20:21:13 +08:00
qinglong bd635425a1 合并模块AES 同步最新时间标签aes-v15.20.18 2026-07-11 20:17:35 +08:00
BigPickle 49414a3938 App/MainActivity: 修复主题切换 crash 及工具栏主题不刷新问题
- App.onCreate 添加 AESThemeUtil.init(null) 初始化 themeStyleIDList,
  修复点击主题菜单时 IndexOutOfBoundsException 崩溃
- MainActivity.onCreate 添加 AESThemeUtil.applyAppCompatTheme(this)
  置于 super.onCreate 之前,使 recreate() 后应用已保存的主题样式
2026-07-02 01:34:58 +08:00
BigPickle 08a47768a1 MainActivity: 接入 AESThemeUtil/DevelopUtils 工具类,替换 Toolbar 为 ASupportToolbar
- MainActivity 新增 onCreateOptionsMenu/onOptionsItemSelected,
  使用 AESThemeUtil.inflateCompatThemeMenu 加载主题菜单、
  DevelopUtils.inflateMenu 加载调试菜单
- 布局文件 Toolbar 替换为 cc.winboll.studio.libaes.views.ASupportToolbar,
  移除 AppBarLayout 包裹层
- MyAppTheme 日间/夜间主题增加 aSupportToolbar 属性引用 AESASupportToolbar 样式
2026-07-02 01:26:50 +08:00
ZhanGSKen 79f161b559 fix: 修复 MyAppTheme 主题继承链,适配 Android 16(SDK 36)
- 修复 view_log.xml 自定义属性解析失败崩溃(需要 APPBaseTheme 的属性集)
- 修复 AppCompatActivity 需要 Theme.AppCompat 子主题的 IllegalStateException
- 父主题改为 Theme.AppCompat.Light.NoActionBar / Theme.AppCompat.NoActionBar
- 新增 values-night/ 日夜模式自动切换支持
- 主色调从 Teal/Orange 更换为 Blue/Amber 专业配色方案
2026-07-01 17:06:20 +08:00
LaizyBoy b14538db47 20260701_163734_738 2026-07-01 16:37:41 +08:00
ZhanGSKen c9e16a9a1e 编译配置完成 2026-07-01 15:53:23 +08:00
ZhanGSKen 42dbfb4238 加入WinBoLL框架文件 2026-07-01 15:47:18 +08:00
ZhanGSKen b274503187 20260701_154438_390 2026-07-01 15:45:11 +08:00
ZhanGSKen b50e4e02ea 修复git管理脚本 2026-07-01 04:49:24 +08:00
ZhanGSKen 84f1c820cb 修复git管理脚本 2026-07-01 04:48:30 +08:00
ZhanGSKen 0e60d75ac6 修复git管理脚本 2026-07-01 04:44:47 +08:00
ZhanGSKen 773cc9f6c1 修复git管理脚本 2026-07-01 04:42:45 +08:00
ZhanGSKen ca33dab4da 修复git管理脚本 2026-07-01 04:33:36 +08:00
ZhanGSKen eee8809485 修复git管理脚本 2026-07-01 04:11:36 +08:00
ZhanGSKen 69f79549e9 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-07-01 04:10:40 +08:00
ZhanGSKen f204cd2608 修复git管理脚本 2026-07-01 04:08:42 +08:00
ZhanGSKen 2d3592690a 更新新版winboll架构 2026-07-01 03:49:09 +08:00
ZhanGSKen 576f8bb5e5 更新新版winboll架构 2026-07-01 03:48:29 +08:00
qinglong e787697d6b 合并模块AES 同步最新时间标签aes-v15.20.17 2026-06-30 19:00:05 +08:00
qinglong 3db3edcf2c 合并模块APPBase 同步最新时间标签appbase-v15.20.34 2026-06-30 19:00:05 +08:00
qinglong 643d08bf22 合并模块AES 同步最新时间标签aes-v15.20.16 2026-06-26 02:00:28 +08:00
qinglong beefb16c32 合并模块APPBase 同步最新时间标签appbase-v15.20.33 2026-06-26 02:00:28 +08:00
qinglong 878009ec9f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-06-26 02:00:28 +08:00
qinglong 2d8b5d515b 合并模块AES 同步最新时间标签aes-v15.20.15 2026-06-03 20:30:22 +08:00
qinglong 1453287667 合并模块APPBase 同步最新时间标签appbase-v15.20.26 2026-06-03 20:30:22 +08:00
qinglong 5fc59f3305 合并模块AES 同步最新时间标签aes-v15.20.14 2026-06-03 08:00:01 +08:00
qinglong b6ba73126f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.7 2026-06-03 08:00:01 +08:00
qinglong bdd8d1e2b6 合并模块APPBase 同步最新时间标签appbase-v15.20.25 2026-06-03 07:00:01 +08:00
qinglong 138ca80e35 合并模块APPBase 同步最新时间标签appbase-v15.20.23 2026-06-03 06:06:41 +08:00
qinglong 7a3b54b78b 合并模块WinBoLL 同步最新时间标签winboll-v15.20.6 2026-06-02 21:00:01 +08:00
qinglong db804d1897 合并模块AES 同步最新时间标签aes-v15.20.12 2026-06-02 08:55:05 +08:00
qinglong 039c8fcd98 合并模块WinBoLL 同步最新时间标签winboll-v15.20.5 2026-06-02 04:00:01 +08:00
qinglong ae63d1ec0a 合并模块AES 同步最新时间标签aes-v15.20.11 2026-06-02 03:00:01 +08:00
qinglong 9e9486b488 合并模块WinBoLL 同步最新时间标签winboll-v15.20.4 2026-06-01 21:00:01 +08:00
qinglong 4b8967b253 合并模块WinBoLL 同步最新时间标签winboll-v15.20.3 2026-05-31 21:00:02 +08:00
qinglong b1059c3f46 合并模块AES 同步最新时间标签aes-v15.20.10 2026-05-27 20:26:41 +08:00
qinglong f3d723fbee 合并模块APPBase 同步最新时间标签appbase-v15.20.22 2026-05-27 15:00:01 +08:00
qinglong e3c30ea9a3 合并模块AES 同步最新时间标签aes-v15.20.9 2026-05-24 10:49:26 +08:00
qinglong ab4cef21f0 合并模块APPBase 同步最新时间标签appbase-v15.20.21 2026-05-24 10:00:01 +08:00
QingLong 2754a2ad7c 合并模块AES 同步最新时间标签aes-v15.20.8 2026-05-20 21:00:01 +08:00
QingLong 8876896cbc 合并模块APPBase 同步最新时间标签appbase-v15.20.20 2026-05-20 17:00:01 +08:00
QingLong 28e6a8ee78 合并模块APPBase 同步最新时间标签appbase-v15.20.19 2026-05-20 15:00:01 +08:00
QingLong 7d872fd14c 合并模块APPBase 同步最新时间标签appbase-v15.20.18 2026-05-20 04:00:01 +08:00
QingLong d6fab2133f 合并模块AES 同步最新时间标签aes-v15.20.7 2026-05-19 21:00:01 +08:00
QingLong 45821b8daa 合并模块APPBase 同步最新时间标签appbase-v15.20.17 2026-05-19 21:00:01 +08:00
QingLong edd312a525 合并模块APPBase 同步最新时间标签appbase-v15.20.16 2026-05-19 19:00:01 +08:00
QingLong aec680e36e 合并模块AES 同步最新时间标签aes-v15.20.6 2026-05-19 01:53:02 +08:00
QingLong 1c6f7783e5 合并模块AES 同步最新时间标签aes-v15.20.5 2026-05-19 01:49:06 +08:00
qinglong 193011a80d 合并模块AES 同步最新时间标签aes-v15.20.4 2026-05-17 21:00:01 +08:00
qinglong eb3d8f4806 合并模块APPBase 同步最新时间标签appbase-v15.20.15 2026-05-17 16:20:01 +08:00
ZhanGSKen aa1b701c8b 合并模块APPBase 同步最新时间标签appbase-v15.20.14 2026-05-17 15:55:39 +08:00
ZhanGSKen fc20bd56b4 合并模块APPBase 同步最新时间标签appbase-v15.20.13 2026-05-17 15:52:14 +08:00
ZhanGSKen e92538850f 合并模块APPBase 同步最新时间标签appbase-v15.20.12 2026-05-17 15:46:18 +08:00
ZhanGSKen 81b7af0ec4 1. 排序规则彻底换掉
不再按版本号排序,按标签创建时间从新到旧,永远拿到最后发布的最新标签
​
2. 增加  -f  强制覆盖,确保拉取最新文件
​
3. 你现在  appbase-v15.20.12  是后打的标签,脚本会直接优先抓取它
​
4. 无差异也会正常同步最新标签版本文件
2026-05-17 15:45:57 +08:00
ZhanGSKen e2b4bddb61 BugFix 2026-05-17 15:43:18 +08:00
ZhanGSKen cbca61ad84 1. 删掉手动拼接标签逻辑,直接取用远程完整标签名,杜绝  appbase- 空格版本
​
2. 使用  git rev-parse --verify 标签^{commit}  精准拿远程标签commit
​
3. 合并来源强制指定  origin/模块名  远程分支
​
4. 空标签直接跳过,不再出现  xxx-  非法标签
​
5. 检出语法标准: git checkout 远程分支 commit -- 目录
2026-05-17 15:37:37 +08:00
ZhanGSKen 28bc3a86e3 1. 版本排序彻底修复
按  大版本.中版本.小版本.迭代号  分段数字排序,优先抓取  appbase-v15.20.12  而不是旧版
​
2. 检出命令修正
统一使用  git checkout 提交哈希 -- 目录  语法,不再报路径不存在
2026-05-17 15:34:14 +08:00
ZhanGSKen 79f7f682b3 合并模块APPBase 来源最新标签:appbase-v15.20.11 提交点:c7184ad6df0092106e535308ba486a9503395042 2026-05-17 15:25:41 +08:00
ZhanGSKen 8fa09a9f99 demoapp、winboll、aes  没有  xxx-版本号  格式标签,跳过属于正常
​
2.  appbase  成功抓到标签和commit,只是命令语法错导致拉取失败,改完即可正常合并
2026-05-17 15:25:10 +08:00
ZhanGSKen 555c5deef9 1.  get_module_latest_tag  → 抓取远程该模块最新Tag
​
2.  get_commit_by_tag  → 用最新Tag查出唯一对应提交Commit
​
3. 校验Tag/Commit非空
​
4. 执行合并命令git checkout origin/模块名  commit哈希:模块文件夹
 
 
 
5. 仅添加对应目录,生成带标签+commit的提交日志
​
6. 无标签直接跳过,不产生任何操作
2026-05-17 15:22:31 +08:00
ZhanGSKen 3707891e0b 1. 调用函数 → 查出模块标签 + 标签绑定提交Commit哈希
​
2. 判定标签/Commit为空直接跳过
​
3. 合并命令: git checkout origin/模块名  commit哈希:模块文件夹
​
4. 仅添加当前模块目录,精准提交
​
5. 提交日志同时记录标签名+提交哈希,方便回溯
2026-05-17 15:17:40 +08:00
ZhanGSKen 0c9c6c9fd4 1. 调试日志重定向  >&2
彻底杜绝调试文字混入标签变量, MOD_TAG  只存纯净标签名
​
2. 拉取标签写法修正
旧: git checkout 标签名 目录 
新: git checkout origin/tags/标签名 -- 目录 
直接从远程标签仓库拉取对应提交点文件夹,100%命中标签代码
​
3. 精准add提交
放弃  git add . ,只添加当前合并的模块文件夹,避免乱提交
​
4. 强制全量同步标签
脚本开头双重 fetch --tags ,保证本地能读取所有远程版本标签
​
5. 空标签直接拦截
匹配不到标签完全跳过,不产生空提交、无效覆盖
​
6. 严格锁定本地分支,防止合并错分支
2026-05-17 15:11:21 +08:00
ZhanGSKen be3d042ade 设置合并最新标签的模块文件夹。取消合并最新提交的文件夹。 2026-05-17 14:22:14 +08:00
qinglong 33dc49267e 合并 APPBase 项目, 调试:模块[appbase] 远程分支最新Commit = 74299bf9b2 2026-05-17 13:00:02 +08:00
ZhanGSKen e3166b639e 操作步骤调整 2026-05-17 12:13:50 +08:00
ZhanGSKen 3d26bbe090 提交内容整理 2026-05-17 12:08:22 +08:00
ZhanGSKen cd375cefc5 直接取远程分支 origin/xxx 最新全量 Commit,不限制子目录
​
2. 优化标签匹配:只要标签前缀是  模块名-  就匹配
​
3. 保留你原有全部脚本结构,只改  get_module_latest_tag  函数
2026-05-17 11:35:41 +08:00
ZhanGSKen 025c095bcd 提交备注自动带上标签名 2026-05-17 11:18:06 +08:00
ZhanGSKen c88a0a52a8 修复标签获取bug 2026-05-17 11:09:45 +08:00
ZhanGSKen dc4eeefa55 更正分支环境检测名称 2026-05-17 11:04:46 +08:00
ZhanGSKen c7d79e160b 添加Projects_Keeper_Tag仓库合并管理脚本 2026-05-17 10:50:04 +08:00
qinglong 771f591a3a 合并 APPBase 项目 2026-05-16 17:30:01 +08:00
ZhanGSKen 8aaec10d74 更新输出提示,区分合并的项目类型 2026-05-16 11:17:41 +08:00
ZhanGSKen a3d5031cdc 添加推送命令 2026-05-16 10:44:24 +08:00
ZhanGSKen 9a7f651d80 合并 APPBase 项目 2026-05-16 10:23:40 +08:00
ZhanGSKen ba3877f3cc 合并 WinBoLL 项目 2026-05-16 10:23:40 +08:00
ZhanGSKen 7e2f48f33a 添加projects-keeper分支项目自动合并脚本。 2026-05-16 10:23:05 +08:00
234 changed files with 10256 additions and 1246 deletions
+3 -45
View File
@@ -94,50 +94,8 @@ lint-results.html
## 忽略 AndroidIDE 临时文件夹
.androidide
## WinBoLL 基础应用(避免上传敏感配置)
## MyWinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
/settings.gradle
/gradle.properties
## WinBoLL 项目配置
#.git
#.gitignore
#.gitmodules
#.gradle
#.winboll
#GenKeyStore
#LICENSE
#LICENSE-Private-Demo
#LICENSE-Private-Demo_docs
#README.md
#aes
#appbase
appkey.jks
appkey.keystore
autonfc
#build.gradle
contacts
debugtemp
gallery
gpsrelaysentinel
#gradle
gradle.properties
#gradle.properties-android-demo
#gradle.properties-androidx-demo
#gradlew
#libaes
#libappbase
libdebugtemp
libgpsrelaysentinel
#libwinboll
local.properties
#local.properties-demo
mymessagemanager
positions
powerbell
settings.gradle
#settings.gradle-demo
#winboll
winboll.properties
#winboll.properties-demo
#/settings.gradle
#/gradle.properties
@@ -0,0 +1,191 @@
#!/system/bin/sh
## 合并其他项目分支的模块源码到projects_keeper分支。
# ====================== 0. 进入目标目录 ======================
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper"
echo "切换工作目录到:$TARGET_DIR"
if ! cd "$TARGET_DIR"; then
echo "=============================================="
echo "错误:无法进入目标目录 $TARGET_DIR"
echo "=============================================="
exit 1
fi
# ====================== 1. 拉取远程最新源码(失败直接退出) ======================
echo "=============================================="
echo "正在拉取远程最新源码..."
echo "=============================================="
if ! git pull; then
echo "=============================================="
echo "错误:拉取远程源码失败!"
echo "请检查网络、仓库冲突或手动处理 git 状态后再执行脚本。"
echo "=============================================="
exit 1
fi
echo "✅ 源码已拉取为最新状态"
echo ""
# ====================== 2. 目录路径检查 ======================
CUR_DIR=$(pwd)
if [ "$CUR_DIR" != "$TARGET_DIR" ]; then
echo "错误:当前目录不是项目根目录!"
echo "请先进入目录:$TARGET_DIR"
exit 1
fi
# ====================== 3. Git 分支检查 ======================
CUR_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
TARGET_BRANCH="projects_keeper"
if [ "$CUR_BRANCH" != "$TARGET_BRANCH" ]; then
echo "错误:当前不在 $TARGET_BRANCH 分支!"
echo "当前分支:$CUR_BRANCH"
echo "请先执行:git checkout $TARGET_BRANCH"
exit 1
fi
# ====================== 4. 预设标准模块列表 ======================
MERGE_OBJECTS_LIST=(
.git
.gitignore
.gitmodules
.winboll
GenKeyStore
LICENSE
LICENSE-Private-Demo
LICENSE-Private-Demo_docs
README.md
aes
appbase
autonfc
build.gradle
contacts
debugtemp
gallery
gpsrelaysentinel
gradle
gradle.properties-android-demo
gradle.properties-androidx-demo
gradlew
libaes
libappbase
libdebugtemp
libgpsrelaysentinel
local.properties-demo
mymessagemanager
positions
powerbell
settings.gradle-demo
winboll
winboll.properties-demo
)
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . .. ======================
REAL_ITEMS=()
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
while IFS= read -r line; do
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
# ====================== 6. 差异比对函数 ======================
check_diff() {
local missing=()
local extra=()
for item in "${MERGE_OBJECTS_LIST[@]}"; do
local found=0
for r in "${REAL_ITEMS[@]}"; do
if [[ "$item" == "$r" ]]; then
found=1
break
fi
done
if (( found == 0 )); then
missing+=("$item")
fi
done
for r in "${REAL_ITEMS[@]}"; do
local found=0
for item in "${MERGE_OBJECTS_LIST[@]}"; do
if [[ "$item" == "$r" ]]; then
found=1
break
fi
done
if (( found == 0 )); then
extra+=("$r")
fi
done
if [[ ${#missing[@]} -gt 0 || ${#extra[@]} -gt 0 ]]; then
echo "=============================================="
echo "【错误】合并环境与脚本预设列表不匹配!"
echo "请手动核对并修改合并脚本。"
echo "=============================================="
if [[ ${#missing[@]} -gt 0 ]]; then
echo -e "\n👉 本地缺失项目:"
for m in "${missing[@]}"; do echo " $m"; done
fi
if [[ ${#extra[@]} -gt 0 ]]; then
echo -e "\n👉 本地多余项目:"
for e in "${extra[@]}"; do echo " $e"; done
fi
echo -e "\n=============================================="
exit 1
fi
}
# 执行差异校验
check_diff
# ====================== 全部校验通过,继续执行合并 ======================
echo -e "#@@@ 开始合并模块源码 @@@#
## 目标合并对象列表:"
for item in "${MERGE_OBJECTS_LIST[@]}"; do
echo "$item"
done
echo -e "## 对象列表结束
"
## 合并 APP 项目
MERGE_APP_PROJECT_LIST=(
WinBoLL
)
echo -e "#@@@ 开始合并应用型模块源码 @@@#
## 目标合并对象列表:"
for item in "${MERGE_APP_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower
git add .
git commit -m "合并 $item 项目"
done
## 合并 LIB 项目
MERGE_LIB_PROJECT_LIST=(
APPBase
AES
)
echo -e "#@@@ 开始合并类库型模块源码 @@@#
## 目标合并对象列表:"
for item in "${MERGE_LIB_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower lib$item_lower
git add .
git commit -m "合并 $item 项目"
done
echo '正在推送 Projects_Keeper 项目'
git push
@@ -0,0 +1,158 @@
#!/system/bin/sh
## 逻辑:按时间取最新标签 → 拉取该标签目录合并
# 按创建时间取模块最新标签(最新打的标签排最后)
get_latest_module_tag(){
local mod=$1
git for-each-ref --sort=-creatordate --format='%(refname)' refs/tags/${mod}-* \
| grep -v '\^{}' \
| head -1 \
| sed 's/refs\/tags\///'
}
# 通过标签获取commit
get_commit_from_tag(){
local tag=$1
git rev-parse --short "${tag}^{commit}"
}
# 工作目录
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper_Tag"
echo "进入目录:${TARGET_DIR}"
cd "${TARGET_DIR}" || exit 1
# 同步远程
echo "========================================"
echo "同步远程分支与全部版本标签"
echo "========================================"
git fetch origin --prune
git fetch origin --tags
echo "同步完成"
echo ""
# 校验目标分支
NOW_BRANCH=$(git symbolic-ref --short HEAD)
TARGET_BRANCH="projects_keeper_tag"
if [ "${NOW_BRANCH}" != "${TARGET_BRANCH}" ];then
echo "错误:请先切换到 ${TARGET_BRANCH} 分支"
exit 1
fi
# 目录结构校验白名单(不含 . ..)
MERGE_OBJECTS_LIST=(
.git
.gitignore
.gitmodules
.winboll
GenKeyStore
LICENSE
LICENSE-Private-Demo
LICENSE-Private-Demo_docs
README.md
aes
appbase
autonfc
build.gradle
contacts
debugtemp
gallery
gpsrelaysentinel
gradle
gradle.properties-android-demo
gradle.properties-androidx-demo
gradlew
libaes
libappbase
libdebugtemp
libgpsrelaysentinel
local.properties-demo
mymessagemanager
positions
powerbell
settings.gradle-demo
winboll
winboll.properties-demo
)
REAL_ITEMS=()
# 标准排序ls输出,循环强制过滤 . 和 ..
while IFS= read -r line; do
# 跳过虚拟目录 . 和 ..
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
check_diff(){
local miss=() extra=()
for i in "${MERGE_OBJECTS_LIST[@]}";do
[[ ! " ${REAL_ITEMS[@]} " =~ " ${i} " ]] && miss+=("$i")
done
for i in "${REAL_ITEMS[@]}";do
[[ ! " ${MERGE_OBJECTS_LIST[@]} " =~ " ${i} " ]] && extra+=("$i")
done
if [[ ${#miss[@]} -gt 0 || ${#extra[@]} -gt 0 ]];then
echo "========================================"
echo "本地目录结构不一致,终止运行"
if [[ ${#miss[@]} -gt 0 ]]; then
echo -e "\n缺失条目:"
for m in "${miss[@]}"; do echo " $m"; done
fi
if [[ ${#extra[@]} -gt 0 ]]; then
echo -e "\n多余条目:"
for e in "${extra[@]}"; do echo " $e"; done
fi
echo "========================================"
exit 1
fi
}
check_diff
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
# 应用型模块
MERGE_APP_PROJECT_LIST=(WinBoLL)
echo -e "---------- 应用型模块 ----------"
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
tag=$(get_latest_module_tag "${low_name}")
if [[ -z "${tag}" ]];then
echo "跳过 ${low_name}:无远程标签"
continue
fi
commit=$(get_commit_from_tag "${tag}")
if [[ -z "${commit}" ]];then
echo "跳过 ${low_name}:标签无有效提交点"
continue
fi
echo "模块:${low_name} 最新时间标签:${tag} 提交ID:${commit}"
# 强制拉取覆盖
git checkout -f "${tag}" -- "${low_name}"
git add "${low_name}"
git commit -m "合并模块${name} 同步最新时间标签${tag}"
done
# 类库模块
MERGE_LIB_PROJECT_LIST=(APPBase AES)
echo -e "---------- 类库模块 ----------"
for name in "${MERGE_LIB_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
tag=$(get_latest_module_tag "${low_name}")
if [[ -z "${tag}" ]];then
echo "跳过 ${low_name}:无远程标签"
continue
fi
commit=$(get_commit_from_tag "${tag}")
if [[ -z "${commit}" ]];then
echo "跳过 ${low_name}:标签无有效提交点"
continue
fi
echo "模块:${low_name} 最新时间标签:${tag} 提交ID:${commit}"
git checkout -f "${tag}" -- "${low_name}" "lib${low_name}"
git add "${low_name}" "lib${low_name}"
git commit -m "合并模块${name} 同步最新时间标签${tag}"
done
echo "全部模块合并执行完毕"
echo "执行推送:git push"
git push
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/bash
# ==============================================================================
# WinBoLL 应用发布脚本
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
# 依赖:build.properties、app_update_description.txt(项目根目录下)
# 使用:./script_name.sh <APP_NAME>
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
# ==============================================================================
# ==================== 常量定义 ====================
# 脚本退出码
EXIT_CODE_SUCCESS=0
EXIT_CODE_ERR_NO_APP_NAME=2
EXIT_CODE_ERR_WORK_DIR=1
EXIT_CODE_ERR_GIT_CHECK=1
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
# Gradle 任务(正式发布)
GRADLE_TASK_PUBLISH="assembleStageRelease"
# Gradle 任务(调试用,注释备用)
# GRADLE_TASK_DEBUG="assembleBetaDebug"
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# 禁用Gradle守护进程
GRADLE_NO_DAEMON="--no-daemon"
# ==================== 函数定义 ====================
# 检查Git源码是否已完全提交(无未提交变更)
# 返回值:0=已完全提交,1=存在未提交变更
function checkGitSources() {
# 配置Git安全目录(解决权限问题)
git config --global --add safe.directory "$(pwd)"
# 检查是否有未提交的变更
if [[ -n $(git diff --stat) ]]; then
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
return 1
fi
echo "[INFO] Git源码检查通过:所有变更已提交。"
return 0
}
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# 返回值:1=用户选择是,0=用户选择否
function askAddWorkflowsTag() {
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
if [[ $answer =~ ^[Yy]$ ]]; then
return 1
else
return 0
fi
}
# 添加WinBoLL正式标签
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWinBoLLTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取publishVersion
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${publish_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到publishVersion${publish_version}"
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>
local tag="${app_name}-v${publish_version}"
echo "[INFO] 准备添加WinBoLL标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] WinBoLL标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] WinBoLL标签${tag}添加成功!"
return 0
}
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWorkflowsTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取baseBetaVersion
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${base_beta_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion${base_beta_version}"
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta
local tag="${app_name}-v${base_beta_version}-beta"
echo "[INFO] 准备添加Workflows标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] Workflows标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] Workflows标签${tag}添加成功!"
return 0
}
# ==================== 主流程开始 ====================
echo "============================================="
echo " WinBoLL 应用发布脚本"
echo "============================================="
# 1. 检查应用名称参数是否指定
if [ -z "$1" ]; then
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
exit ${EXIT_CODE_ERR_NO_APP_NAME}
fi
APP_NAME=$1
echo "[INFO] 待发布应用名称:${APP_NAME}"
# 2. 检查并切换到项目根目录(确保build.properties存在)
echo "[INFO] 当前工作目录:$(pwd)"
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
cd ..
echo "[INFO] 切换后工作目录:$(pwd)"
fi
# 验证最终工作目录是否正确
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
exit ${EXIT_CODE_ERR_WORK_DIR}
fi
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
# 3. 检查Git源码状态
echo "---------------------------------------------"
echo " 步骤1:检查Git源码状态"
echo "---------------------------------------------"
checkGitSources
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Git源码检查失败,脚本终止!"
exit ${EXIT_CODE_ERR_GIT_CHECK}
fi
# 4. 编译Stage Release版本APK(携带aapt2覆盖参数 + --no-daemon
echo "---------------------------------------------"
echo " 步骤2:编译Stage Release APK"
echo "---------------------------------------------"
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
# 调试用(注释正式任务,启用调试任务)
# bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_DEBUG}
bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_PUBLISH}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Gradle编译任务失败!"
exit 1
fi
echo "[INFO] Stage Release APK编译成功!"
# 5. 添加WinBoLL正式标签
echo "---------------------------------------------"
echo " 步骤3:添加WinBoLL标签"
echo "---------------------------------------------"
addWinBoLLTag ${APP_NAME}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
fi
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# echo "---------------------------------------------"
# echo " 步骤4:添加Workflows标签(可选)"
# echo "---------------------------------------------"
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
# askAddWorkflowsTag
# nAskAddWorkflowsTag=$?
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
# addWorkflowsTag ${APP_NAME}
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
# exit 1
# fi
# fi
# 7. 清理更新描述文件
echo "---------------------------------------------"
echo " 步骤5:清理更新描述文件"
echo "---------------------------------------------"
echo "" > "${APP_NAME}/app_update_description.txt"
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
# 8. 提交并推送源码与标签
echo "---------------------------------------------"
echo " 步骤6:提交并推送源码"
echo "---------------------------------------------"
git add .
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
echo "[INFO] 源码提交成功,开始推送..."
# 推送源码到远程仓库
git push origin
# 推送标签到远程仓库
git push origin --tags
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
echo "[INFO] 源码与标签推送成功!"
else
echo "[ERROR] 源码与标签推送失败!"
exit 1
fi
# ==================== 主流程结束 ====================
echo "============================================="
echo " WinBoLL 应用发布完成!"
echo "============================================="
exit ${EXIT_CODE_SUCCESS}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/bash
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# Gradle禁用守护进程参数
GRADLE_NO_DAEMON="--no-daemon"
# 检查是否指定了将要发布的类库名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "No Library name specified : $0"
exit 2
fi
## 正式发布使用
git pull && bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
## 调试使用
#bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
+56 -27
View File
@@ -122,7 +122,6 @@ android {
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
//
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
// 创建目标路径目录
if(!outBuildBckDir.exists()) {
@@ -130,6 +129,7 @@ android {
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
}
if(outBuildBckDir.exists()) {
def targetApkFile = new File(outBuildBckDir, outputFileName)
copy{
from file.outputFile
into outBuildBckDir
@@ -138,6 +138,14 @@ android {
}
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
}
// ========== 设置文件权限为775 ==========
if(targetApkFile.exists()){
exec {
commandLine 'chmod', '775', targetApkFile.absolutePath
}
println "Set file permission to 775 : ${targetApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['buildCount'] != null)
assert (winbollBuildProps['libraryProject'] != null)
@@ -160,8 +168,7 @@ android {
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "\n\n>>> Library Project build.properties saved.\n\n";
}
@@ -172,16 +179,12 @@ android {
//
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -192,12 +195,10 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
// 复制完整版APK
def fullApkFile = new File(outTagDir, outputFileName)
copy{
from file.outputFile
into outTagDir
@@ -206,7 +207,16 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
// 设置权限775。
if(fullApkFile.exists()){
exec {
commandLine 'chmod', '775', fullApkFile.absolutePath
}
println "Set file permission to 775 : ${fullApkFile.absolutePath}"
}
// 复制短版本名APK
def shortApkFile = new File(outTagDir, szCommonTagAPKName)
copy{
from file.outputFile
into outTagDir
@@ -215,6 +225,14 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
// 设置权限775。
if(shortApkFile.exists()){
exec {
commandLine 'chmod', '775', shortApkFile.absolutePath
}
println "Set file permission to 775 : ${shortApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['publishVersion'] != null)
@@ -239,14 +257,11 @@ android {
fos.close();
if(winbollBuildProps['libraryProject'] != "") {
// 如果应用 build.properties 文件设置了类库模块项目文件名
// 就拷贝一份新的编译标志配置到类库项目文件夹
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
assert(winbollBuildPropsFile.exists())
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
@@ -263,17 +278,12 @@ android {
// 如果正在调试发布版,就只生成和输出APK文件,不处理 Git 仓库提交与更新问题。
//
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -284,13 +294,11 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
println '如果是在调试 Stage 版应用包构建,请删除(注:在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
// 复制完整版APK
def debugFullApk = new File(outTagDir, outputFileName)
copy{
from file.outputFile
into outTagDir
@@ -299,7 +307,16 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
// 权限设为775。
if(debugFullApk.exists()){
exec {
commandLine 'chmod', '775', debugFullApk.absolutePath
}
println "Set file permission to 775 : ${debugFullApk.absolutePath}"
}
// 复制短版本名APK
def debugShortApk = new File(outTagDir, szCommonTagAPKName)
copy{
from file.outputFile
into outTagDir
@@ -308,8 +325,13 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
//不保存编译标志配置
// 权限设为775
if(debugShortApk.exists()){
exec {
commandLine 'chmod', '775', debugShortApk.absolutePath
}
println "Set file permission to 775 : ${debugShortApk.absolutePath}"
}
}
}
@@ -328,6 +350,13 @@ android {
}
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
}
// 额外输出文件设置775权限
if(apkFile.exists()){
exec {
commandLine 'chmod', '775', apkFile.absolutePath
}
println "Set file permission to 775 : ${apkFile.absolutePath}"
}
}
}
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+97
View File
@@ -0,0 +1,97 @@
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
# LICENSE-Private-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1. 公共开源分支唯一标识
**文件名:LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2. 私有开发分支唯一标识
**文件名:LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)
+5 -5
View File
@@ -9,15 +9,15 @@ WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与
#### **仓库类型:功能说明**
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
☆ 源码汇总管理 Projects_Keeper**:各类分支源码合并存档,不适宜作为开发库使用。
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
### 3. 源码合并管理推送路线图
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
★ WinBoLL → APPBase → Projects_Keeper
★ WinBoLL → AES → Projects_Keeper
★ WinBoLL → PowerBell → Projects_Keeper
★ WinBoLL → Positions → Projects_Keeper
★ WinBoLL → APPBase → OriginMaster
★ WinBoLL → AES → OriginMaster
★ WinBoLL → PowerBell → OriginMaster
★ WinBoLL → Positions → OriginMaster
## 二、WinBoLL 项目核心信息
+1 -1
View File
@@ -20,7 +20,7 @@ WinBoLL AndroidX 可视化元素类库。
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request
+4 -5
View File
@@ -26,7 +26,10 @@ android {
applicationId "cc.winboll.studio.aes"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android 官方规则
//-  versionCode  类型:int 整型
//- Java int 最大值:2147483647
versionCode 1520000
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
@@ -36,10 +39,6 @@ android {
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 13:11:28 HKT 2026
stageCount=4
#Sat Jul 11 20:07:00 HKT 2026
stageCount=19
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.3
publishVersion=15.20.18
buildCount=0
baseBetaVersion=15.20.4
baseBetaVersion=15.20.19
@@ -65,7 +65,7 @@ public class AboutActivity extends BaseWinBoLLActivity {
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("AES");
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
@@ -5,10 +5,14 @@ package cc.winboll.studio.aes;
* @Date 2024/06/13 19:03:58
* @Describe AES应用类
*/
import android.view.Gravity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
public class App extends GlobalApplication {
@@ -17,13 +21,25 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
try {
super.onCreate();
ToastUtils.init(this);
WinBoLLActivityManager.init(this);
AESThemeUtil.init(null);
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
@@ -84,11 +84,12 @@ public class MainActivity extends DrawerFragmentActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar_main, menu);
// if(App.isDebugging()) {
// getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_studio_debug, menu);
// }
return super.onCreateOptionsMenu(menu);
return true;
}
@Override
@@ -1,11 +1,7 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.winboll.studio.libaes.views.ADsControlView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -20,7 +16,7 @@ public class SettingsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
//ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
@@ -6,12 +6,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1 -1
View File
@@ -20,7 +20,7 @@ WinBoLL 安卓手机端安卓应用开发基础类库。
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request
+4 -1
View File
@@ -26,7 +26,10 @@ android {
applicationId "cc.winboll.studio.appbase"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android 官方规则
//-  versionCode  类型:int 整型
//- Java int 最大值:2147483647
versionCode 1520000
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 09:17:15 HKT 2026
stageCount=10
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.9
publishVersion=15.20.34
buildCount=0
baseBetaVersion=15.20.10
baseBetaVersion=15.20.35
@@ -1,8 +1,11 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,16 +24,24 @@ public class App extends GlobalApplication {
*/
@Override
public void onCreate() {
super.onCreate();
// 如果应用不在调试状态,就根据编译类型设置调试状态
if (isDebugging() != true) {
setIsDebugging(BuildConfig.DEBUG);
}
// release 版调试码
//setIsDebugging(!BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
try {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
/**
@@ -2,8 +2,8 @@ package cc.winboll.studio.appbase.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
/**
+21
View File
@@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# 保持与旧版Gradle插件的兼容
android.disableAutomaticComponentCreation=true
Vendored Regular → Executable
View File
+9 -8
View File
@@ -20,10 +20,11 @@ android {
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
@@ -52,7 +53,7 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意:以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
@@ -61,9 +62,9 @@ dependencies {
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libappbase:15.20.9'
api 'cc.winboll.studio:libappbase:15.20.34'
// 备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.3'
//api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33'
api fileTree(dir: 'libs', include: ['*.jar'])
}
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 13:11:09 HKT 2026
stageCount=4
#Sat Jul 11 20:07:00 HKT 2026
stageCount=19
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.3
publishVersion=15.20.18
buildCount=0
baseBetaVersion=15.20.4
baseBetaVersion=15.20.19
@@ -24,18 +24,18 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import cc.winboll.studio.libaes.DrawerMenuDataAdapter;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.models.DrawerMenuBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libaes.views.ADrawerMenuListView;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import com.baoyz.widget.PullRefreshLayout;
import java.util.ArrayList;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
public abstract class DrawerFragmentActivity extends AppCompatActivity implements IWinBoLLActivity, AdapterView.OnItemClickListener {
@@ -44,7 +44,6 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
static final String SHAREDPREFERENCES_NAME = "SHAREDPREFERENCES_NAME";
static final String DRAWER_THEME_TYPE = "DRAWER_THEME_TYPE";
//protected Context mContext;
ActivityType mActivityType;
ActionBarDrawerToggle mActionBarDrawerToggle;
DrawerLayout mDrawerLayout;
@@ -59,13 +58,14 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
public enum ActivityType { Main, Secondary }
protected volatile AESThemeBean.ThemeType mThemeType;
protected ArrayList<DrawerMenuBean> malDrawerMenuItem;
abstract protected ActivityType initActivityType();
//abstract protected View initContentView(LayoutInflater inflater, ViewGroup rootView);
@Override
protected void onCreate(Bundle savedInstanceState) {
// 替换:使用工具类统一应用主题
AESThemeUtil.applyAppCompatTheme(this);
mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
super.onCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
mActivityType = initActivityType();
@@ -78,53 +78,32 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onDestroy() {
WinBoLLActivityManager.getInstance().registeRemove(this);
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
// 修复:释放广告资源,避免内存泄漏
ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
if (adsBannerView != null) {
adsBannerView.releaseAdResources();
}
// 修复:释放广告资源,避免内存泄漏
// ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
// if (adsBannerView != null) {
// adsBannerView.releaseAdResources();
// }
}
/*@Override
public Intent getIntent() {
// TODO: Implement this method
return super.getIntent();
}
public Context getContext() {
return this.mContext;
}*/
@Override
public MenuInflater getMenuInflater() {
// TODO: Implement this method
return super.getMenuInflater();
}
/*public void setSubtitle(CharSequence context) {
// TODO: Implement this method
getSupportActionBar().setSubtitle(context);
}*/
@Override
public void recreate() {
super.recreate();
}
/*@Override
public boolean moveTaskToBack(boolean nonRoot) {
return super.moveTaskToBack(nonRoot);
}*/
@Override
public void startActivity(Intent intent) {
super.startActivity(intent);
@@ -135,26 +114,6 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
super.startActivityForResult(intent, requestCode, options);
}
/*@Override
public FragmentManager getSupportFragmentManager() {
return super.getSupportFragmentManager();
}
public void setSubtitle(int resId) {
// TODO: Implement this method
getSupportActionBar().setSubtitle(resId);
}
public void setTitle(CharSequence context) {
// TODO: Implement this method
getSupportActionBar().setTitle(context);
}
public void setTitle(int resId) {
// TODO: Implement this method
getSupportActionBar().setTitle(resId);
}*/
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return super.getSharedPreferences(name, mode);
@@ -162,7 +121,6 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
@Override
public Context getApplicationContext() {
// TODO: Implement this method
return super.getApplicationContext();
}
@@ -173,25 +131,27 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
// 替换为 DrawerFragmentActivity 专属点击处理方法
if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
recreate();
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else {
return super.onOptionsItemSelected(item);
}
}
if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else {
return super.onOptionsItemSelected(item);
}
return true;
return true;
}
@Override
protected void onResume() {
super.onResume();
ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
if (adsBannerView != null) {
adsBannerView.resumeADs(DrawerFragmentActivity.this);
}
// ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
// if (adsBannerView != null) {
// adsBannerView.resumeADs(DrawerFragmentActivity.this);
// }
}
void initRootView() {
@@ -213,14 +173,13 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
mPullRefreshLayout = findViewById(R.id.activitydrawerfragmentPullRefreshLayout1);
mPullRefreshLayout.setOnRefreshListener(new PullRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//LogUtils.d(TAG, "onRefresh");
reinitDrawerMenuItemList(malDrawerMenuItem);
mDrawerMenuDataAdapter.notifyDataSetChanged();
mPullRefreshLayout.setRefreshing(false);
}
});
@Override
public void onRefresh() {
reinitDrawerMenuItemList(malDrawerMenuItem);
mDrawerMenuDataAdapter.notifyDataSetChanged();
mPullRefreshLayout.setRefreshing(false);
}
});
malDrawerMenuItem = new ArrayList<DrawerMenuBean>();
@@ -236,68 +195,51 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
mActionBarDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.lib_name, R.string.lib_name) {
@Override
public void onDrawerOpened(View drawerView) {//完全打开时触发
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
mIsDrawerOpened = true;
mIsDrawerOpening = false;
//Toast.makeText(MainActivity.this,"onDrawerOpened",Toast.LENGTH_SHORT).show();
}
@Override
public void onDrawerClosed(View drawerView) {//完全关闭时触发
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
mIsDrawerOpened = false;
mIsDrawerClosing = false;
//Toast.makeText(MainActivity.this,"onDrawerClosed",Toast.LENGTH_SHORT).show();
}
/**
* 当抽屉被滑动的时候调用此方法
* slideOffset表示 滑动的幅度(0-1
*/
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
super.onDrawerSlide(drawerView, slideOffset);
}
/**
* 当抽屉滑动状态改变的时候被调用
* 状态值是STATE_IDLE(闲置--0, STATE_DRAGGING(拖拽的--1, STATE_SETTLING(固定--2)中之一。
*具体状态可以慢慢调试
*/
@Override
public void onDrawerStateChanged(int newState) {
super.onDrawerStateChanged(newState);
}
};
//设置显示旋转菜单
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//通过下面这句实现toolbar和Drawer的联动:如果没有这行代码,箭头是不会随着侧滑菜单的开关而变换的(或者没有箭头),
// 可以尝试一下,不影响正常侧滑
mActionBarDrawerToggle.syncState();
mDrawerLayout.setDrawerListener(mActionBarDrawerToggle);
//去掉侧滑的默认图标(动画箭头图标),也可以选择不去,
//不去的话把这一行注释掉或者改成true,然后把toolbar.setNavigationIcon注释掉就行了
//mActionBarDrawerToggle.setDrawerIndicatorEnabled(false);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mIsDrawerOpened || mIsDrawerOpening) {
mIsDrawerClosing = true;
mIsDrawerOpening = false;
mDrawerLayout.closeDrawer(mPullRefreshLayout);
return;
}
if (!mIsDrawerOpened || mIsDrawerClosing) {
mIsDrawerOpening = true;
mIsDrawerClosing = false;
mDrawerLayout.openDrawer(mPullRefreshLayout);
return;
}
}
});
@Override
public void onClick(View v) {
if (mIsDrawerOpened || mIsDrawerOpening) {
mIsDrawerClosing = true;
mIsDrawerOpening = false;
mDrawerLayout.closeDrawer(mPullRefreshLayout);
return;
}
if (!mIsDrawerOpened || mIsDrawerClosing) {
mIsDrawerOpening = true;
mIsDrawerClosing = false;
mDrawerLayout.openDrawer(mPullRefreshLayout);
return;
}
}
});
initDrawerMenuItemList(malDrawerMenuItem);
}
@@ -305,12 +247,11 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
void initSecondaryRootView() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//LogUtils.d(TAG, "onClick " + Integer.toString(v.getId()));
finish();
}
});
@Override
public void onClick(View v) {
finish();
}
});
}
public <T extends Fragment> int removeFragment(T fragment) {
@@ -375,13 +316,13 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (mActivityType == ActivityType.Main) {
// 主题菜单
AESThemeUtil.inflateMenu(this, menu);
// 调试工具菜单
if (GlobalApplication.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
}
// 应用信息菜单
// 替换为兼容版菜单加载方法
AESThemeUtil.inflateCompatThemeMenu(this, menu);
// 调试工具菜单
if (GlobalApplication.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
}
// 应用信息菜单
getMenuInflater().inflate(R.menu.toolbar_drawerbase, menu);
}
return super.onCreateOptionsMenu(menu);
@@ -392,3 +333,4 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
super.onActivityResult(who, targetFragment, requestCode);
}
}
@@ -8,8 +8,9 @@ package cc.winboll.studio.libaes.models;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
import java.util.ArrayList;
public class AESThemeBean extends BaseBean {
@@ -41,13 +42,28 @@ public class AESThemeBean extends BaseBean {
return name;
}
}
public static void fillThemeStyleIDList(ArrayList<Integer> themeStyleIDList) {
if (themeStyleIDList == null) {
themeStyleIDList = new ArrayList<Integer>();
}
themeStyleIDList.clear();
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.AESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.DepthAESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.SkyAESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.GoldenAESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.BearingAESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.MemorAESTheme);
themeStyleIDList.add(cc.winboll.studio.libaes.R.style.TaoAESTheme);
}
// 保存当前主题
int currentThemeStyleID = getThemeStyleID(ThemeType.AES);
public AESThemeBean() {
}
public AESThemeBean(int currentThemeStyleID) {
this.currentThemeStyleID = currentThemeStyleID;
}
@@ -59,7 +75,7 @@ public class AESThemeBean extends BaseBean {
public int getCurrentThemeTypeID() {
return this.currentThemeStyleID;
}
@Override
public String getName() {
return AESThemeBean.class.getName();
@@ -74,8 +90,7 @@ public class AESThemeBean extends BaseBean {
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
if(super.initObjectsFromJsonReader(jsonReader, name)) { return true; }
else{
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
if (name.equals("currentThemeTypeID")) {
setCurrentThemeTypeID(jsonReader.nextInt());
} else {
@@ -90,7 +105,7 @@ public class AESThemeBean extends BaseBean {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if(!initObjectsFromJsonReader(jsonReader, name)) {
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue();
}
}
@@ -7,7 +7,7 @@ package cc.winboll.studio.libaes.models;
import android.content.Context;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
public class WinBoLLClientServiceBean extends BaseBean {
@@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.views.ASupportToolbar;
import cc.winboll.studio.libappbase.LogUtils;
public class TestASupportToolbarActivity extends AppCompatActivity implements IWinBoLLActivity {
@@ -28,11 +27,12 @@ public class TestASupportToolbarActivity extends AppCompatActivity implements IW
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreate() start");
AESThemeUtil.applyAppTheme(this);
// 替换此处:原 applyAppTheme -> 新方法 applyAppCompatTheme
AESThemeUtil.applyAppCompatTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_testasupporttoolbar);
LogUtils.d(TAG, "setContentView() done");
@@ -45,3 +45,4 @@ public class TestASupportToolbarActivity extends AppCompatActivity implements IW
LogUtils.d(TAG, "onCreate() end");
}
}
@@ -14,10 +14,11 @@ import cc.winboll.studio.libaes.utils.AESThemeUtil;
public class TestAToolbarActivity extends Activity {
public static final String TAG = "TestAToolbarActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
AESThemeUtil.applyAppTheme(this);
// 原生Activity 使用 applyTheme
AESThemeUtil.applyTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_testatoolbar);
Toolbar toolbar = findViewById(R.id.activitytestatoolbarAToolbar1);
@@ -26,3 +27,4 @@ public class TestAToolbarActivity extends Activity {
}
}
@@ -13,199 +13,159 @@ import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import java.util.ArrayList;
public class AESThemeUtil {
public static final String TAG = "AESThemeUtil";
private static final String SHAREDPREFERENCES_NAME = "SHAREDPREFERENCES_NAME";
private static final String DRAWER_THEME_TYPE = "DRAWER_THEME_TYPE";
static final String SHAREDPREFERENCES_NAME = "SHAREDPREFERENCES_NAME";
static final String DRAWER_THEME_TYPE = "DRAWER_THEME_TYPE";
// 私有静态集合,外部不可直接修改
private static ArrayList<Integer> themeStyleIDList = new ArrayList<>();
protected volatile AESThemeBean.ThemeType mThemeType;
// 移除无用实例成员 mThemeType,工具类不保留实例字段
public static <T extends Context> int getThemeTypeID(T context) {
AESThemeBean bean = AESThemeBean.loadBean(context, AESThemeBean.class);
return bean == null ? AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.AES): bean.getCurrentThemeTypeID();
/**
* 初始化主题样式ID集合
*/
public static void init(ArrayList<Integer> themeStyleIDList) {
if (themeStyleIDList == null) {
themeStyleIDList = new ArrayList<Integer>();
AESThemeBean.fillThemeStyleIDList(themeStyleIDList);
}
AESThemeUtil.themeStyleIDList.clear();
AESThemeUtil.themeStyleIDList.addAll(themeStyleIDList);
}
public static <T extends Context> void saveThemeStyleID(T context, int nThemeTypeID) {
AESThemeBean bean = new AESThemeBean(nThemeTypeID);
/**
* 获取当前主题样式ID
*/
public static int getThemeTypeID(Context context) {
AESThemeBean bean = AESThemeBean.loadBean(context, AESThemeBean.class);
return bean == null ? getThemeStyleID(AESThemeBean.ThemeType.AES) : bean.getCurrentThemeTypeID();
}
/**
* 保存主题样式ID
*/
public static void saveThemeStyleID(Context context, int themeTypeID) {
AESThemeBean bean = new AESThemeBean(themeTypeID);
AESThemeBean.saveBean(context, bean);
}
public static <T extends Activity> void applyAppTheme(T activity) {
// ====================== 应用主题 - 规范重载 ======================
/**
* 应用当前持久化主题(通用 Activity)
*/
public static void applyTheme(Activity activity) {
activity.setTheme(getThemeTypeID(activity));
}
public static <T extends AppCompatActivity> void applyAppCompatTheme(T activity) {
/**
* 应用指定主题(通用 Activity)
*/
public static void applyTheme(Activity activity, AESThemeBean.ThemeType themeType) {
activity.setTheme(getThemeStyleID(themeType));
}
/**
* 应用当前持久化主题(AppCompat 兼容 Activity
*/
public static void applyAppCompatTheme(AppCompatActivity activity) {
activity.setTheme(getThemeTypeID(activity));
}
/*public static <T extends WinBoLLActivity> void applyWinBoLLTheme(T activity) {
activity.setTheme(getThemeTypeID(activity.getApplicationContext()));
}*/
public static <T extends Activity> void applyAppTheme(Activity activity, AESThemeBean.ThemeType themeType) {
activity.setTheme(AESThemeBean.getThemeStyleID(themeType));
/**
* 应用指定主题(AppCompat 兼容 Activity
*/
public static void applyAppCompatTheme(AppCompatActivity activity, AESThemeBean.ThemeType themeType) {
activity.setTheme(getThemeStyleID(themeType));
}
public static <T extends AppCompatActivity> void applyAppCompatTheme(Activity activity, AESThemeBean.ThemeType themeType) {
activity.setTheme(AESThemeBean.getThemeStyleID(themeType));
}
/*public static <T extends WinBoLLActivity> void applyWinBoLLTheme(Activity activity, AESThemeBean.ThemeType themeType) {
activity.setTheme(AESThemeBean.getThemeStyleID(themeType));
}*/
public static <T extends Activity> void inflateMenu(T activity, Menu menu) {
// ====================== 加载菜单 ======================
/**
* 加载主题菜单(通用 Activity)
*/
public static void inflateThemeMenu(Activity activity, Menu menu) {
activity.getMenuInflater().inflate(R.menu.toolbar_apptheme, menu);
}
public static <T extends AppCompatActivity> void inflateCompatMenu(T activity, Menu menu) {
/**
* 加载主题菜单(AppCompat Activity
*/
public static void inflateCompatThemeMenu(AppCompatActivity activity, Menu menu) {
activity.getMenuInflater().inflate(R.menu.toolbar_apptheme, menu);
}
/*public static <T extends WinBoLLActivity> void inflateWinBoLLMenu(T activity, Menu menu) {
activity.getMenuInflater().inflate(R.menu.toolbar_apptheme, menu);
}*/
public static <T extends Activity> boolean onAppThemeItemSelected(T activity, MenuItem item) {
int nThemeStyleID;
if (R.id.item_depththeme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.DEPTH);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_skytheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.SKY);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_goldentheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.GOLDEN);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_bearingtheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.BEARING);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_memortheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.MEMOR);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_taotheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.TAO);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_defaulttheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.AES);
saveThemeStyleID(activity, nThemeStyleID);
return true;
// ====================== 菜单点击统一核心逻辑(消除重复代码) ======================
/**
* 主题菜单项点击统一处理
* @param context 上下文(用于持久化)
* @param item 点击的菜单项
* @return 是否消费点击事件
*/
public static boolean handleThemeMenuClick(Context context, MenuItem item) {
int themeStyleId;
int itemId = item.getItemId();
if (R.id.item_depththeme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.DEPTH);
} else if (R.id.item_skytheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.SKY);
} else if (R.id.item_goldentheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.GOLDEN);
} else if (R.id.item_bearingtheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.BEARING);
} else if (R.id.item_memortheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.MEMOR);
} else if (R.id.item_taotheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.TAO);
} else if (R.id.item_defaulttheme == itemId) {
themeStyleId = getThemeStyleID(AESThemeBean.ThemeType.AES);
} else {
return false;
}
return false;
saveThemeStyleID(context, themeStyleId);
return true;
}
public static <T extends AppCompatActivity> boolean onAppCompatThemeItemSelected(T activity, MenuItem item) {
int nThemeStyleID;
if (R.id.item_depththeme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.DEPTH);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_skytheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.SKY);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_goldentheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.GOLDEN);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_bearingtheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.BEARING);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_memortheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.MEMOR);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_taotheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.TAO);
saveThemeStyleID(activity, nThemeStyleID);
return true;
} else if (R.id.item_defaulttheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.AES);
saveThemeStyleID(activity, nThemeStyleID);
return true;
}
return false;
// 对外暴露不同 Activity 类型的入口,内部调用统一核心方法
public static boolean onThemeItemSelected(Activity activity, MenuItem item) {
return handleThemeMenuClick(activity, item);
}
public static <T extends AppCompatActivity> boolean onWinBoLLThemeItemSelected(T activity, MenuItem item) {
int nThemeStyleID;
if (R.id.item_depththeme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.DEPTH);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_skytheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.SKY);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_goldentheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.GOLDEN);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_bearingtheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.BEARING);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_memortheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.MEMOR);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_taotheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.TAO);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_defaulttheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.AES);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
}
return false;
public static boolean onAppCompatThemeItemSelected(AppCompatActivity activity, MenuItem item) {
return handleThemeMenuClick(activity, item);
}
public static <T extends DrawerFragmentActivity> boolean onWinBoLLThemeItemSelected(T activity, MenuItem item) {
int nThemeStyleID;
if (R.id.item_depththeme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.DEPTH);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_skytheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.SKY);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_goldentheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.GOLDEN);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_bearingtheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.BEARING);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_memortheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.MEMOR);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_taotheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.TAO);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
} else if (R.id.item_defaulttheme == item.getItemId()) {
nThemeStyleID = AESThemeBean.getThemeStyleID(AESThemeBean.ThemeType.AES);
saveThemeStyleID(activity.getApplicationContext(), nThemeStyleID);
return true;
}
public static boolean onWinBoLLThemeItemSelected(AppCompatActivity activity, MenuItem item) {
// 使用 Application 上下文保存,避免 Activity 泄漏
return handleThemeMenuClick(activity.getApplicationContext(), item);
}
return false;
public static boolean onWinBoLLThemeItemSelected(DrawerFragmentActivity activity, MenuItem item) {
return handleThemeMenuClick(activity.getApplicationContext(), item);
}
// ====================== 主题类型转换工具 ======================
/**
* 根据枚举获取对应样式ID
*/
public static int getThemeStyleID(AESThemeBean.ThemeType themeType) {
return themeStyleIDList.get(themeType.ordinal());
}
/**
* 根据样式ID反向获取主题枚举
*/
public static AESThemeBean.ThemeType getThemeStyleType(int themeStyleID) {
for (int i = 0; i < themeStyleIDList.size(); i++) {
if (themeStyleIDList.get(i) == themeStyleID) {
return AESThemeBean.ThemeType.values()[i];
}
}
return AESThemeBean.ThemeType.values()[0];
}
}
@@ -56,10 +56,5 @@
</LinearLayout>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>
</LinearLayout>
@@ -50,7 +50,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="0dp"> <!-- 移除顶部多余间距 -- android:background="@drawable/bg_container_border">
android:layout_marginTop="0dp"> <!-- 移除顶部多余间距 - android:background="@drawable/bg_container_border" -->
<ImageView
android:id="@+id/iv_winboll_store"
+9 -9
View File
@@ -3,15 +3,15 @@
<color name="colorTextColor">#FFFFFFFF</color>
<color name="colorPrimary">#FF03AB4E</color>
<color name="colorPrimaryDark">#FF027C39</color>
<color name="colorAccent">#FF3DDC84</color>
<color name="colorPrimaryDark">#FF3DDC84</color>
<color name="colorAccent">#FF027C39</color>
<color name="colorText">#FFFFFB8D</color>
<color name="colorToastFrame">#FFA9A9A9</color>
<color name="colorToastFrame">#FF555555</color>
<color name="colorToastShadow">#FF000000</color>
<color name="colorToastBackgroung">#FFFFFFFF</color>
<color name="colorAToolbarStartColor">#FF7D3F12</color>
<color name="colorAToolbarCenterColor">#FFCC6E2B</color>
<color name="colorAToolbarEndColor">#FFF4B98F</color>
<color name="colorToastBackgroung">#FF3A3A3A</color>
<color name="colorAToolbarStartColor">#FF5A3A1A</color>
<color name="colorAToolbarCenterColor">#FFA05A2A</color>
<color name="colorAToolbarEndColor">#FFD4A07A</color>
<color name="colorACardShadow">@color/colorPrimaryDark</color>
<color name="colorACardFrame">@color/colorPrimary</color>
@@ -24,10 +24,10 @@
<color name="colorOHPCTSSecondaryProgress">@color/colorPrimary</color>
<color name="colorOHPCTSProgress">@color/colorPrimaryDark</color>
<color name="toolbarBackgroundColor">#FF03AB4E</color>
<color name="toolbarBackgroundColor">#FF3DDC84</color>
<color name="toolbarTextColor">#FFFFFFFF</color>
<color name="mainWindowBackgroundColor">#FF2C2C2C</color>
<color name="mainWindowTextColor">#FFFFFFFF</color>
<color name="debugTextColor">#FFFF0000</color>
<color name="debugTextColor">#FFAAAAAA</color>
</resources>
+23 -20
View File
@@ -2,6 +2,9 @@
<resources>
<style name="AESTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">#FF03AB4E</item>
<item name="colorPrimaryDark">#FF3DDC84</item>
<item name="colorAccent">#FF027C39</item>
<item name="themeDebug">@style/DebugActivityTheme</item>
<item name="aboutViewBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">@color/mainWindowTextColor</item>
@@ -22,7 +25,7 @@
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="aToolbar">@style/AESAToolbar</item>
<item name="aSupportToolbar">@style/AESASupportToolbar</item>
@@ -40,52 +43,52 @@
<style name="AESAToolbar">
<item name="attrAToolbarTitleTextColor">@color/colorTextColor</item>
<item name="attrAToolbarStartColor">@color/colorPrimaryDark</item>
<item name="attrAToolbarCenterColor">@color/colorPrimary</item>
<item name="attrAToolbarEndColor">@color/colorAccent</item>
<item name="attrAToolbarStartColor">?attr/colorPrimaryDark</item>
<item name="attrAToolbarCenterColor">?attr/colorPrimary</item>
<item name="attrAToolbarEndColor">?attr/colorAccent</item>
</style>
<style name="AESASupportToolbar">
<item name="attrASupportToolbarTitleTextColor">@color/colorTextColor</item>
<item name="attrASupportToolbarStartColor">@color/colorPrimaryDark</item>
<item name="attrASupportToolbarCenterColor">@color/colorPrimary</item>
<item name="attrASupportToolbarEndColor">@color/colorAccent</item>
<item name="attrASupportToolbarStartColor">?attr/colorPrimaryDark</item>
<item name="attrASupportToolbarCenterColor">?attr/colorPrimary</item>
<item name="attrASupportToolbarEndColor">?attr/colorAccent</item>
</style>
<style name="DepthAESTheme" parent="AESTheme">
<item name="colorPrimary">#FF0065EC</item>
<item name="colorPrimaryDark">#FF004DB4</item>
<item name="colorAccent">#FF4A97FF</item>
<item name="colorPrimaryDark">#FF4A97FF</item>
<item name="colorAccent">#FF004DB4</item>
</style>
<style name="SkyAESTheme" parent="AESTheme">
<item name="colorPrimary">#FF00A6FF</item>
<item name="colorPrimaryDark">#FF007ABB</item>
<item name="colorAccent">#FF84D4FF</item>
<item name="colorPrimaryDark">#FF84D4FF</item>
<item name="colorAccent">#FF007ABB</item>
</style>
<style name="GoldenAESTheme" parent="AESTheme">
<item name="colorPrimary">#FFF0CA11</item>
<item name="colorPrimaryDark">#FFD3AF00</item>
<item name="colorAccent">#FFFFE35C</item>
<item name="colorPrimaryDark">#FFFFE35C</item>
<item name="colorAccent">#FFD3AF00</item>
</style>
<style name="BearingAESTheme" parent="AESTheme">
<item name="colorPrimary">#FF840FFF</item>
<item name="colorPrimaryDark">#FF6900D7</item>
<item name="colorAccent">#FFBA78FF</item>
<item name="colorPrimaryDark">#FFBA78FF</item>
<item name="colorAccent">#FF6900D7</item>
</style>
<style name="MemorAESTheme" parent="AESTheme">
<item name="colorPrimary">#FFFF00F5</item>
<item name="colorPrimaryDark">#FFE500DC</item>
<item name="colorAccent">#FFFF76FA</item>
<item name="colorPrimaryDark">#FFFF76FA</item>
<item name="colorAccent">#FFE500DC</item>
</style>
<style name="TaoAESTheme" parent="AESTheme">
<item name="colorPrimary">#FFACACAC</item>
<item name="colorPrimaryDark">#FF898989</item>
<item name="colorAccent">#FFD8D8D8</item>
<item name="colorPrimary">#FF7E7E7E</item>
<item name="colorPrimaryDark">#FFE2E2E2</item>
<item name="colorAccent">#FF000000</item>
</style>
<style name="NormalDialogStyle" parent="Theme.AppCompat.Dialog">
+1 -1
View File
@@ -28,6 +28,6 @@
<color name="toolbarTextColor">#FFFFFFFF</color>
<color name="mainWindowBackgroundColor">#FFFFFFFF</color>
<color name="mainWindowTextColor">#FF000000</color>
<color name="debugTextColor">#FFFF0000</color>
<color name="debugTextColor">#FF808080</color>
</resources>
+14 -11
View File
@@ -2,6 +2,9 @@
<resources>
<style name="AESTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">#FF03AB4E</item>
<item name="colorPrimaryDark">#FF027C39</item>
<item name="colorAccent">#FF3DDC84</item>
<item name="themeDebug">@style/DebugActivityTheme</item>
<item name="aboutViewBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">@color/mainWindowTextColor</item>
@@ -22,7 +25,7 @@
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="aToolbar">@style/AESAToolbar</item>
<item name="aSupportToolbar">@style/AESASupportToolbar</item>
@@ -40,16 +43,16 @@
<style name="AESAToolbar">
<item name="attrAToolbarTitleTextColor">@color/colorTextColor</item>
<item name="attrAToolbarStartColor">@color/colorPrimaryDark</item>
<item name="attrAToolbarCenterColor">@color/colorPrimary</item>
<item name="attrAToolbarEndColor">@color/colorAccent</item>
<item name="attrAToolbarStartColor">?attr/colorPrimaryDark</item>
<item name="attrAToolbarCenterColor">?attr/colorPrimary</item>
<item name="attrAToolbarEndColor">?attr/colorAccent</item>
</style>
<style name="AESASupportToolbar">
<item name="attrASupportToolbarTitleTextColor">@color/colorTextColor</item>
<item name="attrASupportToolbarStartColor">@color/colorPrimaryDark</item>
<item name="attrASupportToolbarCenterColor">@color/colorPrimary</item>
<item name="attrASupportToolbarEndColor">@color/colorAccent</item>
<item name="attrASupportToolbarStartColor">?attr/colorPrimaryDark</item>
<item name="attrASupportToolbarCenterColor">?attr/colorPrimary</item>
<item name="attrASupportToolbarEndColor">?attr/colorAccent</item>
</style>
<style name="DepthAESTheme" parent="AESTheme">
@@ -83,9 +86,9 @@
</style>
<style name="TaoAESTheme" parent="AESTheme">
<item name="colorPrimary">#FFACACAC</item>
<item name="colorPrimaryDark">#FF898989</item>
<item name="colorAccent">#FFD8D8D8</item>
<item name="colorPrimary">#FF7E7E7E</item>
<item name="colorPrimaryDark">#FF000000</item>
<item name="colorAccent">#FFE2E2E2</item>
</style>
<style name="NormalDialogStyle" parent="Theme.AppCompat.Light.Dialog">
@@ -109,4 +112,4 @@
<item name="@android:windowExitAnimation">@anim/normal_dialog_exit_corner</item>
</style>
</resources>
</resources>
-9
View File
@@ -25,14 +25,5 @@ android {
}
dependencies {
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// Gson
api 'com.google.code.gson:gson:2.8.9'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 添加JSch依赖(SFTP核心,com.jcraft:jsch:0.1.54
api 'com.jcraft:jsch:0.1.54'
api fileTree(dir: 'libs', include: ['*.jar'])
}
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 09:16:45 HKT 2026
stageCount=10
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.9
publishVersion=15.20.34
buildCount=0
baseBetaVersion=15.20.10
baseBetaVersion=15.20.35
+1 -13
View File
@@ -3,24 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".CrashHandler$CrashActivity"
android:name=".CrashActivity"
android:label="CrashActivity"
android:launchMode="singleInstance"
android:process=":CrashActivity"/>
@@ -2,6 +2,7 @@ package cc.winboll.studio.libappbase;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
/**
@@ -0,0 +1,109 @@
package cc.winboll.studio.libappbase;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
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;
public final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
private static final int MENUITEM_COPY = 0;
private static final int MENUITEM_RESTART = 1;
private String mLog;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
mLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
initLayout();
}
private void initLayout() {
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5);
TextView message = new TextView(this);
final int padding = dp2px(16);
message.setPadding(padding, padding, padding, padding);
message.setText(mLog);
message.setTextColor(0xFF000000);
message.setTextIsSelectable(true);
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
getActionBar().setTitle(CrashHandler.TITTLE);
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()) + " Error");
}
@Override
public void onBackPressed() {
restartApp();
}
private void restartApp() {
final Intent intent = getPackageManager()
.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
}
@@ -1,27 +1,13 @@
package cc.winboll.studio.libappbase;
import android.app.Activity;
import android.app.Application;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
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.utils.CrashHandleNotifyUtils;
@@ -220,111 +206,5 @@ public final class CrashHandler {
}
}
// ====================== 内部Activity页面 ======================
/**
* 基础极简崩溃页面
* 保险丝熔断时启动,避免复杂布局二次崩溃
*/
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
private static final int MENUITEM_COPY = 0;
private static final int MENUITEM_RESTART = 1;
private String mLog;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
initLayout();
}
/**
* 动态初始化布局
*/
private void initLayout() {
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5);
TextView message = new TextView(this);
final int padding = dp2px(16);
message.setPadding(padding, padding, padding, padding);
message.setText(mLog);
message.setTextColor(0xFF000000);
message.setTextIsSelectable(true);
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
}
@Override
public void onBackPressed() {
restartApp();
}
/**
* 重启应用
*/
private void restartApp() {
final Intent intent = getPackageManager()
.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* dp转px
*/
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
}
}
@@ -6,6 +6,10 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -125,18 +129,32 @@ public class GlobalApplication extends Application {
*/
@Override
public void onCreate() {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
try {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
// 初始化基础组件(日志、崩溃处理、Toast)
initCoreComponents();
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
restoreDebugStatus();
// 新增:初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
restoreDebugStatus();
// 初始化基础组件(日志、崩溃处理、Toast)
initCoreComponents();
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
/**
@@ -144,7 +162,11 @@ public class GlobalApplication extends Application {
*/
private void initCoreComponents() {
// 初始化日志工具(传入 Application 上下文)
LogUtils.init(this);
// 调试状态下初始化日志工具
if (GlobalApplication.isDebugging()) {
LogUtils.init(this);
}
// 初始化全局异常处理器(捕获应用崩溃信息,用于调试或上报)
CrashHandler.init(this);
// 初始化 Toast 工具(统一 Toast 样式、避免内存泄漏等)
@@ -187,7 +209,7 @@ public class GlobalApplication extends Application {
*/
public static String getAppName(Context context) {
if (context == null) {
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
Log.w(TAG, "getAppName: 上下文为空,返回 null");
return null;
}
PackageManager packageManager = context.getPackageManager();
@@ -203,8 +225,7 @@ public class GlobalApplication extends Application {
return appName;
} catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
Log.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
return null;
@@ -66,7 +66,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
final Intent intent = new Intent();
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashActivity.class);
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder("GlobalCrashActivity onCreate StackTrace");
@@ -27,6 +27,11 @@ public class LogActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(!GlobalApplication.isDebugging()) {
ToastUtils.show("非调试状态日志功能不可用");
finish();
}
// 设置布局文件(包含 LogView 控件)
setContentView(R.layout.activity_log);
@@ -67,45 +67,29 @@ public class LogUtils {
// ====================== 初始化入口 ======================
public static void init(final Context context) {
init(context, LOG_LEVEL.Off);
init(context, LOG_LEVEL.Off);
}
public static void init(final Context context, final LOG_LEVEL logLevel) {
Log.d(TAG, "init 执行日志工具初始化");
_mContext = context;
if (!GlobalApplication.isDebugging()) {
return;
}
Log.d(TAG, "init 执行日志工具初始化");
_mContext = context;
if (GlobalApplication.isDebugging()) {
initDebugDir();
} else {
initReleaseDir();
}
initLogUtilsDir();
initLogConfigBean();
addClassTAGList();
loadTAGBeanSettings();
checkAndTrimLogFileSize();
initLogConfigBean();
addClassTAGList();
loadTAGBeanSettings();
checkAndTrimLogFileSize();
_IsInited = true;
Log.d(TAG, "init 日志工具初始化完成");
_IsInited = true;
Log.d(TAG, "init 执行日志工具初始化完成");
}
// ====================== 目录初始化 ======================
private static void initDebugDir() {
final Context appContext = _mContext.getApplicationContext();
_mfLogCacheDir = new File(appContext.getExternalCacheDir(), TAG);
if (!_mfLogCacheDir.exists()) {
_mfLogCacheDir.mkdirs();
}
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
_mfLogDataDir = appContext.getExternalFilesDir(TAG);
if (!_mfLogDataDir.exists()) {
_mfLogDataDir.mkdirs();
}
_mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
}
private static void initReleaseDir() {
private static void initLogUtilsDir() {
final Context appContext = _mContext.getApplicationContext();
_mfLogCacheDir = new File(appContext.getCacheDir(), TAG);
if (!_mfLogCacheDir.exists()) {
@@ -113,7 +97,7 @@ public class LogUtils {
}
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
_mfLogDataDir = new File(appContext.getFilesDir(), TAG);
_mfLogDataDir = appContext.getExternalFilesDir(TAG);
if (!_mfLogDataDir.exists()) {
_mfLogDataDir.mkdirs();
}
@@ -135,8 +119,8 @@ public class LogUtils {
return;
}
final long MAX_FILE_SIZE = 6291456L;
final long KEEP_FILE_SIZE = 3145728L;
final long KEEP_FILE_SIZE = 25000L; // ~25KB 确保剪贴板可完整复制
final long MAX_FILE_SIZE = 2 * KEEP_FILE_SIZE;
final long fileSize = _mfLogCatchFile.length();
if (fileSize <= MAX_FILE_SIZE) {
@@ -266,6 +250,9 @@ public class LogUtils {
}
public static void setTAGListEnable(final String tag, final boolean isEnable) {
if (!_IsInited) {
return;
}
final Iterator<Map.Entry<String, Boolean>> iterator = mapTAGList.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String, Boolean> entry = iterator.next();
@@ -279,6 +266,9 @@ public class LogUtils {
}
public static void setALlTAGListEnable(final boolean isEnable) {
if (!_IsInited) {
return;
}
for (final Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
entry.setValue(isEnable);
}
@@ -288,15 +278,22 @@ public class LogUtils {
// ====================== 日志级别控制 ======================
public static void setLogLevel(final LOG_LEVEL logLevel) {
if (_mLogUtilsBean == null) {
Log.d(TAG, "setLogLevel LogUtils未初始化,忽略设置日志级别");
return;
}
_mLogUtilsBean.setLogLevel(logLevel);
_mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
}
public static LOG_LEVEL getLogLevel() {
return _mLogUtilsBean.getLogLevel();
return _mLogUtilsBean == null ?LOG_LEVEL.Off: _mLogUtilsBean.getLogLevel();
}
private static boolean isLoggable(final String tag, final LOG_LEVEL logLevel) {
if (!GlobalApplication.isDebugging()) {
return false;
}
if (!_IsInited) {
return false;
}
@@ -2,6 +2,7 @@ package cc.winboll.studio.libappbase;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
/**
@@ -2,6 +2,7 @@ package cc.winboll.studio.libappbase;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
/**
@@ -1,6 +1,7 @@
package cc.winboll.studio.libappbase;
import android.os.FileObserver;
import java.io.File;
import java.lang.ref.WeakReference;
/**
@@ -51,30 +52,38 @@ public class LogViewThread extends Thread {
*/
@Override
public void run() {
// 获取日志缓存目录路径(从 LogUtils 统一获取,确保路径一致性)
String logDirPath = LogUtils.getLogCacheDir().getPath();
LogUtils.d(TAG, "启动日志文件监听,监听目录:" + logDirPath);
// 调试状态进行日志输出任务
if (GlobalApplication.isDebugging()) {
// 获取日志缓存目录路径(从 LogUtils 统一获取,确保路径一致性)
File logDir = LogUtils.getLogCacheDir();
if (logDir == null) {
LogUtils.d(TAG, "日志缓存目录未初始化,线程退出");
return;
}
String logDirPath = logDir.getPath();
LogUtils.d(TAG, "启动日志文件监听,监听目录:" + logDirPath);
// 初始化日志文件监听器(监听目标目录的文件事件)
mLogListener = new LogListener(logDirPath);
// 开始监听文件事件(非阻塞,内部通过 Native 层实现)
mLogListener.startWatching();
// 初始化日志文件监听器(监听目标目录的文件事件)
mLogListener = new LogListener(logDirPath);
// 开始监听文件事件(非阻塞,内部通过 Native 层实现)
mLogListener.startWatching();
// 循环等待退出标志(每 1 秒检查一次,降低 CPU 占用)
while (!isExit()) {
try {
Thread.sleep(1000); // 休眠 1 秒,避免忙等
} catch (InterruptedException e) {
// 线程被中断时,恢复中断标志并退出循环(避免无限阻塞)
Thread.currentThread().interrupt();
LogUtils.d(TAG, "日志监听线程被中断,准备退出。" + e);
break;
}
}
// 循环等待退出标志(每 1 秒检查一次,降低 CPU 占用)
while (!isExit()) {
try {
Thread.sleep(1000); // 休眠 1 秒,避免忙等
} catch (InterruptedException e) {
// 线程被中断时,恢复中断标志并退出循环(避免无限阻塞)
Thread.currentThread().interrupt();
LogUtils.d(TAG, "日志监听线程被中断,准备退出。" + e);
break;
}
}
// 收到退出标志,停止监听并释放资源
mLogListener.stopWatching();
LogUtils.d(TAG, "日志文件监听已停止,线程退出");
// 收到退出标志,停止监听并释放资源
mLogListener.stopWatching();
LogUtils.d(TAG, "日志文件监听已停止,线程退出");
}
}
/**
@@ -0,0 +1,415 @@
package cc.winboll.studio.libappbase.models.libs1520000;
import android.content.Context;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.UTF8FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
/**
* WinBoLL JSON 数据模型基类(抽象类)
* 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑,
* 子类需实现抽象方法,完成自身字段JSON读写业务逻辑
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026/05/19 22:33:00
* @EditTime 2026/05/19 23:15:48
* @param <T> 泛型约束,限定子类必须继承自BaseBean
*/
public abstract class BaseBean<T extends BaseBean> {
// ====================== 静态常量 ======================
/** 日志输出标识TAG */
public static final String TAG = "BaseBean";
/** JSON存储Bean类名字段Key,用于类型校验 */
static final String BEAN_NAME = "BeanName";
// ====================== 构造方法 ======================
/**
* 无参空构造,满足反射实例化要求
*/
public BaseBean() {
}
// ====================== 抽象方法 ======================
/**
* 获取当前实体类全类名
* @return 全限定类名字符串
*/
public abstract String getName();
/**
* 从JSON读取器解析构建实体对象
* @param jsonReader JSON读取流
* @return 解析完成实体对象
* @throws IOException IO读写异常
*/
public abstract T readBeanFromJsonReader(final JsonReader jsonReader) throws IOException;
// ====================== 路径获取相关 ======================
/**
* 获取单个实体默认存储JSON文件路径
* @param context 应用上下文
* @return 文件绝对路径
*/
public String getBeanJsonFilePath(final Context context) {
return context.getExternalFilesDir(TAG) + "/" + getName() + ".json";
}
/**
* 获取实体列表默认存储JSON文件路径
* @param context 应用上下文
* @return 文件绝对路径
*/
public String getBeanListJsonFilePath(final Context context) {
return context.getExternalFilesDir(TAG) + "/" + getName() + "_List.json";
}
// ====================== JSON序列化基础方法 ======================
/**
* 写入基础Bean标识字段至JSON
* @param jsonWriter JSON写入流
* @throws IOException 写入异常
*/
public void writeThisToJsonWriter(final JsonWriter jsonWriter) throws IOException {
jsonWriter.name(BEAN_NAME).value(getName());
}
/**
* 基类通用字段解析回调
* @param jsonReader JSON读取流
* @param name 字段名
* @return 是否完成当前字段解析
* @throws IOException 读取异常
*/
public boolean initObjectsFromJsonReader(final JsonReader jsonReader, final String name) throws IOException {
return false;
}
// ====================== 实体字符串序列化 ======================
@Override
public String toString() {
LogUtils.d(TAG, "执行BaseBean实体转JSON字符串");
final StringWriter stringWriter = new StringWriter();
final JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setIndent(" ");
try {
jsonWriter.beginObject();
writeThisToJsonWriter(jsonWriter);
jsonWriter.endObject();
return stringWriter.toString();
} catch (final IOException e) {
LogUtils.d(TAG, "实体转JSON字符串异常", Thread.currentThread().getStackTrace());
}
return "";
}
// ====================== 列表序列化工具 ======================
/**
* Bean列表转为格式化JSON数组字符串
* @param beanList 实体集合
* @param <T> 实体泛型
* @return JSON字符串
*/
public static <T extends BaseBean> String toStringByBeanList(final ArrayList<T> beanList) {
LogUtils.d(TAG, "执行Bean列表序列化JSON操作");
try {
final StringWriter stringWriter = new StringWriter();
final JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setIndent(" ");
jsonWriter.beginArray();
for (int i = 0; i < beanList.size(); i++) {
jsonWriter.beginObject();
beanList.get(i).writeThisToJsonWriter(jsonWriter);
jsonWriter.endObject();
}
jsonWriter.endArray();
jsonWriter.close();
return stringWriter.toString();
} catch (final IOException e) {
LogUtils.d(TAG, "列表序列化JSON异常", Thread.currentThread().getStackTrace());
}
return "";
}
// ====================== JSON字符串解析实体 ======================
/**
* JSON文本解析为单个实体对象
* @param szBean JSON文本
* @param clazz 实体Class字节码
* @param <T> 实体泛型
* @return 解析完成实体
* @throws IOException 解析IO异常
*/
public static <T extends BaseBean> T parseStringToBean(final String szBean, final Class<T> clazz) throws IOException {
LogUtils.d(TAG, "进入字符串解析实体方法,目标Class:" + clazz.getSimpleName());
final StringReader stringReader = new StringReader(szBean);
final JsonReader jsonReader = new JsonReader(stringReader);
try {
final T beanTemp = clazz.newInstance();
return (T) beanTemp.readBeanFromJsonReader(jsonReader);
} catch (final InstantiationException e) {
LogUtils.d(TAG, "实体反射实例化失败(InstantiationException)", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "实体反射权限访问失败(IllegalAccessException)", Thread.currentThread().getStackTrace());
}
return null;
}
/**
* JSON数组文本解析填充实体列表
* @param szBeanList JSON数组文本
* @param beanList 目标存储集合
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 解析结果
*/
public static <T extends BaseBean> boolean parseStringToBeanList(final String szBeanList, ArrayList<T> beanList, final Class<T> clazz) {
LogUtils.d(TAG, "进入列表字符串解析方法");
try {
if (beanList == null) {
beanList = new ArrayList<T>();
} else {
beanList.clear();
}
final StringReader stringReader = new StringReader(szBeanList);
final JsonReader jsonReader = new JsonReader(stringReader);
jsonReader.beginArray();
while (jsonReader.hasNext()) {
final T beanTemp = clazz.newInstance();
final T bean = (T) beanTemp.readBeanFromJsonReader(jsonReader);
if (bean != null) {
beanList.add(bean);
}
}
jsonReader.endArray();
return true;
} catch (final InstantiationException e) {
LogUtils.d(TAG, "列表解析反射实例化异常", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "列表解析反射权限异常", Thread.currentThread().getStackTrace());
} catch (final IOException e) {
LogUtils.d(TAG, "列表解析JSON读写异常", Thread.currentThread().getStackTrace());
}
return false;
}
// ====================== 文件加载实体 ======================
/**
* 从默认路径加载单个实体
* @param context 上下文
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 加载实体
*/
public static <T extends BaseBean> T loadBean(final Context context, final Class<T> clazz) {
LogUtils.d(TAG, "执行默认路径加载实体数据");
try {
final T beanTemp = clazz.newInstance();
return loadBeanFromFile(beanTemp.getBeanJsonFilePath(context), clazz);
} catch (final InstantiationException e) {
LogUtils.d(TAG, "加载实体反射实例化失败", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "加载实体反射权限失败", Thread.currentThread().getStackTrace());
}
return null;
}
/**
* 自定义文件路径加载单个实体
* @param szFilePath 文件路径
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 实体对象
*/
public static <T extends BaseBean> T loadBeanFromFile(final String szFilePath, final Class<T> clazz) {
LogUtils.d(TAG, "指定路径加载实体,路径:" + szFilePath);
try {
final File file = new File(szFilePath);
if (file.exists()) {
final T beanTemp = clazz.newInstance();
final String json = UTF8FileUtils.readStringFromFile(szFilePath);
return beanTemp.parseStringToBean(json, clazz);
}
} catch (final InstantiationException e) {
LogUtils.d(TAG, "文件加载实体反射实例化异常", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "文件加载实体反射权限异常", Thread.currentThread().getStackTrace());
} catch (final IOException e) {
LogUtils.d(TAG, "文件读取JSON数据异常", Thread.currentThread().getStackTrace());
}
return null;
}
/**
* 默认路径加载实体列表
* @param context 上下文
* @param beanListDst 目标集合
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 加载结果
*/
public static <T extends BaseBean> boolean loadBeanList(final Context context, final ArrayList<T> beanListDst, final Class<T> clazz) {
LogUtils.d(TAG, "默认路径加载实体列表数据");
try {
final T beanTemp = clazz.newInstance();
return loadBeanListFromFile(beanTemp.getBeanListJsonFilePath(context), beanListDst, clazz);
} catch (final InstantiationException e) {
LogUtils.d(TAG, "列表加载反射实例化异常", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "列表加载反射权限异常", Thread.currentThread().getStackTrace());
}
return false;
}
/**
* 自定义路径加载实体列表
* @param szFilePath 文件路径
* @param beanList 目标集合
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 加载结果
*/
public static <T extends BaseBean> boolean loadBeanListFromFile(final String szFilePath, final ArrayList<T> beanList, final Class<T> clazz) {
LogUtils.d(TAG, "指定路径加载实体列表,路径:" + szFilePath);
try {
final File file = new File(szFilePath);
if (file.exists()) {
final String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
return parseStringToBeanList(listJson, beanList, clazz);
}
} catch (final IOException e) {
LogUtils.d(TAG, "列表文件读取异常", Thread.currentThread().getStackTrace());
}
return false;
}
// ====================== 实体数据保存文件 ======================
/**
* 默认路径保存单个实体
* @param context 上下文
* @param bean 待保存实体
* @param <T> 实体泛型
* @return 保存结果
*/
public static <T extends BaseBean> boolean saveBean(final Context context, final T bean) {
return saveBeanToFile(bean.getBeanJsonFilePath(context), bean);
}
/**
* 自定义路径保存单个实体
* @param szFilePath 保存路径
* @param bean 待保存实体
* @param <T> 实体泛型
* @return 保存结果
*/
public static <T extends BaseBean> boolean saveBeanToFile(final String szFilePath, final T bean) {
LogUtils.d(TAG, "执行实体数据写入文件操作");
try {
final String json = bean.toString();
UTF8FileUtils.writeStringToFile(szFilePath, json);
return true;
} catch (final IOException e) {
LogUtils.d(TAG, "实体写入文件异常", Thread.currentThread().getStackTrace());
}
return false;
}
/**
* 默认路径保存实体列表
* @param context 上下文
* @param beanList 实体集合
* @param clazz 实体字节码
* @param <T> 实体泛型
* @return 保存结果
*/
public static <T extends BaseBean> boolean saveBeanList(final Context context, final ArrayList<T> beanList, final Class<T> clazz) {
LogUtils.d(TAG, "默认路径保存实体列表数据");
try {
final T beanTemp = clazz.newInstance();
return saveBeanListToFile(beanTemp.getBeanListJsonFilePath(context), beanList);
} catch (final InstantiationException e) {
LogUtils.d(TAG, "列表保存反射实例化异常", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
LogUtils.d(TAG, "列表保存反射权限异常", Thread.currentThread().getStackTrace());
}
return false;
}
/**
* 自定义路径保存实体列表
* @param szFilePath 保存路径
* @param beanList 实体集合
* @param <T> 实体泛型
* @return 保存结果
*/
public static <T extends BaseBean> boolean saveBeanListToFile(final String szFilePath, final ArrayList<T> beanList) {
LogUtils.d(TAG, "指定路径保存实体列表数据");
try {
final String json = toStringByBeanList(beanList);
UTF8FileUtils.writeStringToFile(szFilePath, json);
return true;
} catch (final IOException e) {
LogUtils.d(TAG, "列表数据写入文件异常", Thread.currentThread().getStackTrace());
}
return false;
}
// ====================== Bean类型一致性校验 ======================
/**
* 校验本地JSON列表内实体类型是否统一
* @param szFilePath 列表文件路径
* @param clazz 目标校验实体Class
* @param <T> 实体泛型
* @return 校验结果信息
*/
public static <T extends BaseBean> String checkIsTheSameBeanListAndFile(final String szFilePath, final Class<T> clazz) {
final StringBuilder sbResult = new StringBuilder();
final String szErrorInfo = "Check Is The Same Bean List And File Error : ";
try {
int sameCount = 0;
int totalCount = 0;
final T beanTemp = clazz.newInstance();
final String targetBeanName = beanTemp.getName();
final String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
final StringReader stringReader = new StringReader(listJson);
final JsonReader jsonReader = new JsonReader(stringReader);
jsonReader.beginArray();
while (jsonReader.hasNext()) {
totalCount++;
jsonReader.beginObject();
while (jsonReader.hasNext()) {
final String name = jsonReader.nextName();
if (BEAN_NAME.equals(name)) {
if (targetBeanName.equals(jsonReader.nextString())) {
sameCount++;
}
} else {
jsonReader.skipValue();
}
}
jsonReader.endObject();
}
jsonReader.endArray();
if (sameCount != totalCount) {
sbResult.append("Total : ").append(totalCount).append(" Diff : ").append(totalCount - sameCount);
}
} catch (final InstantiationException e) {
sbResult.append(szErrorInfo).append(e);
LogUtils.d(TAG, "类型校验反射实例化异常", Thread.currentThread().getStackTrace());
} catch (final IllegalAccessException e) {
sbResult.append(szErrorInfo).append(e);
LogUtils.d(TAG, "类型校验反射权限异常", Thread.currentThread().getStackTrace());
} catch (final IOException e) {
sbResult.append(szErrorInfo).append(e);
LogUtils.d(TAG, "类型校验文件读取解析异常", Thread.currentThread().getStackTrace());
}
return sbResult.toString();
}
}
@@ -1,6 +1,8 @@
package cc.winboll.studio.libappbase.views;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -12,13 +14,12 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.dialogs.DebugHostDialog;
import cc.winboll.studio.libappbase.dialogs.APPValidationDialog;
import cc.winboll.studio.libappbase.models.APPInfo;
/**
@@ -76,12 +77,12 @@ public class AboutView extends LinearLayout {
private EditText metDevUserPassword;
// ===================================== 页面视图控件 =====================================
private DebugSwitchImageView ivAppIcon;
private DebugSwitchInfoImageView ivAppIcon;
private TextView tvAppNameVersion;
private TextView tvAppDesc;
private LinearLayout llFunctionContainer;
private ImageButton ibSebugStepOver;
private ImageButton ibSigngetDialog;
private ImageButton ibDebugUnlock;
private ImageButton ibWinBoLLHostDialog;
// ===================================== 构造方法(按参数从少到多排序) =====================================
@@ -195,12 +196,12 @@ public class AboutView extends LinearLayout {
llFunctionContainer = findViewById(R.id.ll_function_container);
// 功能按钮绑定
ibSebugStepOver = findViewById(R.id.ib_debug_step_over);
ibSigngetDialog = findViewById(R.id.ib_signgetdialog);
ibDebugUnlock = findViewById(R.id.ib_debug_unlock);
ibWinBoLLHostDialog = findViewById(R.id.ib_winbollhostdialog);
// 调试按钮统一只在调试模式显示
ibWinBoLLHostDialog.setVisibility(GlobalApplication.isDebugging() ? View.VISIBLE : View.GONE);
//ibSigngetDialog.setVisibility(GlobalApplication.isDebugging() ? View.VISIBLE : View.GONE);
//ibDebugUnlock.setVisibility(GlobalApplication.isDebugging() ? View.VISIBLE : View.GONE);
ibSebugStepOver.setVisibility(GlobalApplication.isDebugging() ? View.VISIBLE : View.GONE);
// 绑定按钮点击事件
@@ -312,6 +313,47 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, "initAboutPageView():视图组装完成,功能项加载完毕");
}
// ===================================== 调试解锁弹窗 =====================================
private void showDebugUnlockDialog() {
final AlertDialog dialog = new AlertDialog.Builder(mContext).create();
dialog.setTitle("应用调试解锁");
dialog.setCanceledOnTouchOutside(true);
final EditText etToken = new EditText(mContext);
etToken.setHint("请输入调试Token");
dialog.setView(etToken);
dialog.setButton(DialogInterface.BUTTON_POSITIVE, "调试解锁", (DialogInterface.OnClickListener) null);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "关闭", (DialogInterface.OnClickListener) null);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface d) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String inputToken = etToken.getText().toString().trim();
String savedToken = DebugSwitchInfoImageView.getDebugToken();
if (savedToken != null && savedToken.equals(inputToken)) {
GlobalApplication.setIsDebugging(true);
GlobalApplication.saveDebugStatus(GlobalApplication.getInstance());
Toast.makeText(mContext, "调试解锁成功,重启应用后生效", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, "调试Token不匹配", Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
}
});
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}
});
dialog.show();
}
// ===================================== 内部工具/事件方法 =====================================
/**
* 绑定功能按钮点击事件,处理正版校验、调试地址配置弹窗唤起
@@ -328,14 +370,7 @@ public class AboutView extends LinearLayout {
ToastUtils.show("已取消调试状态,重启应用可生效。");
}
});
// 正版校验弹窗
ibSigngetDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "ibSigngetDialog onClick:唤起应用正版校验弹窗");
new APPValidationDialog(mContext, mszAppName, mszAppVersionName).show();
}
});
// 调试地址配置弹窗
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {
@Override
@@ -344,6 +379,15 @@ public class AboutView extends LinearLayout {
new DebugHostDialog(mContext).show();
}
});
// 应用调试解锁按钮
ibDebugUnlock.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "ibDebugUnlock onClick:弹出调试解锁对话框");
showDebugUnlockDialog();
}
});
LogUtils.d(TAG, "setBtnClickListener():功能按钮点击事件绑定完成");
}
@@ -0,0 +1,127 @@
package cc.winboll.studio.libappbase.views;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import java.util.UUID;
import cc.winboll.studio.libappbase.GlobalApplication;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/04/06 19:32
* @Describe 应用Logo控件,连续点击6次弹出调试Token对话框,支持复制与重置
*/
public class DebugSwitchInfoImageView extends ImageView {
public static final String TAG = "DebugSwitchInfoImageView";
// 连续点击计数
private int mClickCount = 0;
// 目标点击次数
private static final int TARGET_CLICK_COUNT = 7;
private static String mDebugToken = null;
private static final String SP_DEBUG_TOKEN = "debug_token_prefs";
private static final String KEY_DEBUG_TOKEN = "debug_token";
public static String getDebugToken() {
if (mDebugToken != null) {
return mDebugToken;
}
Context context = GlobalApplication.getInstance();
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(SP_DEBUG_TOKEN, Context.MODE_PRIVATE);
mDebugToken = sp.getString(KEY_DEBUG_TOKEN, null);
if (mDebugToken == null) {
mDebugToken = UUID.randomUUID().toString();
sp.edit().putString(KEY_DEBUG_TOKEN, mDebugToken).apply();
}
}
return mDebugToken;
}
public static void resetDebugToken() {
Context context = GlobalApplication.getInstance();
if (context != null) {
mDebugToken = UUID.randomUUID().toString();
SharedPreferences sp = context.getSharedPreferences(SP_DEBUG_TOKEN, Context.MODE_PRIVATE);
sp.edit().putString(KEY_DEBUG_TOKEN, mDebugToken).apply();
}
}
private void showDebugTokenDialog() {
final AlertDialog dialog = new AlertDialog.Builder(getContext()).create();
dialog.setTitle("调试Token");
dialog.setMessage(getDebugToken());
dialog.setCanceledOnTouchOutside(false);
dialog.setButton(DialogInterface.BUTTON_POSITIVE, "复制到剪贴板", (DialogInterface.OnClickListener) null);
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, "重置", (DialogInterface.OnClickListener) null);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "关闭", (DialogInterface.OnClickListener) null);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface d) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ClipboardManager cm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText("DebugToken", getDebugToken()));
}
});
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resetDebugToken();
dialog.setMessage(getDebugToken());
}
});
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}
});
dialog.show();
}
public DebugSwitchInfoImageView(Context context) {
super(context);
init();
}
public DebugSwitchInfoImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DebugSwitchInfoImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public DebugSwitchInfoImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mClickCount++;
if (mClickCount >= TARGET_CLICK_COUNT) {
mClickCount = 0;
showDebugTokenDialog();
}
}
});
}
}
@@ -15,7 +15,7 @@
android:paddingRight="16dp"
android:paddingBottom="16dp">
<cc.winboll.studio.libappbase.views.DebugSwitchImageView
<cc.winboll.studio.libappbase.views.DebugSwitchInfoImageView
android:id="@+id/iv_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
@@ -81,7 +81,8 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_key"
android:id="@+id/ib_signgetdialog"
android:id="@+id/ib_debug_unlock"
android:contentDescription="应用调试解锁"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="@null"/>
@@ -15,7 +15,7 @@
android:paddingRight="16dp"
android:paddingBottom="16dp">
<cc.winboll.studio.libappbase.views.DebugSwitchImageView
<cc.winboll.studio.libappbase.views.DebugSwitchInfoImageView
android:id="@+id/iv_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
@@ -81,7 +81,8 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_key"
android:id="@+id/ib_signgetdialog"
android:id="@+id/ib_debug_unlock"
android:contentDescription="应用调试解锁"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="@null"/>
+13 -13
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:theme="?attr/themeDebug"
<!-- Theme removed for compatibility -->
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -12,7 +12,7 @@
android:layout_width="match_parent"
android:layout_height="34dp"
android:layout_alignParentTop="true"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:id="@+id/viewlogRelativeLayoutToolbar">
<Button
@@ -20,8 +20,8 @@
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:text="Clean"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:textColor=" android:textColor="@color/mainWindowTextColor""
android:background=" android:background="@color/toolbarBackgroundColor""
android:layout_centerVertical="true"
android:id="@+id/viewlogButtonClean"
android:layout_marginLeft="5dp"/>
@@ -35,7 +35,7 @@
android:layout_toRightOf="@+id/viewlogButtonClean"
android:layout_centerVertical="true"
android:id="@+id/viewlogTextView1"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:textColor="?attr/toolbarTextColor"/>
<cc.winboll.studio.libappbase.widget.LogTagSpinner
@@ -53,7 +53,7 @@
android:layout_toLeftOf="@+id/viewlogButtonCopy"
android:layout_centerVertical="true"
android:text="Selectable"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:id="@+id/viewlogCheckBoxSelectable"
android:padding="@dimen/log_text_padding"
android:textColor="?attr/toolbarTextColor"/>
@@ -62,8 +62,8 @@
android:layout_width="@dimen/log_button_width"
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:textColor=" android:textColor="@color/mainWindowTextColor""
android:background=" android:background="@color/toolbarBackgroundColor""
android:text="Copy"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
@@ -79,7 +79,7 @@
android:layout_below="@+id/viewlogRelativeLayoutToolbar"
android:id="@+id/viewlogLinearLayout1"
android:gravity="center_vertical"
android:background="?attr/colorTittleBackgound">
android:background=" android:background="@color/toolbarBackgroundColor"">
<CheckBox
android:layout_width="wrap_content"
@@ -88,7 +88,7 @@
android:text="ALL"
android:padding="2dp"
android:id="@+id/viewlogCheckBox1"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:textColor="?attr/toolbarTextColor"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"/>
@@ -99,14 +99,14 @@
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:textColor="?attr/colorText"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:singleLine="true"
android:id="@+id/tagsearch_et"/>
<HorizontalScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="?attr/colorTittleBackgound"
android:background=" android:background="@color/toolbarBackgroundColor""
android:scrollbars="none"
android:padding="2dp"
android:layout_weight="1.0"
@@ -132,7 +132,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorTextBackgound"
android:background="@color/colorTextBackgound"
android:id="@+id/viewlogScrollViewLog">
<TextView
+3
View File
@@ -60,4 +60,7 @@
<!-- DebugLogStyle 应用调试日志样式属性 -->
<attr name="debugTextColor" format="color"/>
<!-- 边框圆角属性 -->
<attr name="borderCornerRadius" format="dimension"/>
</resources>
-1
View File
@@ -1 +0,0 @@
/build
-42
View File
@@ -1,42 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply from: '../.winboll/winboll_lib_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 26
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// Gson
api 'com.google.code.gson:gson:2.8.9'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 添加JSch依赖(SFTP核心,com.jcraft:jsch:0.1.54
api 'com.jcraft:jsch:0.1.54'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.20.3'
api 'cc.winboll.studio:libappbase:15.20.9'
api fileTree(dir: 'libs', include: ['*.jar'])
}
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Fri May 15 08:48:45 GMT 2026
stageCount=2
libraryProject=libwinboll
baseVersion=15.20
publishVersion=15.20.1
buildCount=7
baseBetaVersion=15.20.2
-17
View File
@@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# 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 *;
#}
@@ -1,17 +0,0 @@
package cc.winboll.studio.libwinboll;
import android.app.Activity;
import android.os.Bundle;
import cc.winboll.studio.libappbase.ToastUtils;
public class WinBoLLLibraryActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_winbolllibrary);
ToastUtils.show("WinBoLLLibraryActivity onCreate");
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

@@ -1,11 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cc.winboll.studio.libwinboll.WinBoLLLibraryActivity"/>
</LinearLayout>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="@android:style/Theme.Material.Light">
</style>
</resources>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lib_name">libwinboll</string>
<string name="hello_world">Hello world!</string>
</resources>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="@android:style/Theme.Holo.Light">
</style>
</resources>
+34
View File
@@ -0,0 +1,34 @@
# MyGallery
#### 介绍
我的相册应用
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh gallery
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.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/)
#### 参考文档
+1
View File
@@ -0,0 +1 @@
+119
View File
@@ -0,0 +1,119 @@
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 {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.mygallery"
minSdkVersion 26
// 适配MIUI12
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.0"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
}
}
}
dependencies {
api 'com.google.code.gson:gson:2.10.1'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// 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.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// 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 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意:以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
/*implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
*/
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.20.21'
//api 'cc.winboll.studio:libappbase:15.20.34'
// 备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:libappbase:appbase-v15.21.+'
api 'com.github.ZhanGSKen:libaes:aes-v15.21.+'
api fileTree(dir: 'libs', include: ['*.jar'])
}
+8
View File
@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Aug 23 08:47:32 HKT 2026
stageCount=21
libraryProject=
baseVersion=15.0
publishVersion=15.0.20
buildCount=0
baseBetaVersion=15.0.21
+137
View File
@@ -0,0 +1,137 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类(避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件(避免资源ID混淆)
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法(避免JNI调用失败)
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application tools:replace="android:icon"
android:icon="@drawable/ic_my_gallery_beta">
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
<string name="app_name">MyGallery+</string>
</resources>
+66
View File
@@ -0,0 +1,66 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.mygallery">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_my_gallery"
android:roundIcon="@drawable/ic_my_gallery"
android:label="@string/app_name"
android:theme="@style/GalleryTheme"
android:resizeableActivity="true"
android:name=".App"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title"/>
<activity
android:name=".AlbumActivity"
android:label="@string/app_name"/>
<activity
android:name=".ImageViewerActivity"
android:label="@string/app_name"/>
<activity
android:name=".TrashActivity"
android:label="@string/trash"/>
<activity
android:name=".CropActivity"
android:label="调整封面"/>
<activity
android:name=".AboutActivity"
android:label="@string/about"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>
@@ -0,0 +1,51 @@
package cc.winboll.studio.mygallery;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
public class AboutActivity extends AppCompatActivity {
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "mygallery";
APPInfo appInfo = new APPInfo();
appInfo.setAppName("MyGallery");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("MyGallery");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=MyGallery");
appInfo.setAppAPKName("MyGallery");
appInfo.setAppAPKFolderName("MyGallery");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}
@@ -0,0 +1,25 @@
package cc.winboll.studio.mygallery;
import android.net.Uri;
import cc.winboll.studio.libappbase.LogUtils;
public class Album {
public static final String TAG = "Album";
private String name;
private String path;
private Uri coverUri;
private int imageCount;
public Album(String name, String path, Uri coverUri, int imageCount) {
LogUtils.d(TAG, "Album created: " + name);
this.name = name;
this.path = path;
this.coverUri = coverUri;
this.imageCount = imageCount;
}
public String getName() { return name; }
public String getPath() { return path; }
public Uri getCoverUri() { return coverUri; }
public int getImageCount() { return imageCount; }
}
@@ -0,0 +1,251 @@
package cc.winboll.studio.mygallery;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.mygallery.ImageAdapter.OnImageClickListener;
public class AlbumActivity extends AppCompatActivity {
public static final String TAG = "AlbumActivity";
private static final int PERMISSION_REQUEST_CODE = 101;
public static final String EXTRA_ALBUM_PATH = "album_path";
public static final String EXTRA_ALBUM_NAME = "album_name";
private RecyclerView recyclerView;
private ImageAdapter adapter;
private String albumPath;
private String albumName;
private FloatingActionButton fabScrollTop;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
getSupportActionBar().setTitle(albumName);
prefs = new Preferences(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
adapter = new ImageAdapter();
adapter.setContext(this);
adapter.setAlbumPath(albumPath);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
adapter.setOnImageClickListener(new OnImageClickListener() {
@Override
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
startActivity(intent);
}
});
if (checkPermission()) {
loadImages();
} else {
requestPermission();
}
}
private boolean checkPermission() {
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadImages();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private String getSortOrder(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
case Preferences.SORT_NAME_DESC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
case Preferences.SORT_NAME_ASC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
case Preferences.SORT_TIME_DESC:
default:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
}
}
private void loadImages() {
LogUtils.d(TAG, "loadImages");
ArrayList<Uri> imageUrls = new ArrayList<>();
ArrayList<String> imagePaths = new ArrayList<>();
ContentResolver contentResolver = getContentResolver();
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
String[] selectionArgs = new String[]{albumPath + "%"};
int sortMode = prefs.getAlbumSortMode();
String sortOrder = getSortOrder(sortMode);
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
if (cursor != null) {
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
if (path != null && path.startsWith(albumPath + "/")) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
imageUrls.add(contentUri);
imagePaths.add(path);
}
}
}
}
if (imageUrls.isEmpty()) {
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "No images found");
}
adapter.setData(imageUrls, imagePaths);
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_album, menu);
int sortMode = prefs.getAlbumSortMode();
int menuId = getSortMenuId(sortMode);
MenuItem item = menu.findItem(menuId);
if (item != null) {
item.setChecked(true);
}
MenuItem sortItem = menu.findItem(R.id.action_sort);
if (sortItem != null && sortItem.getSubMenu() != null) {
sortItem.getSubMenu().setGroupCheckable(0, true, true);
}
return true;
}
private int getSortMenuId(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return R.id.sort_time_asc;
case Preferences.SORT_NAME_DESC:
return R.id.sort_name_desc;
case Preferences.SORT_NAME_ASC:
return R.id.sort_name_asc;
case Preferences.SORT_TIME_DESC:
default:
return R.id.sort_time_desc;
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.sort_time_desc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_time_asc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_desc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_asc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
item.setChecked(true);
loadImages();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
loadImages();
}
if (adapter != null) {
adapter.refreshBg();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK) {
loadImages();
}
}
}
@@ -0,0 +1,250 @@
package cc.winboll.studio.mygallery;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import cc.winboll.studio.libappbase.LogUtils;
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
public static final String TAG = "AlbumAdapter";
private ArrayList<Album> albums = new ArrayList<>();
private OnAlbumClickListener listener;
private Preferences prefs;
private int bgType = 0;
private PinnedAlbumDbHelper pinnedDbHelper;
private OnCoverSizeListener coverSizeListener;
private boolean coverSizeReported = false;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnAlbumClickListener {
void onAlbumClick(Album album);
}
public interface OnCoverSizeListener {
void onCoverSize(int width, int height);
}
public static final int DEFAULT_COVER_WIDTH = 240;
public static final int DEFAULT_COVER_HEIGHT = 120;
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
this.listener = listener;
}
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
this.coverSizeListener = listener;
}
public void setData(ArrayList<Album> albums) {
this.albums = albums;
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
notifyDataSetChanged();
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
}
notifyDataSetChanged();
}
public void refreshCover() {
notifyDataSetChanged();
}
public void refreshPinned() {
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
java.util.LinkedHashMap<String, Integer> pinnedPathMap = pinnedDbHelper.getPinnedPathsWithSortOrder();
Set<String> pinnedPaths = pinnedPathMap.keySet();
ArrayList<Album> pinned = new ArrayList<Album>();
ArrayList<Album> unpinned = new ArrayList<Album>();
// 按照排序号顺序添加置顶相册
for (String pinnedPath : pinnedPathMap.keySet()) {
for (Album album : albums) {
if (album.getPath().equals(pinnedPath)) {
pinned.add(album);
break;
}
}
}
// 添加非置顶相册
for (Album album : albums) {
if (!pinnedPaths.contains(album.getPath())) {
unpinned.add(album);
}
}
albums.clear();
albums.addAll(pinned);
albums.addAll(unpinned);
notifyDataSetChanged();
}
}
private void showContextMenu(View view, final Album album) {
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle(album.getName());
final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
String[] items = null;
if (isPinned) {
items = new String[]{"取消置顶", "置顶"};
} else {
items = new String[]{"置顶"};
}
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (pinnedDbHelper != null) {
if (which == 0) {
if (isPinned) {
pinnedDbHelper.unpinAlbum(album.getPath());
} else {
pinnedDbHelper.pinAlbum(album.getPath());
}
} else if (which == 1 && isPinned) {
pinnedDbHelper.repinAlbumToFront(album.getPath());
}
refreshPinned();
}
}
});
builder.show();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_album, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final Album album = albums.get(position);
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
holder.coverImage.setBackgroundResource(getBgRes());
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, album);
return true;
}
});
holder.albumName.setText(album.getName());
holder.imageCount.setText(album.getImageCount() + " photos");
holder.itemView.post(new Runnable() {
@Override
public void run() {
if (!coverSizeReported && prefs != null) {
int savedWidth = prefs.getCoverWidth();
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
int width = holder.coverImage.getWidth();
int height = holder.coverImage.getHeight();
if (width > 0 && height > 0) {
prefs.setCoverWidth(width);
prefs.setCoverHeight(height);
float ratio = (float) width / height;
prefs.setCoverRatio(ratio);
coverSizeReported = true;
if (coverSizeListener != null) {
coverSizeListener.onCoverSize(width, height);
}
}
}
}
}
});
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onAlbumClick(album);
}
}
});
Uri coverUri = album.getCoverUri();
if (coverUri != null) {
String uriString = coverUri.toString();
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
if ("file".equals(coverUri.getScheme())) {
File coverFile = new File(coverUri.getPath());
if (coverFile.exists()) {
Glide.with(holder.coverImage.getContext())
.load(coverFile)
.fitCenter()
.into(holder.coverImage);
return;
}
}
Glide.with(holder.coverImage.getContext())
.load(coverUri)
.fitCenter()
.into(holder.coverImage);
}
}
@Override
public int getItemCount() {
return albums.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView coverImage;
ImageView pinIcon;
TextView albumName;
TextView imageCount;
ViewHolder(View itemView) {
super(itemView);
coverImage = itemView.findViewById(R.id.album_cover);
pinIcon = itemView.findViewById(R.id.pin_icon);
albumName = itemView.findViewById(R.id.album_name);
imageCount = itemView.findViewById(R.id.image_count);
}
}
}
@@ -0,0 +1,131 @@
package cc.winboll.studio.mygallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
public static final String TAG = "AlbumCoverDbHelper";
private static final String DB_NAME = "album_cover.db";
private static final int DB_VERSION = 2;
private static final String TABLE_NAME = "album_covers";
private static final String COLUMN_ALBUM_PATH = "album_path";
private static final String COLUMN_IMAGE_PATH = "image_path";
private static final String COLUMN_CROP_PATH = "crop_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_IMAGE_PATH + " TEXT, "
+ COLUMN_CROP_PATH + " TEXT)";
private static AlbumCoverDbHelper dbHelper;
public static AlbumCoverDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public AlbumCoverDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
try {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
} catch (Exception e) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
values.put(COLUMN_CROP_PATH, cropPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
}
public void setCover(String albumPath, String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
}
public String getCover(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String coverPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
coverPath = cursor.getString(0);
if (coverPath == null) {
coverPath = cursor.getString(1);
}
}
cursor.close();
}
return coverPath;
}
public String getCropPath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String cropPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
cropPath = cursor.getString(0);
}
cursor.close();
}
return cropPath;
}
public String getOriginalImagePath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String imagePath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
imagePath = cursor.getString(0);
}
cursor.close();
}
return imagePath;
}
public void clearCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.putNull(COLUMN_CROP_PATH);
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "clearCover: " + albumPath);
}
public void deleteCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "deleteCover: " + albumPath);
}
}
@@ -0,0 +1,52 @@
package cc.winboll.studio.mygallery;
import cc.winboll.studio.mygallery.utils.BackgroundUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/19
* @Describe 应用全局入口类(继承 GlobalWinBoLLApplication
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
*/
public class App extends GlobalApplication {
public static final String TAG = "App";
@Override
public void onCreate() {
try {
super.onCreate();
WinBoLLActivityManager.init(this);
BackgroundUtils.initFromPreferences(this);
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}
@@ -0,0 +1,313 @@
package cc.winboll.studio.mygallery;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
public class CropActivity extends AppCompatActivity {
public static final String TAG = "CropActivity";
public static final String EXTRA_IMAGE_URI = "image_uri";
public static final String EXTRA_IMAGE_PATH = "image_path";
public static final String EXTRA_ALBUM_PATH = "album_path";
public static final String EXTRA_CROP_WIDTH = "crop_width";
public static final String EXTRA_CROP_HEIGHT = "crop_height";
private CropCanvasView cropCanvasView;
private ZoomContainerView zoomContainer;
private Bitmap originalBitmap;
private String imagePath;
private String albumPath;
private int cropWidth = 240;
private int cropHeight = 120;
private float cropRatio = 2.0f;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crop);
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
prefs = new Preferences(this);
int bgType = prefs.getBgType();
if (cropWidth > 0 && cropHeight > 0) {
cropRatio = (float) cropWidth / cropHeight;
} else {
cropRatio = prefs.getCoverRatio();
cropWidth = (int) (120 * cropRatio);
cropHeight = 120;
}
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveCroppedCover();
}
});
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showCropInfoDialog();
}
});
findViewById(R.id.btn_change_bg).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(CropActivity.this, "修改剪裁背景颜色", Toast.LENGTH_SHORT).show();
}
});
zoomContainer = findViewById(R.id.zoom_container);
SeekBar seekBarZoom = findViewById(R.id.seekbar_zoom);
seekBarZoom.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (zoomContainer != null && fromUser) {
float scale = 0.1f + (progress / 100f) * 4.9f;
zoomContainer.setScaleFactor(scale);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
cropCanvasView = findViewById(R.id.crop_canvas_view);
cropCanvasView.setBackgroundType(bgType);
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
@Override
public void onBackgroundColorChanged(int color) {
}
});
loadImage();
}
private void loadImage() {
try {
if (imagePath != null && new File(imagePath).exists()) {
originalBitmap = BitmapFactory.decodeFile(imagePath);
} else {
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
if (imageUri != null) {
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
}
}
if (originalBitmap != null) {
cropCanvasView.setImageBitmap(originalBitmap);
cropCanvasView.post(new Runnable() {
@Override
public void run() {
initCrop();
}
});
} else {
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
} catch (Exception e) {
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
}
private void initCrop() {
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
int viewW = cropCanvasView.getWidth();
int viewH = cropCanvasView.getHeight();
if (viewW > 0 && viewH > 0) {
cropCanvasView.scaleToView(viewW, viewH);
}
}
private void saveCroppedCover() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
return;
}
try {
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
if (canvasBitmap == null || canvasBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get canvas bitmap", Toast.LENGTH_SHORT).show();
return;
}
RectF cropRect = cropCanvasView.getCropRect();
int bmpX = (int) cropRect.left;
int bmpY = (int) cropRect.top;
int bmpW = (int) cropRect.width();
int bmpH = (int) cropRect.height();
bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
LogUtils.d(TAG, "saveCroppedCover: cropRect=" + cropRect);
LogUtils.d(TAG, "saveCroppedCover: crop=(" + bmpX + "," + bmpY + "," + bmpW + "," + bmpH + ")");
LogUtils.d(TAG, "saveCroppedCover: canvas size=" + canvasBitmap.getWidth() + "x" + canvasBitmap.getHeight());
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
canvasBitmap.recycle();
LogUtils.d(TAG, "saveCroppedCover: cropped size=" + cropped.getWidth() + "x" + cropped.getHeight());
File coverDir = new File(getFilesDir(), "covers");
if (!coverDir.exists()) {
coverDir.mkdirs();
}
File coverFile = new File(coverDir, "cover_" + System.currentTimeMillis() + ".png");
LogUtils.d(TAG, "saveCroppedCover: cover file=" + coverFile.getAbsolutePath());
FileOutputStream fos = new FileOutputStream(coverFile);
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close();
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
cropped.recycle();
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
LogUtils.d(TAG, "saveCroppedCover: cover saved to db, albumPath=" + albumPath);
Intent broadcastIntent = new Intent(Preferences.ACTION_COVER_UPDATED);
sendBroadcast(broadcastIntent);
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
} catch (Exception e) {
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
}
}
private void showCropInfoDialog() {
StringBuilder info = new StringBuilder();
info.append("=== 画布信息 ===\n");
info.append("画布宽度: ").append(cropCanvasView.getCanvasWidth()).append("px\n");
info.append("画布高度: ").append(cropCanvasView.getCanvasHeight()).append("px\n");
info.append("\n=== 背景类型 ===\n");
String[] bgNames = {"灰白相间", "全白", "全黑"};
info.append("背景: ").append(bgNames[cropCanvasView.getBackgroundType()]).append("\n");
int bgColor = cropCanvasView.getBackgroundColor();
info.append("背景颜色: #").append(String.format("%06X", bgColor & 0xFFFFFF)).append("\n");
info.append("拾取坐标: ").append(cropCanvasView.getLastPickImageX()).append(",")
.append(cropCanvasView.getLastPickImageY()).append("\n");
info.append("\n=== 裁剪结果 ===\n");
RectF cropRect = cropCanvasView.getCropRect();
if (cropRect != null) {
info.append("裁剪区域: ").append((int)cropRect.left).append(",")
.append((int)cropRect.top).append(",")
.append((int)cropRect.right).append(",")
.append((int)cropRect.bottom).append("\n");
info.append("裁剪宽度: ").append((int)cropRect.width()).append("px\n");
info.append("裁剪高度: ").append((int)cropRect.height()).append("px\n");
}
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
final Bitmap[] previewHolder = new Bitmap[1];
if (canvasBitmap != null && !canvasBitmap.isRecycled() && cropRect != null) {
int bmpX = Math.max(0, (int) cropRect.left);
int bmpY = Math.max(0, (int) cropRect.top);
int bmpW = Math.min((int) cropRect.width(), canvasBitmap.getWidth() - bmpX);
int bmpH = Math.min((int) cropRect.height(), canvasBitmap.getHeight() - bmpY);
if (bmpW > 0 && bmpH > 0) {
previewHolder[0] = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
}
}
final Bitmap previewBitmap = previewHolder[0];
View dialogView = getLayoutInflater().inflate(R.layout.dialog_crop_info, null);
TextView infoText = dialogView.findViewById(R.id.info_text);
ImageView previewImage = dialogView.findViewById(R.id.preview_image);
final LinearLayout previewImageContainer = dialogView.findViewById(R.id.preview_image_container);
infoText.setText(info.toString());
if (previewBitmap != null) {
previewImage.setImageBitmap(previewBitmap);
}
previewImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int randomColor = 0xFF000000 | new Random().nextInt(0x00FFFFFF);
previewImageContainer.setBackgroundColor(randomColor);
}
});
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("裁剪信息")
.setView(dialogView)
.setPositiveButton("确定", null)
.create();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (previewBitmap != null && !previewBitmap.isRecycled()) {
previewBitmap.recycle();
}
}
});
dialog.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (originalBitmap != null) {
originalBitmap.recycle();
originalBitmap = null;
}
}
}
@@ -0,0 +1,750 @@
package cc.winboll.studio.mygallery;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropCanvasView extends View {
public interface OnBackgroundColorChangedListener {
void onBackgroundColorChanged(int color);
}
public interface OnColorPickedListener {
void onColorPicked(int color);
}
public interface OnColorPickEndListener {
void onColorPickEnd();
}
private OnBackgroundColorChangedListener backgroundColorChangedListener;
private OnColorPickedListener colorPickedListener;
private OnColorPickEndListener colorPickEndListener;
public void setOnBackgroundColorChangedListener(OnBackgroundColorChangedListener listener) {
this.backgroundColorChangedListener = listener;
}
public void setOnColorPickedListener(OnColorPickedListener listener) {
this.colorPickedListener = listener;
}
public void setOnColorPickEndListener(OnColorPickEndListener listener) {
this.colorPickEndListener = listener;
}
private Paint imagePaint;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private int imageWidth;
private int imageHeight;
private float coverRatio = 2.0f;
private int extendHeight;
private int extendWidth;
private int canvasWidth;
private int canvasHeight;
private float minSize = 50;
private RectF imageBounds = new RectF();
private RectF canvasBounds = new RectF();
private Bitmap originalBitmap;
private Bitmap canvasBitmap;
private float bitmapScale = 1.0f;
private Bitmap displayBitmap;
private RectF initialSpanRect;
private float initialSpan;
private int bgType = 2;
private Bitmap tileBitmap;
private BitmapShader tileShader;
private Paint bgPaint;
private int backgroundColor = Color.BLUE;
private boolean colorPickMode = false;
private int previewColor = 0;
private float lastPickX = 0;
private float lastPickY = 0;
private float pickX, pickY;
private float displayScale = 1.0f;
private float displayOffsetX = 0f;
private float displayOffsetY = 0f;
private static final int MAX_DISPLAY_SIZE = 2048;
private float containerScale = 1.0f;
public CropCanvasView(Context context) {
super(context);
init();
}
public CropCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
imagePaint = new Paint();
imagePaint.setFilterBitmap(true);
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
bgType = 2;
bgPaint = new Paint();
initTileBitmap();
}
private void initTileBitmap() {
if (tileBitmap != null && !tileBitmap.isRecycled()) {
tileBitmap.recycle();
tileBitmap = null;
}
tileShader = null;
if (bgType == 0) {
Drawable drawable = getContext().getDrawable(R.drawable.bg_checkerboard);
if (drawable != null) {
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
if (w <= 0) w = 10;
if (h <= 0) h = 10;
tileBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(tileBitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(c);
tileShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
public void setBackgroundType(int type) {
if (bgType != type) {
bgType = type;
initTileBitmap();
invalidate();
}
}
public int getBackgroundType() {
return bgType;
}
private void drawBackground(Canvas canvas) {
if (bgType == 0 && tileShader != null) {
bgPaint.setShader(tileShader);
canvas.drawRect(canvasBounds, bgPaint);
} else if (bgType == 1) {
bgPaint.setShader(null);
bgPaint.setColor(Color.WHITE);
canvas.drawRect(canvasBounds, bgPaint);
} else {
bgPaint.setShader(null);
bgPaint.setColor(Color.BLACK);
canvas.drawRect(canvasBounds, bgPaint);
}
}
public void setImageBitmap(Bitmap bitmap) {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
}
this.originalBitmap = bitmap;
createDisplayBitmap();
}
private void createDisplayBitmap() {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
displayBitmap = null;
}
if (originalBitmap == null || originalBitmap.isRecycled()) {
return;
}
int w = originalBitmap.getWidth();
int h = originalBitmap.getHeight();
float scale = 1.0f;
if (w > MAX_DISPLAY_SIZE || h > MAX_DISPLAY_SIZE) {
scale = Math.min((float) MAX_DISPLAY_SIZE / w, (float) MAX_DISPLAY_SIZE / h);
int newW = (int) (w * scale);
int newH = (int) (h * scale);
displayBitmap = Bitmap.createScaledBitmap(originalBitmap, newW, newH, true);
} else {
displayBitmap = originalBitmap;
}
displayBitmapScale = scale;
}
private float displayBitmapScale = 1.0f;
public Bitmap getOriginalBitmap() {
return originalBitmap;
}
public float getDisplayBitmapScale() {
return displayBitmapScale;
}
public void onContainerScaled() {
containerScale = 1.0f;
invalidate();
}
public void setContainerScale(float scale) {
if (scale > 0 && scale != containerScale) {
containerScale = scale;
invalidate();
}
}
public float getContainerScale() {
return containerScale;
}
public Bitmap getCanvasBitmap() {
if (canvasWidth <= 0 || canvasHeight <= 0) {
return null;
}
Bitmap canvasBmp = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(canvasBmp);
drawBackground(canvas);
if (displayBitmap != null && !displayBitmap.isRecycled()) {
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
}
return canvasBmp;
}
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
this.imageWidth = imgWidth;
this.imageHeight = imgHeight;
this.coverRatio = ratio;
float imgRatio = (float) imgWidth / imgHeight;
if (coverRatio >= imgRatio) {
extendHeight = imgHeight;
extendWidth = (int) (imgHeight * coverRatio);
} else {
extendWidth = imgWidth;
extendHeight = (int) (imgWidth / coverRatio);
}
canvasHeight = Math.max(imageHeight, extendHeight);
canvasWidth = Math.max(imageWidth, extendWidth);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
cropRect = new RectF(canvasBounds);
requestLayout();
invalidate();
}
public void getDisplayMatrix(Matrix matrix) {
if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
return;
}
matrix.reset();
float displayScaleX = (float) getWidth() / canvasWidth;
float displayScaleY = (float) getHeight() / canvasHeight;
displayScale = Math.min(displayScaleX, displayScaleY);
displayOffsetX = (getWidth() - canvasWidth * displayScale) / 2f;
displayOffsetY = (getHeight() - canvasHeight * displayScale) / 2f;
matrix.postTranslate(displayOffsetX, displayOffsetY);
matrix.postScale(displayScale, displayScale);
}
public float screenToImageX(float screenX) {
return (screenX - displayOffsetX) / displayScale;
}
public float screenToImageY(float screenY) {
return (screenY - displayOffsetY) / displayScale;
}
public float imageToScreenX(float imageX) {
return imageX * displayScale + displayOffsetX;
}
public float imageToScreenY(float imageY) {
return imageY * displayScale + displayOffsetY;
}
public int getImageColorAt(float screenX, float screenY) {
Bitmap bmp = (displayBitmap != null) ? displayBitmap : originalBitmap;
if (bmp == null || bmp.isRecycled()) {
return Color.TRANSPARENT;
}
float imgX = screenToImageX(screenX);
float imgY = screenToImageY(screenY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
if (bmpX >= 0 && bmpX < bmp.getWidth() &&
bmpY >= 0 && bmpY < bmp.getHeight()) {
return bmp.getPixel(bmpX, bmpY);
}
return Color.TRANSPARENT;
}
public void setBackgroundColor(int color) {
this.backgroundColor = color;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(color);
}
invalidate();
}
public int getBackgroundColor() {
return backgroundColor;
}
public int getPreviewColor() {
return previewColor;
}
public float getLastPickX() {
return lastPickX;
}
public float getLastPickY() {
return lastPickY;
}
public int getLastPickImageX() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpX, originalBitmap.getWidth() - 1));
}
public int getLastPickImageY() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpY, originalBitmap.getHeight() - 1));
}
public void setColorPickMode(boolean enable) {
this.colorPickMode = enable;
if (enable) {
pickX = -1;
pickY = -1;
}
invalidate();
}
public boolean isColorPickMode() {
return colorPickMode;
}
public void scaleToView(int viewWidth, int viewHeight) {
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
int oldCanvasW = canvasWidth;
int oldCanvasH = canvasHeight;
canvasWidth = (int) (canvasWidth * scale);
canvasHeight = (int) (canvasHeight * scale);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
if (cropRect != null) {
float scaleX = (float) canvasWidth / oldCanvasW;
float scaleY = (float) canvasHeight / oldCanvasH;
cropRect.left *= scaleX;
cropRect.top *= scaleY;
cropRect.right *= scaleX;
cropRect.bottom *= scaleY;
}
requestLayout();
invalidate();
}
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public int getCanvasWidth() {
return canvasWidth;
}
public int getCanvasHeight() {
return canvasHeight;
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
public RectF getImageBounds() {
return new RectF(imageBounds);
}
public float getBitmapScale() {
return displayScale;
}
public float getDisplayScale() {
return displayScale;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = canvasWidth > 0 ? canvasWidth : 0;
int h = canvasHeight > 0 ? canvasHeight : 0;
setMeasuredDimension(w, h);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
getDisplayMatrix(new Matrix());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cropRect == null) return;
if (displayBitmap != null && !displayBitmap.isRecycled()) {
canvas.save();
Matrix matrix = new Matrix();
getDisplayMatrix(matrix);
canvas.concat(matrix);
drawBackground(canvas);
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
canvas.restore();
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
} else {
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
if (event.getPointerCount() == 2) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_POINTER_DOWN) {
initialSpan = getSpan(event);
return true;
} else if (action == MotionEvent.ACTION_MOVE) {
float span = getSpan(event);
if (initialSpan > 0) {
float scale = span / initialSpan;
float centerX = cropRect.centerX();
float centerY = cropRect.centerY();
float newWidth = cropRect.width() * scale;
float newHeight = newWidth / coverRatio;
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
newHeight = newWidth / coverRatio;
float newLeft = centerX - newWidth / 2;
float newTop = centerY - newHeight / 2;
float newRight = centerX + newWidth / 2;
float newBottom = centerY + newHeight / 2;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right &&
newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.set(newLeft, newTop, newRight, newBottom);
}
initialSpan = span;
invalidate();
}
return true;
}
}
float x = event.getX();
float y = event.getY();
RectF bounds = canvasBounds;
if (containerScale != 1.0f) {
x = x / containerScale;
y = y / containerScale;
bounds = new RectF(
canvasBounds.left,
canvasBounds.top,
canvasBounds.right,
canvasBounds.bottom
);
}
if (colorPickMode) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
if (originalBitmap != null && !originalBitmap.isRecycled()) {
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
previewColor = getImageColorAt(x, y);
} else if (canvasBounds.contains(x, y)) {
previewColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
} else {
previewColor = Color.TRANSPARENT;
}
lastPickX = x;
lastPickY = y;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(previewColor);
}
}
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (canvasBounds.contains(x, y)) {
int pickedColor;
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
int colorAtPoint = getImageColorAt(x, y);
pickedColor = colorAtPoint;
backgroundColor = colorAtPoint;
} else {
pickedColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
}
if (colorPickedListener != null) {
colorPickedListener.onColorPicked(pickedColor);
}
}
if (colorPickEndListener != null) {
colorPickEndListener.onColorPickEnd();
}
previewColor = 0;
invalidate();
return true;
}
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= bounds.left && newRight <= bounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= bounds.top && newBottom <= bounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / coverRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
float dx = Math.abs(x - cropRect.left);
float dy = Math.abs(y - cropRect.top);
float dx2 = Math.abs(x - cropRect.right);
float dy2 = Math.abs(y - cropRect.bottom);
if (dx <= touchArea && dy <= touchArea) {
return CORNER_TOP_LEFT;
}
if (dx2 <= touchArea && dy <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (dx <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (dx2 <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
return CORNER_CENTER;
}
return -1;
}
private float getSpan(MotionEvent event) {
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
}
public int getColorAt(float x, float y) {
return getImageColorAt(x, y);
}
}
@@ -0,0 +1,224 @@
package cc.winboll.studio.mygallery;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropOverlayView extends View {
public interface OnCanvasSizeChangedListener {
void onCanvasSizeChanged(RectF canvasBounds);
}
private OnCanvasSizeChangedListener canvasSizeChangedListener;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private float targetRatio = 2.0f;
private RectF canvasBounds = new RectF();
private float minSize = 50;
public CropOverlayView(Context context) {
super(context);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaints();
}
private void initPaints() {
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
}
public void setTargetRatio(float ratio) {
this.targetRatio = ratio;
}
public float getTargetRatio() {
return targetRatio;
}
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
this.canvasSizeChangedListener = listener;
}
public void initCanvas(int canvasW, int canvasH) {
canvasBounds.set(0, 0, canvasW, canvasH);
cropRect = new RectF(0, 0, canvasW, canvasH);
if (canvasSizeChangedListener != null) {
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
}
invalidate();
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.TRANSPARENT);
if (cropRect != null) {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return false;
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / targetRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (cropRect.contains(x, y)) {
return CORNER_CENTER;
}
return -1;
}
}
@@ -0,0 +1,247 @@
package cc.winboll.studio.mygallery;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import cc.winboll.studio.libappbase.LogUtils;
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
public static final String TAG = "ImageAdapter";
private ArrayList<Uri> imageUrls = new ArrayList<>();
private ArrayList<String> imagePaths = new ArrayList<>();
private OnImageClickListener listener;
private int bgType = 0;
private Preferences prefs;
private PinnedImageDbHelper pinnedDbHelper;
private AlbumCoverDbHelper coverDbHelper;
private String albumPath;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnImageClickListener {
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
}
public void setOnImageClickListener(OnImageClickListener listener) {
this.listener = listener;
}
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
this.imageUrls = urls;
this.imagePaths = paths;
sortPinnedFirst();
LogUtils.d(TAG, "setData: " + urls.size() + " images");
notifyDataSetChanged();
}
private void sortPinnedFirst() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
ArrayList<Uri> pinnedUrls = new ArrayList<>();
ArrayList<String> pinnedPaths = new ArrayList<>();
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
ArrayList<String> unpinnedPaths = new ArrayList<>();
for (int i = 0; i < imagePaths.size(); i++) {
String path = imagePaths.get(i);
if (pinnedDbHelper.isPinned(path)) {
pinnedUrls.add(imageUrls.get(i));
pinnedPaths.add(path);
} else {
unpinnedUrls.add(imageUrls.get(i));
unpinnedPaths.add(path);
}
}
imageUrls.clear();
imagePaths.clear();
imageUrls.addAll(pinnedUrls);
imageUrls.addAll(unpinnedUrls);
imagePaths.addAll(pinnedPaths);
imagePaths.addAll(unpinnedPaths);
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
}
public void setAlbumPath(String albumPath) {
this.albumPath = albumPath;
}
public int getCropWidth() {
return prefs != null ? prefs.getCoverWidth() : 240;
}
public int getCropHeight() {
return prefs != null ? prefs.getCoverHeight() : 120;
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
notifyDataSetChanged();
}
}
public void refreshPinned() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
sortPinnedFirst();
notifyDataSetChanged();
}
private void showContextMenu(final View view, final int position) {
final String imagePath = imagePaths.get(position);
final Uri imageUri = imageUrls.get(position);
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
final boolean[] isCover = {false};
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover[0] = originalPath != null && originalPath.equals(imagePath);
}
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle("Image");
String[] items;
if (isPinned[0]) {
if (isCover[0]) {
items = new String[]{"取消置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"取消置顶", "设置为封面"};
}
} else {
if (isCover[0]) {
items = new String[]{"置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"置顶", "设置为封面"};
}
}
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (which == 0) {
if (pinnedDbHelper != null) {
if (isPinned[0]) {
pinnedDbHelper.unpinImage(imagePath);
} else {
pinnedDbHelper.pinImage(imagePath);
}
refreshPinned();
}
} else if (which == 1) {
if (coverDbHelper != null && albumPath != null) {
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
}
} else if (which == 2) {
if (coverDbHelper != null && albumPath != null && isCover[0]) {
coverDbHelper.deleteCover(albumPath);
notifyDataSetChanged();
}
}
}
});
builder.show();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_gallery, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
holder.imageView.setBackgroundResource(getBgRes());
Glide.with(holder.imageView.getContext())
.load(imageUrls.get(position))
.centerCrop()
.into(holder.imageView);
final ArrayList<Uri> urls = imageUrls;
final ArrayList<String> paths = imagePaths;
final String imagePath = imagePaths.get(position);
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
boolean isCover = false;
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover = originalPath != null && originalPath.equals(imagePath);
}
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onImageClick(position, urls, paths);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, position);
return true;
}
});
}
@Override
public int getItemCount() {
return imageUrls.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ImageView pinIcon;
ImageView coverIcon;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
pinIcon = itemView.findViewById(R.id.pin_icon);
coverIcon = itemView.findViewById(R.id.cover_icon);
}
}
}
@@ -0,0 +1,57 @@
package cc.winboll.studio.mygallery;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.PagerAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class ImagePagerAdapter extends PagerAdapter {
public static final String TAG = "ImagePagerAdapter";
private ArrayList<Uri> imageUrls;
public ImagePagerAdapter(ArrayList<Uri> imageUrls) {
this.imageUrls = imageUrls;
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images");
}
@Override
public int getCount() {
return imageUrls.size();
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View view = LayoutInflater.from(container.getContext())
.inflate(R.layout.item_image_pager, container, false);
view.setBackgroundResource(R.color.black);
ImageView imageView = view.findViewById(R.id.image);
Glide.with(imageView.getContext())
.load(imageUrls.get(position))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(imageView);
container.addView(view);
return view;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
}
@@ -0,0 +1,397 @@
package cc.winboll.studio.mygallery;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.viewpager.widget.ViewPager;
import java.io.File;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener {
public static final String TAG = "ImageViewerActivity";
public static final String EXTRA_IMAGE_URLS = "image_urls";
public static final String EXTRA_POSITIONS = "image_positions";
public static final String EXTRA_POSITION = "position";
private ArrayList<Uri> imageUrls;
private ArrayList<String> imagePaths;
private int currentPosition;
private ViewPager viewPager;
private View toolbar;
private ImageButton btnBack;
private ImageButton btnDelete;
private ImageButton btnShare;
private ImageButton btnInfo;
private ImageButton btnGallery;
private GestureDetector gestureDetector;
private TrashManager trashManager;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_image_viewer);
LogUtils.d(TAG, "onCreate");
imageUrls = getIntent().getParcelableArrayListExtra(EXTRA_IMAGE_URLS);
imagePaths = getIntent().getStringArrayListExtra(EXTRA_POSITIONS);
currentPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
trashManager = new TrashManager(this);
prefs = new Preferences(this);
viewPager = findViewById(R.id.view_pager);
toolbar = findViewById(R.id.toolbar);
btnBack = findViewById(R.id.btn_back);
btnDelete = findViewById(R.id.btn_delete);
btnShare = findViewById(R.id.btn_share);
btnInfo = findViewById(R.id.btn_info);
btnGallery = findViewById(R.id.btn_gallery);
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(currentPosition);
viewPager.addOnPageChangeListener(this);
gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
toggleToolbar();
return true;
}
});
viewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});
btnBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
btnDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDeleteDialog();
}
});
btnShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
shareCurrentImage();
}
});
btnInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showImageInfo();
}
});
btnGallery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
Uri imageUri = imageUrls.get(currentPosition);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(imageUri, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "打开相册"));
}
}
});
}
private void toggleToolbar() {
if (toolbar.getVisibility() == View.VISIBLE) {
toolbar.setVisibility(View.GONE);
} else {
toolbar.setVisibility(View.VISIBLE);
}
}
private void showDeleteDialog() {
new AlertDialog.Builder(this)
.setMessage("Delete to trash?")
.setPositiveButton("Yes", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
moveToTrash();
}
})
.setNegativeButton("No", null)
.show();
}
private void moveToTrash() {
LogUtils.d(TAG, "moveToTrash");
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
String imagePath = "";
if (imagePaths != null && currentPosition < imagePaths.size()) {
imagePath = imagePaths.get(currentPosition);
} else {
imagePath = getPathFromUri(imageUrls.get(currentPosition));
}
Uri imageUri = imageUrls.get(currentPosition);
long result = -1;
if (!imagePath.isEmpty()) {
result = trashManager.addToTrash(imagePath);
}
if (result > 0) {
try {
getContentResolver().delete(imageUri, null, null);
} catch (Exception e) {
e.printStackTrace();
}
android.widget.Toast.makeText(this, "Moved to trash", android.widget.Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "Moved to trash");
removeCurrentImage();
} else {
try {
int deleted = getContentResolver().delete(imageUri, null, null);
android.widget.Toast.makeText(this, "Deleted: " + deleted, android.widget.Toast.LENGTH_SHORT).show();
removeCurrentImage();
} catch (Exception e) {
e.printStackTrace();
removeCurrentImage();
}
}
}
}
private void removeCurrentImage() {
imageUrls.remove(currentPosition);
if (imagePaths != null) {
imagePaths.remove(currentPosition);
}
if (imageUrls.isEmpty()) {
finish();
} else {
if (currentPosition >= imageUrls.size()) {
currentPosition = imageUrls.size() - 1;
}
viewPager.setAdapter(new ImagePagerAdapter(imageUrls));
viewPager.setCurrentItem(currentPosition);
}
}
private String getPathFromUri(Uri uri) {
String[] projection = { MediaStore.Images.Media.DATA };
android.database.Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
return cursor.getString(columnIndex);
}
} finally {
cursor.close();
}
}
return uri.getPath();
}
private void shareCurrentImage() {
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
Uri imageUri = imageUrls.get(currentPosition);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("image/*");
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share Image"));
}
}
private void showImageInfo() {
if (currentPosition < 0 || currentPosition >= imageUrls.size()) {
return;
}
String imagePath = "";
if (imagePaths != null && currentPosition < imagePaths.size()) {
imagePath = imagePaths.get(currentPosition);
} else {
imagePath = getPathFromUri(imageUrls.get(currentPosition));
}
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
imageFile = new File(imagePath);
}
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
layout.setPadding(48, 32, 48, 32);
android.widget.TextView labelPath = new android.widget.TextView(this);
labelPath.setText("Path:");
labelPath.setTextColor(getColor(android.R.color.darker_gray));
labelPath.setTextSize(14);
labelPath.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPath);
android.widget.TextView valuePath = new android.widget.TextView(this);
valuePath.setText(imagePath);
valuePath.setTextColor(getColor(android.R.color.black));
valuePath.setTextSize(14);
valuePath.setTextIsSelectable(true);
layout.addView(valuePath);
if (imageFile.exists()) {
long sizeBytes = imageFile.length();
String size;
if (sizeBytes < 1024) {
size = sizeBytes + " B";
} else if (sizeBytes < 1024 * 1024) {
size = String.format("%.2f KB", sizeBytes / 1024.0);
} else {
size = String.format("%.2f MB", sizeBytes / (1024.0 * 1024.0));
}
android.widget.TextView labelSize = new android.widget.TextView(this);
labelSize.setText("Size:");
labelSize.setTextColor(getColor(android.R.color.darker_gray));
labelSize.setTextSize(14);
labelSize.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelSize);
android.widget.TextView valueSize = new android.widget.TextView(this);
valueSize.setText(size);
valueSize.setTextColor(getColor(android.R.color.black));
valueSize.setTextSize(14);
valueSize.setTextIsSelectable(true);
layout.addView(valueSize);
}
try {
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
options.inJustDecodeBounds = true;
android.graphics.BitmapFactory.decodeFile(imagePath, options);
if (options.outWidth > 0 && options.outHeight > 0) {
android.widget.TextView labelPixels = new android.widget.TextView(this);
labelPixels.setText("Pixels:");
labelPixels.setTextColor(getColor(android.R.color.darker_gray));
labelPixels.setTextSize(14);
labelPixels.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPixels);
android.widget.TextView valuePixels = new android.widget.TextView(this);
valuePixels.setText(options.outWidth + " x " + options.outHeight);
valuePixels.setTextColor(getColor(android.R.color.black));
valuePixels.setTextSize(14);
valuePixels.setTextIsSelectable(true);
layout.addView(valuePixels);
}
} catch (Exception e) {
LogUtils.e(TAG, "get pixels error: " + e.getMessage());
}
try {
String[] projection = {
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATE_TAKEN
};
android.database.Cursor cursor = getContentResolver().query(
imageUrls.get(currentPosition), projection, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int dateAddedCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
int dateTakenCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (dateTakenCol >= 0) {
long dateTaken = cursor.getLong(dateTakenCol);
if (dateTaken > 0) {
android.widget.TextView labelTaken = new android.widget.TextView(this);
labelTaken.setText("Date Taken:");
labelTaken.setTextColor(getColor(android.R.color.darker_gray));
labelTaken.setTextSize(14);
labelTaken.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelTaken);
android.widget.TextView valueTaken = new android.widget.TextView(this);
valueTaken.setText(sdf.format(new java.util.Date(dateTaken)));
valueTaken.setTextColor(getColor(android.R.color.black));
valueTaken.setTextSize(14);
valueTaken.setTextIsSelectable(true);
layout.addView(valueTaken);
}
}
if (dateAddedCol >= 0) {
long dateAdded = cursor.getLong(dateAddedCol);
if (dateAdded > 0) {
android.widget.TextView labelAdded = new android.widget.TextView(this);
labelAdded.setText("Date Added:");
labelAdded.setTextColor(getColor(android.R.color.darker_gray));
labelAdded.setTextSize(14);
labelAdded.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelAdded);
android.widget.TextView valueAdded = new android.widget.TextView(this);
valueAdded.setText(sdf.format(new java.util.Date(dateAdded * 1000)));
valueAdded.setTextColor(getColor(android.R.color.black));
valueAdded.setTextSize(14);
valueAdded.setTextIsSelectable(true);
layout.addView(valueAdded);
}
}
}
cursor.close();
}
} catch (Exception e) {
LogUtils.e(TAG, "get date error: " + e.getMessage());
}
new AlertDialog.Builder(this)
.setTitle("Image Info")
.setView(layout)
.setPositiveButton("OK", null)
.show();
}
@Override
public void onPageSelected(int position) {
currentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onBackPressed() {
finish();
}
}
@@ -0,0 +1,419 @@
package cc.winboll.studio.mygallery;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.mygallery.AlbumAdapter.OnAlbumClickListener;
import cc.winboll.studio.mygallery.App;
import cc.winboll.studio.mygallery.utils.BackgroundUtils;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private static final int PERMISSION_REQUEST_CODE = 100;
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
private RecyclerView recyclerView;
private AlbumAdapter adapter;
private Preferences prefs;
private FloatingActionButton fabScrollTop;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
View content = findViewById(android.R.id.content);
if (content != null) {
content.setBackground(BackgroundUtils.getInstance().getDrawable());
}
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
prefs = new Preferences(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
adapter = new AlbumAdapter();
adapter.setContext(this);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
adapter.setOnAlbumClickListener(new OnAlbumClickListener() {
@Override
public void onAlbumClick(Album album) {
Intent intent = new Intent(MainActivity.this, AlbumActivity.class);
intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath());
intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName());
startActivity(intent);
}
});
checkAndRequestPermissions();
}
private void checkAndRequestPermissions() {
LogUtils.i(TAG, "checkAndRequestPermissions");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
} catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
}
return;
}
}
if (checkPermission()) {
loadAlbums();
} else {
requestPermission();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
loadAlbums();
} else {
Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show();
}
}
}
}
private boolean checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadAlbums();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private void loadAlbums() {
executor.execute(new Runnable() {
@Override
public void run() {
final ArrayList<Album> albums = loadAlbumsInBackground();
mainHandler.post(new Runnable() {
@Override
public void run() {
if (albums.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
}
adapter.setData(albums);
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
}
});
}
});
}
private ArrayList<Album> loadAlbumsInBackground() {
LogUtils.d(TAG, "loadAlbumsInBackground");
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
folderPath = Preferences.getDefaultPath();
baseFolder = new File(folderPath);
if (!baseFolder.exists()) {
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
baseFolder = new File(folderPath);
}
}
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
PinnedAlbumDbHelper pinnedDbHelper = PinnedAlbumDbHelper.getInstance(this);
// 获取带排序号的置顶路径映射(已按排序号排序)
java.util.LinkedHashMap<String, Integer> pinnedPathMap = pinnedDbHelper.getPinnedPathsWithSortOrder();
Set<String> pinnedPaths = pinnedPathMap.keySet();
ArrayList<Album> albums = new ArrayList<Album>();
File[] subfolders = baseFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if (subfolders != null) {
for (File subfolder : subfolders) {
String albumPath = subfolder.getAbsolutePath();
String coverPath = coverDbHelper.getCover(albumPath);
Uri coverUri = null;
if (coverPath != null) {
File coverFile = new File(coverPath);
if (coverFile.exists()) {
coverUri = Uri.fromFile(coverFile);
} else {
coverUri = getUriFromPath(coverPath);
}
}
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
if (coverUri == null && !allImages.isEmpty()) {
coverUri = allImages.get(0);
}
if (coverUri != null || !allImages.isEmpty()) {
int imageCount = allImages.size();
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
}
}
}
// 按照排序号顺序添加置顶相册
ArrayList<Album> pinned = new ArrayList<Album>();
ArrayList<Album> unpinned = new ArrayList<Album>();
// 先按照pinnedPathMap的顺序(排序号顺序)添加置顶相册
for (String pinnedPath : pinnedPathMap.keySet()) {
for (Album album : albums) {
if (album.getPath().equals(pinnedPath)) {
pinned.add(album);
break;
}
}
}
// 添加非置顶相册
for (Album album : albums) {
if (!pinnedPaths.contains(album.getPath())) {
unpinned.add(album);
}
}
pinned.addAll(unpinned);
return pinned;
}
private Uri getUriFromPath(String path) {
String[] projection = { MediaStore.Images.Media._ID };
String selection = MediaStore.Images.Media.DATA + " = ?";
String[] selectionArgs = { path };
Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
}
} finally {
cursor.close();
}
}
return null;
}
private ArrayList<Uri> getImagesInFolder(String folderPath) {
ArrayList<Uri> imageUrls = new ArrayList<Uri>();
ContentResolver contentResolver = getContentResolver();
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
String[] selectionArgs = new String[]{folderPath + "/%"};
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder);
if (cursor != null) {
try {
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
if (path != null) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
imageUrls.add(contentUri);
}
}
} finally {
cursor.close();
}
}
return imageUrls;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem debugItem = menu.findItem(R.id.action_debug);
if (debugItem != null) {
debugItem.setVisible(App.isDebugging());
}
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_mi_gallery) {
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
startActivity(intent);
return true;
} else if (id == R.id.action_change_bg_color) {
if (BackgroundUtils.DrawableType.COLOR == BackgroundUtils.getInstance().getDrawableType()) {
ColorPickerDialog dlg = new ColorPickerDialog(this, BackgroundUtils.getInstance().getColor());
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
BackgroundUtils.getInstance().initFromColor(MainActivity.this, color);
View content = findViewById(android.R.id.content);
if (content != null) {
content.setBackground(BackgroundUtils.getInstance().getDrawable());
}
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
}
return true;
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
} else if (id == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
} else if (id == R.id.action_trash) {
startActivity(new Intent(this, TrashActivity.class));
return true;
} else if (id == R.id.action_debug) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
loadAlbums();
}
}
};
@Override
protected void onResume() {
super.onResume();
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
if (checkPermission()) {
loadAlbums();
}
if (adapter != null) {
adapter.refreshBg();
adapter.refreshPinned();
adapter.refreshCover();
}
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(coverUpdatedReceiver);
}
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdownNow();
}
}
@@ -0,0 +1,218 @@
package cc.winboll.studio.mygallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.LinkedHashMap;
public class PinnedAlbumDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedAlbumDbHelper";
private static final String DB_NAME = "pinned_album.db";
private static final int DB_VERSION = 2;
private static final String TABLE_NAME = "pinned_albums";
private static final String COLUMN_PATH = "album_path";
private static final String COLUMN_SORT_ORDER = "sort_order";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_SORT_ORDER + " INTEGER DEFAULT 0)";
private static PinnedAlbumDbHelper dbHelper;
public static PinnedAlbumDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedAlbumDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
// 创建新表
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME + "_new");
db.execSQL("CREATE TABLE " + TABLE_NAME + "_new ("
+ COLUMN_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_SORT_ORDER + " INTEGER DEFAULT 0)");
// 迁移旧数据,为现有记录分配排序号(按原始顺序)
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
int sortOrder = 1;
while (cursor.moveToNext()) {
String path = cursor.getString(0);
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, path);
values.put(COLUMN_SORT_ORDER, sortOrder++);
db.insert(TABLE_NAME + "_new", null, values);
}
cursor.close();
// 删除旧表,重命名新表
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
db.execSQL("ALTER TABLE " + TABLE_NAME + "_new RENAME TO " + TABLE_NAME);
}
}
public void pinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
// 将所有现有置顶数据的排序号加1
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = " + COLUMN_SORT_ORDER + " + 1");
// 插入新数据,排序号为1
ContentValues insertValues = new ContentValues();
insertValues.put(COLUMN_PATH, albumPath);
insertValues.put(COLUMN_SORT_ORDER, 1);
db.insertWithOnConflict(TABLE_NAME, null, insertValues, SQLiteDatabase.CONFLICT_REPLACE);
db.setTransactionSuccessful();
// 查询所有置顶相册的排序号并输出调试信息
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH, COLUMN_SORT_ORDER},
null, null, null, null, COLUMN_SORT_ORDER + " ASC");
StringBuilder sb = new StringBuilder("All pinned albums after pin:\n");
while (cursor.moveToNext()) {
String path = cursor.getString(0);
int sortOrder = cursor.getInt(1);
sb.append(" [").append(sortOrder).append("] ").append(path).append("\n");
}
cursor.close();
LogUtils.d(TAG, sb.toString());
} finally {
db.endTransaction();
}
}
public void unpinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
// 获取要取消置顶的相册的排序号
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_SORT_ORDER},
COLUMN_PATH + " = ?", new String[]{albumPath}, null, null, null);
if (cursor.moveToFirst()) {
int sortNumber = cursor.getInt(0);
cursor.close();
// 将排序号大于该排序号的记录减1
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = " + COLUMN_SORT_ORDER + " - 1 WHERE " + COLUMN_SORT_ORDER + " > ?", new Object[]{sortNumber});
// 删除该相册的置顶记录
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "unpinAlbum: " + albumPath + ", sort number was: " + sortNumber);
} else {
cursor.close();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void repinAlbumToFront(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
// 获取要再次置顶的相册的排序号
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_SORT_ORDER},
COLUMN_PATH + " = ?", new String[]{albumPath}, null, null, null);
if (cursor.moveToFirst()) {
int sortNumber = cursor.getInt(0);
cursor.close();
if (sortNumber > 1) {
// 将排序号小于该记录的记录全部加1(整体后移)
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = " + COLUMN_SORT_ORDER + " + 1 WHERE " + COLUMN_SORT_ORDER + " < ?", new Object[]{sortNumber});
// 该相册置顶到排序号1
ContentValues values = new ContentValues();
values.put(COLUMN_SORT_ORDER, 1);
db.update(TABLE_NAME, values, COLUMN_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "repinAlbumToFront: " + albumPath + ", sort number was: " + sortNumber);
}
// 按当前顺序将排序号重排为连续的1..n
normalizeSortOrders(db);
// 查询所有置顶相册的排序号并输出调试信息
Cursor debugCursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH, COLUMN_SORT_ORDER},
null, null, null, null, COLUMN_SORT_ORDER + " ASC");
StringBuilder sb = new StringBuilder("All pinned albums after repin:\n");
while (debugCursor.moveToNext()) {
sb.append(" [").append(debugCursor.getInt(1)).append("] ").append(debugCursor.getString(0)).append("\n");
}
debugCursor.close();
LogUtils.d(TAG, sb.toString());
} else {
cursor.close();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
// 按排序号升序将所有记录重排为连续的1..n,修正可能出现的序号空洞或重复
private void normalizeSortOrders(SQLiteDatabase db) {
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null,
COLUMN_SORT_ORDER + " ASC");
int sortOrder = 1;
while (cursor.moveToNext()) {
String path = cursor.getString(0);
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = ? WHERE " + COLUMN_PATH + " = ?",
new Object[]{sortOrder++, path});
}
cursor.close();
}
public boolean isPinned(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{albumPath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null,
COLUMN_SORT_ORDER + " ASC");
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
public LinkedHashMap<String, Integer> getPinnedPathsWithSortOrder() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH, COLUMN_SORT_ORDER},
null, null, null, null, COLUMN_SORT_ORDER + " ASC");
LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>();
while (cursor.moveToNext()) {
String path = cursor.getString(0);
int sortOrder = cursor.getInt(1);
map.put(path, sortOrder);
}
cursor.close();
return map;
}
}
@@ -0,0 +1,78 @@
package cc.winboll.studio.mygallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
public class PinnedImageDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedImageDbHelper";
private static final String DB_NAME = "pinned_image.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_images";
private static final String COLUMN_PATH = "image_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
private static PinnedImageDbHelper dbHelper;
public static PinnedImageDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedImageDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedImageDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinImage: " + imagePath);
}
public void unpinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{imagePath});
LogUtils.d(TAG, "unpinImage: " + imagePath);
}
public boolean isPinned(String imagePath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{imagePath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}
@@ -0,0 +1,93 @@
package cc.winboll.studio.mygallery;
import android.content.Context;
import android.content.SharedPreferences;
import cc.winboll.studio.libappbase.LogUtils;
public class Preferences {
public static final String TAG = "Preferences";
private static final String PREF_NAME = "gallery_prefs";
private static final String KEY_FOLDER_PATH = "folder_path";
private static final String KEY_BG_TYPE = "bg_type";
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
private static final String KEY_COVER_WIDTH = "cover_width";
private static final String KEY_COVER_HEIGHT = "cover_height";
private static final String KEY_COVER_RATIO = "cover_ratio";
public static final String ACTION_COVER_UPDATED = "cc.winboll.studio.mygallery.COVER_UPDATED";
private static final int DEFAULT_BG_TYPE = 0;
private static final int DEFAULT_SORT_MODE = 0;
private static final int DEFAULT_COVER_WIDTH = 240;
private static final int DEFAULT_COVER_HEIGHT = 120;
private static final float DEFAULT_COVER_RATIO = 2.0f;
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
public static final int SORT_TIME_DESC = 0;
public static final int SORT_TIME_ASC = 1;
public static final int SORT_NAME_DESC = 2;
public static final int SORT_NAME_ASC = 3;
public static String getDefaultPath() {
return DEFAULT_PATH;
}
private final SharedPreferences prefs;
public Preferences(Context context) {
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public String getFolderPath() {
String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH);
LogUtils.d(TAG, "getFolderPath: " + path);
return path;
}
public void setFolderPath(String path) {
LogUtils.d(TAG, "setFolderPath: " + path);
prefs.edit().putString(KEY_FOLDER_PATH, path).apply();
}
public int getBgType() {
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
}
public void setBgType(int type) {
LogUtils.d(TAG, "setBgType: " + type);
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
}
public int getAlbumSortMode() {
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
}
public void setAlbumSortMode(int mode) {
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
}
public int getCoverWidth() {
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
}
public void setCoverWidth(int width) {
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
}
public int getCoverHeight() {
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
}
public void setCoverHeight(int height) {
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
}
public float getCoverRatio() {
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
}
public void setCoverRatio(float ratio) {
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
}
}
@@ -0,0 +1,46 @@
package cc.winboll.studio.mygallery;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
public class SettingsActivity extends AppCompatActivity {
public static final String TAG = "SettingsActivity";
private Preferences prefs;
private EditText editFolderPath;
private TextView textCurrentPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
LogUtils.d(TAG, "onCreate");
prefs = new Preferences(this);
editFolderPath = findViewById(R.id.edit_folder_path);
textCurrentPath = findViewById(R.id.text_current_path);
Button btnSave = findViewById(R.id.btn_save);
String currentPath = prefs.getFolderPath();
editFolderPath.setText(currentPath);
textCurrentPath.setText("Current: " + currentPath);
btnSave.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String newPath = editFolderPath.getText().toString().trim();
if (!newPath.isEmpty()) {
prefs.setFolderPath(newPath);
textCurrentPath.setText("Current: " + newPath);
}
finish();
}
});
}
}
@@ -0,0 +1,193 @@
package cc.winboll.studio.mygallery;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashActivity extends AppCompatActivity {
public static final String TAG = "TrashActivity";
private static final int PERMISSION_REQUEST_CODE = 102;
private RecyclerView recyclerView;
private TrashAdapter adapter;
private TrashManager trashManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle("Trash");
trashManager = new TrashManager(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
adapter = new TrashAdapter();
recyclerView.setAdapter(adapter);
adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() {
@Override
public void onRestoreClick(int position) {
restoreImage(position);
}
@Override
public void onDeleteClick(int position) {
permanentlyDelete(position);
}
});
if (checkPermission()) {
loadTrash();
} else {
requestPermission();
}
}
private boolean checkPermission() {
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadTrash();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private void loadTrash() {
LogUtils.d(TAG, "loadTrash");
Cursor cursor = trashManager.getTrashList();
ArrayList<TrashItem> items = new ArrayList<TrashItem>();
ArrayList<Uri> uris = new ArrayList<Uri>();
String trashPath = TrashDbHelper.getTrashPath();
File trashDir = new File(trashPath);
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
do {
try {
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path"));
String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder"));
TrashItem item = new TrashItem();
item.id = id;
item.fileName = fileName;
item.originalPath = originalPath;
item.originalFolder = originalFolder;
File trashFile = new File(trashDir, fileName);
if (trashFile.exists()) {
items.add(item);
uris.add(Uri.fromFile(trashFile));
}
} catch (Exception e) {
e.printStackTrace();
}
} while (cursor.moveToNext());
cursor.close();
}
adapter.setData(items, uris);
if (items.isEmpty()) {
Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show();
}
}
private void restoreImage(int position) {
LogUtils.d(TAG, "restoreImage: " + position);
long id = adapter.getItemId(position);
String fileName = adapter.getFileName(position);
String originalPath = adapter.getOriginalPath(position);
if (trashManager.restore(id, fileName, originalPath)) {
Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "Image restored");
loadTrash();
} else {
Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show();
}
}
private void permanentlyDelete(int position) {
long id = adapter.getItemId(position);
String fileName = adapter.getFileName(position);
if (trashManager.deletePermanently(id, fileName)) {
Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show();
loadTrash();
} else {
Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_trash, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_clear_trash) {
trashManager.clearTrash();
Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show();
loadTrash();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
loadTrash();
}
}
public static class TrashItem {
public long id;
public String fileName;
public String originalPath;
public String originalFolder;
}
}
@@ -0,0 +1,111 @@
package cc.winboll.studio.mygallery;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashAdapter extends RecyclerView.Adapter<TrashAdapter.ViewHolder> {
public static final String TAG = "TrashAdapter";
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
private OnTrashClickListener listener;
public interface OnTrashClickListener {
void onRestoreClick(int position);
void onDeleteClick(int position);
}
public void setOnTrashClickListener(OnTrashClickListener listener) {
this.listener = listener;
}
public void setData(ArrayList<TrashActivity.TrashItem> items, ArrayList<Uri> uris) {
this.trashItems = items;
this.imageUrls = uris;
LogUtils.d(TAG, "setData: " + items.size() + " items");
notifyDataSetChanged();
}
public long getItemId(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).id;
}
return -1;
}
public String getFileName(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).fileName;
}
return "";
}
public String getOriginalPath(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).originalPath;
}
return "";
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_trash, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
if (position < imageUrls.size()) {
Glide.with(holder.imageView.getContext())
.load(imageUrls.get(position))
.centerCrop()
.into(holder.imageView);
}
holder.btnRestore.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onRestoreClick(position);
}
}
});
holder.btnDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onDeleteClick(position);
}
}
});
}
@Override
public int getItemCount() {
return trashItems.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ImageView btnRestore;
ImageView btnDelete;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
btnRestore = itemView.findViewById(R.id.btn_restore);
btnDelete = itemView.findViewById(R.id.btn_delete);
}
}
}

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