Compare commits

..

152 Commits

Author SHA1 Message Date
69b18343c9 fix(libappbase): 强制Java 7语法兼容性
- 为libappbase模块添加compileOptions配置,确保源码和目标字节码均为Java 7
- 修复BackupUtils.java中HashMap<>泛型简写语法,改为完整类型声明new HashMap<String, String>(),兼容Java 7编译器
2026-05-10 05:20:54 +08:00
e1bd959842 主流版本阶段性调整 2026-05-10 05:15:39 +08:00
bad38e37ae <libappbase>Library Release 15.15.23 2026-05-10 04:17:16 +08:00
08eb360dbd <appbase>APK 15.15.23 release Publish. 2026-05-10 04:07:17 +08:00
819018b149 fix: appbase 和 libappbase 模块 minSdkVersion 从 21 升级至 26
- appbase/build.gradle: minSdkVersion 21 → 26
- libappbase/build.gradle: minSdkVersion 21 → 26
2026-05-10 04:01:44 +08:00
6e6b262e86 feat(build): 升级 Gradle 构建脚本 Java 版本为 11
将 root build.gradle 中 JavaCompile 任务的 sourceCompatibility 和 targetCompatibility
从 VERSION_1_7 升级至 VERSION_11,与项目需求保持一致。
2026-05-10 03:54:30 +08:00
9665856b1b 简化应用启动窗口配置:移除分屏测试功能,MainActivity 设为唯一启动器
- 删除 MainActivityAlias 分屏窗口类及相关文件
- 移除 AndroidManifest 中的 MainActivityAlias activity 声明
- 从 activity_main.xml 移除分屏测试按钮
- 删除 MainActivity 中的 onSplitScreenMode() 方法
- 为 MainActivity 添加 LAUNCHER intent-filter,作为唯一启动主窗口
2026-05-10 03:50:12 +08:00
052bbce839 Merge branch 'winboll' into appbase 2026-05-10 03:15:46 +08:00
90102f4eea refactor(winboll): 重命名菜单项为 WinBoLLLibraryActivity 2026-05-10 03:06:43 +08:00
759a08cec9 更换类库窗口标识名称 2026-05-10 03:03:00 +08:00
42cc7a2822 feat(winboll): 添加 UnitTest 菜单项跳转到 LibraryActivity
- 在 UnitTest 菜单组添加 LibraryActivity 菜单项
- 实现 MainActivity 中菜单项点击事件处理
- 更新 library.xml 布局显示类全名标识
- buildCount 更新为 25
2026-05-10 02:55:37 +08:00
c9c95d6ab0 refactor(gradle): 强制项目使用Java 7语法并统一API适配范围
- build.gradle: subprojects Java版本从11降级为1.7
- libwinboll/build.gradle: minSdkVersion从21统一为26
- build.properties: 编译计数器自动递增(buildCount: 23→24)
2026-05-09 22:02:59 +08:00
bc9bd47daa 添加类库模块,便于调试类库编译配置。 2026-05-09 21:17:23 +08:00
4bec8c3e9e 忽略编译问题,暂缓新功能叠加。 2026-05-09 21:16:44 +08:00
e726c9d435 <libappbase>Library Release 15.15.22 2026-05-09 20:50:14 +08:00
5277913606 <appbase>APK 15.15.22 release Publish. 2026-05-09 20:49:55 +08:00
c1bd31df2f refactor(libappbase): 完全重构 LogUtils 日志工具类
- 重构目录结构,按功能模块化拆分初始化、私有工具、日志级别等方法
- 补全所有日志重载方法(Error/Warn/Info/Debug/Verbose)
- 优化日志文件裁剪逻辑,保留最新3MB内容
- 新增TAG自动扫描管理机制
- 替换所有内部 LogUtils 调用为 android.util.Log,避免递归嵌套
- 严格遵循 Java 7 语法规范(final 参数、传统 try-catch)
- 优化异常/堆栈格式化输出
2026-05-09 20:43:17 +08:00
c6591e83a5 chore(winboll): 改造winboll模块适配API 26-30并兼容Java 7
- 调整minSdkVersion从23到26,符合API 26-30适配范围要求
- 修复PatternLockActivity.java中3处lambda表达式,
  改为Java 7兼容的匿名内部类形式
- 保持Gradle插件7.2.1、compileSdkVersion 30、
  targetSdkVersion 30及Java 11编译配置不变

Modified files:
- winboll/build.gradle
- winboll/build.properties
- winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java
2026-05-06 13:43:06 +08:00
7119b3b7a5 feat(modules): 新增 appbase 和 libappbase 基础库模块
- 添加 appbase 应用基础模块 (Activities, Resources, Build config)
- 添加 libappbase 通用基础库模块 (Utils, Views, Dialogs, Resources)
- 更新 .gitignore 忽略规则
- 包含日志查看、NFC RSA 操作、全局崩溃处理等基础功能
2026-05-06 12:44:30 +08:00
48d36c6d96 更新.gitignore配置并移除项目特定配置文件
将项目特定的配置文件移至忽略列表,避免上传至版本库:
- 添加 /settings.gradle 和 /gradle.properties 到 .gitignore
- 从版本库中删除 settings.gradle 和 gradle.properties
- 保持项目配置本地化,便于多项目切换管理
2026-05-06 12:40:48 +08:00
2850d3ca3b chore(config): 移除 gradle.properties 配置文件
- 删除项目根目录下的 gradle.properties
2026-05-06 12:34:19 +08:00
74443950c4 chore(config): 调整项目构建配置,取消忽略 settings.gradle
- 修改 .gitignore 取消注释 settings.gradle 和 gradle.properties
- 删除 settings.gradle 文件
2026-05-06 12:25:05 +08:00
0607af429b Merge branch 'winboll' of https://gitea.winboll.cc/Studio/WinBoLL into winboll 2026-05-06 12:21:06 +08:00
55baf0afac Merge branch 'aes' into winboll 2026-05-06 12:06:40 +08:00
1d0dec8de5 降级Java版本从11到7 2026-05-06 12:04:19 +08:00
39e825f03e fix(browser): 外部调用时直接打开传入的网页链接,不再默认加载首页
- BrowserFragment 新增 newInstance(initialUrl) 工厂方法
- initWinBoLLView 优先使用外部传入的 URL 参数
- MainActivity 在创建 Fragment 时即传入外部 URL,避免先加载首页再跳转的闪烁
2026-05-06 11:28:14 +08:00
cd0599d639 feat(browser): 支持外部应用调用传入网页地址
- 在 AndroidManifest.xml 为 MainActivity 添加 http/https 的 intent-filter
- 设置 singleTask 启动模式以复用 Activity 实例
- BrowserFragment 新增 MSG_OPEN_URL 消息处理外部 URL 跳转
- MainActivity 实现 handleExternalUrl 方法,在 onCreate/onNewIntent 中捕获并加载网页
2026-05-06 11:17:13 +08:00
aef5a62e47 feat(network): 全局启用 HTTP 明文流量支持
- 修改 network_security_config.xml 允许所有 HTTP 协议访问
- 移除对 HTTP 访问的域名限制
2026-05-06 11:11:39 +08:00
06253feba8 feat(ollama): 添加 Ollama 模型对话功能
- 新增 OllamaWindowActivity 用于模型对话交互
- 添加 Ollama 配置对话框(API地址、模型、温度、token等)
- 在主菜单中增加 Ollama 窗口入口
- 包含发送、停止、清空等对话控制功能
- 更新 buildCount 至 15
2026-05-06 11:08:04 +08:00
1dbca0f290 添加 Gradle 编译调试信息。 2026-05-04 20:16:25 +08:00
14d0227158 编译输出信息调整 2026-05-04 19:57:39 +08:00
3fcdbabcc9 改进APK应用包输出资源配置。 2026-05-04 19:38:52 +08:00
f3b3036591 feat(pattern-lock): 添加图案密码解锁功能
- 创建带图案打开意图过滤器的 PatternLockActivity
- 构建图案锁布局和点背景样式
- 添加图案锁颜色和字符串资源
- 更新构建计数到 11

注意:图案锁 UI 已创建但尚未集成
2026-04-30 15:11:20 +08:00
28ecc605e1 <winboll>APK 15.11.26 release Publish. 2026-04-30 12:07:31 +08:00
523a8e49e0 更新一下属性命名,清理冗余代码。 2026-04-30 12:03:37 +08:00
59a9e0ee45 添加TermuxButton按钮控件类 2026-04-30 11:48:27 +08:00
cbf1341435 添加TermuxButtonModel数据模型 2026-04-30 10:57:34 +08:00
dadf573675 改进Termux应用调用函数,添加TermuxWorkSpaces按钮响应。 2026-04-30 10:42:50 +08:00
7420a5cd48 添加TermuxWorkSpaces按钮视图 2026-04-30 10:14:27 +08:00
dc6a589db4 调整UI布局 2026-04-30 10:09:12 +08:00
e3f47043ef 更新Termux应用打开方法 2026-04-30 09:58:50 +08:00
a825951aad feat: 在 MyTermuxActivity 中添加 Termux 按钮功能
- 在 activity_my_termux.xml 布局中添加 Termux 按钮(底部居中)
- 在 MyTermuxActivity.java 中实现按钮点击事件
- 调用 TermuxCommandExecutor 执行 Termux 命令
- 移除了空 FrameLayout,简化布局结构
2026-04-30 09:42:08 +08:00
79cb841349 feat: 添加 MyTermuxActivity 菜单及工具栏功能
- MainActivity 添加 MyTermuxActivity 菜单项
- 配置 MyTermuxActivity 注册到 AndroidManifest.xml
- 添加 Toolbar 布局并初始化工具栏
- 设置一级标题应用名称、二级标题活动名称
- 添加返回按钮导航逻辑

