Compare commits
71 Commits
appbase-v1
...
gallery-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 7456db1729 | |||
| 226cbf43fe | |||
| 4a267d5606 | |||
| 4d0d8c4d59 | |||
| 9a43267b63 | |||
| c2618672bb | |||
| 6d9bd175f6 | |||
| ffbecaa31d | |||
| e26df437c5 | |||
| 248fd9d8d8 | |||
| 5b631710a9 | |||
| cda85feddd | |||
| ecad4a7913 | |||
| 2f14443624 | |||
| 21f38be35d | |||
| ab9ff33b72 | |||
| 5bc930e96d | |||
| 6c8867e15c | |||
| 610d3811db | |||
| 2d949eb5a3 | |||
| e6940805d9 | |||
| 1641424276 | |||
| 5d1cdff283 | |||
| da66cea1e5 | |||
| 5eb7441dc7 | |||
| 5f3168e17f | |||
| e3c4bab6c9 | |||
| 2af6427ca8 | |||
| b8c70bef98 | |||
| 7713d6c460 | |||
| 73c69bd665 | |||
| a076fe50cd | |||
| 1512b76c36 | |||
| 850b9af6ec | |||
| 31c1592086 | |||
| b3976a8633 | |||
| ea896228d7 | |||
| d49ecb3943 | |||
| ad3aecf867 | |||
| c417d9732a | |||
| 7bd1357c8c | |||
| 16a2c3c0c8 | |||
| b747d83972 | |||
| f2788dda96 | |||
| ea3a66bebe | |||
| a53a0cbcdc | |||
| 94ac2d9f9c | |||
| 11d7846cd2 | |||
| 21e464bf5e | |||
| 47f27b6bcb | |||
| 013ad7ff1b | |||
| ab6fd0c6b5 | |||
| 3c1afc1c0b | |||
| 395920743b | |||
| 25f4c79860 | |||
| 5440c1ad39 | |||
| 47dd4698b8 | |||
| 861ccb5832 | |||
| 8b1f119e44 | |||
| c6495d1859 | |||
| 15197b123c | |||
| df2e91d390 | |||
| 61ca0d2672 | |||
| 8c18710e36 | |||
| 224bd243e2 | |||
| b30bdc6802 | |||
| 8f0973cb6c | |||
| b9fab2d737 | |||
| 156af54eaa | |||
| fb9dd93162 | |||
| 7e757a456a |
10
.gitignore
vendored
@@ -94,12 +94,8 @@ lint-results.html
|
|||||||
## 忽略 AndroidIDE 临时文件夹
|
## 忽略 AndroidIDE 临时文件夹
|
||||||
.androidide
|
.androidide
|
||||||
|
|
||||||
## WinBoLL 基础应用(避免上传敏感配置)
|
## 忽略模块应用编译配置
|
||||||
|
/settings.gradle
|
||||||
|
/gradle.properties
|
||||||
/winboll.properties
|
/winboll.properties
|
||||||
/local.properties
|
/local.properties
|
||||||
|
|
||||||
## WinBoLL 衍生应用,
|
|
||||||
## 外派类型类库应用需要注释掉以下部分,以便部署通用类库编译配置。
|
|
||||||
## APPBase,AES需要上传以下两种配置。
|
|
||||||
#/settings.gradle
|
|
||||||
#/gradle.properties
|
|
||||||
|
|||||||
236
README.md
@@ -1,175 +1,103 @@
|
|||||||
## OriginMaster
|
# WinBoLL 源生态计划项目说明书
|
||||||
【OriginMaster】WinBoLL 源生态计划。正如话,我需要一个 Point, 去撬动成个地球。
|
|
||||||
WinBoLL系列项目各个项目源码合并推送方向:
|
|
||||||
WinBoLL=>APPBase=>OriginMaster
|
|
||||||
WinBoLL=>AES=>OriginMaster
|
|
||||||
WinBoLL=>PowerBell=>OriginMaster
|
|
||||||
WinBoLL=>Positions=>OriginMaster
|
|
||||||
仓库规划概述:
|
|
||||||
☆ WinBoLL,APPBase,AES,PowerBell与Positions都是开发库。
|
|
||||||
☆ OriginMaster 是分支汇总存档库。
|
|
||||||
☆ OriginMaster 不适宜作为开发库克隆使用,不是应用开发基础库,而是汇总资料库。
|
|
||||||
☆ WinBoLL 库可以作为应用开发的基础继承模板。
|
|
||||||
|
|
||||||
########
|
## 一、项目概述
|
||||||
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
|
|
||||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
|
|
||||||
## WinBoLL 提问
|
|
||||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
|
||||||
能否实现手机编译与电脑编译的源码同步。
|
|
||||||
☁因而 WinBoLL 项目组诞生了。
|
|
||||||
|
|
||||||
## WinBoLL 项目组研发计划
|
### 1. 核心定位
|
||||||
致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。
|
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
|
||||||
也在探索 https://gitea.winboll.cc/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP APK 分发。
|
|
||||||
更想进阶 https://github.com/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP Beta APK 分发。
|
|
||||||
|
|
||||||
## WinBoLL-APP 汗下...
|
### 2. 仓库架构
|
||||||
#### ☁应用何置如此呢。且观用户云云。
|
#### **仓库类型:功能说明**
|
||||||
|
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
|
||||||
|
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
|
||||||
|
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
|
||||||
|
|
||||||
#### ☁ 正当下 ☁ ###
|
### 3. 源码合并管理推送路线图
|
||||||
#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景
|
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
|
||||||
### ☁ WinBoLL 设备资源概述
|
|
||||||
#### ☁ 1. Raid Disk.
|
|
||||||
概述:这是一个矩阵存储类设备。
|
|
||||||
优点:该设备具有数据容错存储功能,
|
|
||||||
数据存储具有特长持久性。
|
|
||||||
缺点:设备使用能源消耗比较高,
|
|
||||||
设备存取速度一般。
|
|
||||||
|
|
||||||
#### ☁ 2. Data Disk.
|
|
||||||
概述:这是一个普通硬盘存储设备
|
|
||||||
优点:该设备独立于操作系统,
|
|
||||||
数据持久性一般,
|
|
||||||
存取能源消耗小于 Raid Disk。
|
|
||||||
缺点:数据存储速度一般,存储能源消耗一般。
|
|
||||||
|
|
||||||
#### ☁ 3. SSD Disk.
|
★ WinBoLL → APPBase → OriginMaster
|
||||||
概述:这是一个 SSD 硬盘存储设备。
|
★ WinBoLL → AES → OriginMaster
|
||||||
优点:存取速度快于 Data Disk 与 Raid Disk,
|
★ WinBoLL → PowerBell → OriginMaster
|
||||||
存取能源消耗小于 Data Disk 与 Raid Disk。
|
★ WinBoLL → Positions → OriginMaster
|
||||||
缺点:数据持久性一般,
|
|
||||||
设备位于操作系统内部文件系统。
|
|
||||||
数据持久性与操作系统挂钩。
|
|
||||||
|
|
||||||
#### ☁ 4. WinBoLL 用户资源概述。
|
|
||||||
1> /home/<用户名> 位于 WinBoLL 操作系统目录下。
|
|
||||||
2> /rdisk/<用户名> 挂载用户 Raid Disk.
|
|
||||||
3> /data/<用户名> 挂载用户 Data Disk.
|
|
||||||
4> /sdcard/<用户名> 挂载用户 SSD Disk.
|
|
||||||
|
|
||||||
#### ☁ 5. WinBoLL-APP 用户资源概述。
|
## 二、WinBoLL 项目核心信息
|
||||||
1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0
|
|
||||||
|
|
||||||
### ☁ 稍稍歇 ☁ ###
|
### 1. 项目简介
|
||||||
### ☁ 急急停 ☁ WinBoLL 应用前置条件
|
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
|
||||||
☁ WinBoLL 主机建立 1Panel MySQL 应用。
|
|
||||||
☁ WinBoLL 主机建立 1Panel Gitea 应用。
|
|
||||||
☁ WinBoLL 主机设置 WinBoLL 应用为非登录状态。
|
|
||||||
☁ WinBoLL 主机建立 WinBoLL 账户与 WinBoLL 用户组。
|
|
||||||
☁ WinBoLL 账户 User ID 为: J。
|
|
||||||
☁ WinBoLL 用户组 Group ID 为: Studio。
|
|
||||||
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 建立 WinBoLL 工作组。
|
|
||||||
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 用户项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/<用户名>/APKs/
|
|
||||||
☁ WinBoLL 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/winboll.properties-demo"(WinBoLL 项目已设置)
|
|
||||||
☁ WinBoLL 项目配置文件为 "<WinBoLL 项目根目录>/.winboll/winboll.properties"
|
|
||||||
☁ WinBoLL 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
|
|
||||||
☁ Gradle 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/local.properties-demo"(WinBoLL 项目已设置)
|
|
||||||
☁ Gradle 项目配置文件为 "<WinBoLL 项目根目录>/local.properties"(WinBoLL 项目已设置)
|
|
||||||
☁ Gradle 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
|
|
||||||
|
|
||||||
### ☁ 登高处 ☁ WinBoLL 应用需求规划
|
### 2. 官方资源
|
||||||
☁ WinBoLL 主机建立 WinBoLL 客户端用户数据库为 MySQL winbollclient 数据库。
|
#### ☆ 官方网站**:https://www.winboll.cc/
|
||||||
☁ WinBoLL 主机设置 WinBoLL 客户端用户信息存储在 winbollclient 数据库中。
|
#### ☆ 源码地址:
|
||||||
☁ MySQL winbollclient 数据库中
|
★ Gitea:https://gitea.winboll.cc/Studio/WinBoLL.git
|
||||||
WinBoLL 客户端用户信息设定为:
|
★ GitHub:https://github.com/ZhanGSKen/WinBoLL.git
|
||||||
<用户名, 验证密码, 验证邮箱, 验证手机, 唯一存储令牌Token, 备用验证邮箱>。
|
★ 码云:https://gitee.com/zhangsken/winboll.git
|
||||||
☁ WinBoLL 项目源码仓库托管在 WinBoLL 1Panel Gitea 目录 /opt/1panel/apps/gitea/gitea/data/git/repositories/studio/app.git中。
|
|
||||||
☁ WinBoLL 主机提供 WinBoLL 1Panel Gitea 应用的 WinBoLL 项目源码仓库存取功能。(Gitea 应用已提供)
|
|
||||||
☁ WinBoLL 主机提供 WinBoLL Gitea 项目仓库存档功能。(Gitea 应用已提供)
|
|
||||||
☁ 提供 WinBoLL 客户端用户登录功能。(Gitea 应用已提供)
|
|
||||||
|
|
||||||
### ☁ 看远方 ☁ ###
|
## 三、应用编译环境检查问题
|
||||||
### ☁ 心忧虑 ☁ WinBoLL-APP 应用前置需求
|
### 核心判断条件:
|
||||||
☁ WinBoLL-APP WinBoLL 项目根目录设定为手机的 /sdcard/WinBoLLStudio/Sources 目录。(需要用户手动建立文件夹)
|
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件,因为编译输出的APK文件需要一个可供保存的环境。
|
||||||
☁ WinBoLL-APP 具有手机 /sdcard/WinBoLL 目录的存储权限。(需要手机操作系统授权)
|
|
||||||
☁ WinBoLL-APP WinBoLL 项目仓库源码存储路径为 /sdcard/WinBoLLStudio/Sources/APP.git(需要用户手动建立文件夹)
|
|
||||||
☁ WinBoLL-APP 项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/APKs/
|
|
||||||
☁ WinBoLL-APP 应用签名验证可定制化。(WinBoLL 项目已提供)
|
|
||||||
☁ WinBoLL-APP 与系列衍生 APP 应用共享 cc.winboll.studio 命名空间资源。(WinBoLL 项目已提供)
|
|
||||||
☁ WinBoLL-APP 用户客户端信息存储在命名空间为 WinBoLL APP MySQLLite 应用的 winbollappclient 数据库中。
|
|
||||||
☁ WinBoLL-APP MySQLLite 应用的 winbollappclient 数据库中,
|
|
||||||
WinBoLL 用户客户端信息设定为:
|
|
||||||
<用户名, 唯一存储令牌Token>。
|
|
||||||
|
|
||||||
### ☁ 云游四方 ☁ ###
|
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
|
||||||
### ☁ 呔! ☁ WinBoLL-APP 应用需求规划
|
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
|
||||||
☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。
|
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
|
||||||
☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。
|
|
||||||
WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。
|
|
||||||
☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。
|
|
||||||
WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。
|
|
||||||
|
|
||||||
|
## 四、前置条件
|
||||||
|
|
||||||
### ☁ 吁! ☁ WinBoLL-APP 共享计划前景
|
### 1. WinBoLL APP 开发环境配置介绍
|
||||||
☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。
|
#### WinBoLL APK 编译输出内容包括:
|
||||||
☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。
|
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
|
||||||
|
☆ "/sdcard/AppProjects/app.apk"文件。
|
||||||
|
#### WinBoLL APK 源码命名空间规范
|
||||||
|
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
|
||||||
|
|
||||||
## ☁ WinBoLL ☁ WinBoLL 主机忧虑
|
## 五、核心需求规划
|
||||||
☁ WinBoLL 将会提供 gitea.winboll.cc 域名用户注册登录功能。
|
|
||||||
☁ WinBoLL 将会提供 WinBoLL-APP 及其衍生应用的 Gitea 仓库管理服务。
|
|
||||||
☁ WinBoLL 将会提供 winboll.cc 域名 WinBoLL 项目组注册登录功能。
|
|
||||||
|
|
||||||
# 本项目要实际运用需要注意以下几个步骤:
|
### 1. WinBoLL 应用安全验证需求
|
||||||
# 在项目根目录下:
|
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
|
||||||
## ★. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。
|
|
||||||
## ★. 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties。
|
|
||||||
## ★. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
|
|
||||||
## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
|
|
||||||
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
|
|
||||||
## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息, 和 APK 文件额外输出路径。
|
|
||||||
|
|
||||||
|
### 2. 手机端源码开发管理需求
|
||||||
|
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
|
||||||
|
|
||||||
# ☆类库型项目编译方法
|
## 六、编译与使用指南
|
||||||
## 先编译类库对应的模块测试项目
|
|
||||||
### 修改模块测试项目的 build.properties 文件
|
|
||||||
设置属性 libraryProject=<类库项目模块文件夹名称>
|
|
||||||
### 再编译测试项目
|
|
||||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
|
||||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
|
||||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
|
||||||
### 最后编译类库项目
|
|
||||||
$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块文件夹名称>
|
|
||||||
#### 类库模块编译命令执行后,编译器会发布到 WinBoLL Nexus Maven 库:Maven 库地址可以参阅根项目目录配置 build.gradle 文件。
|
|
||||||
|
|
||||||
# ☆应用型项目编译方法
|
|
||||||
## 直接调用以下命令编译应用型项目
|
|
||||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
|
||||||
#### 应用模块编译命令执行后,编译器会复制一份 APK 到
|
|
||||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
|
||||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
|
||||||
|
|
||||||
## ☆应用调试编译方法
|
### 1. 项目初始化(必须)
|
||||||
使用以下命令编译调试:
|
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
|
||||||
|
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
|
||||||
|
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
|
||||||
|
#### 4. **签名设置**:
|
||||||
|
☆ **调试编译秘钥制作**:使用 Termux 应用终端,cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
|
||||||
|
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
|
||||||
|
|
||||||
### Beta调试使用:
|
## 七、应用编译命令介绍
|
||||||
$ bash gradlew assembleBetaDebug
|
|
||||||
|
|
||||||
### Stage调试使用:
|
### (1)类库型模块配置要点
|
||||||
$ bash gradlew assembleStageDebug
|
#### 1. **优先修改配置文件**:优先修改应用测试项目(目录为 `"<WinBoLl根目录>/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。
|
||||||
|
#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。
|
||||||
|
#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。
|
||||||
|
|
||||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
### (2)单一应用型模块与类库测试型模块配置要点
|
||||||
|
#### ☆ APK 编译方法:
|
||||||
|
使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>`。
|
||||||
|
#### ☆ 运行后的 APK 输出路径:
|
||||||
|
★ 默认路径 (`$ bash gradlew assembleBetaDebug` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/` 目录。
|
||||||
|
★ 默认路径 (`$ bash assembleStageRelease` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/` 目录。
|
||||||
|
★ 额外输出路径:(假设 `winboll.properties` 文件已配置 `ExtraAPKOutputPath` 属性) 输出至 `ExtraAPKOutputPath` 属性配置的目录下。
|
||||||
|
|
||||||
# 应用版本号命名方式
|
### (3)手机端应用调试命令介绍
|
||||||
## statge 渠道
|
#### ☆ Beta 渠道调试命令
|
||||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
$bash gradlew assembleBetaDebug
|
||||||
如:APPBase_15.7.0
|
|
||||||
## beta 渠道
|
#### ☆ Stage 渠道调试命令
|
||||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
$bash gradlew assembleStageDebug
|
||||||
如:APPBase_15.9.6-beta8_5413
|
|
||||||
|
### (4)服务器端开发命令介绍
|
||||||
|
##### ☆ Stage 渠道应用发布命令为:
|
||||||
|
("<WinBoLl根目录>/settings.gradle"文件需要配置编译模块开启参数,拷贝 settings.gradle-demo 为 settings.gradle 文件取消对应的分支配置部分即可。)
|
||||||
|
$bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
||||||
|
或者是
|
||||||
|
$bash gradlew assembleStageRelease
|
||||||
|
|
||||||
|
|
||||||
|
## 八、WinBoLL 应用 APK 版本号命名规则
|
||||||
|
### ☆ Stage 渠道:
|
||||||
|
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号> (示例: APPBase_15.7.0 )
|
||||||
|
### ☆ Beta 渠道:
|
||||||
|
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> (示例: APPBase_15.9.6-beta8_5413 )
|
||||||
|
|||||||
1
appbase/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,50 +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.appbase"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 30
|
|
||||||
versionCode 1
|
|
||||||
// versionName 更新后需要手动设置
|
|
||||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
|
||||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
|
||||||
versionName "15.15"
|
|
||||||
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'])
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
|
||||||
#Sat Jan 24 11:26:28 HKT 2026
|
|
||||||
stageCount=9
|
|
||||||
libraryProject=libappbase
|
|
||||||
baseVersion=15.15
|
|
||||||
publishVersion=15.15.8
|
|
||||||
buildCount=0
|
|
||||||
baseBetaVersion=15.15.9
|
|
||||||
126
appbase/proguard-rules.pro
vendored
@@ -1,126 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# ============================== 基础通用规则 ==============================
|
|
||||||
# 保留系统组件
|
|
||||||
-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
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_name">AppBase+</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?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: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=".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>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package cc.winboll.studio.appbase;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
import cc.winboll.studio.appbase.R;
|
|
||||||
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/11 12:55
|
|
||||||
* @Describe AboutActivity
|
|
||||||
*/
|
|
||||||
public class AboutActivity extends Activity {
|
|
||||||
|
|
||||||
public static final String TAG = "AboutActivity";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_about);
|
|
||||||
|
|
||||||
// 设置工具栏
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setActionBar(toolbar);
|
|
||||||
getActionBar().setSubtitle(TAG);
|
|
||||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
finish(); // 点击导航栏返回按钮,触发 finish()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
AboutView aboutView = findViewById(R.id.aboutview);
|
|
||||||
aboutView.setAPPInfo(genDefaultAppInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
private APPInfo genDefaultAppInfo() {
|
|
||||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
|
||||||
String branchName = "appbase";
|
|
||||||
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("APPBase");
|
|
||||||
appInfo.setAppGitOwner("Studio");
|
|
||||||
appInfo.setAppGitAPPBranch(branchName);
|
|
||||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
|
||||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=APPBase");
|
|
||||||
appInfo.setAppAPKName("APPBase");
|
|
||||||
appInfo.setAppAPKFolderName("APPBase");
|
|
||||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
|
||||||
return appInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
|
|
||||||
//setIsDebugging(false);
|
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
|
||||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
|
||||||
ToastUtils.init(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用终止时回调(资源释放入口)
|
|
||||||
* 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行
|
|
||||||
* 用于释放全局资源,避免内存泄漏
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放)
|
|
||||||
// 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏)
|
|
||||||
ToastUtils.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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.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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建菜单时回调(加载工具栏菜单)
|
|
||||||
* 初始化 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 导致崩溃
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志测试按钮点击事件(打开日志查看界面)
|
|
||||||
* 启动 LogActivity,用于查看应用运行日志
|
|
||||||
* @param view 触发事件的 View(对应布局中的日志测试按钮)
|
|
||||||
*/
|
|
||||||
public void onLogTest(View view) {
|
|
||||||
// 启动日志查看 Activity(通过静态方法传入上下文,简化跳转逻辑)
|
|
||||||
LogActivity.startLogActivity(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, "startAboutActivity() 调用");
|
|
||||||
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
|
||||||
startActivity(aboutIntent);
|
|
||||||
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<?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="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="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="onAboutActivity"
|
|
||||||
android:layout_margin="10dp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?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>
|
|
||||||
44
build.gradle
@@ -1,6 +1,15 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||||
|
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||||
|
maven { url "https://clojars.org/repo/" }
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
|
||||||
mavenLocal {
|
mavenLocal {
|
||||||
// 设置本地Maven仓库路径
|
// 设置本地Maven仓库路径
|
||||||
url 'file:///sdcard/.m2/repository/'
|
url 'file:///sdcard/.m2/repository/'
|
||||||
@@ -11,19 +20,6 @@ buildscript {
|
|||||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||||
// "WinBoLL Snapshot"
|
// "WinBoLL Snapshot"
|
||||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||||
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
|
||||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
|
||||||
maven { url "https://clojars.org/repo/" }
|
|
||||||
maven { url "https://jitpack.io" }
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//println mavenLocal().url
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//mavenLocal()
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
// 适配MIUI12
|
// 适配MIUI12
|
||||||
@@ -35,6 +31,15 @@ buildscript {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||||
|
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||||
|
maven { url "https://clojars.org/repo/" }
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
|
||||||
mavenLocal {
|
mavenLocal {
|
||||||
// 设置本地Maven仓库路径
|
// 设置本地Maven仓库路径
|
||||||
url 'file:///sdcard/.m2/repository/'
|
url 'file:///sdcard/.m2/repository/'
|
||||||
@@ -45,19 +50,6 @@ allprojects {
|
|||||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||||
// "WinBoLL Snapshot"
|
// "WinBoLL Snapshot"
|
||||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||||
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
|
||||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
|
||||||
maven { url "https://clojars.org/repo/" }
|
|
||||||
maven { url "https://jitpack.io" }
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//println mavenLocal().url
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//mavenLocal()
|
|
||||||
}
|
}
|
||||||
ext {
|
ext {
|
||||||
// 定义全局变量,常用于版本管理
|
// 定义全局变量,常用于版本管理
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# APPBase
|
# Gallery
|
||||||
[](https://jitpack.io/#ZhanGSKen/APPBase)
|
|
||||||
|
|
||||||
#### 介绍
|
#### 介绍
|
||||||
WinBoLL 安卓手机端安卓应用开发基础类库。
|
云宝相册应用
|
||||||
|
|
||||||
#### 软件架构
|
#### 软件架构
|
||||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
@@ -11,8 +10,7 @@ WinBoLL 安卓手机端安卓应用开发基础类库。
|
|||||||
|
|
||||||
#### Gradle 编译说明
|
#### Gradle 编译说明
|
||||||
调试版编译命令 :gradle assembleBetaDebug
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh appbase
|
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||||
阶段版类库发布命令 :git pull &&bash .winboll/bashPublishLIBAddTag.sh libappbase
|
|
||||||
|
|
||||||
#### 使用说明
|
#### 使用说明
|
||||||
|
|
||||||
119
gallery/build.gradle
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: '../.winboll/winboll_app_build.gradle'
|
||||||
|
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||||
|
|
||||||
|
def genVersionName(def versionName){
|
||||||
|
// 检查编译标志位配置
|
||||||
|
assert (winbollBuildProps['stageCount'] != null)
|
||||||
|
assert (winbollBuildProps['baseVersion'] != null)
|
||||||
|
// 保存基础版本号
|
||||||
|
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||||
|
//保存编译标志配置
|
||||||
|
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||||
|
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||||
|
fos.close();
|
||||||
|
|
||||||
|
// 返回编译版本号
|
||||||
|
return "${versionName}." + winbollBuildProps['stageCount']
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
// 适配MIUI12
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "cc.winboll.studio.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'])
|
||||||
|
}
|
||||||
8
gallery/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Sun Apr 26 10:16:07 HKT 2026
|
||||||
|
stageCount=5
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.0
|
||||||
|
publishVersion=15.0.4
|
||||||
|
buildCount=0
|
||||||
|
baseBetaVersion=15.0.5
|
||||||
137
gallery/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# ============================== 基础通用规则 ==============================
|
||||||
|
# 保留系统组件
|
||||||
|
-keep public class * extends android.app.Activity
|
||||||
|
-keep public class * extends android.app.Service
|
||||||
|
-keep public class * extends android.content.BroadcastReceiver
|
||||||
|
-keep public class * extends android.content.ContentProvider
|
||||||
|
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||||
|
-keep public class * extends android.preference.Preference
|
||||||
|
|
||||||
|
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||||
|
-keep class cc.winboll.studio.** { *; }
|
||||||
|
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||||
|
|
||||||
|
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||||
|
-keepclassmembers class * {
|
||||||
|
public static final java.lang.String TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||||
|
-keep class * implements android.os.Parcelable {
|
||||||
|
public static final android.os.Parcelable$Creator *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
|
static final long serialVersionUID;
|
||||||
|
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||||
|
private void writeObject(java.io.ObjectOutputStream);
|
||||||
|
private void readObject(java.io.ObjectInputStream);
|
||||||
|
java.lang.Object writeReplace();
|
||||||
|
java.lang.Object readResolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 R 文件(避免资源ID混淆)
|
||||||
|
-keepclassmembers class **.R$* {
|
||||||
|
public static <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 native 方法(避免JNI调用失败)
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留注解和泛型(避免反射/序列化异常)
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||||
|
-dontwarn java.lang.invoke.*
|
||||||
|
-dontwarn android.support.v8.renderscript.*
|
||||||
|
-dontwarn java.util.function.**
|
||||||
|
|
||||||
|
# ============================== 第三方框架专项规则 ==============================
|
||||||
|
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep interface okhttp3.** { *; }
|
||||||
|
-keep class okhttp3.internal.** { *; }
|
||||||
|
-keep class okio.** { *; }
|
||||||
|
-dontwarn okhttp3.internal.platform.**
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
-dontwarn com.bumptech.glide.**
|
||||||
|
|
||||||
|
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||||
|
-keep class com.google.gson.** { *; }
|
||||||
|
-keep interface com.google.gson.** { *; }
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||||
|
-keep class com.miui.zeus.** { *; }
|
||||||
|
-keep interface com.miui.zeus.** { *; }
|
||||||
|
# 保留米盟日志字段(便于广告加载失败排查)
|
||||||
|
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||||
|
public static final java.lang.String TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||||
|
-keep class androidx.recyclerview.** { *; }
|
||||||
|
-keep interface androidx.recyclerview.** { *; }
|
||||||
|
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||||
|
# XXPermissions 18.63
|
||||||
|
-keep class com.hjq.permissions.** { *; }
|
||||||
|
-keep interface com.hjq.permissions.** { *; }
|
||||||
|
|
||||||
|
# ZXing 二维码(核心解析组件)
|
||||||
|
-keep class com.google.zxing.** { *; }
|
||||||
|
-keep class com.journeyapps.zxing.** { *; }
|
||||||
|
|
||||||
|
# Jsoup HTML解析
|
||||||
|
-keep class org.jsoup.** { *; }
|
||||||
|
|
||||||
|
# Pinyin4j 拼音搜索
|
||||||
|
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||||
|
|
||||||
|
# JSch SSH组件
|
||||||
|
-keep class com.jcraft.jsch.** { *; }
|
||||||
|
|
||||||
|
# AndroidX 基础组件
|
||||||
|
-keep class androidx.appcompat.** { *; }
|
||||||
|
-keep interface androidx.appcompat.** { *; }
|
||||||
|
|
||||||
|
# ============================== 优化与调试配置 ==============================
|
||||||
|
# 优化级别(平衡混淆效果与性能)
|
||||||
|
-optimizationpasses 5
|
||||||
|
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||||
|
|
||||||
|
# 调试辅助(保留行号便于崩溃定位)
|
||||||
|
-verbose
|
||||||
|
-dontpreverify
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
6
gallery/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Put flavor specific strings here -->
|
||||||
|
<string name="app_name">Gallery+</string>
|
||||||
|
</resources>
|
||||||
58
gallery/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
25
gallery/src/main/java/cc/winboll/studio/gallery/Album.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
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 RecyclerView.OnScrollListener scrollListener;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||||
|
adapter = new ImageAdapter();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollListener = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
|
|
||||||
|
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 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 + "%"};
|
||||||
|
String sortOrder = android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||||
|
|
||||||
|
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
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadImages();
|
||||||
|
}
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.refreshBg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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 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 void setOnAlbumClickListener(OnAlbumClickListener listener) {
|
||||||
|
this.listener = 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 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.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);
|
||||||
|
|
||||||
|
// For content:// URIs, try to get the actual file path
|
||||||
|
if ("content".equals(coverUri.getScheme())) {
|
||||||
|
try {
|
||||||
|
android.database.Cursor cursor = holder.coverImage.getContext().getContentResolver()
|
||||||
|
.query(coverUri, new String[]{MediaStore.Images.Media.DATA}, null, null, null);
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||||
|
String filePath = cursor.getString(dataColumn);
|
||||||
|
cursor.close();
|
||||||
|
if (filePath != null) {
|
||||||
|
File actualFile = new File(filePath);
|
||||||
|
LogUtils.d(TAG, "actual file: " + actualFile.getAbsolutePath() + ", exists=" + actualFile.exists());
|
||||||
|
// Use file path instead of content URI for better compatibility
|
||||||
|
Glide.with(holder.coverImage.getContext())
|
||||||
|
.load(actualFile)
|
||||||
|
.centerCrop()
|
||||||
|
.into(holder.coverImage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "query error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to content URI
|
||||||
|
Glide.with(holder.coverImage.getContext())
|
||||||
|
.load(coverUri)
|
||||||
|
.centerCrop()
|
||||||
|
.into(holder.coverImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return albums.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView coverImage;
|
||||||
|
ImageView pinIcon;
|
||||||
|
TextView albumName;
|
||||||
|
TextView imageCount;
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
coverImage = itemView.findViewById(R.id.album_cover);
|
||||||
|
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||||
|
albumName = itemView.findViewById(R.id.album_name);
|
||||||
|
imageCount = itemView.findViewById(R.id.image_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
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 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<String> pinnedPaths = new ArrayList<>();
|
||||||
|
ArrayList<String> unpinnedPaths = new ArrayList<>();
|
||||||
|
for (String path : imagePaths) {
|
||||||
|
if (pinnedDbHelper.isPinned(path)) {
|
||||||
|
pinnedPaths.add(path);
|
||||||
|
} else {
|
||||||
|
unpinnedPaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Comparator<String> pathComparator = new Comparator<String>() {
|
||||||
|
@Override
|
||||||
|
public int compare(String p1, String p2) {
|
||||||
|
return p1.compareToIgnoreCase(p2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Collections.sort(pinnedPaths, pathComparator);
|
||||||
|
Collections.sort(unpinnedPaths, pathComparator);
|
||||||
|
|
||||||
|
ArrayList<Uri> newUrls = new ArrayList<>();
|
||||||
|
ArrayList<String> newPaths = new ArrayList<>();
|
||||||
|
for (String path : pinnedPaths) {
|
||||||
|
int index = imagePaths.indexOf(path);
|
||||||
|
if (index >= 0) {
|
||||||
|
newUrls.add(imageUrls.get(index));
|
||||||
|
newPaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String path : unpinnedPaths) {
|
||||||
|
int index = imagePaths.indexOf(path);
|
||||||
|
if (index >= 0) {
|
||||||
|
newUrls.add(imageUrls.get(index));
|
||||||
|
newPaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageUrls.clear();
|
||||||
|
imagePaths.clear();
|
||||||
|
imageUrls.addAll(newUrls);
|
||||||
|
imagePaths.addAll(newPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(android.content.Context context) {
|
||||||
|
prefs = new Preferences(context);
|
||||||
|
bgType = prefs.getBgType();
|
||||||
|
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||||
|
builder.setTitle("Image");
|
||||||
|
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
|
||||||
|
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 && which == 0) {
|
||||||
|
if (isPinned) {
|
||||||
|
pinnedDbHelper.unpinImage(imagePath);
|
||||||
|
} else {
|
||||||
|
pinnedDbHelper.pinImage(imagePath);
|
||||||
|
}
|
||||||
|
refreshPinned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
imageView = itemView.findViewById(R.id.image);
|
||||||
|
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
ArrayList<Uri> images = getImagesInFolder(subfolder.getAbsolutePath());
|
||||||
|
if (!images.isEmpty()) {
|
||||||
|
Uri latestImage = images.get(0);
|
||||||
|
albums.add(new Album(subfolder.getName(), subfolder.getAbsolutePath(), latestImage, images.size()));
|
||||||
|
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + images.size() + " 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 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_trash) {
|
||||||
|
startActivity(new Intent(this, TrashActivity.class));
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_refresh) {
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_debug) {
|
||||||
|
LogActivity.startLogActivity(this);
|
||||||
|
// Log.d("Gallery", "Debug log message");
|
||||||
|
// Toast.makeText(this, R.string.debug_message, Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (checkPermission()) {
|
||||||
|
scanMediaStore();
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.refreshBg();
|
||||||
|
adapter.refreshPinned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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 int DEFAULT_BG_TYPE = 0;
|
||||||
|
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
34
gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<?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="#81C7F5"/> <!-- 浅蓝色填充 -->
|
<solid android:color="#000000"/>
|
||||||
<corners android:radius="8dp"/> <!-- 8dp 圆角 -->
|
</shape>
|
||||||
</shape>
|
|
||||||
|
|
||||||
30
gallery/src/main/res/drawable/bg_checkerboard.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<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>
|
||||||
8
gallery/src/main/res/drawable/bg_circle_white.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>
|
||||||
5
gallery/src/main/res/drawable/bg_white.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF"/>
|
||||||
|
</shape>
|
||||||
10
gallery/src/main/res/drawable/ic_arrow_up.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?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>
|
||||||
9
gallery/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
12
gallery/src/main/res/drawable/ic_bg.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
||||||
9
gallery/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
gallery/src/main/res/drawable/ic_info.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
@@ -2,169 +2,169 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportHeight="108"
|
||||||
android:viewportHeight="108">
|
android:viewportWidth="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/colorPrimary"
|
android:fillColor="#26A69A"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M9,0L9,108"
|
android:pathData="M9,0L9,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,0L19,108"
|
android:pathData="M19,0L19,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M29,0L29,108"
|
android:pathData="M29,0L29,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M39,0L39,108"
|
android:pathData="M39,0L39,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M49,0L49,108"
|
android:pathData="M49,0L49,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M59,0L59,108"
|
android:pathData="M59,0L59,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M69,0L69,108"
|
android:pathData="M69,0L69,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M79,0L79,108"
|
android:pathData="M79,0L79,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M89,0L89,108"
|
android:pathData="M89,0L89,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M99,0L99,108"
|
android:pathData="M99,0L99,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,9L108,9"
|
android:pathData="M0,9L108,9"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,19L108,19"
|
android:pathData="M0,19L108,19"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,29L108,29"
|
android:pathData="M0,29L108,29"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,39L108,39"
|
android:pathData="M0,39L108,39"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,49L108,49"
|
android:pathData="M0,49L108,49"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,59L108,59"
|
android:pathData="M0,59L108,59"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,69L108,69"
|
android:pathData="M0,69L108,69"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,79L108,79"
|
android:pathData="M0,79L108,79"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,89L108,89"
|
android:pathData="M0,89L108,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M0,99L108,99"
|
android:pathData="M0,99L108,99"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,29L89,29"
|
android:pathData="M19,29L89,29"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,39L89,39"
|
android:pathData="M19,39L89,39"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,49L89,49"
|
android:pathData="M19,49L89,49"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,59L89,59"
|
android:pathData="M19,59L89,59"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,69L89,69"
|
android:pathData="M19,69L89,69"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M19,79L89,79"
|
android:pathData="M19,79L89,79"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M29,19L29,89"
|
android:pathData="M29,19L29,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M39,19L39,89"
|
android:pathData="M39,19L39,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M49,19L49,89"
|
android:pathData="M49,19L49,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M59,19L59,89"
|
android:pathData="M59,19L59,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M69,19L69,89"
|
android:pathData="M69,19L69,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:pathData="M79,19L79,89"
|
android:pathData="M79,19L79,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeWidth="0.8" />
|
||||||
</vector>
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_pin.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
gallery/src/main/res/drawable/ic_restore.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
gallery/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
73
gallery/src/main/res/layout/activity_image_viewer.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?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>
|
||||||
45
gallery/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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>
|
||||||
39
gallery/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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>
|
||||||
48
gallery/src/main/res/layout/item_album.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?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>
|
||||||
28
gallery/src/main/res/layout/item_gallery.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?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/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@color/black"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pin_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="@drawable/bg_circle_white"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:src="@drawable/ic_pin"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:tint="@color/black"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
13
gallery/src/main/res/layout/item_image_pager.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="fitCenter"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
42
gallery/src/main/res/layout/item_trash.xml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?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/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@color/black"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_restore"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_restore"
|
||||||
|
android:contentDescription="Restore"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_delete"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
android:contentDescription="Delete"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
25
gallery/src/main/res/menu/menu_main.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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/action_trash"
|
||||||
|
android:title="@string/trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_refresh"
|
||||||
|
android:title="@string/refresh"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_debug"
|
||||||
|
android:title="@string/debug_log"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
15
gallery/src/main/res/menu/menu_trash.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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/action_trash"
|
||||||
|
android:title="@string/trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_clear_trash"
|
||||||
|
android:title="@string/clear_trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
5
gallery/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
gallery/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gallery/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
gallery/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
gallery/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gallery/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
gallery/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
gallery/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
gallery/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
gallery/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
gallery/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
8
gallery/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#009688</color>
|
||||||
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
|
<color name="colorAccent">#FF9800</color>
|
||||||
|
<color name="black">#000000</color>
|
||||||
|
<color name="white">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
19
gallery/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Gallery</string>
|
||||||
|
<string name="refresh">Refresh</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="settings_title">Settings</string>
|
||||||
|
<string name="folder_path">Folder Path</string>
|
||||||
|
<string name="enter_folder_path">Enter folder path</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="no_images_found">No images found</string>
|
||||||
|
<string name="trash">Trash</string>
|
||||||
|
<string name="clear_trash">Clear Trash</string>
|
||||||
|
<string name="delete_confirm">Delete to trash?</string>
|
||||||
|
<string name="restore_confirm">Restore to original folder?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="debug_log">Debug Log</string>
|
||||||
|
<string name="debug_message">Debug log message</string>
|
||||||
|
</resources>
|
||||||
11
gallery/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -2,9 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools" >
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
<application
|
<application>
|
||||||
tools:replace="android:icon"
|
|
||||||
android:icon="@drawable/ic_winboll_beta">
|
|
||||||
|
|
||||||
<!-- Put flavor specific code here -->
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Project-wide Gradle settings.
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app"s APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
|
||||||
android.enableJetifier=true
|
|
||||||
# 保持与旧版Gradle插件的兼容
|
|
||||||
android.disableAutomaticComponentCreation=true
|
|
||||||
1
libappbase/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apply plugin: 'com.android.library'
|
|
||||||
apply plugin: 'maven-publish'
|
|
||||||
apply from: '../.winboll/winboll_lib_build.gradle'
|
|
||||||
apply from: '../.winboll/winboll_lint_build.gradle'
|
|
||||||
|
|
||||||
android {
|
|
||||||
// 适配MIUI12
|
|
||||||
compileSdkVersion 30
|
|
||||||
buildToolsVersion "30.0.3"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 30
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// 网络连接类库
|
|
||||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
|
||||||
// Gson
|
|
||||||
api 'com.google.code.gson:gson:2.8.9'
|
|
||||||
// Html 解析
|
|
||||||
api 'org.jsoup:jsoup:1.13.1'
|
|
||||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
|
||||||
#Sat Jan 24 11:26:28 HKT 2026
|
|
||||||
stageCount=9
|
|
||||||
libraryProject=libappbase
|
|
||||||
baseVersion=15.15
|
|
||||||
publishVersion=15.15.8
|
|
||||||
buildCount=0
|
|
||||||
baseBetaVersion=15.15.9
|
|
||||||
17
libappbase/proguard-rules.pro
vendored
@@ -1,17 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<manifest
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="cc.winboll.studio.libappbase">
|
|
||||||
|
|
||||||
<!-- 拥有完全的网络访问权限 -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
|
|
||||||
<!-- 读取您共享存储空间中的内容 -->
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
||||||
|
|
||||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
|
|
||||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".CrashHandler$CrashActivity"
|
|
||||||
android:label="CrashActivity"
|
|
||||||
android:launchMode="singleInstance"
|
|
||||||
android:process=":CrashActivity"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".GlobalCrashActivity"
|
|
||||||
android:label="GlobalCrashActivity"
|
|
||||||
android:launchMode="singleInstance"
|
|
||||||
android:process=":GlobalCrashActivity"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".LogActivity"
|
|
||||||
android:label="LogActivity"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleInstance"
|
|
||||||
android:process=":LogActivity">
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name="cc.winboll.studio.libappbase.activities.NfcRsaLoginActivity"/>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.util.JsonReader;
|
|
||||||
import android.util.JsonWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 20:01
|
|
||||||
* @Describe WinBoLL 应用全局数据模型类
|
|
||||||
* 继承自 BaseBean,用于存储和管理应用的核心配置信息(如调试状态),
|
|
||||||
* 支持 JSON 序列化/反序列化,便于数据持久化或跨组件传递
|
|
||||||
*/
|
|
||||||
public class APPModel extends BaseBean {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志打印标签,用于区分当前类的日志输出
|
|
||||||
*/
|
|
||||||
public static final String TAG = "APPModel";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用调试状态标识
|
|
||||||
* true:应用处于调试模式(可输出详细日志、启用调试功能等)
|
|
||||||
* false:应用处于正式模式(关闭调试相关功能,优化性能)
|
|
||||||
*/
|
|
||||||
private boolean isDebugging = false; // 修正拼写:原 isDebuging -> isDebugging(符合命名规范)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 无参构造方法
|
|
||||||
* 初始化调试状态为默认值:false(正式模式)
|
|
||||||
*/
|
|
||||||
public APPModel() {
|
|
||||||
this.isDebugging = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 带参构造方法
|
|
||||||
* 可通过参数指定应用的初始调试状态
|
|
||||||
* @param isDebugging 初始调试状态(true:调试模式;false:正式模式)
|
|
||||||
*/
|
|
||||||
public APPModel(boolean isDebugging) {
|
|
||||||
this.isDebugging = isDebugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置应用调试状态
|
|
||||||
* @param isDebugging 目标调试状态(true:开启调试;false:关闭调试)
|
|
||||||
*/
|
|
||||||
public void setIsDebugging(boolean isDebugging) {
|
|
||||||
this.isDebugging = isDebugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前应用调试状态
|
|
||||||
* @return 调试状态(true:调试中;false:非调试)
|
|
||||||
*/
|
|
||||||
public boolean isDebugging() {
|
|
||||||
return isDebugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法,返回当前类的全限定名
|
|
||||||
* 用于标识数据模型的类类型(可用于反射、序列化校验等场景)
|
|
||||||
* @return 类的全限定名(如:cc.winboll.studio.libappbase.APPModel)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return APPModel.class.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法,将当前模型的字段序列化到 JSON 中
|
|
||||||
* 用于将调试状态等核心数据转换为 JSON 格式(如持久化到文件、网络传输)
|
|
||||||
* @param jsonWriter JSON 写入器对象,用于输出 JSON 数据
|
|
||||||
* @throws IOException 当 JSON 写入失败时抛出(如流关闭、格式错误)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
// 先调用父类方法,序列化父类中的字段(若 BaseBean 有可序列化字段)
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 序列化当前类的调试状态字段:key 为 "isDebuging"(保持与原代码一致,避免兼容性问题),value 为当前状态
|
|
||||||
jsonWriter.name("isDebuging").value(isDebugging());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法,从 JSON 中解析字段并初始化当前对象
|
|
||||||
* 用于将 JSON 格式的配置数据解析为 APPModel 实例(如从文件读取、网络接收后解析)
|
|
||||||
* @param jsonReader JSON 读取器对象,用于读取 JSON 数据
|
|
||||||
* @param name 当前解析的 JSON 字段名
|
|
||||||
* @return true:字段解析成功;false:字段不属于当前类,需由调用者处理
|
|
||||||
* @throws IOException 当 JSON 读取失败时抛出(如流关闭、数据格式错误)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 先调用父类方法,解析父类中的字段(若 BaseBean 有可解析字段)
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
return true; // 父类已处理该字段,直接返回成功
|
|
||||||
} else {
|
|
||||||
// 解析当前类的字段
|
|
||||||
if (name.equals("isDebuging")) {
|
|
||||||
// 读取 JSON 中 "isDebuging" 字段的值,设置为当前对象的调试状态
|
|
||||||
setIsDebugging(jsonReader.nextBoolean());
|
|
||||||
} else {
|
|
||||||
// 字段不属于当前类,返回 false 提示调用者跳过该字段
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 字段解析成功,返回 true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法,从 JSON 读取器中完整解析一个 APPModel 实例
|
|
||||||
* 负责处理 JSON 对象的开始/结束标记,循环解析所有字段
|
|
||||||
* @param jsonReader JSON 读取器对象,用于读取 JSON 数据
|
|
||||||
* @return 解析完成的当前 APPModel 实例(支持链式调用)
|
|
||||||
* @throws IOException 当 JSON 读取失败时抛出(如流关闭、数据格式错误)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
// 开始解析 JSON 对象(对应 JSON 中的 '{')
|
|
||||||
jsonReader.beginObject();
|
|
||||||
// 循环读取 JSON 中的所有字段(直到对象结束)
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
// 获取当前字段名
|
|
||||||
String name = jsonReader.nextName();
|
|
||||||
// 解析字段:若当前类无法处理该字段,则跳过(避免解析异常)
|
|
||||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 结束解析 JSON 对象(对应 JSON 中的 '}')
|
|
||||||
jsonReader.endObject();
|
|
||||||
// 返回解析完成的实例(当前对象)
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.JsonReader;
|
|
||||||
import android.util.JsonWriter;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 20:03
|
|
||||||
* @Describe WinBoLL JSON 数据模型基类(抽象类)
|
|
||||||
* 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑,
|
|
||||||
* 子类(如 APPModel)需实现抽象方法,实现自身字段的 JSON 读写
|
|
||||||
* @param <T> 泛型约束,限定子类必须继承自 BaseBean
|
|
||||||
*/
|
|
||||||
public abstract class BaseBean<T extends BaseBean> {
|
|
||||||
|
|
||||||
/** 日志标签,用于当前基类的日志输出标识 */
|
|
||||||
public static final String TAG = "BaseBean";
|
|
||||||
/** JSON 中存储 Bean 类名的字段键(用于校验 Bean 类型一致性) */
|
|
||||||
static final String BEAN_NAME = "BeanName";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 无参构造方法(子类需默认实现,支持反射实例化)
|
|
||||||
*/
|
|
||||||
public BaseBean() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 抽象方法:获取当前 Bean 的全限定类名
|
|
||||||
* 子类需实现,用于标识 Bean 类型(序列化/校验时使用)
|
|
||||||
* @return 类的全限定名(如:cc.winboll.studio.libappbase.APPModel)
|
|
||||||
*/
|
|
||||||
public abstract String getName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个 Bean 的 JSON 持久化文件路径
|
|
||||||
* 路径:外部存储/应用私有目录/BaseBean/[类名].json
|
|
||||||
* @param context 上下文(用于获取应用存储目录)
|
|
||||||
* @return 单个 Bean 的文件绝对路径
|
|
||||||
*/
|
|
||||||
public String getBeanJsonFilePath(Context context) {
|
|
||||||
return context.getExternalFilesDir(TAG) + "/" + getName() + ".json";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Bean 列表的 JSON 持久化文件路径
|
|
||||||
* 路径:外部存储/应用私有目录/BaseBean/[类名]_List.json
|
|
||||||
* @param context 上下文(用于获取应用存储目录)
|
|
||||||
* @return Bean 列表的文件绝对路径
|
|
||||||
*/
|
|
||||||
public String getBeanListJsonFilePath(Context context) {
|
|
||||||
return context.getExternalFilesDir(TAG) + "/" + getName() + "_List.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 类名写入 JSON(序列化基础字段)
|
|
||||||
* 子类可重写扩展,添加自身字段的 JSON 写入逻辑
|
|
||||||
* @param jsonWriter JSON 写入器(用于输出 JSON 数据)
|
|
||||||
* @throws IOException JSON 写入失败时抛出(如流异常)
|
|
||||||
*/
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
// 写入 Bean 类名字段(用于反序列化时校验类型)
|
|
||||||
jsonWriter.name(BEAN_NAME).value(getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 JSON 读取字段并初始化 Bean(反序列化基础逻辑)
|
|
||||||
* 子类需重写,实现自身字段的解析逻辑
|
|
||||||
* @param jsonReader JSON 读取器(用于读取 JSON 数据)
|
|
||||||
* @param name 当前解析的 JSON 字段名
|
|
||||||
* @return true:字段解析成功(当前类处理);false:字段未处理(需跳过)
|
|
||||||
* @throws IOException JSON 读取失败时抛出(如流异常)
|
|
||||||
*/
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
return false; // 基类未处理任何字段,返回 false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 抽象方法:从 JSON 读取器解析并返回 Bean 实例
|
|
||||||
* 子类需实现,处理自身字段的完整解析逻辑
|
|
||||||
* @param jsonReader JSON 读取器(用于读取 JSON 数据)
|
|
||||||
* @return 解析完成的 Bean 实例
|
|
||||||
* @throws IOException JSON 读取失败时抛出(如流异常)
|
|
||||||
*/
|
|
||||||
abstract public T readBeanFromJsonReader(JsonReader jsonReader) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验 JSON 文件中的 Bean 列表与目标类是否一致
|
|
||||||
* 对比文件中每个 Bean 的类名与目标类名,返回不一致信息
|
|
||||||
* @param szFilePath JSON 文件路径(存储 Bean 列表的文件)
|
|
||||||
* @param clazz 目标 Bean 类(用于校验类型)
|
|
||||||
* @return 空串:校验一致;非空串:不一致信息(总数/差异数)或异常信息
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> String checkIsTheSameBeanListAndFile(String szFilePath, Class<T> clazz) {
|
|
||||||
StringBuilder sbResult = new StringBuilder();
|
|
||||||
String szErrorInfo = "Check Is The Same Bean List And File Error : ";
|
|
||||||
|
|
||||||
try {
|
|
||||||
int sameCount = 0; // 类名匹配的 Bean 数量
|
|
||||||
int totalCount = 0; // 文件中 Bean 总数量
|
|
||||||
|
|
||||||
// 反射创建目标 Bean 实例(用于获取类名)
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
String targetBeanName = beanTemp.getName();
|
|
||||||
// 读取文件中的 JSON 字符串
|
|
||||||
String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
|
|
||||||
StringReader stringReader = new StringReader(listJson);
|
|
||||||
JsonReader jsonReader = new JsonReader(stringReader);
|
|
||||||
|
|
||||||
jsonReader.beginArray(); // 开始解析 JSON 数组(Bean 列表)
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
totalCount++;
|
|
||||||
jsonReader.beginObject(); // 开始解析单个 Bean 对象
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String name = jsonReader.nextName();
|
|
||||||
// 只校验 BEAN_NAME 字段,其他字段跳过
|
|
||||||
if (name.equals(BEAN_NAME)) {
|
|
||||||
// 对比当前 Bean 类名与目标类名
|
|
||||||
if (targetBeanName.equals(jsonReader.nextString())) {
|
|
||||||
sameCount++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
jsonReader.skipValue(); // 跳过非目标字段
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endObject(); // 结束单个 Bean 对象解析
|
|
||||||
}
|
|
||||||
jsonReader.endArray(); // 结束 JSON 数组解析
|
|
||||||
|
|
||||||
// 生成校验结果
|
|
||||||
if (sameCount == totalCount) {
|
|
||||||
return ""; // 全部匹配,返回空串
|
|
||||||
} else {
|
|
||||||
// 部分不匹配,返回统计信息
|
|
||||||
sbResult.append("Total : ").append(totalCount)
|
|
||||||
.append(" Diff : ").append(totalCount - sameCount);
|
|
||||||
}
|
|
||||||
} catch (InstantiationException e) {
|
|
||||||
// 反射实例化失败(如无无参构造)
|
|
||||||
sbResult.append(szErrorInfo).append(e);
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
// 反射访问权限异常
|
|
||||||
sbResult.append(szErrorInfo).append(e);
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 文件读取或 JSON 解析异常
|
|
||||||
sbResult.append(szErrorInfo).append(e);
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return sbResult.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 JSON 字符串解析为目标 Bean 实例
|
|
||||||
* 通过反射创建 Bean 实例,调用子类解析逻辑完成初始化
|
|
||||||
* @param szBean JSON 字符串(单个 Bean 的 JSON 数据)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return 解析成功的 Bean 实例;失败返回 null
|
|
||||||
* @throws IOException JSON 解析失败时抛出
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> T parseStringToBean(String szBean, Class<T> clazz) throws IOException {
|
|
||||||
StringReader stringReader = new StringReader(szBean);
|
|
||||||
JsonReader jsonReader = new JsonReader(stringReader);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 反射创建 Bean 实例
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
// 调用子类解析方法,返回解析后的实例
|
|
||||||
return (T) beanTemp.readBeanFromJsonReader(jsonReader);
|
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
|
||||||
// 反射异常日志记录
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 JSON 字符串解析为 Bean 列表
|
|
||||||
* 清空目标列表,将解析后的 Bean 逐个添加到列表中
|
|
||||||
* @param szBeanList JSON 字符串(Bean 列表的 JSON 数组)
|
|
||||||
* @param beanList 目标列表(存储解析后的 Bean)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return true:解析成功;false:解析失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean parseStringToBeanList(String szBeanList, ArrayList<T> beanList, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
// 初始化目标列表(为空则创建,非空则清空)
|
|
||||||
if (beanList == null) {
|
|
||||||
beanList = new ArrayList<T>();
|
|
||||||
} else {
|
|
||||||
beanList.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
StringReader stringReader = new StringReader(szBeanList);
|
|
||||||
JsonReader jsonReader = new JsonReader(stringReader);
|
|
||||||
|
|
||||||
jsonReader.beginArray(); // 开始解析 JSON 数组
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
// 反射创建 Bean 实例,解析并添加到列表
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
T bean = (T) beanTemp.readBeanFromJsonReader(jsonReader);
|
|
||||||
if (bean != null) {
|
|
||||||
beanList.add(bean);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endArray(); // 结束 JSON 数组解析
|
|
||||||
return true;
|
|
||||||
} catch (InstantiationException | IllegalAccessException | IOException e) {
|
|
||||||
// 异常日志记录
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写 toString(),将 Bean 序列化为格式化的 JSON 字符串
|
|
||||||
* 调用自身序列化逻辑,生成带缩进的 JSON(便于调试)
|
|
||||||
* @return Bean 的 JSON 字符串;失败返回空串
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringWriter stringWriter = new StringWriter();
|
|
||||||
JsonWriter jsonWriter = new JsonWriter(stringWriter);
|
|
||||||
jsonWriter.setIndent(" "); // 设置 JSON 缩进(格式化输出)
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonWriter.beginObject(); // 开始 JSON 对象
|
|
||||||
writeThisToJsonWriter(jsonWriter); // 写入 Bean 字段(子类扩展)
|
|
||||||
jsonWriter.endObject(); // 结束 JSON 对象
|
|
||||||
return stringWriter.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 列表序列化为格式化的 JSON 字符串
|
|
||||||
* 遍历列表,逐个序列化每个 Bean,生成 JSON 数组
|
|
||||||
* @param beanList 待序列化的 Bean 列表
|
|
||||||
* @return 列表的 JSON 字符串;失败返回空串
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> String toStringByBeanList(ArrayList<T> beanList) {
|
|
||||||
try {
|
|
||||||
StringWriter stringWriter = new StringWriter();
|
|
||||||
JsonWriter jsonWriter = new JsonWriter(stringWriter);
|
|
||||||
jsonWriter.setIndent(" "); // 格式化缩进
|
|
||||||
|
|
||||||
jsonWriter.beginArray(); // 开始 JSON 数组
|
|
||||||
for (int i = 0; i < beanList.size(); i++) {
|
|
||||||
jsonWriter.beginObject(); // 单个 Bean 开始
|
|
||||||
beanList.get(i).writeThisToJsonWriter(jsonWriter); // 调用 Bean 自身序列化
|
|
||||||
jsonWriter.endObject(); // 单个 Bean 结束
|
|
||||||
}
|
|
||||||
jsonWriter.endArray(); // 结束 JSON 数组
|
|
||||||
jsonWriter.close();
|
|
||||||
return stringWriter.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从默认路径(getBeanJsonFilePath)加载 Bean 实例
|
|
||||||
* 读取应用私有目录下的 JSON 文件,解析为目标 Bean
|
|
||||||
* @param context 上下文(用于获取文件路径)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return 加载成功的 Bean 实例;失败返回 null
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> T loadBean(Context context, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
// 反射创建 Bean 实例,获取默认文件路径
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
return loadBeanFromFile(beanTemp.getBeanJsonFilePath(context), clazz);
|
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从指定文件路径加载 Bean 实例
|
|
||||||
* 检查文件是否存在,存在则读取 JSON 并解析为目标 Bean
|
|
||||||
* @param szFilePath 目标文件路径(存储 Bean 的 JSON 文件)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return 加载成功的 Bean 实例;失败返回 null
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> T loadBeanFromFile(String szFilePath, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
File file = new File(szFilePath);
|
|
||||||
if (file.exists()) { // 检查文件是否存在
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
// 读取文件 JSON 字符串,解析为 Bean
|
|
||||||
String json = UTF8FileUtils.readStringFromFile(szFilePath);
|
|
||||||
return beanTemp.parseStringToBean(json, clazz);
|
|
||||||
}
|
|
||||||
} catch (InstantiationException | IllegalAccessException | IOException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 保存到默认路径(getBeanJsonFilePath)的文件中
|
|
||||||
* 序列化 Bean 为 JSON,写入应用私有目录下的文件
|
|
||||||
* @param context 上下文(用于获取文件路径)
|
|
||||||
* @param bean 待保存的 Bean 实例
|
|
||||||
* @return true:保存成功;false:保存失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean saveBean(Context context, T bean) {
|
|
||||||
return saveBeanToFile(bean.getBeanJsonFilePath(context), bean);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 保存到指定文件路径
|
|
||||||
* 序列化 Bean 为 JSON 字符串,写入目标文件(覆盖原有内容)
|
|
||||||
* @param szFilePath 目标文件路径(保存 JSON 的文件)
|
|
||||||
* @param bean 待保存的 Bean 实例
|
|
||||||
* @return true:保存成功;false:保存失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean saveBeanToFile(String szFilePath, T bean) {
|
|
||||||
try {
|
|
||||||
// 序列化 Bean 为 JSON 字符串
|
|
||||||
String json = bean.toString();
|
|
||||||
// 写入文件(UTF-8 编码)
|
|
||||||
UTF8FileUtils.writeStringToFile(szFilePath, json);
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从默认路径(getBeanListJsonFilePath)加载 Bean 列表
|
|
||||||
* 读取应用私有目录下的列表 JSON 文件,解析并填充到目标列表
|
|
||||||
* @param context 上下文(用于获取文件路径)
|
|
||||||
* @param beanListDst 目标列表(存储加载后的 Bean)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return true:加载成功;false:加载失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean loadBeanList(Context context, ArrayList<T> beanListDst, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
// 反射创建 Bean 实例,获取默认列表文件路径
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
return loadBeanListFromFile(beanTemp.getBeanListJsonFilePath(context), beanListDst, clazz);
|
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从指定文件路径加载 Bean 列表
|
|
||||||
* 检查文件是否存在,存在则读取 JSON 数组,解析并填充到目标列表
|
|
||||||
* @param szFilePath 目标文件路径(存储列表 JSON 的文件)
|
|
||||||
* @param beanList 目标列表(存储加载后的 Bean)
|
|
||||||
* @param clazz 目标 Bean 类(用于反射实例化)
|
|
||||||
* @return true:加载成功;false:加载失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean loadBeanListFromFile(String szFilePath, ArrayList<T> beanList, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
File file = new File(szFilePath);
|
|
||||||
if (file.exists()) { // 检查文件是否存在
|
|
||||||
// 读取文件中的 JSON 字符串(Bean 列表数组)
|
|
||||||
String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
|
|
||||||
// 解析 JSON 字符串为 Bean 列表,填充到目标列表
|
|
||||||
return parseStringToBeanList(listJson, beanList, clazz);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 日志记录文件读取或解析异常
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 列表保存到默认路径(getBeanListJsonFilePath)的文件中
|
|
||||||
* 序列化列表为 JSON 数组,写入应用私有目录下的文件
|
|
||||||
* @param context 上下文(用于获取文件路径)
|
|
||||||
* @param beanList 待保存的 Bean 列表
|
|
||||||
* @param clazz 目标 Bean 类(用于反射获取保存路径)
|
|
||||||
* @return true:保存成功;false:保存失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean saveBeanList(Context context, ArrayList<T> beanList, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
// 反射创建 Bean 实例,获取默认列表保存路径
|
|
||||||
T beanTemp = clazz.newInstance();
|
|
||||||
return saveBeanListToFile(beanTemp.getBeanListJsonFilePath(context), beanList);
|
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
|
||||||
// 日志记录反射实例化异常
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Bean 列表保存到指定文件路径
|
|
||||||
* 序列化列表为 JSON 数组字符串,写入目标文件(覆盖原有内容)
|
|
||||||
* @param szFilePath 目标文件路径(保存列表 JSON 的文件)
|
|
||||||
* @param beanList 待保存的 Bean 列表
|
|
||||||
* @return true:保存成功;false:保存失败
|
|
||||||
* @param <T> 泛型约束,限定为 BaseBean 子类
|
|
||||||
*/
|
|
||||||
public static <T extends BaseBean> boolean saveBeanListToFile(String szFilePath, ArrayList<T> beanList) {
|
|
||||||
try {
|
|
||||||
// 序列化 Bean 列表为 JSON 字符串(数组格式)
|
|
||||||
String json = toStringByBeanList(beanList);
|
|
||||||
// 将 JSON 字符串写入文件(UTF-8 编码)
|
|
||||||
UTF8FileUtils.writeStringToFile(szFilePath, json);
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 日志记录文件写入或序列化异常
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.HorizontalScrollView;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.lang.Thread.UncaughtExceptionHandler;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 20:14
|
|
||||||
* @Describe * 应用全局崩溃处理类(单例逻辑)
|
|
||||||
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
|
|
||||||
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
|
|
||||||
*/
|
|
||||||
public final class CrashHandler {
|
|
||||||
|
|
||||||
/** 日志标签,用于当前类的日志输出标识 */
|
|
||||||
public static final String TAG = "CrashHandler";
|
|
||||||
|
|
||||||
/** 崩溃报告页面标题 */
|
|
||||||
public static final String TITTLE = "CrashReport";
|
|
||||||
|
|
||||||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
|
||||||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
|
||||||
|
|
||||||
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
|
||||||
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
|
||||||
/** SharedPreferences 中存储「是否发生崩溃」的键 */
|
|
||||||
final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
|
|
||||||
|
|
||||||
/** 崩溃保险丝状态文件路径(存储当前熔断等级) */
|
|
||||||
public static String _CrashCountFilePath;
|
|
||||||
|
|
||||||
/** 系统默认的未捕获异常处理器(用于降级处理,避免 CrashHandler 自身崩溃) */
|
|
||||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化崩溃处理器(默认存储路径)
|
|
||||||
* 调用重载方法,崩溃日志默认存储在应用外部私有目录的 crash 文件夹下
|
|
||||||
* @param app 全局 Application 实例(用于获取存储目录、包信息等)
|
|
||||||
*/
|
|
||||||
public static void init(Application app) {
|
|
||||||
// 初始化崩溃保险丝状态文件路径(外部存储/CrashHandler/IsCrashHandlerCrashHappen.dat)
|
|
||||||
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
|
|
||||||
LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
|
|
||||||
// 调用带目录参数的初始化方法,传入 null 使用默认路径
|
|
||||||
init(app, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化崩溃处理器(指定日志存储目录)
|
|
||||||
* 替换系统默认的未捕获异常处理器,自定义崩溃处理逻辑
|
|
||||||
* @param app 全局 Application 实例
|
|
||||||
* @param crashDir 崩溃日志存储目录(null 则使用默认路径)
|
|
||||||
*/
|
|
||||||
public static void init(final Application app, final String crashDir) {
|
|
||||||
// 设置自定义未捕获异常处理器
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
|
|
||||||
@Override
|
|
||||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
|
||||||
try {
|
|
||||||
// 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
|
|
||||||
tryUncaughtException(thread, throwable);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
// 处理失败时,交给系统默认处理器兜底
|
|
||||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
|
||||||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实际处理崩溃的核心方法
|
|
||||||
* 1. 熔断保险丝(记录崩溃次数);2. 收集崩溃信息;3. 写入日志文件;4. 启动崩溃报告页面
|
|
||||||
* @param thread 发生崩溃的线程
|
|
||||||
* @param throwable 崩溃异常对象(包含堆栈信息)
|
|
||||||
*/
|
|
||||||
private void tryUncaughtException(Thread thread, Throwable throwable) {
|
|
||||||
// 触发崩溃保险丝(每次崩溃熔断一次,降低防护等级)
|
|
||||||
AppCrashSafetyWire.getInstance().burnSafetyWire();
|
|
||||||
|
|
||||||
// 格式化崩溃发生时间(用于日志文件名和内容)
|
|
||||||
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
|
|
||||||
// 创建崩溃日志文件(默认路径:外部存储/crash/[时间].txt)
|
|
||||||
File crashFile = new File(
|
|
||||||
TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash") : new File(crashDir),
|
|
||||||
"crash_" + time + ".txt"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取应用版本信息(版本名、版本号)
|
|
||||||
String versionName = "unknown";
|
|
||||||
long versionCode = 0;
|
|
||||||
try {
|
|
||||||
PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
|
|
||||||
versionName = packageInfo.versionName;
|
|
||||||
// 适配 Android 9.0+(API 28)的版本号获取方式
|
|
||||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
|
||||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
||||||
|
|
||||||
// 将异常堆栈信息转换为字符串
|
|
||||||
String fullStackTrace;
|
|
||||||
{
|
|
||||||
StringWriter sw = new StringWriter();
|
|
||||||
PrintWriter pw = new PrintWriter(sw);
|
|
||||||
throwable.printStackTrace(pw); // 将异常堆栈写入 PrintWriter
|
|
||||||
fullStackTrace = sw.toString();
|
|
||||||
pw.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拼接崩溃信息(设备信息 + 应用信息 + 堆栈信息)
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("************* Crash Head ****************\n");
|
|
||||||
sb.append("Time Of Crash : ").append(time).append("\n");
|
|
||||||
sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n"); // 设备厂商
|
|
||||||
sb.append("Device Model : ").append(Build.MODEL).append("\n"); // 设备型号
|
|
||||||
sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n"); // Android 版本
|
|
||||||
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n"); // SDK 版本
|
|
||||||
sb.append("App VersionName : ").append(versionName).append("\n"); // 应用版本名
|
|
||||||
sb.append("App VersionCode : ").append(versionCode).append("\n"); // 应用版本号
|
|
||||||
sb.append("************* Crash Head ****************\n");
|
|
||||||
sb.append("\n").append(fullStackTrace); // 拼接异常堆栈
|
|
||||||
|
|
||||||
final String errorLog = sb.toString();
|
|
||||||
|
|
||||||
// 将崩溃日志写入文件(忽略写入失败)
|
|
||||||
try {
|
|
||||||
writeFile(crashFile, errorLog);
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
// 启动崩溃报告页面(标签用于代码块折叠)
|
|
||||||
gotoCrashActiviy: {
|
|
||||||
Intent intent = new Intent();
|
|
||||||
LogUtils.d(TAG, "gotoCrashActiviy: ");
|
|
||||||
|
|
||||||
// 根据保险丝状态选择启动的崩溃页面
|
|
||||||
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
|
||||||
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
|
||||||
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
|
||||||
intent.setClass(app, GlobalCrashActivity.class);
|
|
||||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
|
||||||
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
|
||||||
intent.setClass(app, CrashActivity.class);
|
|
||||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
|
||||||
intent.addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
|
||||||
// 如果是 debug 版,启动崩溃页面窗口
|
|
||||||
app.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
// 如果是 release 版,就只发送一个通知
|
|
||||||
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
|
|
||||||
}
|
|
||||||
// 终止当前进程(确保完全重启)
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
|
||||||
System.exit(0);
|
|
||||||
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
|
|
||||||
e.printStackTrace();
|
|
||||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
|
||||||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 其他异常,兜底处理
|
|
||||||
e.printStackTrace();
|
|
||||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
|
||||||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将字符串内容写入文件(创建父目录、覆盖写入)
|
|
||||||
* @param file 目标文件(包含路径)
|
|
||||||
* @param content 待写入的内容(崩溃日志)
|
|
||||||
* @throws IOException 文件创建或写入失败时抛出
|
|
||||||
*/
|
|
||||||
private void writeFile(File file, String content) throws IOException {
|
|
||||||
File parentFile = file.getParentFile();
|
|
||||||
// 父目录不存在则创建
|
|
||||||
if (parentFile != null && !parentFile.exists()) {
|
|
||||||
parentFile.mkdirs();
|
|
||||||
}
|
|
||||||
file.createNewFile(); // 创建文件
|
|
||||||
FileOutputStream fos = new FileOutputStream(file);
|
|
||||||
fos.write(content.getBytes()); // 写入内容(默认 UTF-8 编码)
|
|
||||||
try {
|
|
||||||
fos.close(); // 关闭流
|
|
||||||
} catch (IOException e) {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用崩溃保险丝内部类(单例)
|
|
||||||
* 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略
|
|
||||||
* 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面
|
|
||||||
*/
|
|
||||||
public static final class AppCrashSafetyWire {
|
|
||||||
|
|
||||||
/** 单例实例(volatile 保证多线程可见性) */
|
|
||||||
private static volatile AppCrashSafetyWire _AppCrashSafetyWire;
|
|
||||||
|
|
||||||
/** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */
|
|
||||||
private volatile Integer currentSafetyLevel;
|
|
||||||
/** 最低熔断等级(1,再崩溃则熔断) */
|
|
||||||
private static final int _MINI = 1;
|
|
||||||
/** 最高熔断等级(2,初始状态) */
|
|
||||||
private static final int _MAX = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私有构造方法(单例模式,禁止外部实例化)
|
|
||||||
* 初始化时加载本地存储的熔断等级
|
|
||||||
*/
|
|
||||||
private AppCrashSafetyWire() {
|
|
||||||
LogUtils.d(TAG, "AppCrashSafetyWire()");
|
|
||||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例(双重检查锁定,线程安全)
|
|
||||||
* @return AppCrashSafetyWire 单例
|
|
||||||
*/
|
|
||||||
public static synchronized AppCrashSafetyWire getInstance() {
|
|
||||||
if (_AppCrashSafetyWire == null) {
|
|
||||||
_AppCrashSafetyWire = new AppCrashSafetyWire();
|
|
||||||
}
|
|
||||||
return _AppCrashSafetyWire;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前熔断等级(内存中)
|
|
||||||
* @param currentSafetyLevel 目标等级(1~2)
|
|
||||||
*/
|
|
||||||
public void setCurrentSafetyLevel(int currentSafetyLevel) {
|
|
||||||
this.currentSafetyLevel = currentSafetyLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前熔断等级(内存中)
|
|
||||||
* @return 当前等级(1~2 或 null)
|
|
||||||
*/
|
|
||||||
public int getCurrentSafetyLevel() {
|
|
||||||
return currentSafetyLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存熔断等级到本地文件(持久化,重启应用生效)
|
|
||||||
* @param currentSafetyLevel 待保存的等级
|
|
||||||
*/
|
|
||||||
public void saveCurrentSafetyLevel(int currentSafetyLevel) {
|
|
||||||
LogUtils.d(TAG, "saveCurrentSafetyLevel()");
|
|
||||||
this.currentSafetyLevel = currentSafetyLevel;
|
|
||||||
try {
|
|
||||||
// 序列化等级到文件(ObjectOutputStream 写入 int)
|
|
||||||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath));
|
|
||||||
oos.writeInt(currentSafetyLevel);
|
|
||||||
oos.flush();
|
|
||||||
oos.close();
|
|
||||||
LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel));
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从本地文件加载熔断等级(应用启动时初始化)
|
|
||||||
* @return 加载的等级(文件不存在则初始化为 MAX(2))
|
|
||||||
*/
|
|
||||||
public int loadCurrentSafetyLevel() {
|
|
||||||
LogUtils.d(TAG, "loadCurrentSafetyLevel()");
|
|
||||||
try {
|
|
||||||
File f = new File(_CrashCountFilePath);
|
|
||||||
if (f.exists()) {
|
|
||||||
// 反序列化从文件读取等级
|
|
||||||
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath));
|
|
||||||
currentSafetyLevel = ois.readInt();
|
|
||||||
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel));
|
|
||||||
} else {
|
|
||||||
// 文件不存在,初始化等级为最高(2)并保存
|
|
||||||
currentSafetyLevel = _MAX;
|
|
||||||
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX));
|
|
||||||
saveCurrentSafetyLevel(currentSafetyLevel);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return currentSafetyLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 熔断保险丝(每次崩溃调用,降低防护等级)
|
|
||||||
* @return 熔断后是否仍在防护范围内(true:是;false:已熔断)
|
|
||||||
*/
|
|
||||||
boolean burnSafetyWire() {
|
|
||||||
LogUtils.d(TAG, "burnSafetyWire()");
|
|
||||||
// 加载当前等级
|
|
||||||
int safeLevel = loadCurrentSafetyLevel();
|
|
||||||
// 若在防护范围内(1~2),等级-1 并保存
|
|
||||||
if (isSafetyWireWorking(safeLevel)) {
|
|
||||||
LogUtils.d(TAG, "burnSafetyWire() use");
|
|
||||||
saveCurrentSafetyLevel(safeLevel - 1);
|
|
||||||
// 返回熔断后的状态
|
|
||||||
return isSafetyWireWorking(safeLevel - 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查熔断等级是否在有效范围内(1~2)
|
|
||||||
* @param safetyLevel 待检查的等级
|
|
||||||
* @return true:在范围内(防护有效);false:超出范围(已熔断)
|
|
||||||
*/
|
|
||||||
boolean isSafetyWireWorking(int safetyLevel) {
|
|
||||||
LogUtils.d(TAG, "isSafetyWireOK()");
|
|
||||||
LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
|
|
||||||
|
|
||||||
if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
|
|
||||||
LogUtils.d(TAG, String.format("In Safety Level"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, String.format("Out of Safety Level"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 立即恢复熔断等级到最高(2)
|
|
||||||
* 用于重启应用后重置防护状态
|
|
||||||
*/
|
|
||||||
void resumeToMaximumImmediately() {
|
|
||||||
LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
|
|
||||||
AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭防护(设置等级为最低(1))
|
|
||||||
* 下次崩溃直接熔断
|
|
||||||
*/
|
|
||||||
void off() {
|
|
||||||
LogUtils.d(TAG, "off()");
|
|
||||||
saveCurrentSafetyLevel(_MINI);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查当前保险丝是否有效(防护未熔断)
|
|
||||||
* @return true:有效(等级 1~2);false:已熔断
|
|
||||||
*/
|
|
||||||
boolean isAppCrashSafetyWireOK() {
|
|
||||||
LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
|
|
||||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
|
||||||
return isSafetyWireWorking(currentSafetyLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 延迟恢复保险丝到最高等级(500ms 后)
|
|
||||||
* 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃
|
|
||||||
* @param context 上下文(用于获取主线程 Handler)
|
|
||||||
*/
|
|
||||||
void postResumeCrashSafetyWireHandler(final Context context) {
|
|
||||||
// 主线程延迟 500ms 执行(避免页面启动时阻塞)
|
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
LogUtils.d(TAG, "Handler run()");
|
|
||||||
// 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级
|
|
||||||
if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) {
|
|
||||||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
|
||||||
LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础版崩溃报告页面(保险丝熔断时启动)
|
|
||||||
* 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
|
|
||||||
*/
|
|
||||||
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
|
||||||
/** 菜单标识:复制崩溃日志 */
|
|
||||||
private static final int MENUITEM_COPY = 0;
|
|
||||||
/** 菜单标识:重启应用 */
|
|
||||||
private static final int MENUITEM_RESTART = 1;
|
|
||||||
|
|
||||||
/** 崩溃日志文本(从 CrashHandler 传递过来) */
|
|
||||||
private String mLog;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
// 初始化崩溃保险丝延迟恢复机制
|
|
||||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
|
||||||
|
|
||||||
// 获取传递的崩溃日志
|
|
||||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
|
||||||
// 设置系统默认主题(避免自定义主题冲突)
|
|
||||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
|
||||||
|
|
||||||
// 动态创建布局(避免 XML 布局加载异常)
|
|
||||||
setContentView: {
|
|
||||||
// 垂直滚动视图(处理日志过长)
|
|
||||||
ScrollView contentView = new ScrollView(this);
|
|
||||||
contentView.setFillViewport(true);
|
|
||||||
|
|
||||||
// 水平滚动视图(处理日志行过长)
|
|
||||||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
|
||||||
hw.setBackgroundColor(Color.GRAY); // 背景色设为灰色
|
|
||||||
|
|
||||||
// 日志显示文本框
|
|
||||||
TextView message = new TextView(this);
|
|
||||||
{
|
|
||||||
int padding = dp2px(16); // 内边距 16dp(适配不同屏幕)
|
|
||||||
message.setPadding(padding, padding, padding, padding);
|
|
||||||
message.setText(mLog); // 设置崩溃日志
|
|
||||||
message.setTextColor(Color.BLACK); // 文字黑色
|
|
||||||
message.setTextIsSelectable(true); // 支持文本选择(便于手动复制)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组装布局:TextView -> HorizontalScrollView -> ScrollView
|
|
||||||
hw.addView(message);
|
|
||||||
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
|
||||||
// 设置当前 Activity 布局
|
|
||||||
setContentView(contentView);
|
|
||||||
|
|
||||||
// 配置 ActionBar 标题和副标题
|
|
||||||
getActionBar().setTitle(TITTLE);
|
|
||||||
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写返回键逻辑:点击返回键直接重启应用
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启当前应用(与 GlobalCrashActivity 逻辑一致)
|
|
||||||
* 清除任务栈,启动主 Activity,终止当前进程
|
|
||||||
*/
|
|
||||||
private void restart() {
|
|
||||||
PackageManager pm = getPackageManager();
|
|
||||||
// 获取应用启动意图(默认启动主 Activity)
|
|
||||||
Intent intent = pm.getLaunchIntentForPackage(getPackageName());
|
|
||||||
if (intent != null) {
|
|
||||||
// 设置意图标志:清除原有任务栈,创建新任务
|
|
||||||
intent.addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
// 关闭当前页面,终止进程,确保完全重启
|
|
||||||
finish();
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* dp 转 px(适配不同屏幕密度)
|
|
||||||
* @param dpValue dp 值
|
|
||||||
* @return 转换后的 px 值
|
|
||||||
*/
|
|
||||||
private int dp2px(final float dpValue) {
|
|
||||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
|
||||||
return (int) (dpValue * scale + 0.5f); // 四舍五入确保精度
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单点击事件回调(处理复制、重启)
|
|
||||||
* @param item 被点击的菜单项
|
|
||||||
* @return false:不消费事件(保持默认行为)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case MENUITEM_COPY:
|
|
||||||
// 复制日志到剪贴板
|
|
||||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
|
||||||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
|
||||||
break;
|
|
||||||
case MENUITEM_RESTART:
|
|
||||||
// 恢复保险丝到最高等级,然后重启应用
|
|
||||||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
|
||||||
restart();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 ActionBar 菜单(添加复制、重启项)
|
|
||||||
* @param menu 菜单容器
|
|
||||||
* @return true:显示菜单
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
// 添加「复制」菜单:有空间时显示在 ActionBar,否则放入溢出菜单
|
|
||||||
menu.add(0, MENUITEM_COPY, 0, "Copy")
|
|
||||||
.setOnMenuItemClickListener(this)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
|
||||||
// 添加「重启」菜单:同上
|
|
||||||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
|
||||||
.setOnMenuItemClickListener(this)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 19:56
|
|
||||||
* @Describe 全局 Application 类,用于初始化应用核心组件、管理全局状态(如调试模式)
|
|
||||||
* 需在 AndroidManifest.xml 中配置 android:name=".GlobalApplication" 使其生效
|
|
||||||
*/
|
|
||||||
public class GlobalApplication extends Application {
|
|
||||||
|
|
||||||
/** 日志标签 */
|
|
||||||
public static final String TAG = "GlobalApplication";
|
|
||||||
|
|
||||||
/** 全局 Application 单例实例(volatile 保证多线程可见性,避免指令重排) */
|
|
||||||
private static volatile GlobalApplication sInstance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用调试模式标记(volatile 保证多线程可见性)
|
|
||||||
* true:调试模式(开启日志、调试功能);false:正式模式(关闭调试相关功能)
|
|
||||||
*/
|
|
||||||
private static volatile boolean isDebugging = false;
|
|
||||||
|
|
||||||
// 新增:WinBoLL 服务器主机地址(volatile 保证多线程可见性)
|
|
||||||
private static volatile String winbollHost = null;
|
|
||||||
// 新增:SP 存储相关常量(私有存储,仅当前应用可访问)
|
|
||||||
private static final String SP_NAME = "WinBoLL_SP_CONFIG";
|
|
||||||
private static final String SP_KEY_WINBOLL_HOST = "winboll_host";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取全局 Application 单例实例(外部可通过此方法获取上下文)
|
|
||||||
* @return GlobalApplication 单例(未初始化时返回 null,需确保配置 AndroidManifest)
|
|
||||||
*/
|
|
||||||
public static GlobalApplication getInstance() {
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置应用调试模式
|
|
||||||
* @param debugging 调试模式状态(true/false)
|
|
||||||
*/
|
|
||||||
public static void setIsDebugging(boolean debugging) {
|
|
||||||
isDebugging = debugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存调试模式状态到本地文件(持久化存储,重启应用后生效)
|
|
||||||
* @param application 全局 Application 实例(通过 getInstance() 获取更规范)
|
|
||||||
*/
|
|
||||||
public static void saveDebugStatus(GlobalApplication application) {
|
|
||||||
if (application == null) {
|
|
||||||
LogUtils.e(TAG, "saveDebugStatus: Application 实例为空,保存失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 将调试状态封装为 APPModel 并保存到文件
|
|
||||||
APPModel.saveBeanToFile(
|
|
||||||
getAppModelFilePath(application),
|
|
||||||
new APPModel(isDebugging)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 APPModel 配置文件的存储路径
|
|
||||||
* 路径:应用私有数据目录 / APPModel.json(仅当前应用可访问,安全)
|
|
||||||
* @param application 全局 Application 实例
|
|
||||||
* @return 配置文件绝对路径
|
|
||||||
*/
|
|
||||||
private static String getAppModelFilePath(GlobalApplication application) {
|
|
||||||
return application.getDataDir().getPath() + "/APPModel.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前应用调试模式状态
|
|
||||||
* @return true:调试模式;false:正式模式
|
|
||||||
*/
|
|
||||||
public static boolean isDebugging() {
|
|
||||||
return isDebugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增:设置 WinBoLL 服务器主机地址(同时保存到 SP 持久化)
|
|
||||||
public static void setWinbollHost(String host) {
|
|
||||||
if (sInstance == null) {
|
|
||||||
LogUtils.e(TAG, "setWinbollHost: 应用未初始化,设置失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 检查并补全末尾 / 核心改动
|
|
||||||
if (host != null && !host.isEmpty() && !host.endsWith("/")) {
|
|
||||||
host += "/";
|
|
||||||
}
|
|
||||||
// 更新内存中的字段
|
|
||||||
winbollHost = host;
|
|
||||||
// 保存到 SP 持久化(私有模式,安全)
|
|
||||||
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
|
||||||
sp.edit().putString(SP_KEY_WINBOLL_HOST, host).apply();
|
|
||||||
LogUtils.d(TAG, "setWinbollHost: 服务器地址已设置并持久化,host=" + host);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 新增:获取 WinBoLL 服务器主机地址(优先内存,内存为空则从 SP 读取)
|
|
||||||
public static String getWinbollHost() {
|
|
||||||
if (winbollHost != null) {
|
|
||||||
// 内存中存在,直接返回(提高效率)
|
|
||||||
return winbollHost;
|
|
||||||
}
|
|
||||||
if (sInstance == null) {
|
|
||||||
LogUtils.e(TAG, "getWinbollHost: 应用未初始化,获取失败");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 内存中不存在,从 SP 读取并更新到内存
|
|
||||||
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
|
||||||
winbollHost = sp.getString(SP_KEY_WINBOLL_HOST, "https://console.winboll.cc/");
|
|
||||||
LogUtils.d(TAG, "getWinbollHost: 从 SP 读取服务器地址,host=" + winbollHost);
|
|
||||||
return winbollHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用启动时初始化(仅执行一次)
|
|
||||||
* 初始化核心框架、恢复调试状态、配置全局异常处理等
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
// 初始化单例实例(确保在所有初始化操作前完成)
|
|
||||||
sInstance = this;
|
|
||||||
|
|
||||||
// 初始化基础组件(日志、崩溃处理、Toast)
|
|
||||||
initCoreComponents();
|
|
||||||
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
|
|
||||||
restoreDebugStatus();
|
|
||||||
// 新增:初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
|
|
||||||
initWinbollHost();
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化应用核心组件(日志、崩溃处理、Toast 框架)
|
|
||||||
*/
|
|
||||||
private void initCoreComponents() {
|
|
||||||
// 初始化日志工具(传入 Application 上下文)
|
|
||||||
LogUtils.init(this);
|
|
||||||
// 初始化全局异常处理器(捕获应用崩溃信息,用于调试或上报)
|
|
||||||
CrashHandler.init(this);
|
|
||||||
// 初始化 Toast 工具(统一 Toast 样式、避免内存泄漏等)
|
|
||||||
ToastUtils.init(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 恢复调试模式状态(从本地配置文件读取)
|
|
||||||
* 1. 读取本地 APPModel.json 文件
|
|
||||||
* 2. 读取成功:使用保存的调试状态;读取失败(文件不存在):默认关闭调试并创建配置文件
|
|
||||||
*/
|
|
||||||
private void restoreDebugStatus() {
|
|
||||||
// 从文件加载 APPModel 实例(存储调试状态的模型类)
|
|
||||||
APPModel appModel = APPModel.loadBeanFromFile(
|
|
||||||
getAppModelFilePath(this),
|
|
||||||
APPModel.class
|
|
||||||
);
|
|
||||||
|
|
||||||
if (appModel == null) {
|
|
||||||
// 配置文件不存在,默认关闭调试模式并创建文件
|
|
||||||
setIsDebugging(false);
|
|
||||||
saveDebugStatus(this);
|
|
||||||
LogUtils.d(TAG, "调试配置文件不存在,默认关闭调试模式并创建配置文件");
|
|
||||||
} else {
|
|
||||||
// 配置文件存在,使用保存的调试状态
|
|
||||||
setIsDebugging(appModel.isDebugging());
|
|
||||||
LogUtils.d(TAG, "从配置文件恢复调试模式:" + isDebugging);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增:初始化服务器地址(应用启动时从 SP 读取到内存)
|
|
||||||
private void initWinbollHost() {
|
|
||||||
getWinbollHost(); // 触发从 SP 读取并更新内存
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取应用名称(从 AndroidManifest.xml 的 android:label 读取)
|
|
||||||
* @param context 上下文(建议传入 Application 上下文,避免内存泄漏)
|
|
||||||
* @return 应用名称(读取失败返回 null)
|
|
||||||
*/
|
|
||||||
public static String getAppName(Context context) {
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
PackageManager packageManager = context.getPackageManager();
|
|
||||||
try {
|
|
||||||
// 获取应用信息(包含应用名称、图标等)
|
|
||||||
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(
|
|
||||||
context.getPackageName(), // 当前应用包名
|
|
||||||
0 // 额外标志(0 表示默认获取基本信息)
|
|
||||||
);
|
|
||||||
// 从应用信息中获取应用名称(支持多语言)
|
|
||||||
String appName = (String) packageManager.getApplicationLabel(applicationInfo);
|
|
||||||
LogUtils.d(TAG, "获取应用名称成功:" + appName);
|
|
||||||
return appName;
|
|
||||||
} catch (NameNotFoundException e) {
|
|
||||||
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用终止时调用(仅用于释放全局资源)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate();
|
|
||||||
// 释放单例引用(可选,避免内存泄漏风险)
|
|
||||||
sInstance = null;
|
|
||||||
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import cc.winboll.studio.libappbase.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 19:58
|
|
||||||
* @Describe 应用异常报告观察活动窗口类
|
|
||||||
* 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
|
|
||||||
* 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用
|
|
||||||
*/
|
|
||||||
public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
/** 日志标签(用于调试日志输出,唯一标识当前 Activity) */
|
|
||||||
public static final String TAG = "GlobalCrashActivity";
|
|
||||||
|
|
||||||
/** 菜单标识:复制崩溃日志(用于区分菜单项点击事件) */
|
|
||||||
private static final int MENU_ITEM_COPY = 0;
|
|
||||||
/** 菜单标识:重启应用(用于区分菜单项点击事件) */
|
|
||||||
private static final int MENU_ITEM_RESTART = 1;
|
|
||||||
|
|
||||||
/** 崩溃报告展示自定义视图 */
|
|
||||||
// 负责渲染崩溃日志文本、提供 Toolbar 容器,封装了日志展示和菜单样式控制逻辑
|
|
||||||
private GlobalCrashReportView mCrashReportView;
|
|
||||||
|
|
||||||
/** 崩溃日志文本内容 */
|
|
||||||
// 从 CrashHandler 通过 Intent 传递过来,包含异常堆栈、设备信息等完整崩溃数据
|
|
||||||
private String mCrashLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity 创建时初始化(生命周期核心方法,仅执行一次)
|
|
||||||
* @param savedInstanceState 保存的实例状态(崩溃页面无需恢复状态,此处仅作兼容)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// 初始化崩溃安全防护机制
|
|
||||||
// 作用:防止应用重启后短时间内再次崩溃,由 CrashHandler 内部实现防护逻辑
|
|
||||||
CrashHandler.AppCrashSafetyWire.getInstance()
|
|
||||||
.postResumeCrashSafetyWireHandler(getApplicationContext());
|
|
||||||
|
|
||||||
// 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
|
|
||||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
|
||||||
|
|
||||||
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
|
|
||||||
setContentView(R.layout.activity_globalcrash);
|
|
||||||
|
|
||||||
// 初始化崩溃报告展示视图(通过布局 ID 找到自定义 View 实例)
|
|
||||||
mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
|
|
||||||
// 将崩溃日志设置到视图中,由自定义 View 负责排版和显示
|
|
||||||
mCrashReportView.setReport(mCrashLog);
|
|
||||||
|
|
||||||
// 设置页面的 ActionBar(复用自定义 View 中的 Toolbar 作为系统 ActionBar)
|
|
||||||
setActionBar(mCrashReportView.getToolbar());
|
|
||||||
|
|
||||||
// 配置 ActionBar 标题和副标题(非空判断避免空指针异常)
|
|
||||||
if (getActionBar() != null) {
|
|
||||||
// 设置标题:使用 CrashHandler 中定义的统一标题(如 "应用崩溃报告")
|
|
||||||
getActionBar().setTitle(CrashHandler.TITTLE);
|
|
||||||
// 设置副标题:显示当前应用名称(从全局 Application 工具方法获取)
|
|
||||||
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写返回键点击事件
|
|
||||||
* 逻辑:点击手机返回键时,直接重启应用(而非返回上一页,因崩溃后上一页状态可能异常)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
restartApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启当前应用(核心工具方法)
|
|
||||||
* 实现逻辑:
|
|
||||||
* 1. 获取应用的启动意图(默认启动 AndroidManifest 中配置的主 Activity)
|
|
||||||
* 2. 设置意图标志,清除原有任务栈,避免残留异常页面
|
|
||||||
* 3. 启动主 Activity 并终止当前进程,确保应用完全重启
|
|
||||||
*/
|
|
||||||
private void restartApp() {
|
|
||||||
// 获取 PackageManager 实例(用于获取应用相关信息和意图)
|
|
||||||
PackageManager packageManager = getPackageManager();
|
|
||||||
// 获取应用的启动意图(参数为当前应用包名,返回主 Activity 的意图)
|
|
||||||
Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName());
|
|
||||||
|
|
||||||
if (launchIntent != null) {
|
|
||||||
// 设置意图标志:
|
|
||||||
// FLAG_ACTIVITY_NEW_TASK:创建新的任务栈启动 Activity
|
|
||||||
// FLAG_ACTIVITY_CLEAR_TOP:清除目标 Activity 之上的所有 Activity
|
|
||||||
// FLAG_ACTIVITY_CLEAR_TASK:清除当前任务栈中的所有 Activity
|
|
||||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
// 启动应用主 Activity
|
|
||||||
startActivity(launchIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭当前崩溃报告页面
|
|
||||||
finish();
|
|
||||||
// 终止当前应用进程(确保释放所有资源,避免内存泄漏)
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
|
||||||
// 强制退出虚拟机(彻底终止应用,防止残留线程继续运行)
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单项点击事件回调(实现 MenuItem.OnMenuItemClickListener 接口)
|
|
||||||
* @param item 被点击的菜单项实例
|
|
||||||
* @return boolean:true 表示事件已消费,不再向下传递;false 表示未消费
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
// 根据菜单项 ID 判断点击的是哪个功能
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case MENU_ITEM_COPY:
|
|
||||||
// 点击「复制」菜单,执行复制崩溃日志到剪贴板
|
|
||||||
copyCrashLogToClipboard();
|
|
||||||
break;
|
|
||||||
case MENU_ITEM_RESTART:
|
|
||||||
// 点击「重启」菜单:先恢复崩溃防护机制到最大等级,再重启应用
|
|
||||||
CrashHandler.AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
|
||||||
restartApp();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建页面顶部菜单(ActionBar 菜单)
|
|
||||||
* @param menu 菜单容器,用于添加菜单项
|
|
||||||
* @return boolean:true 表示显示菜单;false 表示不显示
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
// 添加「复制」菜单项:
|
|
||||||
// 参数说明:菜单组 ID(0 表示默认组)、菜单项 ID(MENU_ITEM_COPY)、排序号(0)、菜单文本("Copy")
|
|
||||||
// setOnMenuItemClickListener(this):绑定点击事件到当前 Activity
|
|
||||||
// setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM):有空间时显示在 ActionBar 上,否则放入溢出菜单
|
|
||||||
menu.add(0, MENU_ITEM_COPY, 0, "Copy")
|
|
||||||
.setOnMenuItemClickListener(this)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
|
||||||
|
|
||||||
// 添加「重启」菜单项(参数含义同上)
|
|
||||||
menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
|
|
||||||
.setOnMenuItemClickListener(this)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
|
||||||
|
|
||||||
// 调用自定义视图的方法,更新菜单文字样式(如颜色、字体大小等,由自定义 View 内部实现)
|
|
||||||
mCrashReportView.updateMenuStyle();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将崩溃日志复制到系统剪贴板(工具方法)
|
|
||||||
* 功能:用户点击复制菜单后,将完整崩溃日志存入剪贴板,方便粘贴到聊天工具或文档中
|
|
||||||
*/
|
|
||||||
private void copyCrashLogToClipboard() {
|
|
||||||
// 获取系统剪贴板服务(需通过 getSystemService 方法获取)
|
|
||||||
ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
|
|
||||||
// 创建剪贴板数据:
|
|
||||||
// 参数 1:标签(用于标识剪贴板内容来源,此处用应用包名)
|
|
||||||
// 参数 2:实际复制的文本内容(崩溃日志)
|
|
||||||
ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog);
|
|
||||||
|
|
||||||
// 将数据设置到剪贴板(完成复制操作)
|
|
||||||
clipboardManager.setPrimaryClip(clipData);
|
|
||||||
|
|
||||||
// 显示复制成功的 Toast 提示(告知用户操作结果)
|
|
||||||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
import cc.winboll.studio.libappbase.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 20:21
|
|
||||||
* @Describe 全局崩溃报告视图控件
|
|
||||||
* 用于展示应用崩溃信息,包含顶部工具栏和崩溃日志文本区域,支持自定义配色
|
|
||||||
*/
|
|
||||||
public class GlobalCrashReportView extends LinearLayout {
|
|
||||||
|
|
||||||
// 日志标签
|
|
||||||
public static final String TAG = "GlobalCrashReportView";
|
|
||||||
|
|
||||||
// 上下文对象
|
|
||||||
private Context mContext;
|
|
||||||
// 顶部工具栏(标题栏)
|
|
||||||
private Toolbar mToolbar;
|
|
||||||
// 标题文字颜色
|
|
||||||
private int mTitleColor;
|
|
||||||
// 标题栏背景颜色
|
|
||||||
private int mTitleBackgroundColor;
|
|
||||||
// 日志文本颜色
|
|
||||||
private int mTextColor;
|
|
||||||
// 日志区域背景颜色
|
|
||||||
private int mTextBackgroundColor;
|
|
||||||
// 崩溃日志显示文本控件
|
|
||||||
private TextView mTvReport;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造方法:仅上下文
|
|
||||||
* @param context 上下文
|
|
||||||
*/
|
|
||||||
public GlobalCrashReportView(Context context) {
|
|
||||||
super(context);
|
|
||||||
mContext = context;
|
|
||||||
// 初始化默认配置(无自定义属性)
|
|
||||||
initDefaultConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造方法:上下文 + 自定义属性
|
|
||||||
* @param context 上下文
|
|
||||||
* @param attrs 自定义属性集合
|
|
||||||
*/
|
|
||||||
public GlobalCrashReportView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
mContext = context;
|
|
||||||
// 初始化视图(解析自定义属性)
|
|
||||||
initView(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造方法:上下文 + 自定义属性 + 样式属性
|
|
||||||
* @param context 上下文
|
|
||||||
* @param attrs 自定义属性集合
|
|
||||||
* @param defStyleAttr 样式属性
|
|
||||||
*/
|
|
||||||
public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
mContext = context;
|
|
||||||
// 初始化视图(解析自定义属性)
|
|
||||||
initView(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造方法:上下文 + 自定义属性 + 样式属性 + 样式资源
|
|
||||||
* @param context 上下文
|
|
||||||
* @param attrs 自定义属性集合
|
|
||||||
* @param defStyleAttr 样式属性
|
|
||||||
* @param defStyleRes 样式资源
|
|
||||||
*/
|
|
||||||
public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
mContext = context;
|
|
||||||
// 初始化视图(解析自定义属性)
|
|
||||||
initView(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置标题文字颜色
|
|
||||||
* @param titleColor 颜色值(如 Color.WHITE 或 #FFFFFF)
|
|
||||||
*/
|
|
||||||
public void setTitleColor(int titleColor) {
|
|
||||||
this.mTitleColor = titleColor;
|
|
||||||
// 实时更新工具栏标题颜色
|
|
||||||
if (mToolbar != null) {
|
|
||||||
mToolbar.setTitleTextColor(titleColor);
|
|
||||||
mToolbar.setSubtitleTextColor(titleColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取标题文字颜色
|
|
||||||
* @return 标题文字颜色值
|
|
||||||
*/
|
|
||||||
public int getTitleColor() {
|
|
||||||
return mTitleColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置标题栏背景颜色
|
|
||||||
* @param titleBackgroundColor 颜色值(如 Color.BLACK 或 #000000)
|
|
||||||
*/
|
|
||||||
public void setTitleBackgroundColor(int titleBackgroundColor) {
|
|
||||||
this.mTitleBackgroundColor = titleBackgroundColor;
|
|
||||||
// 实时更新工具栏背景颜色
|
|
||||||
if (mToolbar != null) {
|
|
||||||
mToolbar.setBackgroundColor(titleBackgroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取标题栏背景颜色
|
|
||||||
* @return 标题栏背景颜色值
|
|
||||||
*/
|
|
||||||
public int getTitleBackgroundColor() {
|
|
||||||
return mTitleBackgroundColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置日志文本颜色
|
|
||||||
* @param textColor 颜色值(如 Color.BLACK 或 #000000)
|
|
||||||
*/
|
|
||||||
public void setTextColor(int textColor) {
|
|
||||||
this.mTextColor = textColor;
|
|
||||||
// 实时更新日志文本颜色
|
|
||||||
if (mTvReport != null) {
|
|
||||||
mTvReport.setTextColor(textColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志文本颜色
|
|
||||||
* @return 日志文本颜色值
|
|
||||||
*/
|
|
||||||
public int getTextColor() {
|
|
||||||
return mTextColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置日志区域背景颜色
|
|
||||||
* @param textBackgroundColor 颜色值(如 Color.WHITE 或 #FFFFFF)
|
|
||||||
*/
|
|
||||||
public void setTextBackgroundColor(int textBackgroundColor) {
|
|
||||||
this.mTextBackgroundColor = textBackgroundColor;
|
|
||||||
// 实时更新日志区域和主布局背景颜色
|
|
||||||
if (mTvReport != null) {
|
|
||||||
mTvReport.setBackgroundColor(textBackgroundColor);
|
|
||||||
}
|
|
||||||
setBackgroundColor(textBackgroundColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志区域背景颜色
|
|
||||||
* @return 日志区域背景颜色值
|
|
||||||
*/
|
|
||||||
public int getTextBackgroundColor() {
|
|
||||||
return mTextBackgroundColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化默认配置(无自定义属性时使用)
|
|
||||||
*/
|
|
||||||
private void initDefaultConfig() {
|
|
||||||
// 设置默认配色
|
|
||||||
mTitleColor = Color.WHITE;
|
|
||||||
mTitleBackgroundColor = Color.BLACK;
|
|
||||||
mTextColor = Color.BLACK;
|
|
||||||
mTextBackgroundColor = Color.WHITE;
|
|
||||||
// 加载布局
|
|
||||||
inflateView();
|
|
||||||
// 初始化控件样式
|
|
||||||
initWidgetStyle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化视图(解析自定义属性 + 加载布局 + 设置样式)
|
|
||||||
* @param attrs 自定义属性集合
|
|
||||||
*/
|
|
||||||
private void initView(AttributeSet attrs) {
|
|
||||||
// 解析自定义属性(关联 attrs.xml 中的 GlobalCrashActivity 样式)
|
|
||||||
TypedArray typedArray = mContext.obtainStyledAttributes(
|
|
||||||
attrs,
|
|
||||||
R.styleable.GlobalCrashActivity,
|
|
||||||
R.attr.themeGlobalCrashActivity,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// 读取自定义属性值(无设置时使用默认值)
|
|
||||||
mTitleColor = typedArray.getColor(
|
|
||||||
R.styleable.GlobalCrashActivity_colorTittle,
|
|
||||||
Color.WHITE
|
|
||||||
);
|
|
||||||
mTitleBackgroundColor = typedArray.getColor(
|
|
||||||
R.styleable.GlobalCrashActivity_colorTittleBackgound, // 注:原拼写错误(Backgound→Background),保持与 attrs.xml 一致
|
|
||||||
Color.BLACK
|
|
||||||
);
|
|
||||||
mTextColor = typedArray.getColor(
|
|
||||||
R.styleable.GlobalCrashActivity_colorText,
|
|
||||||
Color.BLACK
|
|
||||||
);
|
|
||||||
mTextBackgroundColor = typedArray.getColor(
|
|
||||||
R.styleable.GlobalCrashActivity_colorTextBackgound, // 注:原拼写错误,保持与 attrs.xml 一致
|
|
||||||
Color.WHITE
|
|
||||||
);
|
|
||||||
|
|
||||||
// 回收 TypedArray,避免内存泄漏
|
|
||||||
typedArray.recycle();
|
|
||||||
|
|
||||||
// 加载布局文件
|
|
||||||
inflateView();
|
|
||||||
// 初始化控件样式
|
|
||||||
initWidgetStyle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载布局文件
|
|
||||||
*/
|
|
||||||
private void inflateView() {
|
|
||||||
// 加载自定义布局(R.layout.view_globalcrashreport)
|
|
||||||
inflate(mContext, R.layout.view_globalcrashreport, this);
|
|
||||||
// 绑定控件
|
|
||||||
mToolbar = findViewById(R.id.viewglobalcrashreportToolbar1);
|
|
||||||
mTvReport = findViewById(R.id.viewglobalcrashreportTextView1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化控件样式(设置配色和基础属性)
|
|
||||||
*/
|
|
||||||
private void initWidgetStyle() {
|
|
||||||
// 设置主布局背景颜色
|
|
||||||
setBackgroundColor(mTextBackgroundColor);
|
|
||||||
|
|
||||||
// 配置工具栏样式
|
|
||||||
if (mToolbar != null) {
|
|
||||||
mToolbar.setBackgroundColor(mTitleBackgroundColor);
|
|
||||||
mToolbar.setTitleTextColor(mTitleColor);
|
|
||||||
mToolbar.setSubtitleTextColor(mTitleColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置日志文本控件样式
|
|
||||||
if (mTvReport != null) {
|
|
||||||
mTvReport.setTextColor(mTextColor);
|
|
||||||
mTvReport.setBackgroundColor(mTextBackgroundColor);
|
|
||||||
// 可选:设置日志文本换行方式(默认已换行,此处增强可读性)
|
|
||||||
mTvReport.setSingleLine(false);
|
|
||||||
mTvReport.setHorizontallyScrolling(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置崩溃报告内容到文本控件
|
|
||||||
* @param report 崩溃日志字符串(通常包含异常信息、调用栈等)
|
|
||||||
*/
|
|
||||||
public void setReport(String report) {
|
|
||||||
if (mTvReport != null) {
|
|
||||||
mTvReport.setText(report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取顶部工具栏对象(用于外部设置标题、添加菜单等)
|
|
||||||
* @return Toolbar 实例
|
|
||||||
*/
|
|
||||||
public Toolbar getToolbar() {
|
|
||||||
return mToolbar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新工具栏菜单文字颜色(与标题颜色保持一致)
|
|
||||||
* 需在菜单加载完成后调用(如 Toolbar 加载菜单后)
|
|
||||||
*/
|
|
||||||
public void updateMenuStyle() {
|
|
||||||
if (mToolbar == null) return;
|
|
||||||
|
|
||||||
// 获取工具栏菜单
|
|
||||||
Menu menu = mToolbar.getMenu();
|
|
||||||
if (menu == null || menu.size() == 0) return;
|
|
||||||
|
|
||||||
// 遍历所有菜单项,设置文字颜色
|
|
||||||
for (int i = 0; i < menu.size(); i++) {
|
|
||||||
MenuItem menuItem = menu.getItem(i);
|
|
||||||
String title = menuItem.getTitle().toString();
|
|
||||||
// 使用 SpannableString 设置文字颜色
|
|
||||||
SpannableString spanString = new SpannableString(title);
|
|
||||||
spanString.setSpan(
|
|
||||||
new ForegroundColorSpan(mTitleColor),
|
|
||||||
0,
|
|
||||||
spanString.length(),
|
|
||||||
0 // Spannable.SPAN_INCLUSIVE_EXCLUSIVE(默认值,包含起始位置,不包含结束位置)
|
|
||||||
);
|
|
||||||
menuItem.setTitle(spanString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import cc.winboll.studio.libappbase.LogView;
|
|
||||||
import cc.winboll.studio.libappbase.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/11/11 20:29
|
|
||||||
* @Describe 应用日志展示 Activity
|
|
||||||
* 用于单独启动窗口展示应用运行日志,依赖 LogView 控件实现日志加载与显示
|
|
||||||
*/
|
|
||||||
public class LogActivity extends Activity {
|
|
||||||
|
|
||||||
/** 日志标签,用于当前 Activity 的日志输出标识 */
|
|
||||||
public static final String TAG = "LogActivity";
|
|
||||||
|
|
||||||
/** 日志展示控件(用于加载和显示应用日志) */
|
|
||||||
private LogView mLogView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
// 设置布局文件(包含 LogView 控件)
|
|
||||||
setContentView(R.layout.activity_log);
|
|
||||||
|
|
||||||
// 绑定布局中的 LogView 控件
|
|
||||||
mLogView = findViewById(R.id.logview);
|
|
||||||
// 启动 LogView 日志加载(如实时刷新日志内容)
|
|
||||||
mLogView.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
// 恢复 Activity 时重新启动 LogView(确保日志持续更新)
|
|
||||||
mLogView.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动日志 Activity 的静态方法(外部调用入口)
|
|
||||||
* 配置 Intent 标志,以多任务/分屏模式启动,避免与主应用任务栈冲突
|
|
||||||
* @param context 上下文(Activity/Fragment),用于启动 Activity
|
|
||||||
*/
|
|
||||||
public static void startLogActivity(Context context) {
|
|
||||||
// 创建启动当前 Activity 的 Intent
|
|
||||||
Intent intent = new Intent(context, LogActivity.class);
|
|
||||||
|
|
||||||
// 添加 Intent 标志:支持分屏/多窗口模式(API 24+)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
|
|
||||||
// 添加 Intent 标志:创建新任务栈(避免并入调用者任务栈)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
// 添加 Intent 标志:标记为新文档(多任务窗口中独立显示)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
|
||||||
// 添加 Intent 标志:允许创建多个任务实例(支持多次启动独立窗口)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
|
||||||
|
|
||||||
// 启动 Activity
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,732 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen@QQ.COM
|
|
||||||
* @Date 2024/08/12 13:44:06
|
|
||||||
* @Describe LogUtils
|
|
||||||
* @Describe 应用日志类(补全所有日志重载方法,适配不同调试场景)
|
|
||||||
*/
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
|
||||||
import dalvik.system.DexFile;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
|
||||||
public class LogUtils {
|
|
||||||
|
|
||||||
public static final String TAG = "LogUtils";
|
|
||||||
|
|
||||||
public static enum LOG_LEVEL { Off, Error, Warn, Info, Debug, Verbose }
|
|
||||||
|
|
||||||
static volatile boolean _IsInited = false;
|
|
||||||
static Context _mContext;
|
|
||||||
// 日志显示时间格式
|
|
||||||
static SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("[yyyyMMdd_HHmmss_SSS]", Locale.getDefault());
|
|
||||||
// 应用日志文件夹
|
|
||||||
static File _mfLogCacheDir;
|
|
||||||
static File _mfLogDataDir;
|
|
||||||
// 应用日志文件
|
|
||||||
static File _mfLogCatchFile;
|
|
||||||
static File _mfLogUtilsBeanFile;
|
|
||||||
static LogUtilsBean _mLogUtilsBean;
|
|
||||||
public static Map<String, Boolean> mapTAGList = new HashMap<String, Boolean>();
|
|
||||||
|
|
||||||
//
|
|
||||||
// 初始化函数
|
|
||||||
//
|
|
||||||
public static void init(Context context) {
|
|
||||||
_mContext = context;
|
|
||||||
init(context, LOG_LEVEL.Off);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// 初始化函数
|
|
||||||
//
|
|
||||||
public static void init(Context context, LOG_LEVEL logLevel) {
|
|
||||||
if (GlobalApplication.isDebugging()) {
|
|
||||||
// 初始化日志缓存文件路径(debug模式:外部存储)
|
|
||||||
_mfLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
|
|
||||||
if (!_mfLogCacheDir.exists()) {
|
|
||||||
_mfLogCacheDir.mkdirs();
|
|
||||||
}
|
|
||||||
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
|
|
||||||
|
|
||||||
// 初始化日志配置文件路径
|
|
||||||
_mfLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
|
|
||||||
if (!_mfLogDataDir.exists()) {
|
|
||||||
_mfLogDataDir.mkdirs();
|
|
||||||
}
|
|
||||||
_mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
|
|
||||||
} else {
|
|
||||||
// 初始化日志缓存文件路径(release模式:内部存储)
|
|
||||||
_mfLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
|
|
||||||
if (!_mfLogCacheDir.exists()) {
|
|
||||||
_mfLogCacheDir.mkdirs();
|
|
||||||
}
|
|
||||||
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
|
|
||||||
|
|
||||||
// 初始化日志配置文件路径
|
|
||||||
_mfLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
|
|
||||||
if (!_mfLogDataDir.exists()) {
|
|
||||||
_mfLogDataDir.mkdirs();
|
|
||||||
}
|
|
||||||
_mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
_mLogUtilsBean = LogUtilsBean.loadBeanFromFile(_mfLogUtilsBeanFile.getPath(), LogUtilsBean.class);
|
|
||||||
if (_mLogUtilsBean == null) {
|
|
||||||
_mLogUtilsBean = new LogUtilsBean();
|
|
||||||
_mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载当前应用下的所有类的 TAG
|
|
||||||
addClassTAGList();
|
|
||||||
loadTAGBeanSettings();
|
|
||||||
_IsInited = true;
|
|
||||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, Boolean> getMapTAGList() {
|
|
||||||
return mapTAGList;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void loadTAGBeanSettings() {
|
|
||||||
ArrayList<LogUtilsClassTAGBean> list = new ArrayList<LogUtilsClassTAGBean>();
|
|
||||||
LogUtilsClassTAGBean.loadBeanList(_mContext, list, LogUtilsClassTAGBean.class);
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
LogUtilsClassTAGBean beanSetting = list.get(i);
|
|
||||||
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
|
|
||||||
if (entry.getKey().equals(beanSetting.getTag())) {
|
|
||||||
entry.setValue(beanSetting.getEnable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void saveTAGBeanSettings() {
|
|
||||||
ArrayList<LogUtilsClassTAGBean> list = new ArrayList<LogUtilsClassTAGBean>();
|
|
||||||
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
|
|
||||||
list.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
|
|
||||||
}
|
|
||||||
LogUtilsClassTAGBean.saveBeanList(_mContext, list, LogUtilsClassTAGBean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void addClassTAGList() {
|
|
||||||
try {
|
|
||||||
// 包名前缀(过滤当前应用的类)
|
|
||||||
String packageNamePrefix = "cc.winboll.studio";
|
|
||||||
List<String> classNames = new ArrayList<>();
|
|
||||||
String apkPath = _mContext.getPackageCodePath();
|
|
||||||
LogUtils.d(TAG, String.format("apkPath : %s", apkPath));
|
|
||||||
|
|
||||||
DexFile dexfile = new DexFile(apkPath);
|
|
||||||
int countTemp = 0;
|
|
||||||
Enumeration<String> entries = dexfile.entries();
|
|
||||||
while (entries.hasMoreElements()) {
|
|
||||||
countTemp++;
|
|
||||||
String className = entries.nextElement();
|
|
||||||
if (className.startsWith(packageNamePrefix)) {
|
|
||||||
classNames.add(className);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtils.d(TAG, String.format("countTemp : %d\nClassNames size : %d", countTemp, classNames.size()));
|
|
||||||
|
|
||||||
for (String className : classNames) {
|
|
||||||
try {
|
|
||||||
Class<?> clazz = Class.forName(className);
|
|
||||||
Field[] fields = clazz.getDeclaredFields();
|
|
||||||
for (Field field : fields) {
|
|
||||||
// 过滤静态、公共、String类型的 TAG 字段
|
|
||||||
if (Modifier.isStatic(field.getModifiers())
|
|
||||||
&& Modifier.isPublic(field.getModifiers())
|
|
||||||
&& field.getType() == String.class
|
|
||||||
&& "TAG".equals(field.getName())) {
|
|
||||||
String tagValue = (String) field.get(null);
|
|
||||||
mapTAGList.put(tagValue, false); // 默认禁用,可通过设置开启
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (NoClassDefFoundError | ClassNotFoundException | IllegalAccessException e) {
|
|
||||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setTAGListEnable(String tag, boolean isEnable) {
|
|
||||||
Iterator<Map.Entry<String, Boolean>> iterator = mapTAGList.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Map.Entry<String, Boolean> entry = iterator.next();
|
|
||||||
if (tag.equals(entry.getKey())) {
|
|
||||||
entry.setValue(isEnable);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveTAGBeanSettings();
|
|
||||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setALlTAGListEnable(boolean isEnable) {
|
|
||||||
Iterator<Map.Entry<String, Boolean>> iterator = mapTAGList.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Map.Entry<String, Boolean> entry = iterator.next();
|
|
||||||
entry.setValue(isEnable);
|
|
||||||
}
|
|
||||||
saveTAGBeanSettings();
|
|
||||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setLogLevel(LOG_LEVEL logLevel) {
|
|
||||||
LogUtils._mLogUtilsBean.setLogLevel(logLevel);
|
|
||||||
_mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LOG_LEVEL getLogLevel() {
|
|
||||||
return LogUtils._mLogUtilsBean.getLogLevel();
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
|
|
||||||
if (!_IsInited) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// TAG 未配置或禁用时,不打印日志
|
|
||||||
if (mapTAGList.get(tag) == null || !mapTAGList.get(tag)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 日志级别不符合时,不打印日志
|
|
||||||
if (!isInTheLevel(logLevel)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isInTheLevel(LOG_LEVEL logLevel) {
|
|
||||||
// 当前日志级别 >= 目标级别时,允许打印(级别顺序:Off < Error < Warn < Info < Debug < Verbose)
|
|
||||||
return LogUtils._mLogUtilsBean.getLogLevel().ordinal() >= logLevel.ordinal();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// 获取应用日志文件夹
|
|
||||||
//
|
|
||||||
public static File getLogCacheDir() {
|
|
||||||
return _mfLogCacheDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 补全所有日志重载方法(Error 级别) =================================
|
|
||||||
/**
|
|
||||||
* Error 级别日志(仅消息)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void e(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Error, szMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error 级别日志(消息 + 异常)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param e 异常对象
|
|
||||||
*/
|
|
||||||
public static void e(String szTAG, String szMessage, Exception e) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error 级别日志(仅异常)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param e 异常对象
|
|
||||||
*/
|
|
||||||
public static void e(String szTAG, Exception e) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
|
|
||||||
String message = "【异常信息】: " + getExceptionInfo(e);
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Error, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error 级别日志(消息 + 异常 + 堆栈)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param e 异常对象
|
|
||||||
* @param listStackTrace 堆栈信息
|
|
||||||
*/
|
|
||||||
public static void e(String szTAG, String szMessage, Exception e, StackTraceElement[] listStackTrace) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
|
|
||||||
sb.append("\n【堆栈信息】: ").append(getStackTraceInfo(listStackTrace));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 补全所有日志重载方法(Warn 级别) =================================
|
|
||||||
/**
|
|
||||||
* Warn 级别日志(仅消息)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void w(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, szMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warn 级别日志(消息 + 异常)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param e 异常对象
|
|
||||||
*/
|
|
||||||
public static void w(String szTAG, String szMessage, Exception e) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warn 级别日志(仅异常)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param e 异常对象
|
|
||||||
*/
|
|
||||||
public static void w(String szTAG, Exception e) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
|
|
||||||
String message = "【异常信息】: " + getExceptionInfo(e);
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 补全所有日志重载方法(Info 级别) =================================
|
|
||||||
/**
|
|
||||||
* Info 级别日志(仅消息)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void i(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Info, szMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Info 级别日志(消息 + 数据对象)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param obj 数据对象(自动转为字符串)
|
|
||||||
*/
|
|
||||||
public static void i(String szTAG, String szMessage, Object obj) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
|
|
||||||
String objStr = obj == null ? "null" : obj.toString();
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【数据对象】: ").append(objStr);
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Info, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 补全所有日志重载方法(Debug 级别) =================================
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(仅消息)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, szMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(消息 + 堆栈)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param listStackTrace 堆栈信息
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, String szMessage, StackTraceElement[] listStackTrace) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
StringBuilder sbMessage = new StringBuilder(szMessage);
|
|
||||||
sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(消息 + 异常)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param e 异常对象
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, String szMessage, Exception e) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(异常 + 堆栈)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param e 异常对象
|
|
||||||
* @param listStackTrace 堆栈信息
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, Exception e, StackTraceElement[] listStackTrace) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
StringBuilder sbMessage = new StringBuilder();
|
|
||||||
sbMessage.append("【异常信息】: ").append(getExceptionInfo(e));
|
|
||||||
sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace));
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(消息 + 数据对象)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param obj 数据对象(自动转为字符串)
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, String szMessage, Object obj) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
String objStr = obj == null ? "null" : obj.toString();
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【数据对象】: ").append(objStr);
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug 级别日志(仅数据对象)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param obj 数据对象(自动转为字符串)
|
|
||||||
*/
|
|
||||||
public static void d(String szTAG, Object obj) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
String objStr = obj == null ? "null" : obj.toString();
|
|
||||||
String message = "【数据对象】: " + objStr;
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 补全所有日志重载方法(Verbose 级别) =================================
|
|
||||||
/**
|
|
||||||
* Verbose 级别日志(仅消息)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void v(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, szMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verbose 级别日志(消息 + 数据对象)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param obj 数据对象(自动转为字符串)
|
|
||||||
*/
|
|
||||||
public static void v(String szTAG, String szMessage, Object obj) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
|
|
||||||
String objStr = obj == null ? "null" : obj.toString();
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【数据对象】: ").append(objStr);
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verbose 级别日志(仅数据对象)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param obj 数据对象(自动转为字符串)
|
|
||||||
*/
|
|
||||||
public static void v(String szTAG, Object obj) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
|
|
||||||
String objStr = obj == null ? "null" : obj.toString();
|
|
||||||
String message = "【数据对象】: " + objStr;
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 新增:通用日志工具方法(补充调试能力) =================================
|
|
||||||
/**
|
|
||||||
* 打印当前线程信息(Debug 级别)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
public static void printThreadInfo(String szTAG, String szMessage) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
|
||||||
Thread currentThread = Thread.currentThread();
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【线程信息】: ")
|
|
||||||
.append("线程名=").append(currentThread.getName())
|
|
||||||
.append(", 线程ID=").append(currentThread.getId())
|
|
||||||
.append(", 线程状态=").append(currentThread.getState().name());
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打印 Map 数据(Debug 级别,格式化输出,便于查看)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param map 要打印的 Map 数据
|
|
||||||
*/
|
|
||||||
public static <K, V> void printMap(String szTAG, String szMessage, Map<K, V> map) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && map != null) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【Map 数据】(size=").append(map.size()).append("):");
|
|
||||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
|
||||||
String keyStr = entry.getKey() == null ? "null" : entry.getKey().toString();
|
|
||||||
String valueStr = entry.getValue() == null ? "null" : entry.getValue().toString();
|
|
||||||
sb.append("\n ").append(keyStr).append(" = ").append(valueStr);
|
|
||||||
}
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打印 List 数据(Debug 级别,格式化输出)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
* @param list 要打印的 List 数据
|
|
||||||
*/
|
|
||||||
public static <T> void printList(String szTAG, String szMessage, List<T> list) {
|
|
||||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && list != null) {
|
|
||||||
StringBuilder sb = new StringBuilder(szMessage);
|
|
||||||
sb.append("\n【List 数据】(size=").append(list.size()).append("):");
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
T item = list.get(i);
|
|
||||||
String itemStr = item == null ? "null" : item.toString();
|
|
||||||
sb.append("\n 索引").append(i).append(" = ").append(itemStr);
|
|
||||||
}
|
|
||||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 私有工具方法(异常/堆栈信息格式化) =================================
|
|
||||||
/**
|
|
||||||
* 格式化异常信息(提取异常类型、消息、简化堆栈)
|
|
||||||
* @param e 异常对象
|
|
||||||
* @return 格式化后的异常字符串
|
|
||||||
*/
|
|
||||||
private static String getExceptionInfo(Exception e) {
|
|
||||||
if (e == null) {
|
|
||||||
return "异常对象为null";
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
// 异常类型 + 异常消息
|
|
||||||
sb.append(e.getClass().getSimpleName()).append(" : ").append(e.getMessage() == null ? "无异常消息" : e.getMessage());
|
|
||||||
// 简化堆栈(取前5行,避免日志过长)
|
|
||||||
StackTraceElement[] stackTrace = e.getStackTrace();
|
|
||||||
if (stackTrace != null && stackTrace.length > 0) {
|
|
||||||
sb.append("\n 简化堆栈(前5行):");
|
|
||||||
int limit = Math.min(stackTrace.length, 5);
|
|
||||||
for (int i = 0; i < limit; i++) {
|
|
||||||
sb.append("\n ").append(stackTrace[i].toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化堆栈信息(提取关键调用链路)
|
|
||||||
* @param stackTrace 堆栈数组
|
|
||||||
* @return 格式化后的堆栈字符串
|
|
||||||
*/
|
|
||||||
private static String getStackTraceInfo(StackTraceElement[] stackTrace) {
|
|
||||||
if (stackTrace == null || stackTrace.length == 0) {
|
|
||||||
return "堆栈信息为空";
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
// 过滤 LogUtils 内部调用,取真实业务调用链路(前8行)
|
|
||||||
int count = 0;
|
|
||||||
for (StackTraceElement element : stackTrace) {
|
|
||||||
// 跳过 LogUtils 自身的堆栈(避免冗余)
|
|
||||||
if (element.getClassName().contains("cc.winboll.studio.libappbase.LogUtils")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sb.append("\n ").append(element.getClassName()).append(".")
|
|
||||||
.append(element.getMethodName()).append("(")
|
|
||||||
.append(element.getFileName()).append(":").append(element.getLineNumber()).append(")");
|
|
||||||
count++;
|
|
||||||
if (count >= 8) { // 限制堆栈长度,避免日志过大
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= 原有核心方法(保留并优化) =================================
|
|
||||||
/**
|
|
||||||
* 日志文件保存函数(优化:增加异常捕获完整性,避免流泄漏)
|
|
||||||
* @param szTAG 标签
|
|
||||||
* @param logLevel 日志级别
|
|
||||||
* @param szMessage 日志消息
|
|
||||||
*/
|
|
||||||
static void saveLog(String szTAG, LogUtils.LOG_LEVEL logLevel, String szMessage) {
|
|
||||||
BufferedWriter out = null;
|
|
||||||
try {
|
|
||||||
// 确保日志文件存在(创建父目录 + 文件)
|
|
||||||
if (!_mfLogCatchFile.exists()) {
|
|
||||||
File parentDir = _mfLogCatchFile.getParentFile();
|
|
||||||
if (parentDir != null && !parentDir.exists()) {
|
|
||||||
parentDir.mkdirs();
|
|
||||||
}
|
|
||||||
_mfLogCatchFile.createNewFile();
|
|
||||||
}
|
|
||||||
// 追加写入日志(UTF-8编码,避免中文乱码)
|
|
||||||
out = new BufferedWriter(new OutputStreamWriter(
|
|
||||||
new FileOutputStream(_mfLogCatchFile, true), "UTF-8"));
|
|
||||||
String logLine = "[" + logLevel + "] "
|
|
||||||
+ mSimpleDateFormat.format(System.currentTimeMillis())
|
|
||||||
+ " [" + szTAG + "]\n"
|
|
||||||
+ szMessage + "\n\n"; // 增加空行,区分不同日志
|
|
||||||
out.write(logLine);
|
|
||||||
out.flush(); // 强制刷新,确保日志及时写入
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 日志写入失败时,打印系统日志(避免递归调用)
|
|
||||||
android.util.Log.e(TAG, "日志写入失败: " + e.getMessage());
|
|
||||||
} finally {
|
|
||||||
// 关闭流,避免资源泄漏(Java 7 手动关闭,无 try-with-resources)
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
android.util.Log.e(TAG, "流关闭失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 历史日志加载函数(优化:增加流关闭,避免内存泄漏)
|
|
||||||
* @return 日志内容字符串(空串表示无日志)
|
|
||||||
*/
|
|
||||||
public static String loadLog() {
|
|
||||||
if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) {
|
|
||||||
return "日志文件不存在";
|
|
||||||
}
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
BufferedReader in = null;
|
|
||||||
try {
|
|
||||||
in = new BufferedReader(new InputStreamReader(
|
|
||||||
new FileInputStream(_mfLogCatchFile), "UTF-8"));
|
|
||||||
String line = "";
|
|
||||||
while ((line = in.readLine()) != null) {
|
|
||||||
sb.append(line);
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
sb.append("日志加载失败: ").append(e.getMessage());
|
|
||||||
LogUtils.e(TAG, "日志加载异常", e);
|
|
||||||
} finally {
|
|
||||||
// 关闭流,避免资源泄漏
|
|
||||||
if (in != null) {
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.e(TAG, "流关闭异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理日志函数(优化:支持清空日志,避免文件过大)
|
|
||||||
*/
|
|
||||||
public static void cleanLog() {
|
|
||||||
if (_mfLogCatchFile == null) {
|
|
||||||
LogUtils.d(TAG, "日志文件未初始化,无需清理");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 清空文件内容(覆盖写入空字符串)
|
|
||||||
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
|
|
||||||
new FileOutputStream(_mfLogCatchFile, false), "UTF-8"));
|
|
||||||
out.write("");
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
LogUtils.d(TAG, "日志已清空,文件路径: " + _mfLogCatchFile.getPath());
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.e(TAG, "日志清空失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查日志文件大小(新增:避免日志文件过大占用内存)
|
|
||||||
* @param maxSizeMB 最大允许大小(MB)
|
|
||||||
* @return true:文件超过限制;false:文件大小正常
|
|
||||||
*/
|
|
||||||
public static boolean checkLogFileSize(int maxSizeMB) {
|
|
||||||
if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 转换为字节(1MB = 1024*1024 字节)
|
|
||||||
long maxSizeByte = maxSizeMB * 1024 * 1024;
|
|
||||||
long fileSize = _mfLogCatchFile.length();
|
|
||||||
LogUtils.d(TAG, String.format("日志文件大小: %.2f MB(限制: %d MB)",
|
|
||||||
fileSize / (1024.0 * 1024), maxSizeMB));
|
|
||||||
return fileSize > maxSizeByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化检查(新增:快速判断 LogUtils 是否初始化完成)
|
|
||||||
* @return true:已初始化;false:未初始化
|
|
||||||
*/
|
|
||||||
public static boolean isInited() {
|
|
||||||
return _IsInited;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示短提示(新增:日志+Toast联动,调试时快速提示)
|
|
||||||
* @param context 上下文
|
|
||||||
* @param message 提示内容
|
|
||||||
*/
|
|
||||||
public static void showShortToast(final Context context, final String message) {
|
|
||||||
if (context == null || message == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 主线程显示Toast,避免子线程崩溃
|
|
||||||
if (Thread.currentThread().getId() == android.os.Process.myTid()) {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
((android.app.Activity) context).runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 同时写入日志
|
|
||||||
LogUtils.d(TAG, "Toast提示: " + message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.util.JsonReader;
|
|
||||||
import android.util.JsonWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2024/08/23 15:39:07
|
|
||||||
* @Describe LogUtils 配置数据模型(继承 BaseBean,实现 JSON 序列化/反序列化)
|
|
||||||
* 封装 LogUtils 的核心配置参数(当前仅日志级别),用于配置的持久化存储与读取
|
|
||||||
*/
|
|
||||||
public class LogUtilsBean extends BaseBean {
|
|
||||||
|
|
||||||
/** 当前类的日志 TAG(用于调试输出) */
|
|
||||||
public static final String TAG = "LogUtilsBean";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局日志级别(默认值:Off,即不输出任何日志)
|
|
||||||
* 关联 LogUtils.LOG_LEVEL 枚举,存储日志输出的级别阈值
|
|
||||||
*/
|
|
||||||
private LogUtils.LOG_LEVEL logLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 无参构造方法(默认初始化日志级别为 Off)
|
|
||||||
* 用于 JSON 反序列化时的实例创建
|
|
||||||
*/
|
|
||||||
public LogUtilsBean() {
|
|
||||||
this.logLevel = LogUtils.LOG_LEVEL.Off;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有参构造方法(指定初始日志级别)
|
|
||||||
* @param logLevel 初始日志级别(如 LogUtils.LOG_LEVEL.Debug)
|
|
||||||
*/
|
|
||||||
public LogUtilsBean(LogUtils.LOG_LEVEL logLevel) {
|
|
||||||
this.logLevel = logLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置日志级别(更新配置时使用)
|
|
||||||
* @param logLevel 目标日志级别
|
|
||||||
*/
|
|
||||||
public void setLogLevel(LogUtils.LOG_LEVEL logLevel) {
|
|
||||||
this.logLevel = logLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前日志级别(读取配置时使用)
|
|
||||||
* @return 当前配置的日志级别
|
|
||||||
*/
|
|
||||||
public LogUtils.LOG_LEVEL getLogLevel() {
|
|
||||||
return logLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别)
|
|
||||||
* @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsBean")
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return LogUtilsBean.class.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:将当前配置对象序列化为 JSON(持久化存储时调用)
|
|
||||||
* 序列化字段:logLevel(存储枚举的 ordinal 值,确保反序列化一致性)
|
|
||||||
* @param jsonWriter JSON 写入器(用于输出 JSON 数据)
|
|
||||||
* @throws IOException JSON 写入异常(如流关闭、格式错误)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
// 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理)
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 序列化日志级别:存储枚举的索引值(如 Off=0、Error=1...),比存储名称更高效
|
|
||||||
jsonWriter.name("logLevel").value(this.getLogLevel().ordinal());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用)
|
|
||||||
* 解析字段:logLevel(通过索引值恢复 LogUtils.LOG_LEVEL 枚举)
|
|
||||||
* @param jsonReader JSON 读取器(用于读取 JSON 数据)
|
|
||||||
* @param name JSON 字段名(当前解析的字段)
|
|
||||||
* @return true:字段解析成功;false:字段不匹配(需父类处理或跳过)
|
|
||||||
* @throws IOException JSON 读取异常(如字段类型不匹配、流中断)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 先让父类处理公共字段,处理成功则直接返回
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 解析当前类专属字段
|
|
||||||
if ("logLevel".equals(name)) {
|
|
||||||
// 通过枚举索引值恢复枚举实例(确保与序列化时的 ordinal 对应)
|
|
||||||
int levelOrdinal = jsonReader.nextInt();
|
|
||||||
this.setLogLevel(LogUtils.LOG_LEVEL.values()[levelOrdinal]);
|
|
||||||
} else {
|
|
||||||
// 字段不匹配,返回 false 表示需要跳过该字段
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 字段解析成功
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法)
|
|
||||||
* 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理
|
|
||||||
* @param jsonReader JSON 读取器(传入待解析的 JSON 流)
|
|
||||||
* @return 解析后的当前 LogUtilsBean 实例(支持链式调用)
|
|
||||||
* @throws IOException JSON 解析异常(如格式错误、字段缺失)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
// 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应)
|
|
||||||
jsonReader.beginObject();
|
|
||||||
// 遍历 JSON 中的所有字段
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String fieldName = jsonReader.nextName();
|
|
||||||
// 解析字段,若字段不匹配则跳过该值(避免解析失败)
|
|
||||||
if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 结束 JSON 对象解析(必须调用,否则会导致流异常)
|
|
||||||
jsonReader.endObject();
|
|
||||||
// 返回当前实例,支持链式调用(如 new LogUtilsBean().readBeanFromJsonReader(reader))
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package cc.winboll.studio.libappbase;
|
|
||||||
|
|
||||||
import android.util.JsonReader;
|
|
||||||
import android.util.JsonWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2025/01/04 14:17:02
|
|
||||||
* @Describe 日志 TAG 过滤配置模型(继承 BaseBean,实现 JSON 序列化/反序列化)
|
|
||||||
* 封装单个日志 TAG 的名称及其启用状态,用于 LogUtils 的 TAG 过滤规则持久化存储与读取
|
|
||||||
*/
|
|
||||||
public class LogUtilsClassTAGBean extends BaseBean {
|
|
||||||
|
|
||||||
/** 当前类的日志 TAG(用于调试输出) */
|
|
||||||
public static final String TAG = "LogUtilsClassTAGBean";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志 TAG 名称(如 "LogViewThread"、"ToastUtils")
|
|
||||||
* 与 LogUtils 中扫描的应用内 TAG 一一对应
|
|
||||||
*/
|
|
||||||
private String tag;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TAG 启用状态(控制该 TAG 的日志是否输出)
|
|
||||||
* true:启用(输出该 TAG 的日志);false:禁用(不输出该 TAG 的日志)
|
|
||||||
*/
|
|
||||||
private Boolean enable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 无参构造方法(默认初始化:TAG 为当前类 TAG,启用状态为 true)
|
|
||||||
* 用于 JSON 反序列化时的实例创建,或默认配置生成
|
|
||||||
*/
|
|
||||||
public LogUtilsClassTAGBean() {
|
|
||||||
this.tag = TAG; // 默认 TAG 为当前类的 TAG
|
|
||||||
this.enable = true; // 默认启用该 TAG 的日志输出
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有参构造方法(指定 TAG 名称和启用状态)
|
|
||||||
* 用于主动创建 TAG 过滤配置实例
|
|
||||||
* @param tag 日志 TAG 名称
|
|
||||||
* @param enable TAG 启用状态(true/false)
|
|
||||||
*/
|
|
||||||
public LogUtilsClassTAGBean(String tag, Boolean enable) {
|
|
||||||
this.tag = tag;
|
|
||||||
this.enable = enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置日志 TAG 名称
|
|
||||||
* @param tag 目标 TAG 名称
|
|
||||||
*/
|
|
||||||
public void setTag(String tag) {
|
|
||||||
this.tag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志 TAG 名称
|
|
||||||
* @return 当前配置的 TAG 名称
|
|
||||||
*/
|
|
||||||
public String getTag() {
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 TAG 启用状态
|
|
||||||
* @param enable 目标启用状态(true:启用;false:禁用)
|
|
||||||
*/
|
|
||||||
public void setEnable(Boolean enable) {
|
|
||||||
this.enable = enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 TAG 启用状态
|
|
||||||
* @return 当前 TAG 的启用状态
|
|
||||||
*/
|
|
||||||
public Boolean getEnable() {
|
|
||||||
return enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别)
|
|
||||||
* @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsClassTAGBean")
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return LogUtilsClassTAGBean.class.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:将当前 TAG 配置对象序列化为 JSON(持久化存储时调用)
|
|
||||||
* 序列化字段:tag(TAG 名称)、enable(启用状态)
|
|
||||||
* @param jsonWriter JSON 写入器(用于输出 JSON 数据)
|
|
||||||
* @throws IOException JSON 写入异常(如流关闭、格式错误)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
// 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理)
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 序列化 TAG 名称
|
|
||||||
jsonWriter.name("tag").value(this.getTag());
|
|
||||||
// 序列化启用状态
|
|
||||||
jsonWriter.name("enable").value(this.getEnable());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用)
|
|
||||||
* 解析字段:tag(TAG 名称)、enable(启用状态)
|
|
||||||
* @param jsonReader JSON 读取器(用于读取 JSON 数据)
|
|
||||||
* @param name JSON 字段名(当前解析的字段)
|
|
||||||
* @return true:字段解析成功;false:字段不匹配(需父类处理或跳过)
|
|
||||||
* @throws IOException JSON 读取异常(如字段类型不匹配、流中断)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 先让父类处理公共字段,处理成功则直接返回
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 解析当前类专属字段
|
|
||||||
if ("tag".equals(name)) {
|
|
||||||
// 读取 TAG 名称并设置
|
|
||||||
this.setTag(jsonReader.nextString());
|
|
||||||
} else if ("enable".equals(name)) {
|
|
||||||
// 读取启用状态并设置
|
|
||||||
this.setEnable(jsonReader.nextBoolean());
|
|
||||||
} else {
|
|
||||||
// 字段不匹配,返回 false 表示需要跳过该字段
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 字段解析成功
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法)
|
|
||||||
* 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理
|
|
||||||
* @param jsonReader JSON 读取器(传入待解析的 JSON 流)
|
|
||||||
* @return 解析后的当前 LogUtilsClassTAGBean 实例(支持链式调用)
|
|
||||||
* @throws IOException JSON 解析异常(如格式错误、字段缺失)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
// 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应)
|
|
||||||
jsonReader.beginObject();
|
|
||||||
// 遍历 JSON 中的所有字段
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String fieldName = jsonReader.nextName();
|
|
||||||
// 解析字段,若字段不匹配则跳过该值(避免解析失败)
|
|
||||||
if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 结束 JSON 对象解析(必须调用,否则会导致流异常)
|
|
||||||
jsonReader.endObject();
|
|
||||||
// 返回当前实例,支持链式调用(如 new LogUtilsClassTAGBean().readBeanFromJsonReader(reader))
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||