Compare commits
81 Commits
powerbell-
...
gallery-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| e62a907378 | |||
| c85ba8324b | |||
| 3e49c33bc1 | |||
| 3aab93cc4d | |||
| 90d8330798 | |||
| b01482470a | |||
| 57c36b09ac | |||
| b5d68dfd1e | |||
| 262d35fb4e | |||
| 5b0bb599bb | |||
| 3955de100d | |||
| 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 |
236
README.md
@@ -1,175 +1,103 @@
|
||||
## OriginMaster
|
||||
【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 源生态计划项目说明书
|
||||
|
||||
########
|
||||
## ☁ ☁ ☁ 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 项目组研发计划
|
||||
致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。
|
||||
也在探索 https://gitea.winboll.cc/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP APK 分发。
|
||||
更想进阶 https://github.com/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP Beta APK 分发。
|
||||
### 1. 核心定位
|
||||
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
|
||||
|
||||
## WinBoLL-APP 汗下...
|
||||
#### ☁应用何置如此呢。且观用户云云。
|
||||
### 2. 仓库架构
|
||||
#### **仓库类型:功能说明**
|
||||
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
|
||||
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
|
||||
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
|
||||
|
||||
#### ☁ 正当下 ☁ ###
|
||||
#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景
|
||||
### ☁ WinBoLL 设备资源概述
|
||||
#### ☁ 1. Raid Disk.
|
||||
概述:这是一个矩阵存储类设备。
|
||||
优点:该设备具有数据容错存储功能,
|
||||
数据存储具有特长持久性。
|
||||
缺点:设备使用能源消耗比较高,
|
||||
设备存取速度一般。
|
||||
|
||||
#### ☁ 2. Data Disk.
|
||||
概述:这是一个普通硬盘存储设备
|
||||
优点:该设备独立于操作系统,
|
||||
数据持久性一般,
|
||||
存取能源消耗小于 Raid Disk。
|
||||
缺点:数据存储速度一般,存储能源消耗一般。
|
||||
### 3. 源码合并管理推送路线图
|
||||
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
|
||||
|
||||
#### ☁ 3. SSD Disk.
|
||||
概述:这是一个 SSD 硬盘存储设备。
|
||||
优点:存取速度快于 Data Disk 与 Raid Disk,
|
||||
存取能源消耗小于 Data Disk 与 Raid Disk。
|
||||
缺点:数据持久性一般,
|
||||
设备位于操作系统内部文件系统。
|
||||
数据持久性与操作系统挂钩。
|
||||
|
||||
#### ☁ 4. WinBoLL 用户资源概述。
|
||||
1> /home/<用户名> 位于 WinBoLL 操作系统目录下。
|
||||
2> /rdisk/<用户名> 挂载用户 Raid Disk.
|
||||
3> /data/<用户名> 挂载用户 Data Disk.
|
||||
4> /sdcard/<用户名> 挂载用户 SSD Disk.
|
||||
★ WinBoLL → APPBase → OriginMaster
|
||||
★ WinBoLL → AES → OriginMaster
|
||||
★ WinBoLL → PowerBell → OriginMaster
|
||||
★ WinBoLL → Positions → OriginMaster
|
||||
|
||||
#### ☁ 5. WinBoLL-APP 用户资源概述。
|
||||
1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0
|
||||
## 二、WinBoLL 项目核心信息
|
||||
|
||||
### ☁ 稍稍歇 ☁ ###
|
||||
### ☁ 急急停 ☁ WinBoLL 应用前置条件
|
||||
☁ 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 项目已设置)
|
||||
### 1. 项目简介
|
||||
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
|
||||
|
||||
### ☁ 登高处 ☁ WinBoLL 应用需求规划
|
||||
☁ WinBoLL 主机建立 WinBoLL 客户端用户数据库为 MySQL winbollclient 数据库。
|
||||
☁ WinBoLL 主机设置 WinBoLL 客户端用户信息存储在 winbollclient 数据库中。
|
||||
☁ MySQL winbollclient 数据库中
|
||||
WinBoLL 客户端用户信息设定为:
|
||||
<用户名, 验证密码, 验证邮箱, 验证手机, 唯一存储令牌Token, 备用验证邮箱>。
|
||||
☁ 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 应用已提供)
|
||||
### 2. 官方资源
|
||||
#### ☆ 官方网站**:https://www.winboll.cc/
|
||||
#### ☆ 源码地址:
|
||||
★ Gitea:https://gitea.winboll.cc/Studio/WinBoLL.git
|
||||
★ GitHub:https://github.com/ZhanGSKen/WinBoLL.git
|
||||
★ 码云:https://gitee.com/zhangsken/winboll.git
|
||||
|
||||
### ☁ 看远方 ☁ ###
|
||||
### ☁ 心忧虑 ☁ WinBoLL-APP 应用前置需求
|
||||
☁ WinBoLL-APP WinBoLL 项目根目录设定为手机的 /sdcard/WinBoLLStudio/Sources 目录。(需要用户手动建立文件夹)
|
||||
☁ 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>。
|
||||
## 三、应用编译环境检查问题
|
||||
### 核心判断条件:
|
||||
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件,因为编译输出的APK文件需要一个可供保存的环境。
|
||||
|
||||
### ☁ 云游四方 ☁ ###
|
||||
### ☁ 呔! ☁ WinBoLL-APP 应用需求规划
|
||||
☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。
|
||||
☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。
|
||||
☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。
|
||||
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
|
||||
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
|
||||
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
|
||||
|
||||
## 四、前置条件
|
||||
|
||||
### ☁ 吁! ☁ WinBoLL-APP 共享计划前景
|
||||
☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。
|
||||
☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。
|
||||
### 1. WinBoLL APP 开发环境配置介绍
|
||||
#### WinBoLL APK 编译输出内容包括:
|
||||
☆ "/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 项目组注册登录功能。
|
||||
## 五、核心需求规划
|
||||
|
||||
# 本项目要实际运用需要注意以下几个步骤:
|
||||
# 在项目根目录下:
|
||||
## ★. 项目模块编译环境设置(必须),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 文件额外输出路径。
|
||||
### 1. WinBoLL 应用安全验证需求
|
||||
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
|
||||
|
||||
### 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调试使用:
|
||||
$ bash gradlew assembleStageDebug
|
||||
### (1)类库型模块配置要点
|
||||
#### 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` 属性配置的目录下。
|
||||
|
||||
# 应用版本号命名方式
|
||||
## statge 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||
如:APPBase_15.7.0
|
||||
## beta 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||
如:APPBase_15.9.6-beta8_5413
|
||||
### (3)手机端应用调试命令介绍
|
||||
#### ☆ Beta 渠道调试命令
|
||||
$bash gradlew assembleBetaDebug
|
||||
|
||||
#### ☆ Stage 渠道调试命令
|
||||
$bash gradlew assembleStageDebug
|
||||
|
||||
### (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 )
|
||||
|
||||
44
build.gradle
@@ -1,6 +1,15 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
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 {
|
||||
// 设置本地Maven仓库路径
|
||||
url 'file:///sdcard/.m2/repository/'
|
||||
@@ -11,19 +20,6 @@ buildscript {
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||
// "WinBoLL Snapshot"
|
||||
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 {
|
||||
// 适配MIUI12
|
||||
@@ -35,6 +31,15 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
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 {
|
||||
// 设置本地Maven仓库路径
|
||||
url 'file:///sdcard/.m2/repository/'
|
||||
@@ -45,19 +50,6 @@ allprojects {
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||
// "WinBoLL Snapshot"
|
||||
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 {
|
||||
// 定义全局变量,常用于版本管理
|
||||
|
||||
34
gallery/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Gallery
|
||||
|
||||
#### 介绍
|
||||
云宝相册应用
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||
|
||||
#### 使用说明
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
@@ -18,19 +18,20 @@ def genVersionName(def versionName){
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.powerbell"
|
||||
minSdkVersion 21
|
||||
applicationId "cc.winboll.studio.gallery"
|
||||
minSdkVersion 23
|
||||
// 适配MIUI12
|
||||
targetSdkVersion 30
|
||||
versionCode 7
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.15"
|
||||
versionName "15.0"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
@@ -40,27 +41,21 @@ android {
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//api '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'
|
||||
|
||||
// uCrop 核心依赖(最新稳定版)
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||
// 兼容AndroidX(若项目用AndroidX,必须添加)
|
||||
//implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
// Html 解析
|
||||
@@ -68,25 +63,57 @@ dependencies {
|
||||
// 二维码类库
|
||||
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'
|
||||
// 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 '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.2'
|
||||
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||
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.2'
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
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 23:52:51 HKT 2026
|
||||
stageCount=7
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.7
|
||||
@@ -67,12 +67,6 @@
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
# ============================== 必要补充规则 ==============================
|
||||
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||
-keep class okhttp3.internal.concurrent.** { *; }
|
||||
-keep class okhttp3.internal.connection.** { *; }
|
||||
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||
-dontwarn okhttp3.internal.connection.RealCall
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
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>
|
||||
66
gallery/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.gallery">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".GlobalWinBoLLApplication"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".AlbumActivity"
|
||||
android:label="@string/app_name"/>
|
||||
|
||||
<activity
|
||||
android:name=".ImageViewerActivity"
|
||||
android:label="@string/app_name"/>
|
||||
|
||||
<activity
|
||||
android:name=".TrashActivity"
|
||||
android:label="@string/trash"/>
|
||||
|
||||
<activity
|
||||
android:name=".CropActivity"
|
||||
android:label="调整封面"/>
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/about"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,51 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||
import cc.winboll.studio.libappbase.views.AboutView;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
AboutView aboutView = findViewById(R.id.aboutview);
|
||||
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||
}
|
||||
|
||||
private APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "gallery";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("Gallery");
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery");
|
||||
appInfo.setAppAPKName("Gallery");
|
||||
appInfo.setAppAPKFolderName("Gallery");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
}
|
||||
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,251 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener;
|
||||
|
||||
public class AlbumActivity extends AppCompatActivity {
|
||||
public static final String TAG = "AlbumActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 101;
|
||||
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||
public static final String EXTRA_ALBUM_NAME = "album_name";
|
||||
private RecyclerView recyclerView;
|
||||
private ImageAdapter adapter;
|
||||
private String albumPath;
|
||||
private String albumName;
|
||||
private FloatingActionButton fabScrollTop;
|
||||
private Preferences prefs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
|
||||
|
||||
getSupportActionBar().setTitle(albumName);
|
||||
|
||||
prefs = new Preferences(this);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||
adapter = new ImageAdapter();
|
||||
adapter.setContext(this);
|
||||
adapter.setAlbumPath(albumPath);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
||||
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
});
|
||||
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstVisible > 0) {
|
||||
fabScrollTop.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
fabScrollTop.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
adapter.setOnImageClickListener(new OnImageClickListener() {
|
||||
@Override
|
||||
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
|
||||
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
|
||||
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
|
||||
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
if (checkPermission()) {
|
||||
loadImages();
|
||||
} else {
|
||||
requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestPermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadImages();
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getSortOrder(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case Preferences.SORT_TIME_ASC:
|
||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
|
||||
case Preferences.SORT_NAME_DESC:
|
||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
|
||||
case Preferences.SORT_NAME_ASC:
|
||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
|
||||
case Preferences.SORT_TIME_DESC:
|
||||
default:
|
||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||
}
|
||||
}
|
||||
|
||||
private void loadImages() {
|
||||
LogUtils.d(TAG, "loadImages");
|
||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
ArrayList<String> imagePaths = new ArrayList<>();
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
|
||||
String[] selectionArgs = new String[]{albumPath + "%"};
|
||||
int sortMode = prefs.getAlbumSortMode();
|
||||
String sortOrder = getSortOrder(sortMode);
|
||||
|
||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||
if (cursor != null) {
|
||||
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(dataColumn);
|
||||
if (path != null && path.startsWith(albumPath + "/")) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
|
||||
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
||||
imageUrls.add(contentUri);
|
||||
imagePaths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrls.isEmpty()) {
|
||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.i(TAG, "No images found");
|
||||
}
|
||||
adapter.setData(imageUrls, imagePaths);
|
||||
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_album, menu);
|
||||
int sortMode = prefs.getAlbumSortMode();
|
||||
int menuId = getSortMenuId(sortMode);
|
||||
MenuItem item = menu.findItem(menuId);
|
||||
if (item != null) {
|
||||
item.setChecked(true);
|
||||
}
|
||||
MenuItem sortItem = menu.findItem(R.id.action_sort);
|
||||
if (sortItem != null && sortItem.getSubMenu() != null) {
|
||||
sortItem.getSubMenu().setGroupCheckable(0, true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int getSortMenuId(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case Preferences.SORT_TIME_ASC:
|
||||
return R.id.sort_time_asc;
|
||||
case Preferences.SORT_NAME_DESC:
|
||||
return R.id.sort_name_desc;
|
||||
case Preferences.SORT_NAME_ASC:
|
||||
return R.id.sort_name_asc;
|
||||
case Preferences.SORT_TIME_DESC:
|
||||
default:
|
||||
return R.id.sort_time_desc;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.sort_time_desc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_time_asc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_name_desc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_name_asc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (checkPermission()) {
|
||||
loadImages();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == 100 && resultCode == RESULT_OK) {
|
||||
loadImages();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
|
||||
public static final String TAG = "AlbumAdapter";
|
||||
private ArrayList<Album> albums = new ArrayList<>();
|
||||
private OnAlbumClickListener listener;
|
||||
private Preferences prefs;
|
||||
private int bgType = 0;
|
||||
private PinnedAlbumDbHelper pinnedDbHelper;
|
||||
private OnCoverSizeListener coverSizeListener;
|
||||
private boolean coverSizeReported = false;
|
||||
|
||||
private int getBgRes() {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
return R.drawable.bg_checkerboard;
|
||||
case 1:
|
||||
return R.drawable.bg_white;
|
||||
case 2:
|
||||
return R.drawable.bg_black;
|
||||
default:
|
||||
return R.drawable.bg_checkerboard;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnAlbumClickListener {
|
||||
void onAlbumClick(Album album);
|
||||
}
|
||||
|
||||
public interface OnCoverSizeListener {
|
||||
void onCoverSize(int width, int height);
|
||||
}
|
||||
|
||||
public static final int DEFAULT_COVER_WIDTH = 240;
|
||||
public static final int DEFAULT_COVER_HEIGHT = 120;
|
||||
|
||||
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
|
||||
this.coverSizeListener = listener;
|
||||
}
|
||||
|
||||
public void setData(ArrayList<Album> albums) {
|
||||
this.albums = sortAlbums(albums);
|
||||
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
|
||||
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : list) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
pinned.addAll(unpinned);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void setContext(android.content.Context context) {
|
||||
prefs = new Preferences(context);
|
||||
bgType = prefs.getBgType();
|
||||
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
|
||||
}
|
||||
|
||||
public void refreshBg() {
|
||||
if (prefs != null) {
|
||||
bgType = prefs.getBgType();
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void refreshCover() {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void refreshPinned() {
|
||||
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : albums) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
Comparator<Album> nameComparator = new Comparator<Album>() {
|
||||
@Override
|
||||
public int compare(Album a1, Album a2) {
|
||||
return a1.getName().compareToIgnoreCase(a2.getName());
|
||||
}
|
||||
};
|
||||
Collections.sort(pinned, nameComparator);
|
||||
Collections.sort(unpinned, nameComparator);
|
||||
albums.clear();
|
||||
albums.addAll(pinned);
|
||||
albums.addAll(unpinned);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void showContextMenu(View view, final Album album) {
|
||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||
builder.setTitle(album.getName());
|
||||
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
||||
String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"};
|
||||
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
if (pinnedDbHelper != null) {
|
||||
if (which == 0) {
|
||||
if (isPinned) {
|
||||
pinnedDbHelper.unpinAlbum(album.getPath());
|
||||
} else {
|
||||
pinnedDbHelper.pinAlbum(album.getPath());
|
||||
}
|
||||
refreshPinned();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_album, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||
final Album album = albums.get(position);
|
||||
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
|
||||
|
||||
holder.coverImage.setBackgroundResource(getBgRes());
|
||||
|
||||
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
||||
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
||||
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
showContextMenu(holder.itemView, album);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
holder.albumName.setText(album.getName());
|
||||
holder.imageCount.setText(album.getImageCount() + " photos");
|
||||
|
||||
holder.itemView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!coverSizeReported && prefs != null) {
|
||||
int savedWidth = prefs.getCoverWidth();
|
||||
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
|
||||
int width = holder.coverImage.getWidth();
|
||||
int height = holder.coverImage.getHeight();
|
||||
if (width > 0 && height > 0) {
|
||||
prefs.setCoverWidth(width);
|
||||
prefs.setCoverHeight(height);
|
||||
float ratio = (float) width / height;
|
||||
prefs.setCoverRatio(ratio);
|
||||
coverSizeReported = true;
|
||||
if (coverSizeListener != null) {
|
||||
coverSizeListener.onCoverSize(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) {
|
||||
listener.onAlbumClick(album);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Uri coverUri = album.getCoverUri();
|
||||
if (coverUri != null) {
|
||||
String uriString = coverUri.toString();
|
||||
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
|
||||
|
||||
if ("file".equals(coverUri.getScheme())) {
|
||||
File coverFile = new File(coverUri.getPath());
|
||||
if (coverFile.exists()) {
|
||||
Glide.with(holder.coverImage.getContext())
|
||||
.load(coverFile)
|
||||
.fitCenter()
|
||||
.into(holder.coverImage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Glide.with(holder.coverImage.getContext())
|
||||
.load(coverUri)
|
||||
.fitCenter()
|
||||
.into(holder.coverImage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return albums.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView coverImage;
|
||||
ImageView pinIcon;
|
||||
TextView albumName;
|
||||
TextView imageCount;
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
coverImage = itemView.findViewById(R.id.album_cover);
|
||||
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||
albumName = itemView.findViewById(R.id.album_name);
|
||||
imageCount = itemView.findViewById(R.id.image_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
|
||||
public static final String TAG = "AlbumCoverDbHelper";
|
||||
private static final String DB_NAME = "album_cover.db";
|
||||
private static final int DB_VERSION = 2;
|
||||
private static final String TABLE_NAME = "album_covers";
|
||||
private static final String COLUMN_ALBUM_PATH = "album_path";
|
||||
private static final String COLUMN_IMAGE_PATH = "image_path";
|
||||
private static final String COLUMN_CROP_PATH = "crop_path";
|
||||
|
||||
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
|
||||
+ COLUMN_IMAGE_PATH + " TEXT, "
|
||||
+ COLUMN_CROP_PATH + " TEXT)";
|
||||
|
||||
private static AlbumCoverDbHelper dbHelper;
|
||||
|
||||
public static AlbumCoverDbHelper getInstance(Context context) {
|
||||
if (dbHelper == null) {
|
||||
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
|
||||
}
|
||||
return dbHelper;
|
||||
}
|
||||
|
||||
public AlbumCoverDbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion < 2) {
|
||||
try {
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
|
||||
} catch (Exception e) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||
values.put(COLUMN_CROP_PATH, cropPath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
|
||||
}
|
||||
|
||||
public void setCover(String albumPath, String imagePath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
|
||||
}
|
||||
|
||||
public String getCover(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String coverPath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
coverPath = cursor.getString(0);
|
||||
if (coverPath == null) {
|
||||
coverPath = cursor.getString(1);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return coverPath;
|
||||
}
|
||||
|
||||
public String getCropPath(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String cropPath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
cropPath = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return cropPath;
|
||||
}
|
||||
|
||||
public String getOriginalImagePath(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String imagePath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
imagePath = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
public void clearCover(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(COLUMN_CROP_PATH);
|
||||
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||
LogUtils.d(TAG, "clearCover: " + albumPath);
|
||||
}
|
||||
|
||||
public void deleteCover(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||
LogUtils.d(TAG, "deleteCover: " + albumPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
public class CropActivity extends AppCompatActivity {
|
||||
public static final String TAG = "CropActivity";
|
||||
public static final String EXTRA_IMAGE_URI = "image_uri";
|
||||
public static final String EXTRA_IMAGE_PATH = "image_path";
|
||||
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||
public static final String EXTRA_CROP_WIDTH = "crop_width";
|
||||
public static final String EXTRA_CROP_HEIGHT = "crop_height";
|
||||
|
||||
private CropCanvasView cropCanvasView;
|
||||
private Bitmap originalBitmap;
|
||||
private String imagePath;
|
||||
private String albumPath;
|
||||
private int cropWidth = 240;
|
||||
private int cropHeight = 120;
|
||||
private float cropRatio = 2.0f;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_crop);
|
||||
|
||||
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
|
||||
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
|
||||
|
||||
Preferences prefs = new Preferences(this);
|
||||
if (cropWidth > 0 && cropHeight > 0) {
|
||||
cropRatio = (float) cropWidth / cropHeight;
|
||||
} else {
|
||||
cropRatio = prefs.getCoverRatio();
|
||||
cropWidth = (int) (120 * cropRatio);
|
||||
cropHeight = 120;
|
||||
}
|
||||
|
||||
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
final ImageView btnColorPick = findViewById(R.id.btn_color_pick);
|
||||
btnColorPick.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean pickMode = !cropCanvasView.isColorPickMode();
|
||||
cropCanvasView.setColorPickMode(pickMode);
|
||||
btnColorPick.setAlpha(pickMode ? 0.5f : 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveCroppedCover();
|
||||
}
|
||||
});
|
||||
|
||||
cropCanvasView = findViewById(R.id.crop_canvas_view);
|
||||
loadImage();
|
||||
}
|
||||
|
||||
private void loadImage() {
|
||||
try {
|
||||
if (imagePath != null && new File(imagePath).exists()) {
|
||||
originalBitmap = BitmapFactory.decodeFile(imagePath);
|
||||
} else {
|
||||
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
|
||||
if (imageUri != null) {
|
||||
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalBitmap != null) {
|
||||
cropCanvasView.setImageBitmap(originalBitmap);
|
||||
cropCanvasView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initCrop();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
|
||||
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void initCrop() {
|
||||
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
|
||||
|
||||
int viewW = cropCanvasView.getWidth();
|
||||
int viewH = cropCanvasView.getHeight();
|
||||
if (viewW > 0 && viewH > 0) {
|
||||
cropCanvasView.scaleToView(viewW, viewH);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCroppedCover() {
|
||||
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||
if (canvasBitmap == null) {
|
||||
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
RectF cropRect = cropCanvasView.getCropRect();
|
||||
|
||||
int cropX = (int) cropRect.left;
|
||||
int cropY = (int) cropRect.top;
|
||||
int cropW = (int) cropRect.width();
|
||||
int cropH = (int) cropRect.height();
|
||||
|
||||
cropX = Math.max(0, Math.min(cropX, canvasBitmap.getWidth() - 1));
|
||||
cropY = Math.max(0, Math.min(cropY, canvasBitmap.getHeight() - 1));
|
||||
cropW = Math.max(1, Math.min(cropW, canvasBitmap.getWidth() - cropX));
|
||||
cropH = Math.max(1, Math.min(cropH, canvasBitmap.getHeight() - cropY));
|
||||
|
||||
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, cropX, cropY, cropW, cropH);
|
||||
|
||||
File cacheDir = getCacheDir();
|
||||
File coverFile = new File(cacheDir, "cover_" + System.currentTimeMillis() + ".png");
|
||||
FileOutputStream fos = new FileOutputStream(coverFile);
|
||||
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.close();
|
||||
|
||||
cropped.recycle();
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
|
||||
|
||||
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
|
||||
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (originalBitmap != null) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class CropCanvasView extends View {
|
||||
private Paint imagePaint;
|
||||
private Paint borderPaint;
|
||||
private Paint cornerPaint;
|
||||
private RectF cropRect;
|
||||
private int touchArea = 50;
|
||||
|
||||
private float lastX, lastY;
|
||||
private int activeCorner = -1;
|
||||
private static final int CORNER_TOP_LEFT = 0;
|
||||
private static final int CORNER_TOP_RIGHT = 1;
|
||||
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||
private static final int CORNER_CENTER = 4;
|
||||
|
||||
private int imageWidth;
|
||||
private int imageHeight;
|
||||
private float coverRatio = 2.0f;
|
||||
|
||||
private int extendHeight;
|
||||
private int extendWidth;
|
||||
private int canvasWidth;
|
||||
private int canvasHeight;
|
||||
private float minSize = 50;
|
||||
|
||||
private RectF imageBounds = new RectF();
|
||||
private RectF canvasBounds = new RectF();
|
||||
private Bitmap originalBitmap;
|
||||
private Bitmap canvasBitmap;
|
||||
private float initialSpan;
|
||||
private int backgroundColor = Color.GREEN;
|
||||
private boolean colorPickMode = false;
|
||||
private float pickX, pickY;
|
||||
|
||||
public CropCanvasView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CropCanvasView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
imagePaint = new Paint();
|
||||
imagePaint.setFilterBitmap(true);
|
||||
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(3);
|
||||
|
||||
cornerPaint = new Paint();
|
||||
cornerPaint.setColor(Color.WHITE);
|
||||
cornerPaint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
public void setImageBitmap(Bitmap bitmap) {
|
||||
this.originalBitmap = bitmap;
|
||||
}
|
||||
|
||||
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
|
||||
this.imageWidth = imgWidth;
|
||||
this.imageHeight = imgHeight;
|
||||
this.coverRatio = ratio;
|
||||
|
||||
float imgRatio = (float) imgWidth / imgHeight;
|
||||
|
||||
if (coverRatio >= imgRatio) {
|
||||
extendHeight = imgHeight;
|
||||
extendWidth = (int) (imgHeight * coverRatio);
|
||||
} else {
|
||||
extendWidth = imgWidth;
|
||||
extendHeight = (int) (imgWidth / coverRatio);
|
||||
}
|
||||
|
||||
canvasHeight = Math.max(imageHeight, extendHeight);
|
||||
canvasWidth = Math.max(imageWidth, extendWidth);
|
||||
|
||||
float left = (canvasWidth - imageWidth) / 2f;
|
||||
float top = (canvasHeight - imageHeight) / 2f;
|
||||
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
cropRect = new RectF(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
createCanvasBitmap();
|
||||
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void createCanvasBitmap() {
|
||||
if (canvasBitmap != null) {
|
||||
canvasBitmap.recycle();
|
||||
}
|
||||
canvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
|
||||
android.graphics.Canvas canvas = new android.graphics.Canvas(canvasBitmap);
|
||||
canvas.drawColor(backgroundColor);
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
canvas.drawBitmap(originalBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap getCanvasBitmap() {
|
||||
return canvasBitmap;
|
||||
}
|
||||
|
||||
public void setBackgroundColor(int color) {
|
||||
this.backgroundColor = color;
|
||||
createCanvasBitmap();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public void setColorPickMode(boolean enable) {
|
||||
this.colorPickMode = enable;
|
||||
if (enable) {
|
||||
pickX = -1;
|
||||
pickY = -1;
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public boolean isColorPickMode() {
|
||||
return colorPickMode;
|
||||
}
|
||||
|
||||
public void scaleToView(int viewWidth, int viewHeight) {
|
||||
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
|
||||
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
|
||||
int oldCanvasW = canvasWidth;
|
||||
int oldCanvasH = canvasHeight;
|
||||
canvasWidth = (int) (canvasWidth * scale);
|
||||
canvasHeight = (int) (canvasHeight * scale);
|
||||
|
||||
float left = (canvasWidth - imageWidth) / 2f;
|
||||
float top = (canvasHeight - imageHeight) / 2f;
|
||||
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
if (cropRect != null) {
|
||||
float scaleX = (float) canvasWidth / oldCanvasW;
|
||||
float scaleY = (float) canvasHeight / oldCanvasH;
|
||||
cropRect.left *= scaleX;
|
||||
cropRect.top *= scaleY;
|
||||
cropRect.right *= scaleX;
|
||||
cropRect.bottom *= scaleY;
|
||||
}
|
||||
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public RectF getCropRect() {
|
||||
return new RectF(cropRect);
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
public int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
public RectF getCanvasBounds() {
|
||||
return new RectF(canvasBounds);
|
||||
}
|
||||
|
||||
public RectF getImageBounds() {
|
||||
return new RectF(imageBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int w = canvasWidth > 0 ? canvasWidth : 0;
|
||||
int h = canvasHeight > 0 ? canvasHeight : 0;
|
||||
setMeasuredDimension(w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
|
||||
canvas.drawBitmap(canvasBitmap, 0, 0, null);
|
||||
}
|
||||
|
||||
if (cropRect != null) {
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
|
||||
|
||||
if (event.getPointerCount() == 2) {
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
initialSpan = getSpan(event);
|
||||
return true;
|
||||
} else if (action == MotionEvent.ACTION_MOVE) {
|
||||
float span = getSpan(event);
|
||||
if (initialSpan > 0) {
|
||||
float scale = span / initialSpan;
|
||||
float centerX = cropRect.centerX();
|
||||
float centerY = cropRect.centerY();
|
||||
float newWidth = cropRect.width() * scale;
|
||||
float newHeight = newWidth / coverRatio;
|
||||
|
||||
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
|
||||
newHeight = newWidth / coverRatio;
|
||||
|
||||
cropRect.set(centerX - newWidth / 2, centerY - newHeight / 2,
|
||||
centerX + newWidth / 2, centerY + newHeight / 2);
|
||||
|
||||
initialSpan = span;
|
||||
invalidate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
if (colorPickMode) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
pickX = x;
|
||||
pickY = y;
|
||||
int color = getColorAt(x, y);
|
||||
setBackgroundColor(color);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
activeCorner = getActiveCorner(x, y);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float dx = x - lastX;
|
||||
float dy = y - lastY;
|
||||
|
||||
if (activeCorner == CORNER_CENTER) {
|
||||
float newLeft = cropRect.left + dx;
|
||||
float newTop = cropRect.top + dy;
|
||||
float newRight = cropRect.right + dx;
|
||||
float newBottom = cropRect.bottom + dy;
|
||||
|
||||
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newRight;
|
||||
}
|
||||
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newBottom;
|
||||
}
|
||||
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
activeCorner = -1;
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||
float newWidth;
|
||||
float newLeft = cropRect.left;
|
||||
float newTop = cropRect.top;
|
||||
float newRight = cropRect.right;
|
||||
float newBottom = cropRect.bottom;
|
||||
|
||||
if (left) {
|
||||
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||
} else {
|
||||
newWidth = nx - cropRect.left;
|
||||
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||
newLeft = cropRect.left;
|
||||
}
|
||||
|
||||
float newHeight = newWidth / coverRatio;
|
||||
|
||||
if (top) {
|
||||
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||
newBottom = newTop + newHeight;
|
||||
} else {
|
||||
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||
newTop = newBottom - newHeight;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newLeft + newWidth;
|
||||
} else {
|
||||
cropRect.right = newRight;
|
||||
cropRect.left = newRight - newWidth;
|
||||
}
|
||||
|
||||
if (top) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newTop + newHeight;
|
||||
} else {
|
||||
cropRect.bottom = newBottom;
|
||||
cropRect.top = newBottom - newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private int getActiveCorner(float x, float y) {
|
||||
float dx = Math.abs(x - cropRect.left);
|
||||
float dy = Math.abs(y - cropRect.top);
|
||||
float dx2 = Math.abs(x - cropRect.right);
|
||||
float dy2 = Math.abs(y - cropRect.bottom);
|
||||
|
||||
if (dx <= touchArea && dy <= touchArea) {
|
||||
return CORNER_TOP_LEFT;
|
||||
}
|
||||
if (dx2 <= touchArea && dy <= touchArea) {
|
||||
return CORNER_TOP_RIGHT;
|
||||
}
|
||||
if (dx <= touchArea && dy2 <= touchArea) {
|
||||
return CORNER_BOTTOM_LEFT;
|
||||
}
|
||||
if (dx2 <= touchArea && dy2 <= touchArea) {
|
||||
return CORNER_BOTTOM_RIGHT;
|
||||
}
|
||||
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
|
||||
return CORNER_CENTER;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private float getSpan(MotionEvent event) {
|
||||
float x0 = event.getX(0);
|
||||
float y0 = event.getY(0);
|
||||
float x1 = event.getX(1);
|
||||
float y1 = event.getY(1);
|
||||
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
|
||||
}
|
||||
|
||||
public int getColorAt(float x, float y) {
|
||||
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
|
||||
int bitmapX = (int) x;
|
||||
int bitmapY = (int) y;
|
||||
if (bitmapX >= 0 && bitmapX < canvasBitmap.getWidth() &&
|
||||
bitmapY >= 0 && bitmapY < canvasBitmap.getHeight()) {
|
||||
return canvasBitmap.getPixel(bitmapX, bitmapY);
|
||||
}
|
||||
}
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class CropOverlayView extends View {
|
||||
public interface OnCanvasSizeChangedListener {
|
||||
void onCanvasSizeChanged(RectF canvasBounds);
|
||||
}
|
||||
|
||||
private OnCanvasSizeChangedListener canvasSizeChangedListener;
|
||||
|
||||
private Paint borderPaint;
|
||||
private Paint cornerPaint;
|
||||
private RectF cropRect;
|
||||
private int touchArea = 50;
|
||||
|
||||
private float lastX, lastY;
|
||||
private int activeCorner = -1;
|
||||
private static final int CORNER_TOP_LEFT = 0;
|
||||
private static final int CORNER_TOP_RIGHT = 1;
|
||||
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||
private static final int CORNER_CENTER = 4;
|
||||
|
||||
private float targetRatio = 2.0f;
|
||||
private RectF canvasBounds = new RectF();
|
||||
private float minSize = 50;
|
||||
|
||||
public CropOverlayView(Context context) {
|
||||
super(context);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
public CropOverlayView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
private void initPaints() {
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(3);
|
||||
|
||||
cornerPaint = new Paint();
|
||||
cornerPaint.setColor(Color.WHITE);
|
||||
cornerPaint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
public void setTargetRatio(float ratio) {
|
||||
this.targetRatio = ratio;
|
||||
}
|
||||
|
||||
public float getTargetRatio() {
|
||||
return targetRatio;
|
||||
}
|
||||
|
||||
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
|
||||
this.canvasSizeChangedListener = listener;
|
||||
}
|
||||
|
||||
public void initCanvas(int canvasW, int canvasH) {
|
||||
canvasBounds.set(0, 0, canvasW, canvasH);
|
||||
|
||||
cropRect = new RectF(0, 0, canvasW, canvasH);
|
||||
|
||||
if (canvasSizeChangedListener != null) {
|
||||
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public RectF getCropRect() {
|
||||
return new RectF(cropRect);
|
||||
}
|
||||
|
||||
public RectF getCanvasBounds() {
|
||||
return new RectF(canvasBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
if (cropRect != null) {
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (cropRect == null || canvasBounds.isEmpty()) return false;
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
activeCorner = getActiveCorner(x, y);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float dx = x - lastX;
|
||||
float dy = y - lastY;
|
||||
|
||||
if (activeCorner == CORNER_CENTER) {
|
||||
float newLeft = cropRect.left + dx;
|
||||
float newTop = cropRect.top + dy;
|
||||
float newRight = cropRect.right + dx;
|
||||
float newBottom = cropRect.bottom + dy;
|
||||
|
||||
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newRight;
|
||||
}
|
||||
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newBottom;
|
||||
}
|
||||
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
activeCorner = -1;
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||
float newWidth;
|
||||
float newLeft = cropRect.left;
|
||||
float newTop = cropRect.top;
|
||||
float newRight = cropRect.right;
|
||||
float newBottom = cropRect.bottom;
|
||||
|
||||
if (left) {
|
||||
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||
} else {
|
||||
newWidth = nx - cropRect.left;
|
||||
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||
newLeft = cropRect.left;
|
||||
}
|
||||
|
||||
float newHeight = newWidth / targetRatio;
|
||||
|
||||
if (top) {
|
||||
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||
newBottom = newTop + newHeight;
|
||||
} else {
|
||||
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||
newTop = newBottom - newHeight;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newLeft + newWidth;
|
||||
} else {
|
||||
cropRect.right = newRight;
|
||||
cropRect.left = newRight - newWidth;
|
||||
}
|
||||
|
||||
if (top) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newTop + newHeight;
|
||||
} else {
|
||||
cropRect.bottom = newBottom;
|
||||
cropRect.top = newBottom - newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private int getActiveCorner(float x, float y) {
|
||||
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||
return CORNER_TOP_LEFT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||
return CORNER_TOP_RIGHT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||
return CORNER_BOTTOM_LEFT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||
return CORNER_BOTTOM_RIGHT;
|
||||
}
|
||||
if (cropRect.contains(x, y)) {
|
||||
return CORNER_CENTER;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -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,247 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
|
||||
public static final String TAG = "ImageAdapter";
|
||||
private ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
private ArrayList<String> imagePaths = new ArrayList<>();
|
||||
private OnImageClickListener listener;
|
||||
private int bgType = 0;
|
||||
private Preferences prefs;
|
||||
private PinnedImageDbHelper pinnedDbHelper;
|
||||
private AlbumCoverDbHelper coverDbHelper;
|
||||
private String albumPath;
|
||||
|
||||
private int getBgRes() {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
return R.drawable.bg_checkerboard;
|
||||
case 1:
|
||||
return R.drawable.bg_white;
|
||||
case 2:
|
||||
return R.drawable.bg_black;
|
||||
default:
|
||||
return R.drawable.bg_checkerboard;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnImageClickListener {
|
||||
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
|
||||
}
|
||||
|
||||
public void setOnImageClickListener(OnImageClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||
this.imageUrls = urls;
|
||||
this.imagePaths = paths;
|
||||
sortPinnedFirst();
|
||||
LogUtils.d(TAG, "setData: " + urls.size() + " images");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void sortPinnedFirst() {
|
||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayList<Uri> pinnedUrls = new ArrayList<>();
|
||||
ArrayList<String> pinnedPaths = new ArrayList<>();
|
||||
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
|
||||
ArrayList<String> unpinnedPaths = new ArrayList<>();
|
||||
for (int i = 0; i < imagePaths.size(); i++) {
|
||||
String path = imagePaths.get(i);
|
||||
if (pinnedDbHelper.isPinned(path)) {
|
||||
pinnedUrls.add(imageUrls.get(i));
|
||||
pinnedPaths.add(path);
|
||||
} else {
|
||||
unpinnedUrls.add(imageUrls.get(i));
|
||||
unpinnedPaths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
imageUrls.clear();
|
||||
imagePaths.clear();
|
||||
imageUrls.addAll(pinnedUrls);
|
||||
imageUrls.addAll(unpinnedUrls);
|
||||
imagePaths.addAll(pinnedPaths);
|
||||
imagePaths.addAll(unpinnedPaths);
|
||||
}
|
||||
|
||||
public void setContext(android.content.Context context) {
|
||||
prefs = new Preferences(context);
|
||||
bgType = prefs.getBgType();
|
||||
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
|
||||
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
|
||||
}
|
||||
|
||||
public void setAlbumPath(String albumPath) {
|
||||
this.albumPath = albumPath;
|
||||
}
|
||||
|
||||
public int getCropWidth() {
|
||||
return prefs != null ? prefs.getCoverWidth() : 240;
|
||||
}
|
||||
|
||||
public int getCropHeight() {
|
||||
return prefs != null ? prefs.getCoverHeight() : 120;
|
||||
}
|
||||
|
||||
public void refreshBg() {
|
||||
if (prefs != null) {
|
||||
bgType = prefs.getBgType();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshPinned() {
|
||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sortPinnedFirst();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void showContextMenu(View view, final int position) {
|
||||
final String imagePath = imagePaths.get(position);
|
||||
final Uri imageUri = imageUrls.get(position);
|
||||
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
|
||||
final boolean[] isCover = {false};
|
||||
if (coverDbHelper != null && albumPath != null) {
|
||||
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
||||
isCover[0] = originalPath != null && originalPath.equals(imagePath);
|
||||
}
|
||||
|
||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||
builder.setTitle("Image");
|
||||
|
||||
String[] items;
|
||||
if (isPinned[0]) {
|
||||
if (isCover[0]) {
|
||||
items = new String[]{"取消置顶", "调整封面", "取消封面"};
|
||||
} else {
|
||||
items = new String[]{"取消置顶", "设置为封面"};
|
||||
}
|
||||
} else {
|
||||
if (isCover[0]) {
|
||||
items = new String[]{"置顶", "调整封面", "取消封面"};
|
||||
} else {
|
||||
items = new String[]{"置顶", "设置为封面"};
|
||||
}
|
||||
}
|
||||
|
||||
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
if (which == 0) {
|
||||
if (pinnedDbHelper != null) {
|
||||
if (isPinned[0]) {
|
||||
pinnedDbHelper.unpinImage(imagePath);
|
||||
} else {
|
||||
pinnedDbHelper.pinImage(imagePath);
|
||||
}
|
||||
refreshPinned();
|
||||
}
|
||||
} else if (which == 1) {
|
||||
if (coverDbHelper != null && albumPath != null) {
|
||||
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
|
||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
|
||||
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
|
||||
}
|
||||
} else if (which == 2) {
|
||||
if (coverDbHelper != null && albumPath != null && isCover[0]) {
|
||||
coverDbHelper.deleteCover(albumPath);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_gallery, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||
holder.imageView.setBackgroundResource(getBgRes());
|
||||
|
||||
Glide.with(holder.imageView.getContext())
|
||||
.load(imageUrls.get(position))
|
||||
.centerCrop()
|
||||
.into(holder.imageView);
|
||||
|
||||
final ArrayList<Uri> urls = imageUrls;
|
||||
final ArrayList<String> paths = imagePaths;
|
||||
final String imagePath = imagePaths.get(position);
|
||||
|
||||
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
|
||||
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isCover = false;
|
||||
if (coverDbHelper != null && albumPath != null) {
|
||||
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
||||
isCover = originalPath != null && originalPath.equals(imagePath);
|
||||
}
|
||||
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
|
||||
|
||||
holder.itemView.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) {
|
||||
listener.onImageClick(position, urls, paths);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
showContextMenu(holder.itemView, position);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return imageUrls.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView imageView;
|
||||
ImageView pinIcon;
|
||||
ImageView coverIcon;
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
imageView = itemView.findViewById(R.id.image);
|
||||
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||
coverIcon = itemView.findViewById(R.id.cover_icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,357 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogActivity;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
public static final String TAG = "MainActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 100;
|
||||
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
|
||||
private RecyclerView recyclerView;
|
||||
private AlbumAdapter adapter;
|
||||
private Preferences prefs;
|
||||
private FloatingActionButton fabScrollTop;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
prefs = new Preferences(this);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
|
||||
adapter = new AlbumAdapter();
|
||||
adapter.setContext(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
||||
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
});
|
||||
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstVisible > 0) {
|
||||
fabScrollTop.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
fabScrollTop.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
adapter.setOnAlbumClickListener(new OnAlbumClickListener() {
|
||||
@Override
|
||||
public void onAlbumClick(Album album) {
|
||||
Intent intent = new Intent(MainActivity.this, AlbumActivity.class);
|
||||
intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath());
|
||||
intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName());
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
|
||||
private void checkAndRequestPermissions() {
|
||||
LogUtils.i(TAG, "checkAndRequestPermissions");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
||||
} catch (Exception e) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkPermission()) {
|
||||
loadAlbums();
|
||||
} else {
|
||||
requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
loadAlbums();
|
||||
} else {
|
||||
Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return Environment.isExternalStorageManager();
|
||||
}
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestPermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadAlbums();
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAlbums() {
|
||||
LogUtils.d(TAG, "loadAlbums");
|
||||
String folderPath = prefs.getFolderPath();
|
||||
File baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
|
||||
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
|
||||
folderPath = Preferences.getDefaultPath();
|
||||
baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
if (!baseFolder.exists()) {
|
||||
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
|
||||
baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
ArrayList<Album> albums = new ArrayList<>();
|
||||
|
||||
FileFilter directoryFilter = new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory();
|
||||
}
|
||||
};
|
||||
File[] subfolders = baseFolder.listFiles(directoryFilter);
|
||||
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
|
||||
if (subfolders != null) {
|
||||
for (File subfolder : subfolders) {
|
||||
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
|
||||
String albumPath = subfolder.getAbsolutePath();
|
||||
String coverPath = coverDbHelper.getCover(albumPath);
|
||||
Uri coverUri = null;
|
||||
if (coverPath != null) {
|
||||
File coverFile = new File(coverPath);
|
||||
if (coverFile.exists()) {
|
||||
coverUri = Uri.fromFile(coverFile);
|
||||
} else {
|
||||
coverUri = getUriFromPath(coverPath);
|
||||
}
|
||||
}
|
||||
if (coverUri == null) {
|
||||
ArrayList<Uri> images = getImagesInFolder(albumPath);
|
||||
if (!images.isEmpty()) {
|
||||
coverUri = images.get(0);
|
||||
}
|
||||
}
|
||||
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
|
||||
if (coverUri != null || !allImages.isEmpty()) {
|
||||
if (coverUri == null && !allImages.isEmpty()) {
|
||||
coverUri = allImages.get(0);
|
||||
}
|
||||
int imageCount = allImages.size();
|
||||
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
|
||||
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (albums.isEmpty()) {
|
||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.i(TAG, "No albums found");
|
||||
}
|
||||
adapter.setData(albums);
|
||||
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
|
||||
}
|
||||
|
||||
private Uri getUriFromPath(String path) {
|
||||
String[] projection = { MediaStore.Images.Media._ID };
|
||||
String selection = MediaStore.Images.Media.DATA + " = ?";
|
||||
String[] selectionArgs = { path };
|
||||
try (Cursor cursor = getContentResolver().query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection, selection, selectionArgs, null)) {
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
||||
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ArrayList<Uri> getImagesInFolder(String folderPath) {
|
||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
|
||||
String[] selectionArgs = new String[]{folderPath + "/%"};
|
||||
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||
|
||||
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
|
||||
|
||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||
if (cursor != null) {
|
||||
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
|
||||
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(dataColumn);
|
||||
if (path != null) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
||||
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
||||
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
|
||||
imageUrls.add(contentUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_settings) {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_about) {
|
||||
startActivity(new Intent(this, AboutActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_trash) {
|
||||
startActivity(new Intent(this, TrashActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_debug) {
|
||||
LogActivity.startLogActivity(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (checkPermission()) {
|
||||
scanMediaStore();
|
||||
loadAlbums();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
adapter.refreshPinned();
|
||||
adapter.refreshCover();
|
||||
}
|
||||
}
|
||||
|
||||
private void scanMediaStore() {
|
||||
String folderPath = prefs.getFolderPath();
|
||||
File baseFolder = new File(folderPath);
|
||||
if (baseFolder.exists() && baseFolder.isDirectory()) {
|
||||
File[] subfolders = baseFolder.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory();
|
||||
}
|
||||
});
|
||||
if (subfolders != null) {
|
||||
ArrayList<String> paths = new ArrayList<>();
|
||||
for (File subfolder : subfolders) {
|
||||
File[] images = subfolder.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
String lower = name.toLowerCase();
|
||||
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
||||
|| lower.endsWith(".png") || lower.endsWith(".gif")
|
||||
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
|
||||
}
|
||||
});
|
||||
if (images != null) {
|
||||
for (File img : images) {
|
||||
paths.add(img.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
|
||||
String[] pathArray = paths.toArray(new String[0]);
|
||||
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,90 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class Preferences {
|
||||
public static final String TAG = "Preferences";
|
||||
private static final String PREF_NAME = "gallery_prefs";
|
||||
private static final String KEY_FOLDER_PATH = "folder_path";
|
||||
private static final String KEY_BG_TYPE = "bg_type";
|
||||
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
|
||||
private static final String KEY_COVER_WIDTH = "cover_width";
|
||||
private static final String KEY_COVER_HEIGHT = "cover_height";
|
||||
private static final String KEY_COVER_RATIO = "cover_ratio";
|
||||
private static final int DEFAULT_BG_TYPE = 0;
|
||||
private static final int DEFAULT_SORT_MODE = 0;
|
||||
private static final int DEFAULT_COVER_WIDTH = 240;
|
||||
private static final int DEFAULT_COVER_HEIGHT = 120;
|
||||
private static final float DEFAULT_COVER_RATIO = 2.0f;
|
||||
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
|
||||
|
||||
public static final int SORT_TIME_DESC = 0;
|
||||
public static final int SORT_TIME_ASC = 1;
|
||||
public static final int SORT_NAME_DESC = 2;
|
||||
public static final int SORT_NAME_ASC = 3;
|
||||
|
||||
public static String getDefaultPath() {
|
||||
return DEFAULT_PATH;
|
||||
}
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getFolderPath() {
|
||||
String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH);
|
||||
LogUtils.d(TAG, "getFolderPath: " + path);
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setFolderPath(String path) {
|
||||
LogUtils.d(TAG, "setFolderPath: " + path);
|
||||
prefs.edit().putString(KEY_FOLDER_PATH, path).apply();
|
||||
}
|
||||
|
||||
public int getBgType() {
|
||||
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
|
||||
}
|
||||
|
||||
public void setBgType(int type) {
|
||||
LogUtils.d(TAG, "setBgType: " + type);
|
||||
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
|
||||
}
|
||||
|
||||
public int getAlbumSortMode() {
|
||||
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
|
||||
}
|
||||
|
||||
public void setAlbumSortMode(int mode) {
|
||||
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
|
||||
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
|
||||
}
|
||||
|
||||
public int getCoverWidth() {
|
||||
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
|
||||
}
|
||||
|
||||
public void setCoverWidth(int width) {
|
||||
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
|
||||
}
|
||||
|
||||
public int getCoverHeight() {
|
||||
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
|
||||
}
|
||||
|
||||
public void setCoverHeight(int height) {
|
||||
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
|
||||
}
|
||||
|
||||
public float getCoverRatio() {
|
||||
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
|
||||
}
|
||||
|
||||
public void setCoverRatio(float ratio) {
|
||||
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
|
||||
}
|
||||
}
|
||||
@@ -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"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/darker_gray" />
|
||||
<corners android:radius="6dp" />
|
||||
</shape>
|
||||
|
||||
<solid android:color="#000000"/>
|
||||
</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>
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="line">
|
||||
android:shape="oval">
|
||||
<solid android:color="#CCFFFFFF"/>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#E0E0E0"/>
|
||||
</shape>
|
||||
android:color="#000000"/>
|
||||
</shape>
|
||||
@@ -1,7 +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="@android:color/holo_blue_light" />
|
||||
<corners android:radius="6dp" />
|
||||
</shape>
|
||||
|
||||
<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>
|
||||
10
gallery/src/main/res/drawable/ic_close.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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
10
gallery/src/main/res/drawable/ic_color_pick.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="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
|
||||
</vector>
|
||||
13
gallery/src/main/res/drawable/ic_cover.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
|
||||
</vector>
|
||||
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"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#FF009DCB"
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</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>
|
||||
10
gallery/src/main/res/drawable/ic_sort.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="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
|
||||
</vector>
|
||||
33
gallery/src/main/res/layout/activity_about.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<cc.winboll.studio.libappbase.views.AboutView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/aboutview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
56
gallery/src/main/res/layout/activity_crop.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/colorPrimary"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_close"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_close"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_color_pick"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_color_pick"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:text="完成"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.gallery.CropCanvasView
|
||||
android:id="@+id/crop_canvas_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
||||
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>
|
||||
41
gallery/src/main/res/layout/item_gallery.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/bg_circle_white"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_cover"
|
||||
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>
|
||||
26
gallery/src/main/res/menu/menu_album.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_sort"
|
||||
android:icon="@drawable/ic_sort"
|
||||
android:title="排序"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/sort_time_desc"
|
||||
android:title="时间倒序"/>
|
||||
<item
|
||||
android:id="@+id/sort_time_asc"
|
||||
android:title="时间正序"/>
|
||||
<item
|
||||
android:id="@+id/sort_name_desc"
|
||||
android:title="名称倒序"/>
|
||||
<item
|
||||
android:id="@+id/sort_name_asc"
|
||||
android:title="名称正序"/>
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
8
gallery/src/main/res/menu/menu_crop.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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_done"
|
||||
android:title="完成"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
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_about"
|
||||
android:title="@string/about"
|
||||
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>
|
||||
21
gallery/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<resources>
|
||||
<string name="app_name">Gallery</string>
|
||||
<string name="app_description">WinBoLL Album App</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="about">About</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>
|
||||
12
gallery/src/stage/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
1
powerbell/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,112 +0,0 @@
|
||||
# PowerBell
|
||||
|
||||
#### 介绍
|
||||
一个接收手机电量信息的应用,当电量值达到设定范围时会提醒用户。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :gradle assembleStageRelease
|
||||
|
||||
#### 使用说明
|
||||
|
||||
在安卓系统中需要设置两个权限允许。
|
||||
1.自启动权限允许。
|
||||
2.省电策略-无限制权限允许。
|
||||
3.设置背景图片需要读写手机存储权限。
|
||||
4.要在锁屏充电的时候提醒,还需要设置允许锁屏通知权限。
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@qq.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
|
||||
AndroidManifest.xml详解
|
||||
https://www.jianshu.com/p/3b5b89d4e154
|
||||
|
||||
CrashHandler自定义异常处理
|
||||
https://www.jianshu.com/p/9a3d800a429a
|
||||
|
||||
Android用Intent启动Activity的方法
|
||||
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
|
||||
|
||||
Android开发:最详细的 Toolbar 开发实践总结
|
||||
https://www.jianshu.com/p/79604c3ddcae
|
||||
|
||||
Using the App Toolbar
|
||||
https://guides.codepath.com/android/using-the-app-toolbar
|
||||
|
||||
Android的onCreateOptionsMenu()创建菜单Menu详解
|
||||
https://www.cnblogs.com/spring87/p/4312538.html
|
||||
|
||||
Android通知栏-Notification(通知消息)
|
||||
https://blog.csdn.net/qq_35507234/article/details/90676587
|
||||
|
||||
android之PendingIntent的使用
|
||||
https://blog.csdn.net/qq_16628781/article/details/51548324
|
||||
|
||||
change seekbar color android” Code Answer
|
||||
https://www.codegrepper.com/code-examples/whatever/change+seekbar+color+android
|
||||
|
||||
如何选择开源项目许可证
|
||||
https://www.zhihu.com/question/28292322
|
||||
|
||||
Android最简单的自定义布局Notification
|
||||
https://blog.csdn.net/acesheep_911/article/details/81458784?utm_medium=distribute.wap_relevant.none-task-blog-2~default~baidujs_title~default-0.wap_blog_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3
|
||||
|
||||
Android中通知栏Notification详解以及自定义Notification
|
||||
https://blog.csdn.net/daitu_liang/article/details/50246803
|
||||
|
||||
Android 图像系列: 将本地图片加载到Drawable
|
||||
https://blog.csdn.net/qzone123222/article/details/7930035
|
||||
|
||||
android 从相册选择,Android开发从相册中选取照片
|
||||
https://blog.csdn.net/weixin_42146086/article/details/117570917
|
||||
|
||||
Android 任务栈简介
|
||||
https://blog.csdn.net/qq_34368586/article/details/107653912
|
||||
|
||||
Android用Intent启动Activity的方法
|
||||
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
|
||||
|
||||
Android中使用dimen定义尺寸
|
||||
https://blog.csdn.net/yuzhiboyi/article/details/7696174
|
||||
|
||||
declare-styleable:自定义控件的属性
|
||||
https://blog.csdn.net/congqingbin/article/details/7869730
|
||||
|
||||
安卓自定义滑动解锁控件
|
||||
https://blog.csdn.net/lp506954774/article/details/72677018
|
||||
|
||||
Android 添加菜单和返回按钮
|
||||
https://blog.csdn.net/my_tiantian/article/details/77822173
|
||||
|
||||
Android Button的基本使用
|
||||
https://www.cnblogs.com/yishaochu/p/5783605.html
|
||||
|
||||
Android应用中实现系统“分享”接口
|
||||
https://blog.csdn.net/lowprofile_coding/article/details/37656255
|
||||
|
||||
Android 关于mimeType的使用
|
||||
https://blog.csdn.net/dorytmx/article/details/80972248
|
||||
|
||||
使用GitHub Actions实现Android自动打包apk
|
||||
https://blog.csdn.net/ZZL23333/article/details/115798615?app_version=6.0.0&code=app_1562916241&csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22115798615%22%2C%22source%22%3A%22weixin_38986226%22%7D&uLinkId=usr1mkqgl919blen&utm_source=app
|
||||
@@ -1,8 +0,0 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Mar 11 04:24:14 HKT 2026
|
||||
stageCount=17
|
||||
libraryProject=
|
||||
baseVersion=15.15
|
||||
publishVersion=15.15.16
|
||||
buildCount=0
|
||||
baseBetaVersion=15.15.17
|
||||
@@ -1,279 +0,0 @@
|
||||
#!/bin/bash
|
||||
# PowerBell软著版本号快速修改+生成脚本
|
||||
# 无需手动改主脚本,输入版本号直接运行
|
||||
|
||||
# 颜色输出函数
|
||||
red_echo() { echo -e "\033[31m$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m$1\033[0m"; }
|
||||
|
||||
# 1. 提示用户输入新版本号
|
||||
blue_echo "==== 请输入软著版本号(格式示例:V15、V15.0.1) ===="
|
||||
read -p "输入版本号:" NEW_VERSION
|
||||
|
||||
# 校验版本号格式(避免特殊符号)
|
||||
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
|
||||
red_echo "错误:版本号格式无效!请遵循「V+数字」格式(如V15、V15.0.1),不含特殊符号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
|
||||
SOFTWARE_NAME="PowerBell"
|
||||
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
|
||||
LINES_PER_PAGE=55
|
||||
|
||||
# 3. 生成主脚本(自动替换新版本号)
|
||||
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
|
||||
cat > build_copyright_pdf_temp.sh << EOF
|
||||
#!/bin/bash
|
||||
# PowerBell软著PDF生成脚本(版本:$NEW_VERSION)
|
||||
red_echo() { echo -e "\033[31m\$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m\$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
|
||||
|
||||
# 配置项(已自动替换为${NEW_VERSION})
|
||||
SOFTWARE_NAME="$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION="$NEW_VERSION"
|
||||
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE=$LINES_PER_PAGE
|
||||
|
||||
# 步骤1:检查依赖
|
||||
blue_echo "==== 1/7 检查并安装依赖 ===="
|
||||
sudo apt update > /dev/null 2>&1
|
||||
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
|
||||
for pkg in "\${REQUIRED_PKGS[@]}"; do
|
||||
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
|
||||
green_echo "安装依赖:\$pkg"
|
||||
sudo apt install -y "\$pkg" > /dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
# 步骤2:生成纯文本源码
|
||||
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
|
||||
cat > generate_source.py << GEN_EOF
|
||||
import os
|
||||
PROJECT_PATH = "./"
|
||||
OUTPUT_TXT = "PowerBell_Core_Source.txt"
|
||||
INCLUDE_EXT = [".java", ".kt"]
|
||||
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
|
||||
MIN_LINE_COUNT = 3
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
|
||||
def clean_text(text):
|
||||
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
|
||||
|
||||
def generate_source_txt():
|
||||
valid_files = []
|
||||
main_dir = os.path.join(PROJECT_PATH, "src", "main")
|
||||
if not os.path.exists(main_dir):
|
||||
print("Error: src/main directory not found!")
|
||||
return
|
||||
for root, dirs, files in os.walk(main_dir):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] in INCLUDE_EXT:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
|
||||
if len(code_lines) >= MIN_LINE_COUNT:
|
||||
valid_files.append(file_path)
|
||||
except:
|
||||
continue
|
||||
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
|
||||
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
|
||||
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
|
||||
for idx, file_path in enumerate(valid_files, 1):
|
||||
f.write(f"\\n{'='*60}\\n")
|
||||
f.write(f"文件 \{idx}:\{file_path.replace(PROJECT_PATH, '')}\\n")
|
||||
f.write(f"{'='*60}\\n\\n")
|
||||
try:
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
except UnicodeDecodeError:
|
||||
with open(file_path, "r", encoding="gbk") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
f.write(content)
|
||||
f.write("\\n\\n")
|
||||
except Exception as e:
|
||||
f.write(f"文件读取失败:\{str(e)}\\n\\n")
|
||||
continue
|
||||
print(f"有效源码文件数:\{len(valid_files)}")
|
||||
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_source_txt()
|
||||
GEN_EOF
|
||||
|
||||
python3 generate_source.py
|
||||
if [ ! -f "PowerBell_Core_Source.txt" ]; then
|
||||
red_echo "纯文本源码生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤3:生成带版本号页眉的HTML
|
||||
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
|
||||
cat > txt2html.py << TXT_EOF
|
||||
import os
|
||||
TXT_FILE = "PowerBell_Core_Source.txt"
|
||||
HTML_FILE = "PowerBell_Source.html"
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE = $LINES_PER_PAGE
|
||||
|
||||
CSS_STYLE = """
|
||||
<style>
|
||||
@page {{
|
||||
size: A4;
|
||||
margin: 10mm 5mm;
|
||||
@top-center {{
|
||||
content: "{} {} - 源代码(著作权人:{})";
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}}
|
||||
@bottom-center {{
|
||||
content: "页码 " counter(page) " / " counter(pages);
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
}}
|
||||
}}
|
||||
body {{
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
padding: 5mm 0 0 0;
|
||||
counter-reset: code-line;
|
||||
}}
|
||||
.file-header {{
|
||||
background: #f0f0f0;
|
||||
padding: 3px;
|
||||
margin: 6px 0;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}}
|
||||
.code-block {{
|
||||
white-space: pre;
|
||||
margin-left: 8px;
|
||||
line-height: 1.1;
|
||||
counter-increment: code-line;
|
||||
}}
|
||||
.code-block:before {{
|
||||
content: counter(code-line) " ";
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}}
|
||||
.page-break {{ page-break-after: always; counter-reset: code-line; }}
|
||||
</style>
|
||||
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
|
||||
|
||||
def txt_to_html():
|
||||
with open(TXT_FILE, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
|
||||
content_lines = content.split("\\n")[2:]
|
||||
content_clean = "\\n".join(content_lines)
|
||||
blocks = content_clean.split("====")
|
||||
|
||||
line_count = 0
|
||||
for block in blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
if "文件 " in block and ":" in block:
|
||||
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
|
||||
html_content += f"<div class='file-header'>\{file_header}</div>"
|
||||
code_part = block.split("\\n")[1:] if "\\n" in block else []
|
||||
block = "\\n".join(code_part)
|
||||
code_lines = block.split("\\n")
|
||||
for line in code_lines:
|
||||
if line.strip() or line_count > 0:
|
||||
line_count += 1
|
||||
html_content += f"<div class='code-block'>\{line}</div>"
|
||||
if line_count >= LINES_PER_PAGE:
|
||||
html_content += "<div class='page-break'></div>"
|
||||
line_count = 0
|
||||
html_content += "</body></html>"
|
||||
with open(HTML_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
print(f"HTML文件路径:\{os.path.abspath(HTML_FILE)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
txt_to_html()
|
||||
TXT_EOF
|
||||
|
||||
python3 txt2html.py
|
||||
if [ ! -f "PowerBell_Source.html" ]; then
|
||||
red_echo "HTML文件生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤4:生成完整PDF
|
||||
blue_echo -e "\n==== 4/7 生成完整PDF(版本:${NEW_VERSION}) ===="
|
||||
wkhtmltopdf --page-size A4 \
|
||||
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
|
||||
--encoding utf-8 \
|
||||
--no-images --disable-javascript \
|
||||
--enable-local-file-access \
|
||||
--no-stop-slow-scripts \
|
||||
PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
|
||||
if [ ! -f "PowerBell_soft_full.pdf" ]; then
|
||||
red_echo "完整PDF生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤5:截取60页
|
||||
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
|
||||
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "源码完整PDF总页数:\$TOTAL_PAGES 页"
|
||||
|
||||
if [ "\$TOTAL_PAGES" -le 60 ]; then
|
||||
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
green_echo "源码不足60页,直接使用完整PDF"
|
||||
else
|
||||
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
|
||||
START_PAGE=\$((TOTAL_PAGES - 29))
|
||||
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
|
||||
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
|
||||
green_echo "源码超过60页,已截取前30页+后30页合并为60页"
|
||||
fi
|
||||
|
||||
# 步骤6:验证规范
|
||||
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
|
||||
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "最终PDF页数:\$FINAL_PAGES 页"
|
||||
green_echo "每页代码行数:\$LINES_PER_PAGE 行(≥50行)"
|
||||
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER)"
|
||||
|
||||
# 步骤7:清理临时文件
|
||||
blue_echo -e "\n==== 7/7 清理临时文件 ===="
|
||||
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
green_echo "临时文件清理完成!"
|
||||
|
||||
# 输出结果
|
||||
green_echo -e "\n====================================="
|
||||
green_echo "✅ $SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功!🎉"
|
||||
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
|
||||
green_echo "💡 可直接提交软著登记,无需手动修改!"
|
||||
green_echo "====================================="
|
||||
EOF
|
||||
|
||||
# 4. 赋予执行权限并运行
|
||||
chmod +x build_copyright_pdf_temp.sh
|
||||
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
|
||||
./build_copyright_pdf_temp.sh
|
||||
|
||||
# 5. 删除临时主脚本(可选,保留则注释此行)
|
||||
rm -f build_copyright_pdf_temp.sh
|
||||
|
||||
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_launcher_beta">
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name_cn1">能源钟★</string>
|
||||
<string name="app_name_cn2">泡额呗额☆</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">PowerBell+</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
</shortcuts>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -1,332 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 运行“specialUse”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
<!-- 开机启动 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<!-- 显示通知 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- PACKAGE_USAGE_STATS -->
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
|
||||
|
||||
<!-- BATTERY_STATS -->
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<!-- 请求忽略电池优化 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<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"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission"/>
|
||||
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false"/>
|
||||
|
||||
<queries>
|
||||
|
||||
<package android:name="com.miui.securitycenter"/>
|
||||
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:process=":main"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme_Default"
|
||||
android:persistent="true"
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityEN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmainen1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityCN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name_cn1"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityCN2"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name_cn2"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn2"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BackgroundSettingsActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="image/jpeg"/>
|
||||
|
||||
<data android:mimeType="image/jpg"/>
|
||||
|
||||
<data android:mimeType="image/png"/>
|
||||
|
||||
<data android:mimeType="image/webp"/>
|
||||
|
||||
<data android:mimeType="image/*"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BatteryReporterActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.PixelPickerActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BatteryReportActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".unittest.MainUnitTestActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.ShortcutActionActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true"/>
|
||||
|
||||
<receiver
|
||||
android:process=":main"
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":assistant"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.TTSPlayService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.ThoughtfulService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"
|
||||
android:process=":main">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 517 B |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,320 +0,0 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.content.Context;
|
||||
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;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
|
||||
/**
|
||||
* 应用全局入口类
|
||||
* 适配:Java7 语法规范 | Android API30 系统版本
|
||||
* 核心策略:极致强制缓存 - 无论内存紧张程度,永不自动清理任何缓存(Bitmap/视图控件/路径记录)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025-12-29 15:30:00
|
||||
* @LastModified 2026-01-02 19:01:00
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
// ====================================== 常量区 - 置顶排序 (按功能归类) ======================================
|
||||
// 基础日志TAG
|
||||
private static final String TAG = "App";
|
||||
// 缓存保护专用TAG
|
||||
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
|
||||
// 电池无效值常量(修复拼写错误:INVALID_BATTERY_VALUE)
|
||||
private static final int INVALID_BATTERY_VALUE = -1;
|
||||
|
||||
// 组件跳转常量
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
|
||||
// 动作跳转常量
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
// ====================================== 静态属性区 - 全局单例/状态 (按核心程度排序) ======================================
|
||||
// 应用单例
|
||||
private static App sApp;
|
||||
|
||||
// 配置与缓存工具 (全局单例)
|
||||
public static AppConfigUtils sAppConfigUtils;
|
||||
private static AppCacheUtils sAppCacheUtils;
|
||||
|
||||
// 资源与视图缓存 (强制驻留,极致缓存核心)
|
||||
public static BackgroundSourceUtils sBackgroundSourceUtils;
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
|
||||
// 系统状态 (电池电量)
|
||||
public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
|
||||
// 系统工具 (通知管理器)
|
||||
private static NotificationManagerUtils sNotificationManagerUtils;
|
||||
|
||||
// ====================================== 成员属性区 - 非静态成员 (广播接收器) ======================================
|
||||
private GlobalApplicationReceiver mGlobalReceiver;
|
||||
|
||||
// ====================================== 公共静态方法 - 单例/工具获取 (对外入口) ======================================
|
||||
/**
|
||||
* 获取应用全局单例实例
|
||||
* @return 应用单例App实例
|
||||
*/
|
||||
public static App getInstance() {
|
||||
LogUtils.d(TAG, "【getInstance】应用单例获取方法调用 | 当前实例:" + sApp);
|
||||
return sApp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置工具类单例实例
|
||||
* @param context 上下文对象
|
||||
* @return 配置工具类AppConfigUtils实例
|
||||
*/
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "【getAppConfigUtils】配置工具获取方法调用 | 入参Context类型:" + contextClass);
|
||||
|
||||
if (sAppConfigUtils == null) {
|
||||
sAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "【getAppConfigUtils】配置工具实例为空,已初始化新实例");
|
||||
}
|
||||
return sAppConfigUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存工具类单例实例
|
||||
* @param context 上下文对象
|
||||
* @return 缓存工具类AppCacheUtils实例
|
||||
*/
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具获取方法调用 | 入参Context类型:" + contextClass);
|
||||
|
||||
if (sAppCacheUtils == null) {
|
||||
sAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具实例为空,已初始化新实例");
|
||||
}
|
||||
return sAppCacheUtils;
|
||||
}
|
||||
|
||||
// ====================================== 公共成员方法 - 业务逻辑 (实例方法) ======================================
|
||||
/**
|
||||
* 清除电池历史数据
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
LogUtils.d(TAG, "【clearBatteryHistory】清除电池历史数据方法调用");
|
||||
if (sAppCacheUtils != null) {
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
LogUtils.d(TAG, "【clearBatteryHistory】电池历史数据清除成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "【clearBatteryHistory】电池历史数据清除失败 | 缓存工具实例sAppCacheUtils为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视图缓存实例
|
||||
* @return 视图缓存MemoryCachedBackgroundView实例
|
||||
*/
|
||||
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
|
||||
LogUtils.d(TAG, "【getMemoryCachedBackgroundView】视图缓存获取方法调用 | 当前实例:" + sMemoryCachedBackgroundView);
|
||||
return sMemoryCachedBackgroundView;
|
||||
}
|
||||
|
||||
// ====================================== 公共静态方法 - 业务逻辑 (全局工具方法) ======================================
|
||||
/**
|
||||
* 手动清理所有缓存(仅主动调用生效,符合极致缓存策略)
|
||||
*/
|
||||
public static void manualClearAllCache() {
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存方法调用 | 仅主动触发生效");
|
||||
|
||||
// 清理Bitmap缓存
|
||||
if (sBitmapCacheUtils != null) {
|
||||
sBitmapCacheUtils.clearAllCache();
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】Bitmap缓存已清理");
|
||||
}
|
||||
|
||||
// 仅置空视图缓存引用,不销毁实例(极致缓存策略)
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】视图缓存引用已置空 | 实例保留");
|
||||
sMemoryCachedBackgroundView = null;
|
||||
}
|
||||
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存操作完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知消息(仅调试模式下生效)
|
||||
* @param title 通知标题
|
||||
* @param content 通知内容
|
||||
*/
|
||||
public static void notifyMessage(String title, String content) {
|
||||
LogUtils.d(TAG, "【notifyMessage】发送通知消息方法调用 | 标题:" + title + " | 内容:" + content);
|
||||
|
||||
boolean canSend = isDebugging() && sApp != null && sNotificationManagerUtils != null;
|
||||
if (canSend) {
|
||||
NotificationMessage message = new NotificationMessage(title, content, "");
|
||||
sNotificationManagerUtils.showMessageNotification(sApp, message);
|
||||
LogUtils.d(TAG, "【notifyMessage】通知消息发送成功");
|
||||
} else {
|
||||
LogUtils.d(TAG, "【notifyMessage】通知消息发送失败 | 条件不满足:调试模式=" + isDebugging() + " | 应用实例=" + (sApp != null) + " | 通知工具=" + (sNotificationManagerUtils != null));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 生命周期方法 - 应用全局生命周期 (重写父类方法) ======================================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "【onCreate】应用启动生命周期方法调用 | 开始初始化应用...");
|
||||
|
||||
// 初始化应用单例与调试模式
|
||||
sApp = this;
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
LogUtils.d(TAG, "【onCreate】应用单例已初始化 | 调试模式:" + BuildConfig.DEBUG);
|
||||
|
||||
// 初始化核心组件
|
||||
initBaseTools();
|
||||
initUtils();
|
||||
initReceiver();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】应用初始化完成 | 极致强制缓存策略已激活");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "【onTerminate】应用终止生命周期方法调用 | 开始释放非缓存资源...");
|
||||
|
||||
// 释放非缓存资源
|
||||
ToastUtils.release();
|
||||
releaseNotificationManager();
|
||||
releaseReceiver();
|
||||
|
||||
// 核心策略:不清理任何缓存
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onTerminate】极致缓存策略生效 | 所有缓存将保留在内存中");
|
||||
LogUtils.d(TAG, "【onTerminate】非缓存资源释放完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onTrimMemory】系统内存修剪回调 | 内存等级:" + level + " | 忽略修剪,缓存强制保护");
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onLowMemory】系统低内存回调 | 极致缓存策略生效 | 不执行任何缓存清理操作");
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
// ====================================== 私有初始化方法 - 组件初始化 (按依赖顺序排序) ======================================
|
||||
/**
|
||||
* 初始化基础工具类(Activity管理、Toast、通知管理器)
|
||||
*/
|
||||
private void initBaseTools() {
|
||||
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化开始...");
|
||||
WinBoLLActivityManager.init(this);
|
||||
ToastUtils.init(this);
|
||||
sNotificationManagerUtils = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化核心工具与缓存(极致强制驻留,缓存核心)
|
||||
*/
|
||||
private void initUtils() {
|
||||
LogUtils.d(TAG, "【initUtils】核心工具与缓存初始化开始 | 极致缓存策略激活");
|
||||
|
||||
// 1. 配置与基础缓存工具初始化
|
||||
sAppConfigUtils = getAppConfigUtils(this);
|
||||
sAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
// 2. 资源与Bitmap缓存工具初始化(永久驻留)
|
||||
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
sBackgroundSourceUtils.loadSettings();
|
||||
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "【initUtils】资源与Bitmap缓存工具初始化完成 | 永久驻留内存");
|
||||
|
||||
// 3. 视图缓存初始化(永久驻留,无实例则创建)
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
|
||||
if (sMemoryCachedBackgroundView == null) {
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, sBackgroundSourceUtils.getCurrentBackgroundBean(), true);
|
||||
LogUtils.d(TAG, "【initUtils】视图缓存无现有实例,已创建新实例");
|
||||
}
|
||||
LogUtils.d(TAG, "【initUtils】视图缓存初始化完成 | 永久驻留内存");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局广播接收器
|
||||
*/
|
||||
private void initReceiver() {
|
||||
LogUtils.d(TAG, "【initReceiver】全局广播接收器注册开始...");
|
||||
mGlobalReceiver = new GlobalApplicationReceiver(this);
|
||||
mGlobalReceiver.registerAction();
|
||||
LogUtils.d(TAG, "【initReceiver】全局广播接收器注册完成");
|
||||
}
|
||||
|
||||
// ====================================== 私有释放方法 - 资源释放 (按创建逆序排序) ======================================
|
||||
/**
|
||||
* 释放全局广播接收器
|
||||
*/
|
||||
private void releaseReceiver() {
|
||||
LogUtils.d(TAG, "【releaseReceiver】全局广播接收器释放开始...");
|
||||
if (mGlobalReceiver != null) {
|
||||
mGlobalReceiver.unregisterAction();
|
||||
mGlobalReceiver = null;
|
||||
LogUtils.d(TAG, "【releaseReceiver】全局广播接收器释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放通知管理器资源
|
||||
*/
|
||||
private void releaseNotificationManager() {
|
||||
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放开始...");
|
||||
if (sNotificationManagerUtils != null) {
|
||||
sNotificationManagerUtils.release();
|
||||
sNotificationManagerUtils = null;
|
||||
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 私有辅助方法 - 日志/工具 (辅助功能) ======================================
|
||||
/**
|
||||
* 记录当前缓存详细状态(用于调试监控,极致缓存策略监控)
|
||||
*/
|
||||
private void logDetailedCacheStatus() {
|
||||
LogUtils.d(TAG, "【logDetailedCacheStatus】缓存状态监控日志开始...");
|
||||
|
||||
// Bitmap缓存状态
|
||||
if (sBitmapCacheUtils != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 有效");
|
||||
try {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】Bitmap缓存数量:" + sBitmapCacheUtils.getCacheCount());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(CACHE_PROTECT_TAG, "【缓存详情】获取Bitmap缓存数量失败", e);
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 未初始化");
|
||||
}
|
||||
|
||||
// 视图缓存状态
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 有效");
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】视图实例数量:" + MemoryCachedBackgroundView.getInstanceCount());
|
||||
} else {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 引用已置空(实例可能保留)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,637 +0,0 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewStub;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.activities.AboutActivity;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
|
||||
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
|
||||
import cc.winboll.studio.powerbell.activities.SettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryStyle;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity;
|
||||
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageUtils;
|
||||
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.views.BatteryStyleView;
|
||||
import cc.winboll.studio.powerbell.views.MainContentView;
|
||||
|
||||
/**
|
||||
* 应用核心主活动
|
||||
* 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener {
|
||||
|
||||
// ======================== 静态常量区(抽离魔法值,按功能分类)========================
|
||||
public static final String TAG = "MainActivity";
|
||||
private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
|
||||
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
|
||||
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
|
||||
private static final long DELAY_LOAD_NON_CRITICAL = 500L;
|
||||
|
||||
// Handler 消息常量
|
||||
public static final int MSG_RELOAD_APPCONFIG = 0;
|
||||
public static final int MSG_CURRENTVALUEBATTERY = 1;
|
||||
public static final int MSG_LOAD_BACKGROUND = 2;
|
||||
private static final int MSG_UPDATE_SERVICE_SWITCH = 3;
|
||||
private static final int MSG_UPDATE_BATTERYDRAWABLE = 4;
|
||||
|
||||
// ======================== 静态成员区(全局共享,管控生命周期)========================
|
||||
private static MainActivity sMainActivity;
|
||||
private static Handler sGlobalHandler;
|
||||
|
||||
// ======================== 工具类实例区(单例化,避免重复初始化)========================
|
||||
private PermissionUtils mPermissionUtils;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
|
||||
// ======================== 应用核心实例区 =========================
|
||||
private App mApplication;
|
||||
private MainContentView mMainContentView;
|
||||
private ControlCenterServiceBean mServiceControlBean;
|
||||
|
||||
// ======================== 基础视图组件区 =========================
|
||||
private Toolbar mToolbar;
|
||||
private ViewStub mAdsViewStub;
|
||||
private ADsBannerView mADsBannerView;
|
||||
private Drawable mFrameDrawable;
|
||||
private Menu mMenu;
|
||||
|
||||
// ======================== 生命周期方法区(按系统调用顺序排列)========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 调用 | savedInstanceState: " + savedInstanceState);
|
||||
|
||||
initGlobalHandler();
|
||||
setContentView(R.layout.activity_main);
|
||||
initPermissionUtils();
|
||||
initMainContentView();
|
||||
initCriticalView();
|
||||
initCoreUtilsAsync();
|
||||
loadNonCriticalViewDelayed();
|
||||
|
||||
// 处理首次启动参数
|
||||
handleReloadBackgroundParam(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
LogUtils.d(TAG, "onNewIntent() 调用 | intent: " + intent);
|
||||
// 关键:更新Activity持有的Intent,确保后续获取最新值
|
||||
setIntent(intent);
|
||||
// 统一处理刷新背景参数
|
||||
handleReloadBackgroundParam(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate() 调用 | savedInstanceState: " + savedInstanceState);
|
||||
mPermissionUtils.startPermissionRequest(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume() 调用");
|
||||
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(this);
|
||||
LogUtils.d(TAG, "onResume: 广告视图已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
LogUtils.d(TAG, "onPause() 调用");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy() 调用");
|
||||
|
||||
// 释放广告资源
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
mADsBannerView = null;
|
||||
LogUtils.d(TAG, "onDestroy: 广告资源已释放");
|
||||
}
|
||||
// 释放核心视图
|
||||
if (mMainContentView != null) {
|
||||
mMainContentView.releaseResources();
|
||||
mMainContentView = null;
|
||||
LogUtils.d(TAG, "onDestroy: 核心视图资源已释放");
|
||||
}
|
||||
// 销毁Handler防止内存泄漏
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.removeCallbacksAndMessages(null);
|
||||
sGlobalHandler = null;
|
||||
LogUtils.d(TAG, "onDestroy: 全局Handler已销毁");
|
||||
}
|
||||
// 释放Drawable
|
||||
if (mFrameDrawable != null) {
|
||||
mFrameDrawable.setCallback(null);
|
||||
mFrameDrawable = null;
|
||||
LogUtils.d(TAG, "onDestroy: 框架Drawable已释放");
|
||||
}
|
||||
// 置空所有引用,消除内存泄漏风险
|
||||
sMainActivity = null;
|
||||
mPermissionUtils = null;
|
||||
mAppConfigUtils = null;
|
||||
mBgSourceUtils = null;
|
||||
mServiceControlBean = null;
|
||||
mMenu = null;
|
||||
mApplication = null;
|
||||
mToolbar = null;
|
||||
mAdsViewStub = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "onActivityResult() 调用 | requestCode: " + requestCode + " | resultCode: " + resultCode + " | data: " + data);
|
||||
mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_BACKGROUND_SETTINGS_ACTIVITY && sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
|
||||
LogUtils.d(TAG, "onActivityResult: 发送背景加载消息");
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 菜单与导航方法区 ========================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu() 调用 | menu: " + menu);
|
||||
mMenu = menu;
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
|
||||
// 调试模式加载测试菜单
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu: 已加载测试菜单");
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected() 调用 | itemId: " + item.getItemId());
|
||||
// 主题切换处理
|
||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
Intent mainIntent = new Intent(MainActivity.this, MainActivity.class);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_ACCENTCOLOR, true);
|
||||
startActivity(mainIntent);
|
||||
return true;
|
||||
}
|
||||
// 开发者功能处理
|
||||
if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
return true;
|
||||
}
|
||||
// 菜单点击事件分发
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
break;
|
||||
case R.id.action_battery_report:
|
||||
startActivity(new Intent(this, BatteryReportActivity.class));
|
||||
break;
|
||||
case R.id.action_clearrecord:
|
||||
startActivity(new Intent(this, ClearRecordActivity.class));
|
||||
break;
|
||||
case R.id.action_changepicture:
|
||||
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_BACKGROUND_SETTINGS_ACTIVITY);
|
||||
break;
|
||||
case R.id.action_unittestactivity:
|
||||
startActivity(new Intent(this, MainUnitTestActivity.class));
|
||||
break;
|
||||
case R.id.action_unittest2activity:
|
||||
startActivity(new Intent(this, MainUnitTest2Activity.class));
|
||||
break;
|
||||
case R.id.action_about:
|
||||
startAboutActivity();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupToolbar() {
|
||||
super.setupToolbar();
|
||||
LogUtils.d(TAG, "setupToolbar() 调用");
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
LogUtils.d(TAG, "setupToolbar: 已隐藏返回按钮");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
LogUtils.d(TAG, "onBackPressed() 调用");
|
||||
moveTaskToBack(true);
|
||||
LogUtils.d(TAG, "onBackPressed: 应用已退至后台");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
LogUtils.d(TAG, "dispatchKeyEvent() 调用 | event: " + event);
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// ======================== 核心初始化方法区 ========================
|
||||
private void initPermissionUtils() {
|
||||
LogUtils.d(TAG, "initPermissionUtils() 调用");
|
||||
mPermissionUtils = PermissionUtils.getInstance();
|
||||
LogUtils.d(TAG, "initPermissionUtils: 权限工具类已初始化");
|
||||
}
|
||||
|
||||
private void initGlobalHandler() {
|
||||
LogUtils.d(TAG, "initGlobalHandler() 调用");
|
||||
if (sGlobalHandler == null) {
|
||||
sGlobalHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
// Activity已销毁则跳过消息处理
|
||||
if (sMainActivity == null || sMainActivity.isFinishing() || sMainActivity.isDestroyed()) {
|
||||
LogUtils.w(TAG, "handleMessage: Activity已销毁,跳过消息 | what: " + msg.what);
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "handleMessage() 调用 | what: " + msg.what);
|
||||
|
||||
switch (msg.what) {
|
||||
case MSG_RELOAD_APPCONFIG:
|
||||
sMainActivity.updateViewData();
|
||||
break;
|
||||
case MSG_CURRENTVALUEBATTERY:
|
||||
if (sMainActivity.mMainContentView != null) {
|
||||
sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1);
|
||||
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value: " + msg.arg1);
|
||||
}
|
||||
break;
|
||||
case MSG_LOAD_BACKGROUND:
|
||||
sMainActivity.reloadBackground();
|
||||
break;
|
||||
case MSG_UPDATE_SERVICE_SWITCH:
|
||||
sMainActivity.updateServiceSwitchUI();
|
||||
break;
|
||||
case MSG_UPDATE_BATTERYDRAWABLE:
|
||||
sMainActivity.updateBatteryDrawable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已创建");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已存在,无需重复创建");
|
||||
}
|
||||
}
|
||||
|
||||
private void initMainContentView() {
|
||||
LogUtils.d(TAG, "initMainContentView() 调用");
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
mMainContentView = new MainContentView(this, rootView, this);
|
||||
LogUtils.d(TAG, "initMainContentView: 核心内容视图已初始化");
|
||||
}
|
||||
|
||||
private void initCriticalView() {
|
||||
LogUtils.d(TAG, "initCriticalView() 调用");
|
||||
sMainActivity = this;
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
if (mToolbar != null) {
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
|
||||
}
|
||||
mAdsViewStub = findViewById(R.id.stub_ads_banner);
|
||||
LogUtils.d(TAG, "initCriticalView: 广告ViewStub已获取");
|
||||
}
|
||||
|
||||
private void initCoreUtilsAsync() {
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync() 调用");
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId: " + Thread.currentThread().getId());
|
||||
mApplication = (App) getApplication();
|
||||
mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext());
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
|
||||
|
||||
// 初始化服务控制配置
|
||||
mServiceControlBean = ControlCenterServiceBean.loadBean(getApplicationContext(), ControlCenterServiceBean.class);
|
||||
if (mServiceControlBean == null) {
|
||||
mServiceControlBean = new ControlCenterServiceBean(false);
|
||||
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置不存在,已创建默认配置");
|
||||
}
|
||||
|
||||
// 根据配置启停服务
|
||||
final boolean isServiceEnable = mServiceControlBean.isEnableService();
|
||||
final boolean isServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置状态 | isServiceEnable: " + isServiceEnable + " | isServiceAlive: " + isServiceAlive);
|
||||
|
||||
if (isServiceEnable && !isServiceAlive) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ControlCenterService.startControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已启动");
|
||||
}
|
||||
});
|
||||
} else if (!isServiceEnable && isServiceAlive) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ControlCenterService.stopControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已停止");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主线程更新UI
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁,跳过UI更新");
|
||||
return;
|
||||
}
|
||||
// 适配API30,兼容低版本Drawable加载
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme());
|
||||
} else {
|
||||
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame);
|
||||
}
|
||||
updateViewData();
|
||||
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: UI更新消息已发送");
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void loadNonCriticalViewDelayed() {
|
||||
LogUtils.d(TAG, "loadNonCriticalViewDelayed() 调用 | 延迟时长: " + DELAY_LOAD_NON_CRITICAL + "ms");
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "loadNonCriticalViewDelayed: Activity已销毁,跳过广告加载");
|
||||
return;
|
||||
}
|
||||
loadAdsView();
|
||||
}
|
||||
}, DELAY_LOAD_NON_CRITICAL);
|
||||
}
|
||||
|
||||
// ======================== 视图操作方法区 ========================
|
||||
private void handleReloadBackgroundParam(Intent intent) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam() 调用 | intent: " + intent);
|
||||
if (intent == null) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
|
||||
if (isReloadAccentColor) {
|
||||
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
|
||||
App.sBackgroundSourceUtils.saveSettings();
|
||||
}
|
||||
|
||||
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
|
||||
if (isReloadBackgroundView) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
|
||||
reloadBackgroundView();
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadBackgroundView() {
|
||||
LogUtils.d(TAG, "reloadBackgroundView() 调用");
|
||||
mMainContentView.reloadBackgroundView();
|
||||
}
|
||||
|
||||
private void loadAdsView() {
|
||||
LogUtils.d(TAG, "loadAdsView() 调用");
|
||||
if (mAdsViewStub == null) {
|
||||
LogUtils.e(TAG, "loadAdsView: 广告ViewStub为空,加载失败");
|
||||
return;
|
||||
}
|
||||
if (mADsBannerView == null) {
|
||||
View adsView = mAdsViewStub.inflate();
|
||||
mADsBannerView = adsView.findViewById(R.id.adsbanner);
|
||||
LogUtils.d(TAG, "loadAdsView: 广告视图已加载");
|
||||
} else {
|
||||
LogUtils.d(TAG, "loadAdsView: 广告视图已存在,无需重复加载");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateViewData() {
|
||||
LogUtils.d(TAG, "updateViewData() 调用");
|
||||
if (mMainContentView == null || mFrameDrawable == null) {
|
||||
LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败");
|
||||
return;
|
||||
}
|
||||
mMainContentView.updateViewData(mFrameDrawable);
|
||||
LogUtils.d(TAG, "updateViewData: 视图数据已更新");
|
||||
}
|
||||
|
||||
void updateBatteryDrawable() {
|
||||
BatteryStyle batteryStyle = BatteryStyleView.getSavedBatteryStyle(this);
|
||||
mMainContentView.updateBatteryDrawable(batteryStyle);
|
||||
}
|
||||
|
||||
public static void sendUpdateBatteryDrawableMessage() {
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_BATTERYDRAWABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadBackground() {
|
||||
LogUtils.d(TAG, "reloadBackground() 调用");
|
||||
if (mMainContentView == null || mBgSourceUtils == null) {
|
||||
LogUtils.e(TAG, "reloadBackground: 核心视图或背景工具类为空,加载失败");
|
||||
return;
|
||||
}
|
||||
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
|
||||
if (currentBgBean != null) {
|
||||
mMainContentView.backgroundView.loadByBackgroundBean(currentBgBean, true);
|
||||
LogUtils.d(TAG, "reloadBackground: 已加载自定义背景");
|
||||
} else {
|
||||
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
|
||||
LogUtils.d(TAG, "reloadBackground: 已加载默认背景");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateServiceSwitchUI() {
|
||||
LogUtils.d(TAG, "updateServiceSwitchUI() 调用");
|
||||
if (mMainContentView == null || mServiceControlBean == null) {
|
||||
LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败");
|
||||
return;
|
||||
}
|
||||
boolean configEnabled = mServiceControlBean.isEnableService();
|
||||
mMainContentView.setServiceSwitchEnabled(false);
|
||||
mMainContentView.setServiceSwitchChecked(configEnabled);
|
||||
mMainContentView.setServiceSwitchEnabled(true);
|
||||
LogUtils.d(TAG, "updateServiceSwitchUI: 服务开关已更新 | 状态: " + configEnabled);
|
||||
}
|
||||
|
||||
// ======================== 服务与线程管理方法区 ========================
|
||||
private void toggleServiceEnableState(boolean isEnable) {
|
||||
LogUtils.d(TAG, "toggleServiceEnableState() 调用 | 目标状态: " + isEnable);
|
||||
if (mServiceControlBean == null) {
|
||||
LogUtils.e(TAG, "toggleServiceEnableState: 服务配置为空,切换失败");
|
||||
return;
|
||||
}
|
||||
mServiceControlBean.setIsEnableService(isEnable);
|
||||
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务配置已保存");
|
||||
|
||||
// UI开关联动服务启停
|
||||
if (isEnable) {
|
||||
if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) {
|
||||
ControlCenterService.startControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务已启动");
|
||||
}
|
||||
} else {
|
||||
ControlCenterService.stopControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务已停止");
|
||||
}
|
||||
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
|
||||
}
|
||||
|
||||
// ======================== 页面跳转方法区 ========================
|
||||
private void startAboutActivity() {
|
||||
LogUtils.d(TAG, "startAboutActivity() 调用");
|
||||
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
APPInfo appInfo = genDefaultAppInfo();
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
|
||||
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
|
||||
}
|
||||
|
||||
// ======================== 消息发送方法区 ========================
|
||||
private void notifyServiceAppConfigChange() {
|
||||
LogUtils.d(TAG, "notifyServiceAppConfigChange() 调用");
|
||||
ControlCenterService.sendAppConfigStatusUpdateMessage(this);
|
||||
reloadAppConfig();
|
||||
LogUtils.d(TAG, "notifyServiceAppConfigChange: 服务配置已通知更新");
|
||||
}
|
||||
|
||||
public static void reloadAppConfig() {
|
||||
LogUtils.d(TAG, "reloadAppConfig() 调用");
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG);
|
||||
LogUtils.d(TAG, "reloadAppConfig: 配置重载消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "reloadAppConfig: 全局Handler为空,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendCurrentBatteryValueMessage(int value) {
|
||||
LogUtils.d(TAG, "sendCurrentBatteryValueMessage() 调用 | 电量: " + value);
|
||||
if (sGlobalHandler != null) {
|
||||
Message msg = sGlobalHandler.obtainMessage(MSG_CURRENTVALUEBATTERY);
|
||||
msg.arg1 = value;
|
||||
sGlobalHandler.sendMessage(msg);
|
||||
LogUtils.d(TAG, "sendCurrentBatteryValueMessage: 电量消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "sendCurrentBatteryValueMessage: 全局Handler为空,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 辅助工具方法区 ========================
|
||||
private APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "powerbell";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_launcher);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
|
||||
appInfo.setAppAPKName("PowerBell");
|
||||
appInfo.setAppAPKFolderName("PowerBell");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
|
||||
// ======================== MainContentView 事件回调区 ========================
|
||||
@Override
|
||||
public void onChargeReminderSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onChargeReminderSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUsageReminderSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onUsageReminderSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onServiceSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
toggleServiceEnableState(isChecked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChargeReminderProgressChanged(int progress) {
|
||||
LogUtils.d(TAG, "onChargeReminderProgressChanged() 调用 | progress: " + progress);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUsageReminderProgressChanged(int progress) {
|
||||
LogUtils.d(TAG, "onUsageReminderProgressChanged() 调用 | progress: " + progress);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,994 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 背景设置页面(支持图片选择、拍照、裁剪、像素拾取、调色板等功能)
|
||||
* 核心:基于强制缓存策略,支持预览与设置提交分离,保留操作状态
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class BackgroundSettingsActivity extends WinBoLLActivity {
|
||||
// ====================== 常量定义(按功能分类排序)======================
|
||||
public static final String TAG = "BackgroundSettingsActivity";
|
||||
|
||||
// 系统版本常量
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
|
||||
// 请求码(按功能分组,从小到大排序)
|
||||
public static final int REQUEST_SELECT_PICTURE = 0;
|
||||
public static final int REQUEST_TAKE_PHOTO = 1;
|
||||
public static final int REQUEST_CROP_IMAGE = 2;
|
||||
private static final int REQUEST_PIXELPICKER = 1001;
|
||||
private static final int REQUEST_CAMERA_PERMISSION = 1004;
|
||||
|
||||
// Bitmap解析常量
|
||||
private static final int BITMAP_MAX_SIZE = 2048;
|
||||
private static final int BITMAP_MAX_SAMPLE_SIZE = 16;
|
||||
|
||||
// ====================== 成员变量(按依赖优先级+功能分类)======================
|
||||
// 工具类实例
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
private BitmapCacheUtils mBitmapCache;
|
||||
|
||||
// 视图组件
|
||||
private Toolbar mToolbar;
|
||||
private BackgroundView mBackgroundView;
|
||||
|
||||
// 状态标记(volatile保证多线程可见性)
|
||||
private volatile boolean isCommitSettings = false;
|
||||
private volatile boolean isPreviewBackgroundChanged = false;
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列)======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 开始初始化");
|
||||
setContentView(R.layout.activity_background_settings);
|
||||
|
||||
// 初始化核心组件
|
||||
initCoreComponents();
|
||||
// 初始化Toolbar与点击事件
|
||||
initToolbar();
|
||||
initClickListeners();
|
||||
LogUtils.d(TAG, "onCreate() 视图与事件绑定完成");
|
||||
|
||||
// 处理分享意图或初始化预览
|
||||
handleIntentOrPreview();
|
||||
// 初始化预览环境并刷新
|
||||
initPreviewEnvironment();
|
||||
|
||||
LogUtils.d(TAG, "onCreate() 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate() 执行双重刷新预览");
|
||||
|
||||
// 监听视图布局完成事件
|
||||
mBackgroundView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
// 移除监听,避免重复回调
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
mBackgroundView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
mBackgroundView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
|
||||
// 此时已获取真实宽高
|
||||
int width = mBackgroundView.getWidth();
|
||||
int height = mBackgroundView.getHeight();
|
||||
LogUtils.d(TAG, String.format("onPostCreate() 获取视图尺寸 | width=%d | height=%d", width, height));
|
||||
if (width > 0 && height > 0) {
|
||||
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(BackgroundSettingsActivity.this);
|
||||
appConfigUtils.loadAppConfig();
|
||||
appConfigUtils.mAppConfigBean.setDefaultFrameWidth(width);
|
||||
appConfigUtils.mAppConfigBean.setDefaultFrameHeight(height);
|
||||
appConfigUtils.saveAppConfig();
|
||||
LogUtils.d(TAG, "onPostCreate() 保存默认相框尺寸成功");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, String.format("onActivityResult() | requestCode=%d | resultCode=%d | data=%s",
|
||||
requestCode, resultCode, data != null ? data.toString() : "null"));
|
||||
|
||||
try {
|
||||
if (resultCode != RESULT_OK) {
|
||||
LogUtils.d(TAG, String.format("onActivityResult() 操作取消 | requestCode=%d", requestCode));
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
handleActivityResult(requestCode, data);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("onActivityResult() 异常 | requestCode=%d | 异常信息=%s",
|
||||
requestCode, e.getMessage()));
|
||||
ToastUtils.show("操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
LogUtils.d(TAG, String.format("finish() | isCommitSettings=%b | isPreviewBackgroundChanged=%b",
|
||||
isCommitSettings, isPreviewBackgroundChanged));
|
||||
if (isCommitSettings) {
|
||||
super.finish();
|
||||
} else {
|
||||
handleFinishConfirmation();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 权限回调方法(单独分类)======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, String.format("onRequestPermissionsResult() | requestCode=%d | 权限数量=%d | 结果数量=%d",
|
||||
requestCode, permissions.length, grantResults.length));
|
||||
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
handleCameraPermissionResult(grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 界面初始化方法(Toolbar + 点击事件)======================
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
private void initClickListeners() {
|
||||
LogUtils.d(TAG, "initClickListeners() 开始绑定按钮点击事件");
|
||||
// 绑定所有按钮点击事件
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton3, onTakePhotoClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton4, onSelectPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton5, onNetworkBackgroundDialog);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton6, onCropPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton7, onCropFreePictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
|
||||
LogUtils.d(TAG, "initClickListeners() 按钮点击事件绑定完成");
|
||||
}
|
||||
|
||||
// 通用按钮绑定工具方法
|
||||
private void bindClickListener(int resId, View.OnClickListener listener) {
|
||||
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d", resId));
|
||||
View view = findViewById(resId);
|
||||
if (view != null) {
|
||||
view.setOnClickListener(listener);
|
||||
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d 绑定成功", resId));
|
||||
} else {
|
||||
LogUtils.e(TAG, String.format("bindClickListener() | 未找到视图:%d", resId));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 按钮点击事件(按功能分类)======================
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onOriginNullClickListener() | 取消背景图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onOriginNullClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onSelectPictureClickListener() | 选择图片");
|
||||
launchImageSelector();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onNetworkBackgroundDialog() | 打开网络背景对话框");
|
||||
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener() {
|
||||
@Override
|
||||
public void onConfirm(String szConfirmFilePath) {
|
||||
LogUtils.d(TAG, String.format("网络背景确认 onConfirm() | 文件路径=%s", szConfirmFilePath));
|
||||
// 拷贝文件到预览数据并启动裁剪
|
||||
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
LogUtils.d(TAG, "网络背景取消 onCancel()");
|
||||
}
|
||||
});
|
||||
networkBackgroundDialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCropPictureClickListener() | 固定比例裁剪");
|
||||
startImageCrop(false);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCropFreePictureClickListener() | 自由裁剪");
|
||||
startImageCrop(true);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onTakePhotoClickListener() | 拍照");
|
||||
// 动态申请相机权限
|
||||
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "拍照准备 | 相机权限未授予,发起申请");
|
||||
ActivityCompat.requestPermissions(
|
||||
BackgroundSettingsActivity.this,
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
REQUEST_CAMERA_PERMISSION);
|
||||
return;
|
||||
}
|
||||
handleTakePhoto();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onReceivedPictureClickListener() | 恢复收到的图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onReceivedPictureClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onPixelPickerClickListener() | 像素拾取");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onPixelPickerClickListener() | 预览Bean为空");
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
return;
|
||||
}
|
||||
String targetImagePath = previewBean.getBackgroundFilePath();
|
||||
File targetFile = new File(targetImagePath);
|
||||
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
LogUtils.e(TAG, String.format("像素拾取失败 | 文件无效:%s", targetImagePath));
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", targetImagePath);
|
||||
startActivityForResult(intent, REQUEST_PIXELPICKER);
|
||||
LogUtils.d(TAG, String.format("像素拾取启动 | 路径:%s", targetImagePath));
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCleanPixelClickListener() | 清空像素颜色");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onCleanPixelClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int oldColor = previewBean.getPixelColor();
|
||||
previewBean.setPixelColor(ImageUtils.getColorAccent(BackgroundSettingsActivity.this));
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
ToastUtils.show("像素颜色已清空");
|
||||
LogUtils.d(TAG, String.format("像素清空 | 旧颜色:#%08X", oldColor));
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onColorPaletteClickListener() | 调色板按钮");
|
||||
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onColorPaletteClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int initialColor = previewBean.getPixelColor();
|
||||
LogUtils.d(TAG, String.format("调色板 | 初始颜色:#%08X", initialColor));
|
||||
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
previewBean.setPixelColor(color);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
LogUtils.d(TAG, String.format("颜色选择 | 选中颜色:#%08X", color));
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "调色板 | 对话框已显示");
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 工具方法(通用工具 + 视图工具)======================
|
||||
/**
|
||||
* 生成 FileProvider Uri,适配 Android 7.0+
|
||||
* @param file 目标文件
|
||||
* @return 适配后的Uri,失败返回null
|
||||
*/
|
||||
public Uri getFileProviderUri(File file) {
|
||||
LogUtils.d(TAG, String.format("getFileProviderUri() | 文件路径:%s", (file != null ? file.getAbsolutePath() : "null")));
|
||||
if (file == null) {
|
||||
LogUtils.e(TAG, "getFileProviderUri() | 文件为空");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
|
||||
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("getFileProviderUri() | 生成Uri失败:%s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Bitmap 是否有效(未被回收且不为空)
|
||||
* @param bitmap 目标Bitmap
|
||||
* @return 有效返回true,否则false
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
boolean isValid = bitmap != null && !bitmap.isRecycled();
|
||||
LogUtils.d(TAG, String.format("isBitmapValid() | Bitmap有效性校验:%b", isValid));
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
LogUtils.d(TAG, "doubleRefreshPreview() 开始双重刷新预览");
|
||||
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
|
||||
LogUtils.w(TAG, "双重刷新 跳过:对象为空或Activity已结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
LogUtils.d(TAG, "双重刷新 第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("双重刷新 第一重异常:%s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二重刷新(延迟执行)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
LogUtils.d(TAG, "双重刷新 第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("双重刷新 第二重异常:%s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(按功能分类)======================
|
||||
/**
|
||||
* 初始化核心组件(工具类+视图)
|
||||
*/
|
||||
private void initCoreComponents() {
|
||||
LogUtils.d(TAG, "initCoreComponents() 开始初始化");
|
||||
// 初始化视图
|
||||
mBackgroundView = findViewById(R.id.background_view);
|
||||
if (mBackgroundView == null) {
|
||||
LogUtils.e(TAG, "initCoreComponents() | BackgroundView未找到");
|
||||
}
|
||||
// 初始化工具类
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBitmapCache = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "initCoreComponents() 视图与工具类加载完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理意图或初始化预览
|
||||
*/
|
||||
private void handleIntentOrPreview() {
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() 开始处理");
|
||||
if (handleShareIntent()) {
|
||||
ToastUtils.show("已接收分享图片");
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() | 处理分享意图成功");
|
||||
} else {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() | 加载当前背景配置");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化预览环境
|
||||
*/
|
||||
private void initPreviewEnvironment() {
|
||||
LogUtils.d(TAG, "initPreviewEnvironment() 开始初始化");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "initPreviewEnvironment() 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分享意图
|
||||
* @return 处理成功返回true,否则false
|
||||
*/
|
||||
private boolean handleShareIntent() {
|
||||
LogUtils.d(TAG, "handleShareIntent() 开始处理");
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
LogUtils.d(TAG, String.format("分享处理 | action:%s,type:%s", action, type));
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
showSharePreviewDialog();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示分享图片预览对话框
|
||||
*/
|
||||
private void showSharePreviewDialog() {
|
||||
LogUtils.d(TAG, "showSharePreviewDialog() 开始显示");
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() {
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
|
||||
LogUtils.d(TAG, String.format("分享确认 | Uri:%s", uriRecivedPicture.toString()));
|
||||
if (putUriFileToPreviewSource(uriRecivedPicture)) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
dlg.show();
|
||||
LogUtils.d(TAG, "分享处理 | 显示图片预览对话框");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片类型
|
||||
* @param mimeType MIME类型
|
||||
* @return 是图片返回true,否则false
|
||||
*/
|
||||
private boolean isImageType(String mimeType) {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerMimeType = mimeType.toLowerCase();
|
||||
LogUtils.d(TAG, String.format("isImageType() | mimeType: %s, lowerMimeType: %s", mimeType, lowerMimeType));
|
||||
return lowerMimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
*/
|
||||
private void launchImageSelector() {
|
||||
LogUtils.d(TAG, "launchImageSelector() 启动图片选择器");
|
||||
Intent[] intents = createImageSelectorIntents();
|
||||
Intent validIntent = findValidIntent(intents);
|
||||
|
||||
if (validIntent != null) {
|
||||
launchImageChooser(validIntent);
|
||||
} else {
|
||||
showNoGalleryDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片选择器意图数组
|
||||
* @return 意图数组
|
||||
*/
|
||||
private Intent[] createImageSelectorIntents() {
|
||||
LogUtils.d(TAG, "createImageSelectorIntents() 开始创建");
|
||||
Intent[] intents = new Intent[3];
|
||||
// ACTION_GET_CONTENT
|
||||
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
getContentIntent.setType("image/*");
|
||||
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[0] = getContentIntent;
|
||||
|
||||
// ACTION_PICK
|
||||
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
pickIntent.setType("image/*");
|
||||
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[1] = pickIntent;
|
||||
|
||||
// ACTION_OPEN_DOCUMENT(API19+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
openDocIntent.setType("image/*");
|
||||
openDocIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
intents[2] = openDocIntent;
|
||||
}
|
||||
LogUtils.d(TAG, "createImageSelectorIntents() 意图数组创建完成");
|
||||
return intents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找有效的意图
|
||||
* @param intents 意图数组
|
||||
* @return 有效意图,无则返回null
|
||||
*/
|
||||
private Intent findValidIntent(Intent[] intents) {
|
||||
LogUtils.d(TAG, "findValidIntent() 开始查找");
|
||||
for (Intent intent : intents) {
|
||||
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
|
||||
LogUtils.d(TAG, "findValidIntent() | 找到有效意图");
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "findValidIntent() | 无有效意图");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
* @param validIntent 有效意图
|
||||
*/
|
||||
private void launchImageChooser(Intent validIntent) {
|
||||
LogUtils.d(TAG, "launchImageChooser() 启动选择器");
|
||||
Intent chooser = Intent.createChooser(validIntent, "选择图片");
|
||||
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
|
||||
LogUtils.d(TAG, "launchImageChooser() | 启动图片选择");
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示无相册应用提示对话框
|
||||
*/
|
||||
private void showNoGalleryDialog() {
|
||||
LogUtils.d(TAG, "showNoGalleryDialog() | 无相册应用");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("未找到相册应用,请安装后重试");
|
||||
new AlertDialog.Builder(BackgroundSettingsActivity.this)
|
||||
.setTitle("无图片选择应用")
|
||||
.setMessage("需要安装相册应用才能选择图片")
|
||||
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
launchGalleryMarket();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用商店下载相册
|
||||
*/
|
||||
private void launchGalleryMarket() {
|
||||
LogUtils.d(TAG, "launchGalleryMarket() 启动应用商店");
|
||||
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
|
||||
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
|
||||
if (marketIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(marketIntent);
|
||||
LogUtils.d(TAG, "launchGalleryMarket() | 启动成功");
|
||||
} else {
|
||||
ToastUtils.show("无法打开应用商店");
|
||||
LogUtils.e(TAG, "launchGalleryMarket() | 启动失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理操作取消或失败
|
||||
*/
|
||||
private void handleOperationCancelOrFail() {
|
||||
LogUtils.d(TAG, "handleOperationCancelOrFail() 操作取消或失败");
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
ToastUtils.show("操作取消或失败");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照逻辑(权限通过后执行)
|
||||
*/
|
||||
void handleTakePhoto() {
|
||||
LogUtils.d(TAG, "handleTakePhoto() 开始处理拍照");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleTakePhoto() | 预览Bean为空");
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
File takePhotoFile = new File(previewBean.getBackgroundFilePath());
|
||||
if (!takePhotoFile.exists()) {
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
LogUtils.e(TAG, String.format("handleTakePhoto() | 文件不存在:%s", takePhotoFile.getAbsolutePath()));
|
||||
return;
|
||||
}
|
||||
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
try {
|
||||
Uri photoUri = getFileProviderUri(takePhotoFile);
|
||||
if (photoUri == null) {
|
||||
throw new Exception("生成FileProvider Uri失败");
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
LogUtils.d(TAG, String.format("handleTakePhoto() | Uri:%s", photoUri.toString()));
|
||||
} catch (Exception e) {
|
||||
String errMsg = "拍照启动异常:" + e.getMessage();
|
||||
ToastUtils.show(errMsg.substring(0, 20));
|
||||
LogUtils.e(TAG, String.format("handleTakePhoto() | %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理ActivityResult分发
|
||||
* @param requestCode 请求码
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleActivityResult(int requestCode, Intent data) {
|
||||
LogUtils.d(TAG, String.format("handleActivityResult() | 处理请求码:%d", requestCode));
|
||||
switch (requestCode) {
|
||||
case REQUEST_SELECT_PICTURE:
|
||||
handleSelectPictureResult(data);
|
||||
break;
|
||||
case REQUEST_TAKE_PHOTO:
|
||||
handleTakePhotoResult(data);
|
||||
break;
|
||||
case REQUEST_CROP_IMAGE:
|
||||
handleCropImageResult(data);
|
||||
break;
|
||||
case REQUEST_PIXELPICKER:
|
||||
handlePixelPickerResult();
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, String.format("handleActivityResult() | 未知requestCode:%d", requestCode));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleTakePhotoResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleTakePhotoResult() 处理拍照结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleTakePhotoResult() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
|
||||
startImageCrop(false);
|
||||
LogUtils.d(TAG, "handleTakePhotoResult() | 已启动裁剪");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选图结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleSelectPictureResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() 处理选图结果");
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
LogUtils.e(TAG, "handleSelectPictureResult() | Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, String.format("handleSelectPictureResult() | 系统返回Uri : %s", selectedImage.toString()));
|
||||
|
||||
// 申请持久化权限(API33+)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() | 已添加持久化权限");
|
||||
}
|
||||
|
||||
// 同步文件并启动裁剪
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() | 路径绑定完成");
|
||||
startImageCrop(false);
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "handleSelectPictureResult() | 文件复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Uri 文件同步到预览 Bean
|
||||
* @param srcUriFile 源Uri
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(Uri srcUriFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源Uri:%s", srcUriFile.toString()));
|
||||
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
LogUtils.e(TAG, "putUriFileToPreviewSource() | Uri解析路径为空");
|
||||
return false;
|
||||
}
|
||||
File srcFile = new File(filePath);
|
||||
return putUriFileToPreviewSource(srcFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 File 同步到预览 Bean
|
||||
* @param srcFile 源文件
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(File srcFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源文件:%s", srcFile.getAbsolutePath()));
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
File dstFile = new File(previewBean.getBackgroundFilePath());
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 目标文件:%s", dstFile.getAbsolutePath()));
|
||||
if (FileUtils.copyFile(srcFile, dstFile)) {
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件拷贝成功");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件无法拷贝");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleCropImageResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleCropImageResult() 处理裁剪结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleCropImageResult() | 预览Bean为空");
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
File cropTempFile = new File(previewBean.getBackgroundScaledCompressFilePath());
|
||||
boolean isFileExist = cropTempFile.exists();
|
||||
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
|
||||
long fileSize = isFileExist ? cropTempFile.length() : 0;
|
||||
boolean isCropSuccess = isFileExist && isFileReadable && fileSize > 100;
|
||||
|
||||
if (isCropSuccess) {
|
||||
handleCropSuccess(previewBean, fileSize);
|
||||
} else {
|
||||
handleCropFailure(isFileExist, isFileReadable, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪成功
|
||||
* @param previewBean 预览Bean
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
|
||||
LogUtils.d(TAG, String.format("handleCropSuccess() | 裁剪成功,文件大小:%d", fileSize));
|
||||
isPreviewBackgroundChanged = true;
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪失败
|
||||
* @param isFileExist 文件是否存在
|
||||
* @param isFileReadable 文件是否可读
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
|
||||
LogUtils.e(TAG, String.format("handleCropFailure() | 裁剪失败,文件状态:存在=%b,可读=%b,大小=%d",
|
||||
isFileExist, isFileReadable, fileSize));
|
||||
handleOperationCancelOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理像素拾取结果
|
||||
*/
|
||||
private void handlePixelPickerResult() {
|
||||
LogUtils.d(TAG, "handlePixelPickerResult() 处理像素拾取结果");
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相机权限申请结果
|
||||
* @param grantResults 权限结果数组
|
||||
*/
|
||||
private void handleCameraPermissionResult(int[] grantResults) {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() 处理相机权限结果");
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予成功");
|
||||
handleTakePhoto();
|
||||
} else {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予失败");
|
||||
ToastUtils.show("相机权限被拒绝,无法拍照");
|
||||
// 引导用户到设置页面开启权限(用户选择不再询问时)
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
|
||||
launchAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用设置页面
|
||||
*/
|
||||
private void launchAppSettings() {
|
||||
LogUtils.d(TAG, "launchAppSettings() 启动应用设置页面");
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
ToastUtils.show("请在设置中开启相机权限");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Finish确认对话框
|
||||
*/
|
||||
private void handleFinishConfirmation() {
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() 处理Finish确认");
|
||||
if (isPreviewBackgroundChanged) {
|
||||
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
mBgSourceUtils.commitPreviewSourceToCurrent();
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
Intent mainIntent = new Intent(BackgroundSettingsActivity.this, MainActivity.class);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
|
||||
startActivity(mainIntent);
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() | 确认设置,启动MainActivity并刷新背景");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() | 取消设置,关闭页面");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片裁剪
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
*/
|
||||
private void startImageCrop(boolean isFreeCrop) {
|
||||
LogUtils.d(TAG, String.format("startImageCrop() | 是否自由裁剪:%b", isFreeCrop));
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "startImageCrop() | 预览Bean为空");
|
||||
ToastUtils.show("裁剪失败:无有效图片");
|
||||
return;
|
||||
}
|
||||
int width = isFreeCrop ? 0 : mBackgroundView.getWidth();
|
||||
int height = isFreeCrop ? 0 : mBackgroundView.getHeight();
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
previewBean,
|
||||
width,
|
||||
height,
|
||||
isFreeCrop,
|
||||
REQUEST_CROP_IMAGE);
|
||||
LogUtils.d(TAG, String.format("startImageCrop() | 目标尺寸:%dx%d", width, height));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,598 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 电池报告页面,统计应用24小时运行时长与电池消耗情况
|
||||
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量(按功能分类) =========================
|
||||
public static final String TAG = "BatteryReportActivity";
|
||||
private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数
|
||||
private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数
|
||||
|
||||
// ======================== 成员变量(按依赖优先级+功能分类) =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private RecyclerView rvBatteryReport;
|
||||
private EditText etSearch;
|
||||
|
||||
// 数据与适配器
|
||||
private BatteryReportAdapter adapter;
|
||||
private List<AppBatteryModel> dataList = new ArrayList<>();
|
||||
private List<AppBatteryModel> filteredList = new ArrayList<>();
|
||||
|
||||
// 电池相关
|
||||
private BroadcastReceiver batteryReceiver;
|
||||
private int batteryCapacity = 5400; // 电池容量(mAh)
|
||||
private float lastBatteryPercent = 100.0f; // 上次电池百分比
|
||||
private long lastCheckTime = System.currentTimeMillis(); // 上次检查时间戳
|
||||
|
||||
// 缓存相关
|
||||
private Map<String, Long> appRunTimeCache = new HashMap<>();
|
||||
private Map<String, String> packageToAppNameCache = new HashMap<>();
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法(按执行顺序排列) =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_battery_report);
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化PackageManager
|
||||
mPackageManager = getPackageManager();
|
||||
LogUtils.d(TAG, "【onCreate】基础组件初始化完成");
|
||||
|
||||
// 权限检查(Java7 传统条件判断)
|
||||
if (!hasUsageStatsPermission(this)) {
|
||||
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
|
||||
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
|
||||
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
|
||||
loadAllAppPackage();
|
||||
preCacheAllAppNames();
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateInitial24hTotalConsumption();
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【onCreate】数据初始化完成,原始数据量:" + dataList.size());
|
||||
|
||||
// 初始化适配器
|
||||
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
|
||||
rvBatteryReport.setAdapter(adapter);
|
||||
LogUtils.d(TAG, "【onCreate】适配器初始化完成,过滤后数据量:" + filteredList.size());
|
||||
|
||||
// 绑定搜索监听 + 注册电池广播
|
||||
bindSearchListener();
|
||||
registerBatteryReceiver();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Java7 显式非空判断
|
||||
if (batteryReceiver != null) {
|
||||
unregisterReceiver(batteryReceiver);
|
||||
LogUtils.d(TAG, "【onDestroy】电池广播已注销");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化RecyclerView与搜索框
|
||||
etSearch = (EditText) findViewById(R.id.et_search);
|
||||
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
|
||||
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 搜索监听绑定方法 =========================
|
||||
private void bindSearchListener() {
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String keyword = s.toString().trim();
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + keyword);
|
||||
filterAppsByPackageAndName(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 电池广播注册方法 =========================
|
||||
private void registerBatteryReceiver() {
|
||||
batteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int level = intent.getIntExtra("level", 100);
|
||||
int scale = intent.getIntExtra("scale", 100);
|
||||
float currentPercent = (float) level / scale * 100;
|
||||
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
|
||||
|
||||
if (currentPercent < lastBatteryPercent) {
|
||||
float dropPercent = lastBatteryPercent - currentPercent;
|
||||
long duration = System.currentTimeMillis() - lastCheckTime;
|
||||
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
|
||||
|
||||
// 更新运行时长并计算耗电
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
|
||||
}
|
||||
|
||||
// 刷新记录
|
||||
lastBatteryPercent = currentPercent;
|
||||
lastCheckTime = System.currentTimeMillis();
|
||||
}
|
||||
};
|
||||
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
|
||||
}
|
||||
|
||||
// ======================== 权限检查方法 =========================
|
||||
/**
|
||||
* 检查是否拥有使用情况访问权限
|
||||
* @param context 上下文
|
||||
* @return 拥有权限返回true,否则返回false
|
||||
*/
|
||||
private boolean hasUsageStatsPermission(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP,不支持使用情况访问权限");
|
||||
return false;
|
||||
}
|
||||
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
if (manager == null) {
|
||||
LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_MINUTE_MS;
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
boolean hasPermission = statsList != null && !statsList.isEmpty();
|
||||
LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果:" + hasPermission);
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
// ======================== 数据加载与缓存方法 =========================
|
||||
/**
|
||||
* 加载所有应用包名,初始化数据模型
|
||||
*/
|
||||
private void loadAllAppPackage() {
|
||||
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
dataList.clear();
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
|
||||
for (ApplicationInfo appInfo : appList) {
|
||||
String packageName = appInfo.packageName;
|
||||
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
|
||||
}
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
|
||||
}
|
||||
|
||||
/**
|
||||
* 预缓存所有应用名称,减少PackageManager重复调用
|
||||
*/
|
||||
private void preCacheAllAppNames() {
|
||||
packageToAppNameCache.clear();
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String appName = getAppNameByPackage(packageName);
|
||||
packageToAppNameCache.put(packageName, appName);
|
||||
}
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过包名获取应用名称,带异常处理
|
||||
* @param packageName 应用包名
|
||||
* @return 应用名称,获取失败返回包名
|
||||
*/
|
||||
private String getAppNameByPackage(String packageName) {
|
||||
LogUtils.v(TAG, "【getAppNameByPackage】查询包名:" + packageName);
|
||||
try {
|
||||
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
|
||||
return mPackageManager.getApplicationLabel(appInfo).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
return packageName;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运行时长到数据模型
|
||||
*/
|
||||
private void updateAppRunTimeToModel() {
|
||||
int updateCount = 0;
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
|
||||
model.setRunTime(runTime);
|
||||
if (runTime > 0) {
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用24小时运行时长
|
||||
* @return 应用包名-运行时长(ms)映射
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
Map<String, Long> runTimeMap = new HashMap<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_DAY_MS; // 近24小时
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
for (android.app.usage.UsageStats stats : statsList) {
|
||||
long runTimeMs = stats.getTotalTimeInForeground();
|
||||
String packageName = stats.getPackageName();
|
||||
runTimeMap.put(packageName, runTimeMs);
|
||||
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
|
||||
if (packageName.equals("aidepro.top")) {
|
||||
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量:" + runTimeMap.size());
|
||||
return runTimeMap;
|
||||
}
|
||||
|
||||
// ======================== 核心计算方法 =========================
|
||||
/**
|
||||
* 初始化时计算24小时累计耗电(赋值给totalConsumption)
|
||||
* 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗
|
||||
*/
|
||||
private void calculateInitial24hTotalConsumption() {
|
||||
long total24hRunTime = 0;
|
||||
// 1. 计算24小时内所有应用总运行时长
|
||||
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
|
||||
total24hRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
|
||||
|
||||
// 2. 按运行时长占比分配24小时累计耗电
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
|
||||
|
||||
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
|
||||
float initialTotalConsumption = batteryCapacity * ratio;
|
||||
model.setTotalConsumption(initialTotalConsumption);
|
||||
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh");
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单次耗电(赋值给consumption)+ 累加至累计耗电
|
||||
* @param dropPercent 电池下降百分比
|
||||
* @param runTimeMap 应用运行时长映射
|
||||
*/
|
||||
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】开始计算,电池下降百分比:" + dropPercent);
|
||||
long totalSingleRunTime = 0;
|
||||
// 1. 计算本次电池下降期间的总运行时长
|
||||
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
|
||||
totalSingleRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长:" + formatRunTime(totalSingleRunTime));
|
||||
|
||||
// 2. 遍历计算每个应用的单次耗电并累加
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
|
||||
|
||||
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
|
||||
model.setConsumption(singleConsumption);
|
||||
|
||||
// 累加至累计耗电
|
||||
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
|
||||
model.setTotalConsumption(newTotalConsumption);
|
||||
model.setRunTime(appSingleRunTime);
|
||||
|
||||
LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s:单次耗电%.1f mAh,累计耗电%.1f mAh",
|
||||
packageName, singleConsumption, newTotalConsumption));
|
||||
}
|
||||
|
||||
// 3. 按累计耗电降序排序
|
||||
Collections.sort(dataList, new Comparator<AppBatteryModel>() {
|
||||
@Override
|
||||
public int compare(AppBatteryModel m1, AppBatteryModel m2) {
|
||||
return Float.compare(m2.getTotalConsumption(), m1.getTotalConsumption());
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 重新过滤并刷新列表
|
||||
filterAppsByPackageAndName(etSearch.getText().toString().trim());
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成,列表已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 双维度过滤(包名+应用名)
|
||||
* @param keyword 搜索关键词
|
||||
*/
|
||||
private void filterAppsByPackageAndName(String keyword) {
|
||||
filteredList.clear();
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空,显示全部应用,数量:" + filteredList.size());
|
||||
} else {
|
||||
String lowerKeyword = keyword.toLowerCase();
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String packageNameLower = packageName.toLowerCase();
|
||||
String appName = packageToAppNameCache.get(packageName);
|
||||
String appNameLower = appName.toLowerCase();
|
||||
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword) || appNameLower.contains(lowerKeyword);
|
||||
if (isMatched) {
|
||||
filteredList.add(model);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size());
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 格式化运行时长
|
||||
* @param runTimeMs 运行时长(ms)
|
||||
* @return 格式化后的运行时长字符串
|
||||
*/
|
||||
private String formatRunTime(long runTimeMs) {
|
||||
if (runTimeMs <= 0) {
|
||||
return "0秒";
|
||||
}
|
||||
long seconds = runTimeMs / 1000;
|
||||
long hours = seconds / 3600;
|
||||
long minutes = (seconds % 3600) / 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%d时%d分%d秒", hours, minutes, seconds);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%d分%d秒", minutes, seconds);
|
||||
} else {
|
||||
return String.format("%d秒", seconds);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:数据模型 =========================
|
||||
/**
|
||||
* 应用电池数据模型
|
||||
* - consumption:单次耗电(两次电池广播间的消耗)
|
||||
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加)
|
||||
* - runTime:运行时长(ms)
|
||||
*/
|
||||
public static class AppBatteryModel {
|
||||
private String packageName; // 应用包名(核心标识)
|
||||
private float consumption; // 单次耗电(mAh)
|
||||
private float totalConsumption;// 累计耗电(mAh)
|
||||
private long runTime; // 运行时长(ms)
|
||||
|
||||
// Java7 显式构造
|
||||
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
|
||||
this.packageName = packageName;
|
||||
this.consumption = consumption;
|
||||
this.totalConsumption = totalConsumption;
|
||||
this.runTime = runTime;
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public float getConsumption() {
|
||||
return consumption;
|
||||
}
|
||||
|
||||
public void setConsumption(float consumption) {
|
||||
this.consumption = consumption;
|
||||
}
|
||||
|
||||
public float getTotalConsumption() {
|
||||
return totalConsumption;
|
||||
}
|
||||
|
||||
public void setTotalConsumption(float totalConsumption) {
|
||||
this.totalConsumption = totalConsumption;
|
||||
}
|
||||
|
||||
public long getRunTime() {
|
||||
return runTime;
|
||||
}
|
||||
|
||||
public void setRunTime(long runTime) {
|
||||
this.runTime = runTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:RecyclerView适配器 =========================
|
||||
/**
|
||||
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
|
||||
*/
|
||||
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
|
||||
private Context mContext;
|
||||
private List<AppBatteryModel> mDataList;
|
||||
private PackageManager mPm;
|
||||
private Map<String, String> mPackageToNameCache;
|
||||
|
||||
// Java7 显式构造
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
PackageManager pm, Map<String, String> packageToNameCache) {
|
||||
this.mContext = context;
|
||||
this.mDataList = dataList;
|
||||
this.mPm = pm;
|
||||
this.mPackageToNameCache = packageToNameCache;
|
||||
LogUtils.d(TAG, "【BatteryReportAdapter】适配器构造完成,数据量:" + dataList.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(mContext)
|
||||
.inflate(android.R.layout.simple_list_item_2, parent, false);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
// Java7 显式非空判断
|
||||
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
|
||||
holder.tvAppName.setText("未知应用");
|
||||
holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒");
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
AppBatteryModel model = mDataList.get(position);
|
||||
String packageName = model.getPackageName();
|
||||
String appName = "";
|
||||
|
||||
// 优先从缓存获取应用名
|
||||
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
|
||||
appName = mPackageToNameCache.get(packageName);
|
||||
} else {
|
||||
// 缓存无数据时兜底查询
|
||||
try {
|
||||
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
|
||||
appName = mPm.getApplicationLabel(appInfo).toString();
|
||||
if (mPackageToNameCache != null) {
|
||||
mPackageToNameCache.put(packageName, appName);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
|
||||
holder.tvAppName.setText(appName);
|
||||
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
|
||||
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
|
||||
model.getTotalConsumption(), runTimeStr);
|
||||
holder.tvConsumption.setText(totalConsumptionText);
|
||||
|
||||
// 显示优化
|
||||
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
|
||||
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
|
||||
holder.tvAppName.setTextSize(16);
|
||||
holder.tvConsumption.setTextSize(14);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mDataList == null ? 0 : mDataList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder:绑定系统布局控件
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAppName; // 应用名称
|
||||
TextView tvConsumption; // 累计耗电 + 运行时长
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
|
||||
tvConsumption = (TextView) itemView.findViewById(android.R.id.text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量(按功能分类) =========================
|
||||
public static final String TAG = "ClearRecordActivity";
|
||||
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
|
||||
|
||||
// ======================== 成员变量(按依赖优先级+功能分类) =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private TextView mtvRecordText;
|
||||
private TextView tvAOHPCTCSeekBarMSG;
|
||||
private AOHPCTCSeekBar aOHPCTCSeekBar;
|
||||
|
||||
// 应用与配置
|
||||
private App mApplication;
|
||||
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法(按执行顺序排列) =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_clearrecord);
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
|
||||
|
||||
// 初始化应用实例
|
||||
mApplication = (App) getApplication();
|
||||
LogUtils.d(TAG, "【onCreate】应用实例初始化完成");
|
||||
|
||||
// 初始化核心逻辑
|
||||
initView();
|
||||
initSeekBar();
|
||||
initRecordText();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化Toolbar与显示文本组件
|
||||
*/
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回按钮,关闭当前页面");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化显示文本组件
|
||||
tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.activityclearrecordTextView1);
|
||||
mtvRecordText = (TextView) findViewById(R.id.activityclearrecordTextView2);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
|
||||
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动清理控件,设置回调监听
|
||||
*/
|
||||
private void initSeekBar() {
|
||||
aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
|
||||
aOHPCTCSeekBar.setThumbOffset(0);
|
||||
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发,开始执行记录清理逻辑");
|
||||
// 清理电池历史记录
|
||||
mApplication.clearBatteryHistory();
|
||||
// 发送广播更新前台通知
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
// 提示清理成功
|
||||
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
|
||||
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成,已发送前台通知更新广播");
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成,回调监听已绑定");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
|
||||
*/
|
||||
void initRecordText() {
|
||||
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
|
||||
String szRecordText;
|
||||
|
||||
// 判空处理:避免空列表导致异常
|
||||
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
|
||||
szRecordText = getString(R.string.msg_no_battery_record);
|
||||
LogUtils.d(TAG, "【initRecordText】无电池记录数据,显示空记录提示文本");
|
||||
} else {
|
||||
// 根据配置切换显示格式
|
||||
if (mIsShowRecordWithEnter) {
|
||||
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
LogUtils.d(TAG, String.format("【initRecordText】使用带换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
|
||||
} else {
|
||||
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
LogUtils.d(TAG, String.format("【initRecordText】使用无换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
|
||||
}
|
||||
}
|
||||
|
||||
mtvRecordText.setText(szRecordText);
|
||||
LogUtils.d(TAG, "【initRecordText】记录显示文本刷新完成");
|
||||
}
|
||||
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 切换记录显示格式(带换行/不带换行)
|
||||
* @param view 触发事件的Switch控件
|
||||
*/
|
||||
public void onShowRecordWithEnter(View view) {
|
||||
Switch swShowRecordWithEnter = (Switch) view;
|
||||
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
|
||||
LogUtils.d(TAG, String.format("【onShowRecordWithEnter】记录显示格式切换,带换行显示:%b", mIsShowRecordWithEnter));
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 像素拾取页面,支持加载图片并拾取指定位置像素颜色,同步至背景配置
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/22 14:15
|
||||
*/
|
||||
public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "PixelPickerActivity";
|
||||
public static final String EXTRA_IMAGE_PATH = "imagePath"; // 图片路径传递键
|
||||
// 提示文本常量
|
||||
private static final String MSG_IMAGE_LOADED = "图片已加载,点击获取像素值";
|
||||
private static final String MSG_NO_IMAGE_PATH = "未找到图片路径";
|
||||
private static final String MSG_IMAGE_LOAD_FAILED = "图片加载失败";
|
||||
private static final String MSG_FILE_NOT_EXIST = "图片文件不存在";
|
||||
private static final String MSG_FILE_NOT_FOUND = "图片文件未找到";
|
||||
private static final String MSG_PIXEL_OUT_OF_RANGE = "像素坐标超出范围";
|
||||
private static final String MSG_TOUCH_OUT_OF_IMAGE = "点击位置超出图片显示范围";
|
||||
private static final String MSG_PIXEL_CALC_FAILED = "计算像素位置失败";
|
||||
private static final String MSG_PIXEL_RECORDED = "已记录像素值";
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private ImageView imageView;
|
||||
private TextView infoText;
|
||||
private ViewGroup imageContainer;
|
||||
private RelativeLayout mainLayout;
|
||||
// 图片与像素数据
|
||||
private Bitmap originalBitmap; // 原始图片Bitmap(用于像素拾取)
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pixelpicker);
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 加载传递的图片
|
||||
loadImageFromIntent();
|
||||
// 绑定图片触摸事件
|
||||
bindImageTouchListener();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "【onResume】PixelPickerActivity 恢复显示");
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 回收Bitmap资源,避免内存泄漏
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
LogUtils.d(TAG, "【onDestroy】原始图片Bitmap资源已回收");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】PixelPickerActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化所有UI组件
|
||||
*/
|
||||
private void initView() {
|
||||
imageView = findViewById(R.id.imageView);
|
||||
infoText = findViewById(R.id.infoText);
|
||||
imageContainer = findViewById(R.id.imageContainer);
|
||||
mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化工具栏,设置导航与标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 从Intent中获取图片路径并加载图片
|
||||
*/
|
||||
private void loadImageFromIntent() {
|
||||
String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径:" + imagePath);
|
||||
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText(MSG_NO_IMAGE_PATH);
|
||||
LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定路径的图片
|
||||
* @param imagePath 图片文件路径
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
File file = new File(imagePath);
|
||||
if (file.exists()) {
|
||||
// 解码图片(加载原图)
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = 1;
|
||||
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
|
||||
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
infoText.setText(MSG_IMAGE_LOADED);
|
||||
LogUtils.d(TAG, "【loadImage】图片加载成功,尺寸:" + originalBitmap.getWidth() + "x" + originalBitmap.getHeight());
|
||||
} else {
|
||||
infoText.setText(MSG_IMAGE_LOAD_FAILED);
|
||||
LogUtils.e(TAG, "【loadImage】图片解码失败");
|
||||
}
|
||||
} else {
|
||||
infoText.setText(MSG_FILE_NOT_EXIST);
|
||||
LogUtils.w(TAG, "【loadImage】图片文件不存在:" + imagePath);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
infoText.setText(MSG_FILE_NOT_FOUND);
|
||||
LogUtils.e(TAG, "【loadImage】图片文件未找到:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示像素颜色信息对话框
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
* @param x 像素X坐标
|
||||
* @param y 像素Y坐标
|
||||
*/
|
||||
private void showPixelDialog(final int pixelColor, int x, int y) {
|
||||
final Dialog dialog = new Dialog(this);
|
||||
dialog.setContentView(R.layout.dialog_pixel);
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// 设置颜色预览与信息展示
|
||||
TextView colorView = dialog.findViewById(R.id.pixelColorView);
|
||||
TextView infoTextView = dialog.findViewById(R.id.colorInfoText);
|
||||
colorView.setBackgroundColor(pixelColor);
|
||||
|
||||
String colorInfo = String.format(
|
||||
"RGB: (%d, %d, %d)\n" +
|
||||
"ARGB: #%08X\n" +
|
||||
"实际像素位置: (%d, %d)",
|
||||
Color.red(pixelColor),
|
||||
Color.green(pixelColor),
|
||||
Color.blue(pixelColor),
|
||||
pixelColor,
|
||||
x, y);
|
||||
infoTextView.setText(colorInfo);
|
||||
LogUtils.d(TAG, "【showPixelDialog】显示像素信息:" + colorInfo);
|
||||
|
||||
// 确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
// 保存像素颜色到背景配置
|
||||
savePixelColor(pixelColor);
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存拾取的像素颜色到背景配置
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
*/
|
||||
private void savePixelColor(int pixelColor) {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
LogUtils.d(TAG, "【savePixelColor】像素颜色已保存:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步背景颜色为拾取的像素颜色
|
||||
*/
|
||||
void setBackgroundColor() {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
int pixelColor = bean.getPixelColor();
|
||||
mainLayout.setBackgroundColor(pixelColor);
|
||||
LogUtils.d(TAG, "【setBackgroundColor】背景颜色已同步:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 绑定图片容器的触摸事件,处理像素拾取逻辑
|
||||
*/
|
||||
private void bindImageTouchListener() {
|
||||
imageContainer.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) {
|
||||
float touchX = event.getX();
|
||||
float touchY = event.getY();
|
||||
LogUtils.v(TAG, "【onTouch】触摸坐标:(" + touchX + ", " + touchY + ")");
|
||||
|
||||
try {
|
||||
// 获取图片在窗口中的位置与尺寸
|
||||
int[] imageLocation = new int[2];
|
||||
imageView.getLocationInWindow(imageLocation);
|
||||
int imageWidth = imageView.getWidth();
|
||||
int imageHeight = imageView.getHeight();
|
||||
LogUtils.v(TAG, "【onTouch】图片显示尺寸:" + imageWidth + "x" + imageHeight + ",位置:(" + imageLocation[0] + ", " + imageLocation[1] + ")");
|
||||
|
||||
// 计算缩放比例
|
||||
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
|
||||
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
|
||||
LogUtils.v(TAG, "【onTouch】图片缩放比例:X=" + scaleX + ",Y=" + scaleY);
|
||||
|
||||
// 调整触摸坐标到图片显示区域坐标系
|
||||
float adjustedX = touchX - imageLocation[0];
|
||||
float adjustedY = touchY - imageLocation[1];
|
||||
LogUtils.v(TAG, "【onTouch】调整后触摸坐标:(" + adjustedX + ", " + adjustedY + ")");
|
||||
|
||||
// 检查是否在图片显示范围内
|
||||
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
|
||||
// 计算原始图片的像素坐标
|
||||
int pixelX = (int) (adjustedX * scaleX);
|
||||
int pixelY = (int) (adjustedY * scaleY);
|
||||
LogUtils.v(TAG, "【onTouch】计算后像素坐标:(" + pixelX + ", " + pixelY + ")");
|
||||
|
||||
// 检查像素坐标是否在原始图片范围内
|
||||
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() && pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
|
||||
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
|
||||
showPixelDialog(pixelColor, pixelX, pixelY);
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_OUT_OF_RANGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】像素坐标超出原始图片范围");
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_TOUCH_OUT_OF_IMAGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】触摸位置超出图片显示范围");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_CALC_FAILED, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.e(TAG, "【onTouch】计算像素位置失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindImageTouchListener】图片触摸事件已绑定");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
|
||||
Intent intent = new Intent(this, BackgroundSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
LogUtils.d(TAG, "【onBackPressed】返回键触发,页面关闭");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 应用设置窗口,提供应用配置项的统一入口
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025年11月27日14时26分00秒
|
||||
* @LastEditTime 2026-02-28
|
||||
* @Describe 应用设置窗口(主开关联动子开关启用/禁用,主开关关闭则子开关禁用并取消勾选)
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量区 =========================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
// ======================== 成员属性区 =========================
|
||||
private Toolbar mToolbar;
|
||||
private CheckBox cbUsePowerTts; // 用电TTS(主开关)
|
||||
private CheckBox cbChargeTts; // 充电TTS(主开关)
|
||||
private CheckBox cbUseageTtsBattary; // 用电TTS带电量提醒(子开关)
|
||||
private CheckBox cbChargeTtsBattary; // 充电TTS带电量提醒(子开关)
|
||||
private CheckBox cbTtsWhenNotifyBattery; // 👉 新增:通知电量时同时播放TTS
|
||||
|
||||
// ======================== 接口实现区 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期区 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
LogUtils.d(TAG, "onCreate: 应用设置页面初始化开始");
|
||||
|
||||
initToolbar();
|
||||
initTtsCheckBoxes();
|
||||
initTtsCheckBoxStatus();
|
||||
|
||||
LogUtils.d(TAG, "onCreate: 应用设置页面初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化区 =========================
|
||||
/**
|
||||
* 初始化顶部工具栏
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 工具栏初始化开始");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar-navigationOnClick: 点击导航返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar: 工具栏初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定TTS相关复选框控件
|
||||
*/
|
||||
private void initTtsCheckBoxes() {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定开始");
|
||||
cbUsePowerTts = findViewById(R.id.activitysettingsCheckBox1);
|
||||
cbChargeTts = findViewById(R.id.activitysettingsCheckBox2);
|
||||
cbUseageTtsBattary = findViewById(R.id.activitysettingsCheckBox3);
|
||||
cbChargeTtsBattary = findViewById(R.id.activitysettingsCheckBox4);
|
||||
cbTtsWhenNotifyBattery = findViewById(R.id.activitysettingsCheckBox5); // 👉 新增绑定
|
||||
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化TTS复选框初始状态
|
||||
*/
|
||||
private void initTtsCheckBoxStatus() {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化开始");
|
||||
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
|
||||
if (bean == null) {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: 未读取到配置Bean,创建新实例");
|
||||
bean = new ThoughtfulServiceBean();
|
||||
}
|
||||
|
||||
boolean useMainOpen = bean.isEnableUsePowerTts();
|
||||
boolean chargeMainOpen = bean.isEnableChargeTts();
|
||||
cbUsePowerTts.setChecked(useMainOpen);
|
||||
cbChargeTts.setChecked(chargeMainOpen);
|
||||
cbUseageTtsBattary.setChecked(bean.isEnableUseageTtsWithBattary());
|
||||
cbChargeTtsBattary.setChecked(bean.isEnableChargeTtsWithBattary());
|
||||
cbTtsWhenNotifyBattery.setChecked(bean.isEnableTtsWhenNotifyBattery()); // 👉 新增赋值
|
||||
cbUseageTtsBattary.setEnabled(useMainOpen);
|
||||
cbChargeTtsBattary.setEnabled(chargeMainOpen);
|
||||
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: 主开关状态-用电TTS:" + useMainOpen + " 充电TTS:" + chargeMainOpen);
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 事件响应区 =========================
|
||||
/**
|
||||
* 悬浮窗权限检查入口
|
||||
*/
|
||||
public void onCheckTTSDrawOverlaysPermission(View view) {
|
||||
LogUtils.d(TAG, "onCheckTTSDrawOverlaysPermission: 触发悬浮窗权限检查");
|
||||
canDrawOverlays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用电TTS主开关点击事件
|
||||
*/
|
||||
public void onEnableUsePowerTts(View view) {
|
||||
boolean isChecked = cbUsePowerTts.isChecked();
|
||||
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS主开关点击,切换后状态=" + isChecked);
|
||||
// 主开关联动子开关
|
||||
cbUseageTtsBattary.setEnabled(isChecked);
|
||||
// 保存配置
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableUsePowerTts(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电TTS主开关点击事件
|
||||
*/
|
||||
public void onEnableChargeTts(View view) {
|
||||
boolean isChecked = cbChargeTts.isChecked();
|
||||
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS主开关点击,切换后状态=" + isChecked);
|
||||
// 主开关联动子开关
|
||||
cbChargeTtsBattary.setEnabled(isChecked);
|
||||
// 保存配置
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableChargeTts(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用电TTS带电量提醒子开关点击事件
|
||||
*/
|
||||
public void onEnableUseageTtsWithBattary(View view) {
|
||||
boolean isChecked = cbUseageTtsBattary.isChecked();
|
||||
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒开关点击,切换后状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableUseageTtsWithBattary(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电TTS带电量提醒子开关点击事件
|
||||
*/
|
||||
public void onEnableChargeTtsWithBattary(View view) {
|
||||
boolean isChecked = cbChargeTtsBattary.isChecked();
|
||||
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒开关点击,切换后状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableChargeTtsWithBattary(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 👉 新增:允许通知电量消息时同时播放TTS语音
|
||||
*/
|
||||
public void onEnableTtsWhenNotifyBattery(View view) {
|
||||
boolean isChecked = cbTtsWhenNotifyBattery.isChecked();
|
||||
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 通知电量时播TTS开关状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableTtsWhenNotifyBattery(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 保存完成");
|
||||
}
|
||||
|
||||
// ======================== 工具方法区 =========================
|
||||
/**
|
||||
* 获取配置Bean实例,避免重复代码
|
||||
*/
|
||||
private ThoughtfulServiceBean getThoughtfulServiceBean() {
|
||||
LogUtils.d(TAG, "getThoughtfulServiceBean: 获取配置Bean");
|
||||
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
|
||||
if (bean == null) {
|
||||
LogUtils.d(TAG, "getThoughtfulServiceBean: 配置Bean为空,创建新实例");
|
||||
bean = new ThoughtfulServiceBean();
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮窗权限检查与请求
|
||||
*/
|
||||
void canDrawOverlays() {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限检查开始");
|
||||
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 未开启悬浮窗权限,发起请求");
|
||||
showDrawOverlayRequestDialog();
|
||||
} else {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限已开启");
|
||||
ToastUtils.show("悬浮窗权限已开启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示悬浮窗权限请求对话框
|
||||
*/
|
||||
private void showDrawOverlayRequestDialog() {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog: 显示悬浮窗权限请求弹窗");
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("权限请求")
|
||||
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog-去设置: 点击跳转权限页面");
|
||||
dialog.dismiss();
|
||||
jumpToDrawOverlaySettings();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog-稍后: 点击取消请求");
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗权限设置页面(反射适配低版本)
|
||||
*/
|
||||
private void jumpToDrawOverlaySettings() {
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置页面");
|
||||
try {
|
||||
Class<?> settingsClazz = Settings.class;
|
||||
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
String action = (String) actionField.get(null);
|
||||
|
||||
Intent intent = new Intent(action);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转权限页面意图已发送");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
|
||||
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||