修改文件:MainActivity.java, MyTermuxActivity.java, activity_my_termux.xml, toolbar_main.xml, strings.xml, AndroidManifest.xml, gradlew
2026-04-30 08:56:49 +08:00
d3c40efffa 添加我的Termux活动类 2026-04-30 08:34:29 +08:00
4baee6f0e1 <libappbase>Library Release 15.15.21 2026-04-28 17:08:33 +08:00
8f6b615949 <appbase>APK 15.15.21 release Publish. 2026-04-28 17:08:05 +08:00
d02d57d4dd 添加LogUtils日志文件自动裁剪功能 2026-04-28 17:05:50 +08:00
e337bb7a04 <libappbase>Library Release 15.15.20 2026-04-27 20:19:13 +08:00
9ae848e4c2 <appbase>APK 15.15.20 release Publish. 2026-04-27 20:18:59 +08:00
9c66f61891 调整MainActivity按钮顺序将关于应用置顶 2026-04-27 20:15:54 +08:00
bfaf3543b9 添加多窗口支持与LogActivity独立任务栈 2026-04-27 20:04:17 +08:00
b44fe3aaf3 添加分屏测试功能支持多窗口MainActivity 2026-04-27 19:27:25 +08:00
d518ac50a9 优化LogActivity分屏模式支持Android 11适配 2026-04-27 18:40:36 +08:00
d532eae971 调整onLogTest调用LogActivity分屏模式 2026-04-27 17:58:53 +08:00
f661acbbbc 添加LogActivity重载函数支持分屏模式切换 2026-04-27 17:33:51 +08:00
ecced75a4d 调整AboutActivity工具栏与MainActivity一致 2026-04-25 12:10:17 +08:00
5e5d34c90c 调整BaseFunctionItemView派生控件高度间隔为无间隔 2026-04-25 10:43:18 +08:00
85a0d39498 添加BaseFunctionItemView类视图1像素美化边框 2026-04-25 10:40:50 +08:00
c542d8dca7 源码整理 2026-04-25 10:25:59 +08:00
ccbdb4010e 调整一下libappbase模块中layout_about_view.xml的布局文件。缩小一下布局中控件的高度间隔。 2026-04-25 10:22:20 +08:00
fe4060f00e <libaes>Library Release 15.15.9 2026-04-25 04:16:44 +08:00
676a3466ef <aes>APK 15.15.9 release Publish. 2026-04-25 04:16:30 +08:00
d6243b052d 更新基础类库 2026-04-25 04:14:59 +08:00
2e7b9173f2 <libappbase>Library Release 15.15.19 2026-04-25 04:12:02 +08:00
4f12a5de4f <appbase>APK 15.15.19 release Publish. 2026-04-25 04:11:46 +08:00
7ab399e520 编译调试 2026-04-25 04:10:29 +08:00
dd2d9f3e55 Merge branch 'appbase' of https://gitea.winboll.cc/Studio/WinBoLL.git into appbase 2026-04-25 04:07:49 +08:00
098516d4d7 Merge branch 'winboll' into appbase 2026-04-25 04:07:03 +08:00
5d72ee1a6a 更新基础类库 2026-04-25 04:05:05 +08:00
b1fab5ce46 <libappbase>Library Release 15.15.18 2026-04-25 03:59:02 +08:00
eeb64b00b8 <aes>APK 15.15.8 release Publish. 2026-04-14 22:35:57 +08:00
8bcd803404 编译测试 2026-04-14 22:33:26 +08:00
76d20c32bf Merge branch 'winboll' into aes 2026-04-14 22:30:47 +08:00
e68098aa10 <appbase>APK 15.15.18 release Publish. 2026-04-10 05:38:18 +08:00
d673ba46a1 更正应用验证时使用的应用包名称配置 2026-04-10 05:36:14 +08:00
5a1342156f <appbase>APK 15.15.17 release Publish. 2026-04-06 20:39:30 +08:00
4e1784d99f 更换App为APP。以适配应用版本检查时使用的APK应用包名称APPBase。 2026-04-06 20:38:50 +08:00
069e5a66ad <appbase>APK 15.15.16 release Publish. 2026-04-06 20:25:21 +08:00
e9a1dca8ca 应用调试功能切换Logo准备完成。 2026-04-06 20:23:37 +08:00
7e3a3d1446 添加调试状态切换Logo控件 2026-04-06 19:42:34 +08:00
7414cd0f33 <appbase>APK 15.15.15 release Publish. 2026-03-25 20:52:43 +08:00
b2b3f949b7 更新Git仓库项目名称 2026-03-25 20:51:24 +08:00
83c1b888b6 <appbase>APK 15.15.14 release Publish. 2026-03-25 20:36:14 +08:00
6afc81939d <appbase>APK 15.15.13 release Publish. 2026-03-25 20:17:45 +08:00
1cf4c67b4f <appbase>APK 15.15.12 release Publish. 2026-03-25 20:17:45 +08:00
89697f8c49 Merge remote-tracking branch 'origin/winboll' into appbase 2026-03-25 20:16:31 +08:00
5419fad1cf 移除通用FTP服务应用数据备份功能 2026-03-25 19:47:06 +08:00
17d1c2f321 Merge remote-tracking branch 'origin/winboll' into appbase 2026-02-04 13:14:53 +08:00
9e2affbc4d Merge remote-tracking branch 'origin/winboll' into aes 2026-02-04 13:14:30 +08:00
aed4aa1a86 Merge remote-tracking branch 'origin/winboll' into aes 2026-02-04 12:40:44 +08:00
447b7fa5a8 Merge remote-tracking branch 'origin/winboll' into appbase 2026-02-04 12:39:22 +08:00
dae39b43d6 添加FTP备份目标保存路径设置。 2026-01-31 21:03:39 +08:00
530316b976 添加data与sdcard两种应用数据测试。 2026-01-31 20:07:56 +08:00
3f924b004c 完成应用Data区数据备份测试。 2026-01-31 19:47:12 +08:00
1db94b52e6 完成二次备份点击功能 2026-01-31 18:52:01 +08:00
55c653af09 应用备份打包上传功能完成 2026-01-31 14:18:26 +08:00
9d97d6ed94 正在调试FTP应用备份功能。 2026-01-30 21:38:04 +08:00
e21bb9058d <libappbase>Library Release 15.15.11 2026-01-24 20:32:30 +08:00
ad6175f977 <appbase>APK 15.15.11 release Publish. 2026-01-24 20:32:20 +08:00
8b659f4b24 编译参数修复 2026-01-24 20:31:49 +08:00
13b841f923 <libappbase>Library Release 15.15.10 2026-01-24 20:29:06 +08:00
e9ad701db4 <appbase>APK 15.15.10 release Publish. 2026-01-24 19:51:55 +08:00
0aaf71f285 添加对签名证书修改后的证书识别能力。 2026-01-24 19:50:43 +08:00
4ea2b5fad0 <libappbase>Library Release 15.15.9 2026-01-24 12:32:11 +08:00
760fe4613f <appbase>APK 15.15.9 release Publish. 2026-01-24 12:31:02 +08:00
a656dfcc62 编译参数修复 2026-01-24 12:30:26 +08:00
e9605fa991 源码整理 2026-01-24 12:28:29 +08:00
8546b6c8ad 应用校验对话框UI显示调整完成。 2026-01-24 12:17:28 +08:00
f5ddefa895 <libappbase>Library Release 15.15.8 2026-01-24 11:26:46 +08:00
35527374da <appbase>APK 15.15.8 release Publish. 2026-01-24 11:26:28 +08:00
2751ce4a39 APK校验接口调试完成 2026-01-24 11:16:37 +08:00
730022a9f0 固定APK调试文件测试成功 2026-01-23 21:05:41 +08:00
a3bc90d9b8 <libappbase>Library Release 15.15.7 2026-01-23 03:11:58 +08:00
32ee7c8845 <appbase>APK 15.15.7 release Publish. 2026-01-23 03:11:07 +08:00
6e34ee73e9 应用签名校验模块测试完成。 2026-01-23 03:09:19 +08:00
7eed7357f0 正在调整应用介绍窗口服务主机切换部分功能。。。 2026-01-23 00:29:17 +08:00
d20192cb36 正在改造WinBoLL主机切换对话框与按钮。。。 2026-01-22 21:19:39 +08:00
5846784940 应用签名联网验证模块完成。 2026-01-22 20:41:57 +08:00
ef64d6a317 添加按照tag编译Class的脚本,优化应用启动与单元测试流程。 2026-01-21 16:13:00 +08:00
8b2a8328eb <libappbase>Library Release 15.15.6 2026-01-20 21:18:00 +08:00
88a20d9a85 <appbase>APK 15.15.6 release Publish. 2026-01-20 21:17:40 +08:00
aeaea253cb 应用指纹校验对话框显示优化。 2026-01-20 21:16:17 +08:00
4890ca42cc <libappbase>Library Release 15.15.5 2026-01-20 20:54:58 +08:00
2896b6401b <appbase>APK 15.15.5 release Publish. 2026-01-20 20:54:24 +08:00
1aa270482e 添加应用指纹校验功能 2026-01-20 20:52:49 +08:00
3f544f6097 <libaes>Library Release 15.15.7 2026-01-13 16:46:38 +08:00
6b44f852a8 <aes>APK 15.15.7 release Publish. 2026-01-13 16:46:27 +08:00
952c8d8017 移除BaseWinBoLLActivity作为类库使用,应用需自定义基础窗口类。 2026-01-13 16:45:31 +08:00
80b4b87e95 <libaes>Library Release 15.15.6 2026-01-13 16:27:36 +08:00
8b99844d0c <aes>APK 15.15.6 release Publish. 2026-01-13 16:27:29 +08:00
9f46f400b0 bugfix 2026-01-13 16:26:46 +08:00
40ea79c6b7 <libaes>Library Release 15.15.5 2026-01-13 16:12:37 +08:00
64693e384e <aes>APK 15.15.5 release Publish. 2026-01-13 16:11:30 +08:00
aebf83bc44 完善基础窗口类的公开方法 2026-01-13 16:10:31 +08:00
7ae716bccb <libaes>Library Release 15.15.4 2026-01-13 15:37:39 +08:00
3e67a5d0a4 <aes>APK 15.15.4 release Publish. 2026-01-13 15:37:28 +08:00
05a1fb1302 取消窗口创建时的吐司调试信息。 2026-01-13 15:36:23 +08:00
aa2e8e1a72 <libaes>Library Release 15.15.3 2026-01-13 15:29:53 +08:00
622d474410 <aes>APK 15.15.3 release Publish. 2026-01-13 15:29:29 +08:00
504b78c04e 优化基础窗口管理类。 2026-01-13 15:25:01 +08:00
7ee79a44c7 <libaes>Library Release 15.15.2 2026-01-13 11:19:29 +08:00
e459791c67 <aes>APK 15.15.2 release Publish. 2026-01-13 11:19:15 +08:00
749ec3d562 APPBase 类库版本更新为 15.15.4 2026-01-13 11:13:54 +08:00
1f154de3ee <libaes>Library Release 15.15.1 2026-01-13 03:37:58 +08:00
98c51e01c6 <aes>APK 15.15.1 release Publish. 2026-01-13 03:37:01 +08:00
bc908f5d7c 编译测试 2026-01-13 03:33:51 +08:00
b9613efca3 复制 https://gitea.winboll.cc/Studio/AES_Bck20260112_122031_590.git 项目最新AES模块源码 2026-01-13 03:32:37 +08:00
3f96da4987 <libappbase>Library Release 15.15.4 2026-01-13 03:23:37 +08:00
1b265d50b2 <appbase>APK 15.15.4 release Publish. 2026-01-13 03:23:17 +08:00
89b8f975bb 编译测试 2026-01-13 03:20:35 +08:00
28d5f8823c 编译测试 2026-01-13 02:56:40 +08:00
735f842661 复制 https://gitea.winboll.cc/Studio/APPBase_Bck20260112_122031_590.git 项目最新appbase源码 2026-01-13 02:55:23 +08:00
340 changed files with 20225 additions and 4481 deletions

6
.gitignore vendored
View File

@@ -94,8 +94,8 @@ lint-results.html
## 忽略 AndroidIDE 临时文件夹 ## 忽略 AndroidIDE 临时文件夹
.androidide .androidide
## 忽略模块应用编译配置 ## WinBoLL 基础应用(避免上传敏感配置)
/settings.gradle
/gradle.properties
/winboll.properties /winboll.properties
/local.properties /local.properties
/settings.gradle
/gradle.properties

View File

@@ -66,8 +66,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_1_7
} }
// 应用包输出配置 // 应用包输出配置
@@ -101,11 +101,14 @@ android {
// 创建 WinBoLL Studio 发布接口文件夹 // 创建 WinBoLL Studio 发布接口文件夹
File fWinBoLLStudioDir = file("/sdcard/WinBoLLStudio/APKs"); File fWinBoLLStudioDir = file("/sdcard/WinBoLLStudio/APKs");
// 如果配置了APK接口文件夹路径就设置应用APK输出文件夹为接口文件夹。
if(winbollProps != null && winbollProps['APKOutputPath'] != null ) {
fWinBoLLStudioDir = file(winbollProps['APKOutputPath']);
}
if(!fWinBoLLStudioDir.exists()) { if(!fWinBoLLStudioDir.exists()) {
//fWinBoLLStudioDir.mkdirs(); println "[ WinBoLLStudio ] : " + fWinBoLLStudioDir.getAbsolutePath() + " Folder does not exist."
// 如果没有发布接口文件就不用进行APK发布和源码管理操作 println '[ WinBoLLStudio ] : The APKOutputPath property is not defined in winboll.properties, please configure APK output folder first.'
// 当前编译环境不是 WinBoLL 主机, 以下将忽略APK发布和源码管理操作。
println 'The current compilation environment is not in WinBoLL host, and the following APK publishing and source management operations will be ignore.'
} else { } else {
/// WINBOLL 主机的 APK 发布和源码管理操作 /// /// WINBOLL 主机的 APK 发布和源码管理操作 ///
variant.getAssembleProvider().get().doFirst { variant.getAssembleProvider().get().doFirst {

View File

@@ -44,7 +44,9 @@ WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与
### 1. WinBoLL APP 开发环境配置介绍 ### 1. WinBoLL APP 开发环境配置介绍
#### WinBoLL APK 编译输出内容包括: #### WinBoLL APK 编译输出内容包括:
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。 ☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
winboll.properties 文件的 APKOutputPath 属性可配置这个 APK 输出目录的路径。
☆ "/sdcard/AppProjects/app.apk"文件。 ☆ "/sdcard/AppProjects/app.apk"文件。
winboll.properties 文件的 ExtraAPKOutputPath 属性可配置这个 APK 额外输出文件的路径
#### WinBoLL APK 源码命名空间规范 #### WinBoLL APK 源码命名空间规范
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。 ☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。

1
aes/.gitignore vendored Normal file
View File

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

View File

@@ -1,7 +1,8 @@
# Gallery # AES
[![](https://jitpack.io/v/ZhanGSKen/AES.svg)](https://jitpack.io/#ZhanGSKen/AES)
#### 介绍 #### 介绍
云宝相册应用 WinBoLL AndroidX 可视化元素类库。
#### 软件架构 #### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。 适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
@@ -10,7 +11,8 @@
#### Gradle 编译说明 #### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug 调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh gallery 阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh aes
阶段版类库发布命令 git pull &&bash .winboll/bashPublishLIBAddTag.sh libaes
#### 使用说明 #### 使用说明

48
aes/build.gradle Normal file
View File

@@ -0,0 +1,48 @@
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.aes"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.15"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
api project(':libaes')
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
aes/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Apr 25 04:16:42 HKT 2026
stageCount=10
libraryProject=libaes
baseVersion=15.15
publishVersion=15.15.9
buildCount=0
baseBetaVersion=15.15.10

View File

@@ -2,5 +2,5 @@
<resources> <resources>
<!-- Put flavor specific strings here --> <!-- Put flavor specific strings here -->
<string name="app_name">Gallery+</string> <string name="app_name">AES+</string>
</resources> </resources>

View File

@@ -0,0 +1,45 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.aes">
<!-- 对正在运行的应用重新排序 -->
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/MyAESTheme"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".TestActivityManagerActivity"/>
<activity android:name=".SettingsActivity"/>
<activity android:name=".AboutActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,78 @@
package cc.winboll.studio.aes;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.aes.R;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 11:25
* @Describe 应用介绍窗口
*/
public class AboutActivity extends BaseWinBoLLActivity {
public static final String TAG = "AboutActivity";
private Toolbar mToolbar;
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
// 设置工具栏
initToolbar();
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
WinBoLLActivityManager.getInstance().resumeActivity(MainActivity.class);
WinBoLLActivityManager.getInstance().finish(AboutActivity.this);
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "aes";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("AES");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=AES");
appInfo.setAppAPKName("AES");
appInfo.setAppAPKFolderName("AES");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}

View File

@@ -0,0 +1,34 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/13 19:03:58
* @Describe AES应用类
*/
import android.view.Gravity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
public class App extends GlobalApplication {
public static final String TAG = "App";
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}

View File

@@ -0,0 +1,45 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 16:35
* @Describe BaseWinBollActivity 【继承AppCompatActivity保留核心能力不额外暴露方法】
* 继承链路BaseWinBoLLActivity → AppCompatActivity → FragmentActivityAppCompat能力天然继承可用
*/
public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "BaseWinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
super.onCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
}
// 子类必须实现getTag(),确保唯一标识
@Override
public abstract String getTag();
@Override
public Activity getActivity() {
return this;
}
}

View File

@@ -0,0 +1,196 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/13 19:05:52
* @Describe 应用主窗口
*/
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Toast;
import cc.winboll.studio.aes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.dialogs.LocalFileSelectDialog;
import cc.winboll.studio.libaes.dialogs.StoragePathDialog;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.DrawerMenuBean;
import cc.winboll.studio.libaes.unittests.SecondaryLibraryActivity;
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
import cc.winboll.studio.libaes.unittests.TestASupportToolbarActivity;
import cc.winboll.studio.libaes.unittests.TestAToolbarActivity;
import cc.winboll.studio.libaes.unittests.TestDrawerFragmentActivity;
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import java.util.ArrayList;
public class MainActivity extends DrawerFragmentActivity {
public static final String TAG = "MainActivity";
TestAButtonFragment mTestAButtonFragment;
TestViewPageFragment mTestViewPageFragment;
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mTestAButtonFragment == null) {
mTestAButtonFragment = new TestAButtonFragment();
addFragment(mTestAButtonFragment);
}
showFragment(mTestAButtonFragment);
//setSubtitle(TAG);
//ToastUtils.show("onCreate");
}
@Override
public void initDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
super.initDrawerMenuItemList(listDrawerMenu);
LogUtils.d(TAG, "initDrawerMenuItemList");
//listDrawerMenu.clear();
// 添加抽屉菜单项
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
notifyDrawerMenuDataChanged();
}
@Override
public void reinitDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
super.reinitDrawerMenuItemList(listDrawerMenu);
LogUtils.d(TAG, "reinitDrawerMenuItemList");
//listDrawerMenu.clear();
// 添加抽屉菜单项
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
notifyDrawerMenuDataChanged();
}
@Override
public DrawerFragmentActivity.ActivityType initActivityType() {
return DrawerFragmentActivity.ActivityType.Main;
}
@Override
public boolean onCreateOptionsMenu(Menu 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);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
super.onItemClick(parent, view, position, id);
switch (position) {
case 0 : {
if (mTestAButtonFragment == null) {
mTestAButtonFragment = new TestAButtonFragment();
addFragment(mTestAButtonFragment);
}
showFragment(mTestAButtonFragment);
break;
}
case 1 : {
if (mTestViewPageFragment == null) {
mTestViewPageFragment = new TestViewPageFragment();
addFragment(mTestViewPageFragment);
}
showFragment(mTestViewPageFragment);
break;
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int nItemId = item.getItemId();
if (item.getItemId() == R.id.item_testactivitymanager) {
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, TestActivityManagerActivity.class);
//ToastUtils.show("item_testactivitymanager");
} else
if (nItemId == R.id.item_atoast) {
Toast.makeText(getApplication(), "item_testatoast", Toast.LENGTH_SHORT).show();
} else if (nItemId == R.id.item_atoolbar) {
Intent intent = new Intent(this, TestAToolbarActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_asupporttoolbar) {
Intent intent = new Intent(this, TestASupportToolbarActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_colordialog) {
ColorPickerDialog dlg = new ColorPickerDialog(this, getResources().getColor(R.color.colorPrimary));
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
} else if (nItemId == R.id.item_dialogstoragepath) {
final StoragePathDialog dialog = new StoragePathDialog(this, 0);
dialog.setOnOKClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
dialog.show();
} else if (nItemId == R.id.item_localfileselectdialog) {
final LocalFileSelectDialog dialog = new LocalFileSelectDialog(this);
dialog.setOnOKClickListener(new LocalFileSelectDialog.OKClickListener() {
@Override
public void onOKClick(String sz) {
Toast.makeText(getApplication(), sz, Toast.LENGTH_SHORT).show();
//dialog.dismiss();
}
});
dialog.open();
} else if (nItemId == R.id.item_secondarylibraryactivity) {
Intent intent = new Intent(this, SecondaryLibraryActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_drawerfragmentactivity) {
Intent intent = new Intent(this, TestDrawerFragmentActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_about) {
// Intent intent = new Intent(this, AboutActivity.class);
// startActivity(intent);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,39 @@
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>
* @Date 2025/11/26 18:01
* @Describe SettingsActivity
*/
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
// public void onModeSelected(ADsMode selectedMode) {
// if (selectedMode == ADsMode.STANDALONE) {
// // 处理单机模式逻辑(如释放米盟资源)
// ToastUtils.show("STANDALONE");
// } else if (selectedMode == ADsMode.MIMO_SDK) {
// // 处理米盟SDK模式逻辑如初始化SDK
// ToastUtils.show("MIMO_SDK");
// }
// }
// });
}
}

View File

@@ -0,0 +1,33 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.os.Bundle;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/28 21:07
* @Describe 窗口管理类测试窗口
*/
public class TestActivityManagerActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "TestActivityManagerActivity";
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_testactivitymanager);
}
}

View File

@@ -0,0 +1,60 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 00:11
* @Describe WinBoLL 窗口基础类
*/
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, String.format("onResume %s", getTag()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
/*if (item.getItemId() == R.id.item_log) {
WinBoLLActivityManager.getInstance().startLogActivity(this);
return true;
} else if (item.getItemId() == R.id.item_home) {
startActivity(new Intent(this, MainActivity.class));
return true;
}*/
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
WinBoLLActivityManager.getInstance().finish(this);
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 阴影部分 -->
<!-- 个人觉得更形象的表达top代表下边的阴影高度left代表右边的阴影宽度。其实也就是相对应的offsetsolid中的颜色是阴影的颜色也可以设置角度等等 -->
<item
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0F000000"
android:startColor="#0F000000" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
<!-- 背景部分 -->
<!-- 形象的表达bottom代表背景部分在上边缘超出阴影的高度right代表背景部分在左边超出阴影的宽度相对应的offset -->
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="5dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0FFFFFFF"
android:startColor="#FFFFFFFF" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="窗口管理类测试窗口"/>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/item_testactivitymanager"
android:title="TestActivityManager"/>
<item
android:id="@+id/item_log"
android:title="LogActivity"/>
<item
android:id="@+id/item_colordialog"
android:title="ColorDialog"/>
<item
android:id="@+id/item_dialogstoragepath"
android:title="StoragePathDialog"/>
<item
android:id="@+id/item_localfileselectdialog"
android:title="LocalFileSelectDialog"/>
<item
android:id="@+id/item_atoolbar"
android:title="Test AToolbar"/>
<item
android:id="@+id/item_asupporttoolbar"
android:title="Test ASupportToolbar"/>
<item
android:id="@+id/item_atoast"
android:title="Test AToast"/>
<item
android:id="@+id/item_secondarylibraryactivity"
android:title="Test SecondaryLibraryActivity"/>
<item
android:id="@+id/item_drawerfragmentactivity"
android:title="Test DrawerFragmentActivity"/>
<item
android:id="@+id/item_settings"
android:title="Settings"/>
<item
android:id="@+id/item_about"
android:title="About"/>
</menu>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF00B322</color>
<color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AES</string>
<string name="app_description">WinBoLL AndroidX 可视化元素类库。</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAESTheme" parent="AESTheme">
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">winboll.cc</domain>
</domain-config>
</network-security-config>

1
appbase/.gitignore vendored Normal file
View File

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

36
appbase/README.md Normal file
View File

@@ -0,0 +1,36 @@
# APPBase
[![](https://jitpack.io/v/ZhanGSKen/APPBase.svg)](https://jitpack.io/#ZhanGSKen/APPBase)
#### 介绍
WinBoLL 安卓手机端安卓应用开发基础类库。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh appbase
阶段版类库发布命令 git pull &&bash .winboll/bashPublishLIBAddTag.sh libappbase
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -0,0 +1 @@

50
appbase/build.gradle Normal file
View File

@@ -0,0 +1,50 @@
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.appbase"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.16"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 确保 Java 7 兼容性(已适配项目技术栈)
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
api project(':libappbase')
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
appbase/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat May 09 21:14:54 GMT 2026
stageCount=0
libraryProject=libappbase
baseVersion=15.16
publishVersion=15.16.0
buildCount=1
baseBetaVersion=15.16.1

126
appbase/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,126 @@
# 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 *;
#}
# ============================== 基础通用规则 ==============================
# 保留系统组件
-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 public class * extends com.winboll.WinBoLLActivity
#-keep public class * extends com.winboll.WinBoLLFragment
# 主包名
-keep class cc.winboll.studio.*.** { *; }
# beta包名
-keep class cc.winboll.studio.*.beta.** { *; }
-keepclassmembers class cc.winboll.studio.*.** { *; }
-keepclassmembers class cc.winboll.studio.*.beta.** { *; }
# 保留所有类中的 public static final String TAG 字段
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类
-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 文件
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法
-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.**
# ============================== 第三方框架规则 ==============================
# Retrofit + OkHttp
-keep class retrofit2.** { *; }
-keep interface retrofit2.** { *; }
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okio.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Glide 4.x
-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 *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
# GreenDAO 3.x
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
# 实体类包名(按实际调整)
#-keep class cc.winboll.studio.appbase.model.** { *; }
# ButterKnife 8.x
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.BindView <fields>;
@butterknife.OnClick <methods>;
}
# EventBus 3.x
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# ============================== 优化与调试 ==============================
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-verbose
-dontpreverify
-dontusemixedcaseclassnames
# 保留行号(便于崩溃定位)
-keepattributes SourceFile,LineNumberTable

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase">
<application
android:name=".App"
android:icon="@drawable/ic_winboll"
android:label="@string/app_name"
android:theme="@style/MyAPPBaseTheme"
android:resizeableActivity="true"
android:process=":App">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:resizeableActivity="true"
android:launchMode="singleTop"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".Main2Activity"
android:label="@string/app_name"
android:exported="true"
android:resizeableActivity="true"
android:launchMode="singleTop"
android:taskAffinity="cc.winboll.studio.appbase.Main2Activity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
<activity android:name=".GlobalApplication$CrashActivity"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
<activity android:name=".AboutActivity"/>
</application>
</manifest>

View File

@@ -1,14 +1,20 @@
package cc.winboll.studio.gallery; package cc.winboll.studio.appbase;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import androidx.appcompat.app.AppCompatActivity; import android.widget.Toolbar;
import androidx.appcompat.widget.Toolbar; import cc.winboll.studio.appbase.R;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo; import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView; import cc.winboll.studio.libappbase.views.AboutView;
public class AboutActivity extends AppCompatActivity { /**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/11 12:55
* @Describe AboutActivity
*/
public class AboutActivity extends Activity {
public static final String TAG = "AboutActivity"; public static final String TAG = "AboutActivity";
@@ -18,14 +24,7 @@ public class AboutActivity extends AppCompatActivity {
setContentView(R.layout.activity_about); setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
AboutView aboutView = findViewById(R.id.aboutview); AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo()); aboutView.setAPPInfo(genDefaultAppInfo());
@@ -33,18 +32,18 @@ public class AboutActivity extends AppCompatActivity {
private APPInfo genDefaultAppInfo() { private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用"); LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "gallery"; String branchName = "appbase";
APPInfo appInfo = new APPInfo(); APPInfo appInfo = new APPInfo();
appInfo.setAppName("Gallery"); appInfo.setAppName("APPBase");
appInfo.setAppIcon(R.drawable.ic_winboll); appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description)); appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL"); appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio"); appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName); appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName); appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery"); appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=APPBase");
appInfo.setAppAPKName("Gallery"); appInfo.setAppAPKName("APPBase");
appInfo.setAppAPKFolderName("Gallery"); appInfo.setAppAPKFolderName("APPBase");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成"); LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo; return appInfo;
} }

View File

@@ -0,0 +1,46 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/01/05 09:54:42
* @Describe 应用全局入口类(继承基础库 GlobalApplication
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
*/
public class App extends GlobalApplication {
/** 当前应用类的日志 TAG用于调试输出标识日志来源 */
public static final String TAG = "App";
/**
* 应用创建时回调(全局初始化入口)
* 在应用进程启动时执行,仅调用一次,用于初始化全局工具类、第三方库等
*/
@Override
public void onCreate() {
super.onCreate();
// 如果应用不在调试状态,就根据编译类型设置调试状态
if (isDebugging() != true) {
setIsDebugging(BuildConfig.DEBUG);
}
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
}
/**
* 应用终止时回调(资源释放入口)
* 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行
* 用于释放全局资源,避免内存泄漏
*/
@Override
public void onTerminate() {
super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放)
// 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏)
ToastUtils.release();
}
}

View File

@@ -0,0 +1,20 @@
package cc.winboll.studio.appbase;
import android.os.Bundle;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
public class Main2Activity extends MainActivity {
public static final String TAG = "Main2Activity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setActionBar(toolbar);
}
}
}

View File

@@ -0,0 +1,177 @@
package cc.winboll.studio.appbase;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
import cc.winboll.studio.appbase.model.TestBean;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 未标注(建议补充创建日期)
* @Describe 应用主界面 Activity入口界面
* 包含功能测试按钮崩溃测试、日志查看、Toast测试、顶部工具栏菜单功能是应用交互的核心入口
*/
public class MainActivity extends Activity {
/** 当前 Activity 的日志 TAG用于调试输出标识日志来源 */
public static final String TAG = "MainActivity";
/** 顶部工具栏(用于展示标题、菜单,绑定布局中的 Toolbar 控件) */
private Toolbar mToolbar;
/**
* Activity 创建时回调(初始化界面)
* 在 Activity 首次创建时执行,用于加载布局、初始化控件、设置事件监听
* @param savedInstanceState 保存 Activity 状态的 Bundle如屏幕旋转时的数据恢复
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//ToastUtils.show("onCreate"); // 显示 Activity 创建提示(调试用)
setContentView(R.layout.activity_main); // 加载主界面布局
// 初始化 Toolbar 并设置为 ActionBar
mToolbar = findViewById(R.id.toolbar);
setActionBar(mToolbar); // 将 Toolbar 替代系统默认 ActionBar
initTestData();
}
void initTestData() {
TestBean bean1 = new TestBean();
bean1.setTestNum1(456);
TestBean.saveBeanToFile(getFilesDir().getAbsolutePath() + getTestBeanRelativePath(), bean1);
TestBean bean2 = new TestBean();
bean2.setTestNum1(789);
TestBean.saveBeanToFile(getExternalFilesDir(null).getAbsolutePath() + getTestBeanRelativePath(), bean2);
}
String getTestBeanRelativePath() {
return "/BaseBaen/"+TestBean.class.getName()+".json";
}
/**
* 创建菜单时回调(加载工具栏菜单)
* 初始化 ActionBar 菜单,加载自定义菜单布局
* @param menu 菜单对象(用于承载菜单项)
* @return true显示菜单false不显示菜单
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 加载菜单布局R.menu.toolbar_main 为自定义菜单文件)
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* 菜单 item 点击时回调(处理菜单事件)
* 响应 Toolbar 菜单项的点击事件,执行对应业务逻辑
* @param item 被点击的菜单项
* @return true消费点击事件false不消费传递给父类
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.item_home:
// 点击 "首页/官网" 菜单项,唤起浏览器打开指定网站
openWebsiteInBrowser(this);
break;
// 可扩展其他菜单项(如设置、关于等)的处理逻辑
}
return super.onOptionsItemSelected(item);
}
/**
* 崩溃测试按钮点击事件(触发应用崩溃,用于调试异常捕获)
* 故意执行非法操作(循环获取不存在的字符串资源),强制应用崩溃
* @param view 触发事件的 View对应布局中的崩溃测试按钮
*/
public void onCrashTest(View view) {
// 循环从 Integer.MIN_VALUE 到 Integer.MAX_VALUE获取不存在的字符串资源 ID触发崩溃
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
getString(i); // i 超出资源 ID 范围,抛出 Resources.NotFoundException 导致崩溃
}
}
public void onLogTestNewTask(View view) {
LogActivity.startLogActivity(this, true);
}
/**
* 日志测试按钮点击事件(打开日志查看界面)
* 启动 LogActivity用于查看应用运行日志
* @param view 触发事件的 View对应布局中的日志测试按钮
*/
public void onLogTest(View view) {
LogActivity.startLogActivity(this, false);
}
/**
* Toast 工具测试按钮点击事件(测试全局 Toast 功能)
* 测试主线程、子线程中 Toast 的显示效果,验证 ToastUtils 的可用性
* @param view 触发事件的 View对应布局中的 Toast 测试按钮)
*/
public void onToastUtilsTest(View view) {
LogUtils.d(TAG, "onToastUtilsTest"); // 打印调试日志,标识进入 Toast 测试
ToastUtils.show("Hello, WinBoLL!"); // 主线程显示 Toast
// 开启子线程,延迟 2 秒后显示 Toast测试子线程 Toast 兼容性)
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000); // 线程休眠 2 秒
// 若 ToastUtils 已处理主线程切换,此处可直接调用;否则需通过 Handler 切换到主线程
ToastUtils.show("Thread.sleep(2000);ToastUtils.show...");
} catch (InterruptedException e) {
// 捕获线程中断异常(如线程被销毁时),不做处理(测试场景)
e.printStackTrace();
}
}
}).start();
}
/**
* 唤起系统默认浏览器打开指定网站(跳转至应用官网)
* 通过 Intent.ACTION_VIEW 隐式意图,触发浏览器打开目标 URL
* @param context 上下文对象(如 Activity、Application此处为 MainActivity
*/
public void openWebsiteInBrowser(Context context) {
String url = "https://www.winboll.cc"; // 目标网站 URL应用官网
// 构建隐式意图ACTION_VIEW 表示查看指定数据Uri 为网站地址)
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
// 设置标志:在新的任务栈中启动 Activity避免与当前应用任务栈混淆
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动意图(唤起浏览器)
context.startActivity(intent);
}
public void onAboutActivity(View view) {
LogUtils.d(TAG, "onAboutActivity() 调用");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
startActivity(aboutIntent);
}
public void onMultiInstance(View view) {
LogUtils.d(TAG, "onMultiInstance() 多开窗口按钮已点击");
ToastUtils.show("多开窗口:已启动新窗口");
android.content.Intent intent = new android.content.Intent(this, Main2Activity.class);
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
LogUtils.d(TAG, "onMultiInstance() 准备启动Main2Activity");
startActivity(intent);
LogUtils.d(TAG, "onMultiInstance() Main2Activity已启动");
}
}

View File

@@ -0,0 +1,154 @@
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 java.io.IOException;
/**
* 测试实体类
* 继承BaseBean实现JSON序列化/反序列化能力提供基础int类型属性的封装与数据持久化支持
* 适配Java7语法遵循BaseBean统一的反射识别、JSON读写规范
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/31 19:16:00
* @LastEditTime 2026/02/01 10:46:00
*/
public class TestBean extends BaseBean {
// ====================================== 常量定义 ======================================
/** 当前类的日志 TAG用于调试输出 */
public static final String TAG = "TestBean";
// ====================================== 成员属性 ======================================
/**
* 测试数字属性默认值123
* 基础int类型属性用于测试BaseBean的JSON序列化/反序列化能力
*/
private int testNum1;
// ====================================== 构造方法 ======================================
/**
* 无参构造器(默认初始化)
* 给testNum1赋值默认值123满足反射实例化、JSON解析的无参构造要求
*/
public TestBean() {
this.testNum1 = 123;
LogUtils.d(TAG, "TestBean无参构造器调用testNum1默认初始化值" + this.testNum1);
}
/**
* 有参构造器(自定义初始化)
* @param testNum1 测试数字初始值
*/
public TestBean(int testNum1) {
this.testNum1 = testNum1;
LogUtils.d(TAG, "TestBean有参构造器调用传入testNum1" + testNum1);
}
// ====================================== Get/Set 方法 ======================================
/**
* 设置测试数字属性值
* @param testNum1 待设置的int类型值
*/
public void setTestNum1(int testNum1) {
LogUtils.d(TAG, "setTestNum1调用传入参数" + testNum1);
this.testNum1 = testNum1;
}
/**
* 获取测试数字属性值
* @return 当前testNum1的int类型值
*/
public int getTestNum1() {
LogUtils.d(TAG, "getTestNum1调用返回值" + this.testNum1);
return testNum1;
}
// ====================================== 重写父类BaseBean方法 ======================================
/**
* 重写父类方法:获取当前类的全限定名
* 用于BaseBean反射识别、类名匹配等统一逻辑
* @return 类全限定名cc.winboll.studio.appbase.model.TestBean
*/
@Override
public String getName() {
LogUtils.d(TAG, "getName方法调用返回类全限定名" + TestBean.class.getName());
return TestBean.class.getName();
}
/**
* 重写父类方法将当前对象序列化为JSON持久化存储专用
* 遵循BaseBean规范先执行父类序列化逻辑再处理子类专属字段
* @param jsonWriter JSON写入器外部传入的JSON流操作实例
* @throws IOException JSON写入异常流关闭、格式错误等
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter调用传入参数JsonWriter" + jsonWriter);
// 执行父类公共字段的序列化逻辑
super.writeThisToJsonWriter(jsonWriter);
// 序列化子类专属字段testNum1
jsonWriter.name("testNum1").value(this.getTestNum1());
LogUtils.d(TAG, "writeThisToJsonWriter执行完成已序列化testNum1" + this.getTestNum1());
}
/**
* 重写父类方法从JSON字段初始化当前对象属性解析JSON专用
* 先让父类处理公共字段再匹配子类专属字段不匹配则返回false跳过
* @param jsonReader JSON读取器外部传入的JSON流操作实例
* @param name 当前解析的JSON字段名
* @return true-字段解析成功false-字段不匹配,需跳过/父类处理
* @throws IOException JSON读取异常字段类型不匹配、流中断等
*/
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
LogUtils.d(TAG, "initObjectsFromJsonReader调用传入参数name=" + name + "JsonReader=" + jsonReader);
// 父类优先处理公共字段,处理成功则直接返回
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.d(TAG, "initObjectsFromJsonReader字段" + name + "由父类BaseBean处理成功");
return true;
}
// 解析子类专属字段
if ("testNum1".equals(name)) {
this.setTestNum1(jsonReader.nextInt());
LogUtils.d(TAG, "initObjectsFromJsonReader解析testNum1成功值为" + this.getTestNum1());
} else {
LogUtils.w(TAG, "initObjectsFromJsonReader字段" + name + "不匹配返回false跳过解析");
// 字段不匹配返回false表示跳过
return false;
}
return true;
}
/**
* 重写父类方法从JSON读取器完整解析并初始化当前对象JSON解析入口
* 负责JSON对象的开始/结束标识处理,遍历所有字段并调用字段解析方法
* 严格遵循writeThisToJsonWriter的序列化结构保证解析一致性
* @param jsonReader JSON读取器外部传入的JSON流操作实例
* @return 解析后的当前TestBean实例支持链式调用
* @throws IOException JSON解析异常格式错误、字段缺失、流异常等
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader调用传入参数JsonReader" + jsonReader);
// 开始解析JSON对象与序列化结构保持一致
jsonReader.beginObject();
// 遍历所有JSON字段
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
LogUtils.d(TAG, "readBeanFromJsonReader开始解析字段fieldName=" + fieldName);
// 解析字段,不匹配则跳过该值
if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader字段" + fieldName + "解析失败,已跳过该值");
}
}
// 结束JSON对象解析必须调用避免流异常
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader执行完成JSON解析结束当前TestBean实例testNum1" + this.getTestNum1());
// 返回当前实例,支持链式调用
return this;
}
}

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#FFFFFF"/> <solid android:color="#81C7F5"/> <!-- 浅蓝色填充 -->
<corners android:radius="8dp"/> <!-- 8dp 圆角 -->
</shape> </shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:spacing="12dp">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="关于应用"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onAboutActivity"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用崩溃测试"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onCrashTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试(新窗口)"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTestNewTask"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用吐司测试"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onToastUtilsTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="多开窗口"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#81C7F5"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onMultiInstance"
android:layout_margin="10dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@android:color/white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main2Activity"
android:textSize="24sp"
android:textColor="@color/gray_900"/>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_home"
android:title="Home"
android:icon="@drawable/ic_winboll"
android:showAsAction="always"/>
</menu>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AboutView">
<attr name="app_name" format="string" />
<attr name="app_apkfoldername" format="string" />
<attr name="app_apkname" format="string" />
<attr name="app_gitname" format="string" />
<attr name="app_gitowner" format="string" />
<attr name="app_gitappbranch" format="string" />
<attr name="app_gitappsubprojectfolder" format="string" />
<attr name="appdescription" format="string" />
<attr name="appicon" format="reference" />
<attr name="is_adddebugtools" format="boolean" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF00B322</color>
<color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APPBase</string>
<string name="app_description">WinBoLL 安卓手机端安卓应用开发基础类库。</string>
<string name="app_normal">Click here is switch to Normal APP</string>
<string name="app_debug">Click here is switch to APP DEBUG</string>
<string name="gitea_home">GITEA HOME</string>
<string name="app_update">APP UPDATE</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
<item name="themeGlobalCrashActivity">@style/MyGlobalCrashActivityTheme</item>
</style>
<style name="MyGlobalCrashActivityTheme" parent="GlobalCrashActivityTheme">
<item name="colorTittle">#FFFFFFFF</item>
<item name="colorTittleBackgound">#FF00A4B3</item>
<item name="colorText">#FFFFFFFF</item>
<item name="colorTextBackgound">#FF000000</item>
</style>
</resources>

View File

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

View File

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

View File

@@ -98,7 +98,6 @@ allprojects {
options.compilerArgs << "-parameters" options.compilerArgs << "-parameters"
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
// 可选:确保编码一致
options.encoding = "UTF-8" options.encoding = "UTF-8"
} }
} }

View File

@@ -1,119 +0,0 @@
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.gallery"
minSdkVersion 23
// 适配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.15.9'
api 'cc.winboll.studio:libappbase:15.15.19'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Apr 26 23:52:51 HKT 2026
stageCount=7
libraryProject=
baseVersion=15.0
publishVersion=15.0.6
buildCount=0
baseBetaVersion=15.0.7

View File

@@ -1,66 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.gallery">
<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="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:name=".GlobalWinBoLLApplication"
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>

View File

@@ -1,25 +0,0 @@
package cc.winboll.studio.gallery;
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; }
}

View File

@@ -1,251 +0,0 @@
package cc.winboll.studio.gallery;
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.gallery.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();
}
}
}

View File

@@ -1,255 +0,0 @@
package cc.winboll.studio.gallery;
import android.net.Uri;
import android.provider.MediaStore;
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.Collections;
import java.util.Comparator;
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 = sortAlbums(albums);
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
notifyDataSetChanged();
}
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
return list;
}
ArrayList<Album> pinned = new ArrayList<>();
ArrayList<Album> unpinned = new ArrayList<>();
for (Album album : list) {
if (pinnedDbHelper.isPinned(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
pinned.addAll(unpinned);
return pinned;
}
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()) {
ArrayList<Album> pinned = new ArrayList<>();
ArrayList<Album> unpinned = new ArrayList<>();
for (Album album : albums) {
if (pinnedDbHelper.isPinned(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
Comparator<Album> nameComparator = new Comparator<Album>() {
@Override
public int compare(Album a1, Album a2) {
return a1.getName().compareToIgnoreCase(a2.getName());
}
};
Collections.sort(pinned, nameComparator);
Collections.sort(unpinned, nameComparator);
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());
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
String[] items = isPinned ? new String[]{"取消置顶"} : 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());
}
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 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);
}
}
}

View File

@@ -1,131 +0,0 @@
package cc.winboll.studio.gallery;
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);
}
}

View File

@@ -1,170 +0,0 @@
package cc.winboll.studio.gallery;
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.Toast;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
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 Bitmap originalBitmap;
private String imagePath;
private String albumPath;
private int cropWidth = 240;
private int cropHeight = 120;
private float cropRatio = 2.0f;
@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);
Preferences prefs = new Preferences(this);
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();
}
});
final ImageView btnColorPick = findViewById(R.id.btn_color_pick);
btnColorPick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean pickMode = !cropCanvasView.isColorPickMode();
cropCanvasView.setColorPickMode(pickMode);
btnColorPick.setAlpha(pickMode ? 0.5f : 1.0f);
}
});
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveCroppedCover();
}
});
cropCanvasView = findViewById(R.id.crop_canvas_view);
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() {
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
if (canvasBitmap == null) {
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
return;
}
try {
RectF cropRect = cropCanvasView.getCropRect();
int cropX = (int) cropRect.left;
int cropY = (int) cropRect.top;
int cropW = (int) cropRect.width();
int cropH = (int) cropRect.height();
cropX = Math.max(0, Math.min(cropX, canvasBitmap.getWidth() - 1));
cropY = Math.max(0, Math.min(cropY, canvasBitmap.getHeight() - 1));
cropW = Math.max(1, Math.min(cropW, canvasBitmap.getWidth() - cropX));
cropH = Math.max(1, Math.min(cropH, canvasBitmap.getHeight() - cropY));
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, cropX, cropY, cropW, cropH);
File cacheDir = getCacheDir();
File coverFile = new File(cacheDir, "cover_" + System.currentTimeMillis() + ".png");
FileOutputStream fos = new FileOutputStream(coverFile);
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close();
cropped.recycle();
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
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();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (originalBitmap != null) {
originalBitmap.recycle();
originalBitmap = null;
}
}
}

View File

@@ -1,400 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Bitmap;
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 CropCanvasView extends View {
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 initialSpan;
private int backgroundColor = Color.GREEN;
private boolean colorPickMode = false;
private float pickX, pickY;
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);
}
public void setImageBitmap(Bitmap bitmap) {
this.originalBitmap = bitmap;
}
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(0, 0, canvasWidth, canvasHeight);
createCanvasBitmap();
requestLayout();
invalidate();
}
private void createCanvasBitmap() {
if (canvasBitmap != null) {
canvasBitmap.recycle();
}
canvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
android.graphics.Canvas canvas = new android.graphics.Canvas(canvasBitmap);
canvas.drawColor(backgroundColor);
if (originalBitmap != null && !originalBitmap.isRecycled()) {
canvas.drawBitmap(originalBitmap, imageBounds.left, imageBounds.top, imagePaint);
}
}
public Bitmap getCanvasBitmap() {
return canvasBitmap;
}
public void setBackgroundColor(int color) {
this.backgroundColor = color;
createCanvasBitmap();
invalidate();
}
public int getBackgroundColor() {
return backgroundColor;
}
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);
}
@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 onDraw(Canvas canvas) {
super.onDraw(canvas);
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
canvas.drawBitmap(canvasBitmap, 0, 0, null);
}
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 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;
cropRect.set(centerX - newWidth / 2, centerY - newHeight / 2,
centerX + newWidth / 2, centerY + newHeight / 2);
initialSpan = span;
invalidate();
}
return true;
}
}
float x = event.getX();
float y = event.getY();
if (colorPickMode) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
pickX = x;
pickY = y;
int color = getColorAt(x, y);
setBackgroundColor(color);
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 >= 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 / 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) {
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
int bitmapX = (int) x;
int bitmapY = (int) y;
if (bitmapX >= 0 && bitmapX < canvasBitmap.getWidth() &&
bitmapY >= 0 && bitmapY < canvasBitmap.getHeight()) {
return canvasBitmap.getPixel(bitmapX, bitmapY);
}
}
return Color.TRANSPARENT;
}
}

View File

@@ -1,224 +0,0 @@
package cc.winboll.studio.gallery;
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;
}
}

View File

@@ -1,44 +0,0 @@
package cc.winboll.studio.gallery;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/04/24 15:23
*/
public class GlobalWinBoLLApplication extends GlobalApplication {
public static final String TAG = "GlobalWinBoLLApplication";
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate");
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
// 设置 Toast 布局样式
//ToastUtils.setView(R.layout.view_toast);
//ToastUtils.setStyle(new WhiteToastStyle());
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate");
ToastUtils.release();
}
}

View File

@@ -1,247 +0,0 @@
package cc.winboll.studio.gallery;
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(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 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);
}
}
}

View File

@@ -1,72 +0,0 @@
package cc.winboll.studio.gallery;
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;
private int bgType;
public ImagePagerAdapter(ArrayList<Uri> imageUrls, int bgType) {
this.imageUrls = imageUrls;
this.bgType = bgType;
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images, bgType=" + bgType);
}
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;
}
}
@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(getBgRes());
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;
}
}

View File

@@ -1,438 +0,0 @@
package cc.winboll.studio.gallery;
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 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 btnBg;
private int bgType = 0;
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);
bgType = prefs.getBgType();
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);
btnBg = findViewById(R.id.btn_bg);
applyBg();
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls, bgType);
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();
}
});
btnBg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switchBg();
}
});
}
private void toggleToolbar() {
if (toolbar.getVisibility() == View.VISIBLE) {
toolbar.setVisibility(View.GONE);
} else {
toolbar.setVisibility(View.VISIBLE);
}
}
private void applyBg() {
int bgRes;
switch (bgType) {
case 0:
bgRes = R.drawable.bg_checkerboard;
break;
case 1:
bgRes = R.drawable.bg_white;
break;
case 2:
bgRes = R.drawable.bg_black;
break;
default:
bgRes = R.drawable.bg_checkerboard;
}
View container = findViewById(R.id.container);
if (container != null) {
container.setBackgroundResource(bgRes);
}
}
private void switchBg() {
final String[] bgNames = {"灰白相间", "全白", "全黑"};
final int[] bgResources = {R.drawable.bg_checkerboard, R.drawable.bg_white, R.drawable.bg_black};
new AlertDialog.Builder(this)
.setTitle("选择背景")
.setSingleChoiceItems(bgNames, bgType, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
bgType = which;
prefs.setBgType(which);
int currentItem = viewPager.getCurrentItem();
View container = findViewById(R.id.container);
if (container != null) {
container.setBackgroundResource(bgResources[which]);
}
viewPager.setAdapter(new ImagePagerAdapter(imageUrls, bgType));
viewPager.setCurrentItem(currentItem);
dialog.dismiss();
}
})
.setNegativeButton("取消", null)
.show();
}
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, bgType));
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();
}
}

View File

@@ -1,357 +0,0 @@
package cc.winboll.studio.gallery;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
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.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 com.google.android.material.floatingactionbutton.FloatingActionButton;
import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogActivity;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.util.ArrayList;
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;
@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);
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() {
LogUtils.d(TAG, "loadAlbums");
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
folderPath = Preferences.getDefaultPath();
baseFolder = new File(folderPath);
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
if (!baseFolder.exists()) {
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
baseFolder = new File(folderPath);
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
}
}
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
ArrayList<Album> albums = new ArrayList<>();
FileFilter directoryFilter = new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
};
File[] subfolders = baseFolder.listFiles(directoryFilter);
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
if (subfolders != null) {
for (File subfolder : subfolders) {
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
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);
}
}
if (coverUri == null) {
ArrayList<Uri> images = getImagesInFolder(albumPath);
if (!images.isEmpty()) {
coverUri = images.get(0);
}
}
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
if (coverUri != null || !allImages.isEmpty()) {
if (coverUri == null && !allImages.isEmpty()) {
coverUri = allImages.get(0);
}
int imageCount = allImages.size();
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
}
}
}
if (albums.isEmpty()) {
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "No albums found");
}
adapter.setData(albums);
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
}
private Uri getUriFromPath(String path) {
String[] projection = { MediaStore.Images.Media._ID };
String selection = MediaStore.Images.Media.DATA + " = ?";
String[] selectionArgs = { path };
try (Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null) {
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));
}
}
}
return null;
}
private ArrayList<Uri> getImagesInFolder(String folderPath) {
ArrayList<Uri> imageUrls = new ArrayList<>();
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";
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
if (cursor != null) {
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
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));
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
imageUrls.add(contentUri);
}
}
}
}
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
return imageUrls;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
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);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
scanMediaStore();
loadAlbums();
}
if (adapter != null) {
adapter.refreshBg();
adapter.refreshPinned();
adapter.refreshCover();
}
}
private void scanMediaStore() {
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
if (baseFolder.exists() && baseFolder.isDirectory()) {
File[] subfolders = baseFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if (subfolders != null) {
ArrayList<String> paths = new ArrayList<>();
for (File subfolder : subfolders) {
File[] images = subfolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
String lower = name.toLowerCase();
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|| lower.endsWith(".png") || lower.endsWith(".gif")
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
}
});
if (images != null) {
for (File img : images) {
paths.add(img.getAbsolutePath());
}
}
}
if (!paths.isEmpty()) {
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
String[] pathArray = paths.toArray(new String[0]);
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
}
});
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
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 PinnedAlbumDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedAlbumDbHelper";
private static final String DB_NAME = "pinned_album.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_albums";
private static final String COLUMN_PATH = "album_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
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) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, albumPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinAlbum: " + albumPath);
}
public void unpinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "unpinAlbum: " + albumPath);
}
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, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
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;
}
}

View File

@@ -1,90 +0,0 @@
package cc.winboll.studio.gallery;
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";
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();
}
}

View File

@@ -1,46 +0,0 @@
package cc.winboll.studio.gallery;
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();
}
});
}
}

View File

@@ -1,193 +0,0 @@
package cc.winboll.studio.gallery;
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;
}
}

View File

@@ -1,111 +0,0 @@
package cc.winboll.studio.gallery;
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 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);
}
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import java.io.File;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashDbHelper extends SQLiteOpenHelper {
public static final String TAG = "TrashDbHelper";
private static final String DB_NAME = "trash.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "trash_items";
private static final String COL_ID = "_id";
private static final String COL_FILE_NAME = "file_name";
private static final String COL_ORIGINAL_PATH = "original_path";
private static final String COL_ORIGINAL_FOLDER = "original_folder";
private static final String COL_DELETE_TIME = "delete_time";
public TrashDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
LogUtils.d(TAG, "onCreate");
db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +
COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COL_FILE_NAME + " TEXT, " +
COL_ORIGINAL_PATH + " TEXT, " +
COL_ORIGINAL_FOLDER + " TEXT, " +
COL_DELETE_TIME + " INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LogUtils.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public long insert(String fileName, String originalPath, String originalFolder) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COL_FILE_NAME, fileName);
values.put(COL_ORIGINAL_PATH, originalPath);
values.put(COL_ORIGINAL_FOLDER, originalFolder);
values.put(COL_DELETE_TIME, System.currentTimeMillis());
return db.insert(TABLE_NAME, null, values);
}
public Cursor getAll() {
SQLiteDatabase db = getReadableDatabase();
return db.query(TABLE_NAME, null, null, null, null, null, COL_DELETE_TIME + " DESC");
}
public int delete(long id) {
SQLiteDatabase db = getWritableDatabase();
return db.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(id)});
}
public void clear() {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
public static String getTrashPath() {
File trashDir = new File(Environment.getExternalStorageDirectory(), ".Trash");
if (!trashDir.exists()) {
trashDir.mkdirs();
}
return trashDir.getAbsolutePath();
}
}

View File

@@ -1,158 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import java.io.File;
import java.util.UUID;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashManager {
public static final String TAG = "TrashManager";
private final Context context;
private final TrashDbHelper dbHelper;
public TrashManager(Context context) {
LogUtils.d(TAG, "TrashManager created");
this.context = context;
this.dbHelper = new TrashDbHelper(context);
}
public long addToTrash(String imagePath) {
LogUtils.d(TAG, "addToTrash: " + imagePath);
File sourceFile = new File(imagePath);
if (!sourceFile.exists()) {
return -1;
}
String uniqueId = UUID.randomUUID().toString();
String extension = getExtension(imagePath);
String newFileName = uniqueId + extension;
String trashPath = TrashDbHelper.getTrashPath();
File destFile = new File(trashPath, newFileName);
if (sourceFile.renameTo(destFile)) {
String originalFolder = sourceFile.getParent();
long result = dbHelper.insert(newFileName, imagePath, originalFolder);
LogUtils.i(TAG, "Added to trash: " + newFileName);
return result;
}
LogUtils.e(TAG, "Failed to move to trash");
return -1;
}
public Cursor getTrashList() {
return dbHelper.getAll();
}
public boolean restore(long id, String fileName, String originalPath) {
LogUtils.i(TAG, "restore: " + fileName + " -> " + originalPath);
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
LogUtils.d(TAG, "trashFile exists: " + trashFile.exists() + ", path: " + trashFile.getAbsolutePath());
if (!trashFile.exists()) {
LogUtils.e(TAG, "trashFile not exists: " + trashFile.getAbsolutePath());
return false;
}
File originalFolder = new File(originalPath).getParentFile();
LogUtils.d(TAG, "originalFolder: " + originalFolder + ", exists: " + (originalFolder != null && originalFolder.exists()));
if (originalFolder != null && !originalFolder.exists()) {
boolean created = originalFolder.mkdirs();
LogUtils.d(TAG, "mkdirs result: " + created + ", path: " + originalFolder.getAbsolutePath());
}
File originalFile = new File(originalPath);
String restoreName = originalFile.getName();
File restoreFile = new File(originalFolder, restoreName);
LogUtils.d(TAG, "restoreFile: " + restoreFile.getAbsolutePath() + ", exists: " + restoreFile.exists());
boolean renameResult = trashFile.renameTo(restoreFile);
LogUtils.d(TAG, "renameTo result: " + renameResult);
if (renameResult) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored: " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
// Try copy + delete if rename failed
LogUtils.i(TAG, "renameTo failed, trying copy + delete");
try {
java.io.InputStream in = new java.io.FileInputStream(trashFile);
java.io.OutputStream out = new java.io.FileOutputStream(restoreFile);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
in.close();
out.close();
boolean deleted = trashFile.delete();
LogUtils.d(TAG, "copy+delete result: " + deleted);
if (deleted) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored (copy): " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
} catch (Exception e) {
LogUtils.e(TAG, "copy failed: " + e.getMessage());
}
LogUtils.e(TAG, "Failed to restore: " + fileName);
return false;
}
public boolean deletePermanently(long id, String fileName) {
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
boolean deleted = trashFile.delete();
if (deleted) {
dbHelper.delete(id);
}
return deleted;
}
public void clearTrash() {
Cursor cursor = getTrashList();
if (cursor != null) {
while (cursor.moveToNext()) {
try {
int colIndex = cursor.getColumnIndexOrThrow("_id");
if (!cursor.isNull(colIndex)) {
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
trashFile.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
cursor.close();
}
dbHelper.clear();
}
private String getExtension(String path) {
int lastDot = path.lastIndexOf('.');
if (lastDot > 0) {
return path.substring(lastDot);
}
return ".jpg";
}
private void scanMedia(String filePath) {
LogUtils.d(TAG, "scanMedia: " + filePath);
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new android.media.MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
}
});
}
}

View File

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

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10"
android:viewportHeight="10">
<path
android:fillColor="#808080"
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
</vector>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#CCFFFFFF"/>
<stroke
android:width="1dp"
android:color="#000000"/>
</shape>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
<path
android:fillColor="#808080"
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
</vector>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
<path
android:fillColor="#000000"
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,-1H5v2h14V4z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/aboutview"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/colorPrimary"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/btn_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"/>
<ImageView
android:id="@+id/btn_color_pick"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_color_pick"
android:background="?attr/selectableItemBackgroundBorderless"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/btn_done"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="完成"
android:textColor="@android:color/white"
android:textSize="16sp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
</LinearLayout>
<cc.winboll.studio.gallery.CropCanvasView
android:id="@+id/crop_canvas_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>

View File

@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:clickable="true"
android:focusable="true">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"/>
<LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#CC000000"
android:orientation="horizontal"
android:padding="4dp">
<ImageButton
android:id="@+id/btn_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_back"
android:contentDescription="Back"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageButton
android:id="@+id/btn_share"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_share"
android:contentDescription="Share"/>
<ImageButton
android:id="@+id/btn_info"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_info"
android:contentDescription="Info"/>
<ImageButton
android:id="@+id/btn_bg"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_bg"
android:contentDescription="Background"/>
<ImageButton
android:id="@+id/btn_delete"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_delete"
android:contentDescription="Delete"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"/>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scroll_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_arrow_up"
android:visibility="gone"
app:backgroundTint="@color/colorAccent"
app:tint="@color/white"/>
</FrameLayout>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/folder_path"
android:textSize="16sp"
android:textStyle="bold"/>
<EditText
android:id="@+id/edit_folder_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/enter_folder_path"
android:inputType="text"/>
<TextView
android:id="@+id/text_current_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="#888888"/>
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/save"/>
</LinearLayout>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp">
<ImageView
android:id="@+id/album_cover"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/album_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#80000000"
android:padding="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"/>
<ImageView
android:id="@+id/pin_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="6dp"
android:background="@android:color/transparent"
android:padding="3dp"
android:src="@drawable/ic_pin"
android:visibility="gone"/>
<TextView
android:id="@+id/image_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:background="#80000000"
android:padding="4dp"
android:textColor="@color/white"
android:textSize="10sp"/>
</FrameLayout>

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