Compare commits
8 Commits
gallery-v1
...
positions-
| Author | SHA1 | Date | |
|---|---|---|---|
| a93cad67a4 | |||
| db264eb85a | |||
| 8361cb0728 | |||
| 92f94f462f | |||
| 22c719d87c | |||
| 94483067cb | |||
| f21b69c64c | |||
| 30123efd4e |
236
README.md
236
README.md
@@ -1,103 +1,175 @@
|
||||
# WinBoLL 源生态计划项目说明书
|
||||
## 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 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 项目组诞生了。
|
||||
|
||||
### 1. 核心定位
|
||||
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
|
||||
## WinBoLL 项目组研发计划
|
||||
致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。
|
||||
也在探索 https://gitea.winboll.cc/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP APK 分发。
|
||||
更想进阶 https://github.com/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP Beta APK 分发。
|
||||
|
||||
### 2. 仓库架构
|
||||
#### **仓库类型:功能说明**
|
||||
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
|
||||
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
|
||||
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
|
||||
## WinBoLL-APP 汗下...
|
||||
#### ☁应用何置如此呢。且观用户云云。
|
||||
|
||||
### 3. 源码合并管理推送路线图
|
||||
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
|
||||
#### ☁ 正当下 ☁ ###
|
||||
#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景
|
||||
### ☁ WinBoLL 设备资源概述
|
||||
#### ☁ 1. Raid Disk.
|
||||
概述:这是一个矩阵存储类设备。
|
||||
优点:该设备具有数据容错存储功能,
|
||||
数据存储具有特长持久性。
|
||||
缺点:设备使用能源消耗比较高,
|
||||
设备存取速度一般。
|
||||
|
||||
#### ☁ 2. Data Disk.
|
||||
概述:这是一个普通硬盘存储设备
|
||||
优点:该设备独立于操作系统,
|
||||
数据持久性一般,
|
||||
存取能源消耗小于 Raid Disk。
|
||||
缺点:数据存储速度一般,存储能源消耗一般。
|
||||
|
||||
★ WinBoLL → APPBase → OriginMaster
|
||||
★ WinBoLL → AES → OriginMaster
|
||||
★ WinBoLL → PowerBell → OriginMaster
|
||||
★ WinBoLL → Positions → OriginMaster
|
||||
#### ☁ 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 项目核心信息
|
||||
#### ☁ 5. WinBoLL-APP 用户资源概述。
|
||||
1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0
|
||||
|
||||
### 1. 项目简介
|
||||
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
|
||||
### ☁ 稍稍歇 ☁ ###
|
||||
### ☁ 急急停 ☁ 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 项目已设置)
|
||||
|
||||
### 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 应用需求规划
|
||||
☁ 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 应用已提供)
|
||||
|
||||
## 三、应用编译环境检查问题
|
||||
### 核心判断条件:
|
||||
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件,因为编译输出的APK文件需要一个可供保存的环境。
|
||||
### ☁ 看远方 ☁ ###
|
||||
### ☁ 心忧虑 ☁ 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>。
|
||||
|
||||
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
|
||||
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
|
||||
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
|
||||
### ☁ 云游四方 ☁ ###
|
||||
### ☁ 呔! ☁ WinBoLL-APP 应用需求规划
|
||||
☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。
|
||||
☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。
|
||||
☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。
|
||||
|
||||
## 四、前置条件
|
||||
|
||||
### 1. WinBoLL APP 开发环境配置介绍
|
||||
#### WinBoLL APK 编译输出内容包括:
|
||||
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
|
||||
☆ "/sdcard/AppProjects/app.apk"文件。
|
||||
#### WinBoLL APK 源码命名空间规范
|
||||
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
|
||||
### ☁ 吁! ☁ WinBoLL-APP 共享计划前景
|
||||
☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。
|
||||
☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。
|
||||
|
||||
## 五、核心需求规划
|
||||
## ☁ WinBoLL ☁ WinBoLL 主机忧虑
|
||||
☁ WinBoLL 将会提供 gitea.winboll.cc 域名用户注册登录功能。
|
||||
☁ WinBoLL 将会提供 WinBoLL-APP 及其衍生应用的 Gitea 仓库管理服务。
|
||||
☁ WinBoLL 将会提供 winboll.cc 域名 WinBoLL 项目组注册登录功能。
|
||||
|
||||
### 1. WinBoLL 应用安全验证需求
|
||||
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
|
||||
# 本项目要实际运用需要注意以下几个步骤:
|
||||
# 在项目根目录下:
|
||||
## ★. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。
|
||||
## ★. 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties。
|
||||
## ★. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
|
||||
## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
|
||||
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
|
||||
## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息, 和 APK 文件额外输出路径。
|
||||
|
||||
### 2. 手机端源码开发管理需求
|
||||
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
|
||||
|
||||
## 六、编译与使用指南
|
||||
# ☆类库型项目编译方法
|
||||
## 先编译类库对应的模块测试项目
|
||||
### 修改模块测试项目的 build.properties 文件
|
||||
设置属性 libraryProject=<类库项目模块文件夹名称>
|
||||
### 再编译测试项目
|
||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
### 最后编译类库项目
|
||||
$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块文件夹名称>
|
||||
#### 类库模块编译命令执行后,编译器会发布到 WinBoLL Nexus Maven 库:Maven 库地址可以参阅根项目目录配置 build.gradle 文件。
|
||||
|
||||
# ☆应用型项目编译方法
|
||||
## 直接调用以下命令编译应用型项目
|
||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
||||
#### 应用模块编译命令执行后,编译器会复制一份 APK 到
|
||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
|
||||
### 1. 项目初始化(必须)
|
||||
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
|
||||
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
|
||||
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
|
||||
#### 4. **签名设置**:
|
||||
☆ **调试编译秘钥制作**:使用 Termux 应用终端,cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
|
||||
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
|
||||
## ☆应用调试编译方法
|
||||
使用以下命令编译调试:
|
||||
|
||||
## 七、应用编译命令介绍
|
||||
### Beta调试使用:
|
||||
$ bash gradlew assembleBetaDebug
|
||||
|
||||
### (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 库。
|
||||
### Stage调试使用:
|
||||
$ bash gradlew assembleStageDebug
|
||||
|
||||
### (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` 属性配置的目录下。
|
||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
|
||||
### (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 )
|
||||
# 应用版本号命名方式
|
||||
## statge 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||
如:APPBase_15.7.0
|
||||
## beta 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||
如:APPBase_15.9.6-beta8_5413
|
||||
|
||||
44
build.gradle
44
build.gradle
@@ -1,15 +1,6 @@
|
||||
// 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/'
|
||||
@@ -20,6 +11,19 @@ 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
|
||||
@@ -31,15 +35,6 @@ 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/'
|
||||
@@ -50,6 +45,19 @@ 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 {
|
||||
// 定义全局变量,常用于版本管理
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri May 01 21:09:33 HKT 2026
|
||||
stageCount=16
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.15
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.16
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Put flavor specific strings here -->
|
||||
<string name="app_name">Gallery+</string>
|
||||
</resources>
|
||||
@@ -1,66 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.gallery">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".GlobalWinBoLLApplication"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".AlbumActivity"
|
||||
android:label="@string/app_name"/>
|
||||
|
||||
<activity
|
||||
android:name=".ImageViewerActivity"
|
||||
android:label="@string/app_name"/>
|
||||
|
||||
<activity
|
||||
android:name=".TrashActivity"
|
||||
android:label="@string/trash"/>
|
||||
|
||||
<activity
|
||||
android:name=".CropActivity"
|
||||
android:label="调整封面"/>
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/about"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class Album {
|
||||
public static final String TAG = "Album";
|
||||
private String name;
|
||||
private String path;
|
||||
private Uri coverUri;
|
||||
private int imageCount;
|
||||
|
||||
public Album(String name, String path, Uri coverUri, int imageCount) {
|
||||
LogUtils.d(TAG, "Album created: " + name);
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this.coverUri = coverUri;
|
||||
this.imageCount = imageCount;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getPath() { return path; }
|
||||
public Uri getCoverUri() { return coverUri; }
|
||||
public int getImageCount() { return imageCount; }
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener;
|
||||
|
||||
public class AlbumActivity extends AppCompatActivity {
|
||||
public static final String TAG = "AlbumActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 101;
|
||||
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||
public static final String EXTRA_ALBUM_NAME = "album_name";
|
||||
private RecyclerView recyclerView;
|
||||
private ImageAdapter adapter;
|
||||
private String albumPath;
|
||||
private String albumName;
|
||||
private FloatingActionButton fabScrollTop;
|
||||
private Preferences prefs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
|
||||
|
||||
getSupportActionBar().setTitle(albumName);
|
||||
|
||||
prefs = new Preferences(this);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||
adapter = new ImageAdapter();
|
||||
adapter.setContext(this);
|
||||
adapter.setAlbumPath(albumPath);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
||||
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
});
|
||||
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstVisible > 0) {
|
||||
fabScrollTop.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
fabScrollTop.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
adapter.setOnImageClickListener(new OnImageClickListener() {
|
||||
@Override
|
||||
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
|
||||
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
|
||||
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
|
||||
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
if (checkPermission()) {
|
||||
loadImages();
|
||||
} else {
|
||||
requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestPermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadImages();
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getSortOrder(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case Preferences.SORT_TIME_ASC:
|
||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
|
||||
case Preferences.SORT_NAME_DESC:
|
||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
|
||||
case Preferences.SORT_NAME_ASC:
|
||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
|
||||
case Preferences.SORT_TIME_DESC:
|
||||
default:
|
||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||
}
|
||||
}
|
||||
|
||||
private void loadImages() {
|
||||
LogUtils.d(TAG, "loadImages");
|
||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
ArrayList<String> imagePaths = new ArrayList<>();
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
|
||||
String[] selectionArgs = new String[]{albumPath + "%"};
|
||||
int sortMode = prefs.getAlbumSortMode();
|
||||
String sortOrder = getSortOrder(sortMode);
|
||||
|
||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||
if (cursor != null) {
|
||||
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(dataColumn);
|
||||
if (path != null && path.startsWith(albumPath + "/")) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
|
||||
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
||||
imageUrls.add(contentUri);
|
||||
imagePaths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrls.isEmpty()) {
|
||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.i(TAG, "No images found");
|
||||
}
|
||||
adapter.setData(imageUrls, imagePaths);
|
||||
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_album, menu);
|
||||
int sortMode = prefs.getAlbumSortMode();
|
||||
int menuId = getSortMenuId(sortMode);
|
||||
MenuItem item = menu.findItem(menuId);
|
||||
if (item != null) {
|
||||
item.setChecked(true);
|
||||
}
|
||||
MenuItem sortItem = menu.findItem(R.id.action_sort);
|
||||
if (sortItem != null && sortItem.getSubMenu() != null) {
|
||||
sortItem.getSubMenu().setGroupCheckable(0, true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int getSortMenuId(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case Preferences.SORT_TIME_ASC:
|
||||
return R.id.sort_time_asc;
|
||||
case Preferences.SORT_NAME_DESC:
|
||||
return R.id.sort_name_desc;
|
||||
case Preferences.SORT_NAME_ASC:
|
||||
return R.id.sort_name_asc;
|
||||
case Preferences.SORT_TIME_DESC:
|
||||
default:
|
||||
return R.id.sort_time_desc;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.sort_time_desc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_time_asc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_name_desc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
} else if (itemId == R.id.sort_name_asc) {
|
||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
|
||||
item.setChecked(true);
|
||||
loadImages();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (checkPermission()) {
|
||||
loadImages();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == 100 && resultCode == RESULT_OK) {
|
||||
loadImages();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
|
||||
public static final String TAG = "AlbumAdapter";
|
||||
private ArrayList<Album> albums = new ArrayList<>();
|
||||
private OnAlbumClickListener listener;
|
||||
private Preferences prefs;
|
||||
private int bgType = 0;
|
||||
private PinnedAlbumDbHelper pinnedDbHelper;
|
||||
private OnCoverSizeListener coverSizeListener;
|
||||
private boolean coverSizeReported = false;
|
||||
|
||||
private int getBgRes() {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
return R.drawable.bg_checkerboard;
|
||||
case 1:
|
||||
return R.drawable.bg_white;
|
||||
case 2:
|
||||
return R.drawable.bg_black;
|
||||
default:
|
||||
return R.drawable.bg_checkerboard;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnAlbumClickListener {
|
||||
void onAlbumClick(Album album);
|
||||
}
|
||||
|
||||
public interface OnCoverSizeListener {
|
||||
void onCoverSize(int width, int height);
|
||||
}
|
||||
|
||||
public static final int DEFAULT_COVER_WIDTH = 240;
|
||||
public static final int DEFAULT_COVER_HEIGHT = 120;
|
||||
|
||||
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
|
||||
this.coverSizeListener = listener;
|
||||
}
|
||||
|
||||
public void setData(ArrayList<Album> albums) {
|
||||
this.albums = sortAlbums(albums);
|
||||
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
|
||||
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : list) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
pinned.addAll(unpinned);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void setContext(android.content.Context context) {
|
||||
prefs = new Preferences(context);
|
||||
bgType = prefs.getBgType();
|
||||
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
|
||||
}
|
||||
|
||||
public void refreshBg() {
|
||||
if (prefs != null) {
|
||||
bgType = prefs.getBgType();
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void refreshCover() {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void refreshPinned() {
|
||||
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : albums) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
Comparator<Album> nameComparator = new Comparator<Album>() {
|
||||
@Override
|
||||
public int compare(Album a1, Album a2) {
|
||||
return a1.getName().compareToIgnoreCase(a2.getName());
|
||||
}
|
||||
};
|
||||
Collections.sort(pinned, nameComparator);
|
||||
Collections.sort(unpinned, nameComparator);
|
||||
albums.clear();
|
||||
albums.addAll(pinned);
|
||||
albums.addAll(unpinned);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void showContextMenu(View view, final Album album) {
|
||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||
builder.setTitle(album.getName());
|
||||
final 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
|
||||
public static final String TAG = "AlbumCoverDbHelper";
|
||||
private static final String DB_NAME = "album_cover.db";
|
||||
private static final int DB_VERSION = 2;
|
||||
private static final String TABLE_NAME = "album_covers";
|
||||
private static final String COLUMN_ALBUM_PATH = "album_path";
|
||||
private static final String COLUMN_IMAGE_PATH = "image_path";
|
||||
private static final String COLUMN_CROP_PATH = "crop_path";
|
||||
|
||||
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
|
||||
+ COLUMN_IMAGE_PATH + " TEXT, "
|
||||
+ COLUMN_CROP_PATH + " TEXT)";
|
||||
|
||||
private static AlbumCoverDbHelper dbHelper;
|
||||
|
||||
public static AlbumCoverDbHelper getInstance(Context context) {
|
||||
if (dbHelper == null) {
|
||||
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
|
||||
}
|
||||
return dbHelper;
|
||||
}
|
||||
|
||||
public AlbumCoverDbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion < 2) {
|
||||
try {
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
|
||||
} catch (Exception e) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||
values.put(COLUMN_CROP_PATH, cropPath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
|
||||
}
|
||||
|
||||
public void setCover(String albumPath, String imagePath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
|
||||
}
|
||||
|
||||
public String getCover(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String coverPath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
coverPath = cursor.getString(0);
|
||||
if (coverPath == null) {
|
||||
coverPath = cursor.getString(1);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return coverPath;
|
||||
}
|
||||
|
||||
public String getCropPath(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String cropPath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
cropPath = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return cropPath;
|
||||
}
|
||||
|
||||
public String getOriginalImagePath(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
|
||||
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
String imagePath = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
imagePath = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
public void clearCover(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(COLUMN_CROP_PATH);
|
||||
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||
LogUtils.d(TAG, "clearCover: " + albumPath);
|
||||
}
|
||||
|
||||
public void deleteCover(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||
LogUtils.d(TAG, "deleteCover: " + albumPath);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
public class BgSelectorDialog extends Dialog {
|
||||
|
||||
public interface OnBgSelectedListener {
|
||||
void onBgSelected(int bgType);
|
||||
}
|
||||
|
||||
private int currentBgType;
|
||||
private OnBgSelectedListener listener;
|
||||
|
||||
public BgSelectorDialog(Context context, int currentBgType) {
|
||||
super(context);
|
||||
this.currentBgType = currentBgType;
|
||||
setContentView(R.layout.dialog_bg_selector);
|
||||
initViews();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
RadioGroup radioGroup = findViewById(R.id.radio_group_bg);
|
||||
RadioButton radioCheckerboard = findViewById(R.id.radio_checkerboard);
|
||||
RadioButton radioWhite = findViewById(R.id.radio_white);
|
||||
RadioButton radioBlack = findViewById(R.id.radio_black);
|
||||
|
||||
switch (currentBgType) {
|
||||
case 0:
|
||||
radioCheckerboard.setChecked(true);
|
||||
break;
|
||||
case 1:
|
||||
radioWhite.setChecked(true);
|
||||
break;
|
||||
case 2:
|
||||
radioBlack.setChecked(true);
|
||||
break;
|
||||
}
|
||||
|
||||
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||
int which = 0;
|
||||
if (checkedId == R.id.radio_white) {
|
||||
which = 1;
|
||||
} else if (checkedId == R.id.radio_black) {
|
||||
which = 2;
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onBgSelected(which);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnBgSelectedListener(OnBgSelectedListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
public class CropActivity extends AppCompatActivity {
|
||||
public static final String TAG = "CropActivity";
|
||||
public static final String EXTRA_IMAGE_URI = "image_uri";
|
||||
public static final String EXTRA_IMAGE_PATH = "image_path";
|
||||
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||
public static final String EXTRA_CROP_WIDTH = "crop_width";
|
||||
public static final String EXTRA_CROP_HEIGHT = "crop_height";
|
||||
|
||||
private CropCanvasView cropCanvasView;
|
||||
private ZoomContainerView zoomContainer;
|
||||
private Bitmap originalBitmap;
|
||||
private String imagePath;
|
||||
private String albumPath;
|
||||
private int cropWidth = 240;
|
||||
private int cropHeight = 120;
|
||||
private float cropRatio = 2.0f;
|
||||
private Preferences prefs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_crop);
|
||||
|
||||
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
|
||||
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
|
||||
|
||||
prefs = new Preferences(this);
|
||||
int bgType = prefs.getBgType();
|
||||
if (cropWidth > 0 && cropHeight > 0) {
|
||||
cropRatio = (float) cropWidth / cropHeight;
|
||||
} else {
|
||||
cropRatio = prefs.getCoverRatio();
|
||||
cropWidth = (int) (120 * cropRatio);
|
||||
cropHeight = 120;
|
||||
}
|
||||
|
||||
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
final ImageView btnBg = findViewById(R.id.btn_color_pick);
|
||||
btnBg.setImageResource(R.drawable.ic_bg);
|
||||
btnBg.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showBgDialog();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveCroppedCover();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showCropInfoDialog();
|
||||
}
|
||||
});
|
||||
|
||||
zoomContainer = findViewById(R.id.zoom_container);
|
||||
|
||||
SeekBar seekBarZoom = findViewById(R.id.seekbar_zoom);
|
||||
seekBarZoom.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (zoomContainer != null && fromUser) {
|
||||
float scale = 0.1f + (progress / 100f) * 4.9f;
|
||||
zoomContainer.setScaleFactor(scale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
|
||||
cropCanvasView = findViewById(R.id.crop_canvas_view);
|
||||
|
||||
final View colorView = findViewById(R.id.color_view);
|
||||
final GradientDrawable colorDrawable = (GradientDrawable) colorView.getBackground();
|
||||
|
||||
cropCanvasView.setBackgroundType(bgType);
|
||||
updateColorView(colorDrawable, bgType);
|
||||
|
||||
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
|
||||
@Override
|
||||
public void onBackgroundColorChanged(int color) {
|
||||
}
|
||||
});
|
||||
|
||||
loadImage();
|
||||
}
|
||||
|
||||
private void updateColorView(GradientDrawable drawable, int bgType) {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
drawable.setColor(0xFF808080);
|
||||
break;
|
||||
case 1:
|
||||
drawable.setColor(0xFFFFFFFF);
|
||||
break;
|
||||
case 2:
|
||||
default:
|
||||
drawable.setColor(0xFF000000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showBgDialog() {
|
||||
final View colorView = findViewById(R.id.color_view);
|
||||
final GradientDrawable colorDrawable = (GradientDrawable) colorView.getBackground();
|
||||
final int currentBgType = cropCanvasView.getBackgroundType();
|
||||
|
||||
BgSelectorDialog dialog = new BgSelectorDialog(this, currentBgType);
|
||||
dialog.setOnBgSelectedListener(new BgSelectorDialog.OnBgSelectedListener() {
|
||||
@Override
|
||||
public void onBgSelected(int which) {
|
||||
cropCanvasView.setBackgroundType(which);
|
||||
prefs.setBgType(which);
|
||||
updateColorView(colorDrawable, which);
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void loadImage() {
|
||||
try {
|
||||
if (imagePath != null && new File(imagePath).exists()) {
|
||||
originalBitmap = BitmapFactory.decodeFile(imagePath);
|
||||
} else {
|
||||
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
|
||||
if (imageUri != null) {
|
||||
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalBitmap != null) {
|
||||
cropCanvasView.setImageBitmap(originalBitmap);
|
||||
cropCanvasView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initCrop();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
|
||||
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void initCrop() {
|
||||
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
|
||||
|
||||
int viewW = cropCanvasView.getWidth();
|
||||
int viewH = cropCanvasView.getHeight();
|
||||
if (viewW > 0 && viewH > 0) {
|
||||
cropCanvasView.scaleToView(viewW, viewH);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCroppedCover() {
|
||||
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||
if (canvasBitmap == null || canvasBitmap.isRecycled()) {
|
||||
Toast.makeText(this, "Failed to get canvas bitmap", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
RectF cropRect = cropCanvasView.getCropRect();
|
||||
|
||||
int bmpX = (int) cropRect.left;
|
||||
int bmpY = (int) cropRect.top;
|
||||
int bmpW = (int) cropRect.width();
|
||||
int bmpH = (int) cropRect.height();
|
||||
|
||||
bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
|
||||
bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
|
||||
bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
|
||||
bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
|
||||
|
||||
LogUtils.d(TAG, "saveCroppedCover: cropRect=" + cropRect);
|
||||
LogUtils.d(TAG, "saveCroppedCover: crop=(" + bmpX + "," + bmpY + "," + bmpW + "," + bmpH + ")");
|
||||
LogUtils.d(TAG, "saveCroppedCover: canvas size=" + canvasBitmap.getWidth() + "x" + canvasBitmap.getHeight());
|
||||
|
||||
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
|
||||
canvasBitmap.recycle();
|
||||
|
||||
LogUtils.d(TAG, "saveCroppedCover: cropped size=" + cropped.getWidth() + "x" + cropped.getHeight());
|
||||
|
||||
File coverDir = new File(getFilesDir(), "covers");
|
||||
if (!coverDir.exists()) {
|
||||
coverDir.mkdirs();
|
||||
}
|
||||
File coverFile = new File(coverDir, "cover_" + System.currentTimeMillis() + ".png");
|
||||
LogUtils.d(TAG, "saveCroppedCover: cover file=" + coverFile.getAbsolutePath());
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(coverFile);
|
||||
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.close();
|
||||
|
||||
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
|
||||
|
||||
cropped.recycle();
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "saveCroppedCover: cover saved to db, albumPath=" + albumPath);
|
||||
|
||||
Intent broadcastIntent = new Intent(Preferences.ACTION_COVER_UPDATED);
|
||||
sendBroadcast(broadcastIntent);
|
||||
|
||||
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
|
||||
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showCropInfoDialog() {
|
||||
StringBuilder info = new StringBuilder();
|
||||
info.append("=== 画布信息 ===\n");
|
||||
info.append("画布宽度: ").append(cropCanvasView.getCanvasWidth()).append("px\n");
|
||||
info.append("画布高度: ").append(cropCanvasView.getCanvasHeight()).append("px\n");
|
||||
|
||||
info.append("\n=== 背景类型 ===\n");
|
||||
String[] bgNames = {"灰白相间", "全白", "全黑"};
|
||||
info.append("背景: ").append(bgNames[cropCanvasView.getBackgroundType()]).append("\n");
|
||||
int bgColor = cropCanvasView.getBackgroundColor();
|
||||
info.append("背景颜色: #").append(String.format("%06X", bgColor & 0xFFFFFF)).append("\n");
|
||||
info.append("拾取坐标: ").append(cropCanvasView.getLastPickImageX()).append(",")
|
||||
.append(cropCanvasView.getLastPickImageY()).append("\n");
|
||||
|
||||
info.append("\n=== 裁剪结果 ===\n");
|
||||
RectF cropRect = cropCanvasView.getCropRect();
|
||||
if (cropRect != null) {
|
||||
info.append("裁剪区域: ").append((int)cropRect.left).append(",")
|
||||
.append((int)cropRect.top).append(",")
|
||||
.append((int)cropRect.right).append(",")
|
||||
.append((int)cropRect.bottom).append("\n");
|
||||
info.append("裁剪宽度: ").append((int)cropRect.width()).append("px\n");
|
||||
info.append("裁剪高度: ").append((int)cropRect.height()).append("px\n");
|
||||
}
|
||||
|
||||
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||
final Bitmap[] previewHolder = new Bitmap[1];
|
||||
if (canvasBitmap != null && !canvasBitmap.isRecycled() && cropRect != null) {
|
||||
int bmpX = Math.max(0, (int) cropRect.left);
|
||||
int bmpY = Math.max(0, (int) cropRect.top);
|
||||
int bmpW = Math.min((int) cropRect.width(), canvasBitmap.getWidth() - bmpX);
|
||||
int bmpH = Math.min((int) cropRect.height(), canvasBitmap.getHeight() - bmpY);
|
||||
if (bmpW > 0 && bmpH > 0) {
|
||||
previewHolder[0] = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
|
||||
}
|
||||
}
|
||||
|
||||
final Bitmap previewBitmap = previewHolder[0];
|
||||
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_crop_info, null);
|
||||
TextView infoText = dialogView.findViewById(R.id.info_text);
|
||||
ImageView previewImage = dialogView.findViewById(R.id.preview_image);
|
||||
LinearLayout previewImageContainer = dialogView.findViewById(R.id.preview_image_container);
|
||||
infoText.setText(info.toString());
|
||||
if (previewBitmap != null) {
|
||||
previewImage.setImageBitmap(previewBitmap);
|
||||
}
|
||||
previewImage.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int randomColor = 0xFF000000 | new Random().nextInt(0x00FFFFFF);
|
||||
previewImageContainer.setBackgroundColor(randomColor);
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("裁剪信息")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("确定", null)
|
||||
.create();
|
||||
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
if (previewBitmap != null && !previewBitmap.isRecycled()) {
|
||||
previewBitmap.recycle();
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (originalBitmap != null) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,750 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class CropCanvasView extends View {
|
||||
public interface OnBackgroundColorChangedListener {
|
||||
void onBackgroundColorChanged(int color);
|
||||
}
|
||||
|
||||
public interface OnColorPickedListener {
|
||||
void onColorPicked(int color);
|
||||
}
|
||||
|
||||
public interface OnColorPickEndListener {
|
||||
void onColorPickEnd();
|
||||
}
|
||||
|
||||
private OnBackgroundColorChangedListener backgroundColorChangedListener;
|
||||
private OnColorPickedListener colorPickedListener;
|
||||
private OnColorPickEndListener colorPickEndListener;
|
||||
|
||||
public void setOnBackgroundColorChangedListener(OnBackgroundColorChangedListener listener) {
|
||||
this.backgroundColorChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setOnColorPickedListener(OnColorPickedListener listener) {
|
||||
this.colorPickedListener = listener;
|
||||
}
|
||||
|
||||
public void setOnColorPickEndListener(OnColorPickEndListener listener) {
|
||||
this.colorPickEndListener = listener;
|
||||
}
|
||||
private Paint imagePaint;
|
||||
private Paint borderPaint;
|
||||
private Paint cornerPaint;
|
||||
private RectF cropRect;
|
||||
private int touchArea = 50;
|
||||
|
||||
private float lastX, lastY;
|
||||
private int activeCorner = -1;
|
||||
private static final int CORNER_TOP_LEFT = 0;
|
||||
private static final int CORNER_TOP_RIGHT = 1;
|
||||
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||
private static final int CORNER_CENTER = 4;
|
||||
|
||||
private int imageWidth;
|
||||
private int imageHeight;
|
||||
private float coverRatio = 2.0f;
|
||||
|
||||
private int extendHeight;
|
||||
private int extendWidth;
|
||||
private int canvasWidth;
|
||||
private int canvasHeight;
|
||||
private float minSize = 50;
|
||||
|
||||
private RectF imageBounds = new RectF();
|
||||
private RectF canvasBounds = new RectF();
|
||||
private Bitmap originalBitmap;
|
||||
private Bitmap canvasBitmap;
|
||||
private float bitmapScale = 1.0f;
|
||||
private Bitmap displayBitmap;
|
||||
private RectF initialSpanRect;
|
||||
private float initialSpan;
|
||||
private int bgType = 2;
|
||||
private Bitmap tileBitmap;
|
||||
private BitmapShader tileShader;
|
||||
private Paint bgPaint;
|
||||
private int backgroundColor = Color.BLUE;
|
||||
private boolean colorPickMode = false;
|
||||
private int previewColor = 0;
|
||||
private float lastPickX = 0;
|
||||
private float lastPickY = 0;
|
||||
private float pickX, pickY;
|
||||
|
||||
private float displayScale = 1.0f;
|
||||
private float displayOffsetX = 0f;
|
||||
private float displayOffsetY = 0f;
|
||||
private static final int MAX_DISPLAY_SIZE = 2048;
|
||||
private float containerScale = 1.0f;
|
||||
|
||||
public CropCanvasView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CropCanvasView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
imagePaint = new Paint();
|
||||
imagePaint.setFilterBitmap(true);
|
||||
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(3);
|
||||
|
||||
cornerPaint = new Paint();
|
||||
cornerPaint.setColor(Color.WHITE);
|
||||
cornerPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
bgType = 2;
|
||||
bgPaint = new Paint();
|
||||
initTileBitmap();
|
||||
}
|
||||
|
||||
private void initTileBitmap() {
|
||||
if (tileBitmap != null && !tileBitmap.isRecycled()) {
|
||||
tileBitmap.recycle();
|
||||
tileBitmap = null;
|
||||
}
|
||||
tileShader = null;
|
||||
|
||||
if (bgType == 0) {
|
||||
Drawable drawable = getContext().getDrawable(R.drawable.bg_checkerboard);
|
||||
if (drawable != null) {
|
||||
int w = drawable.getIntrinsicWidth();
|
||||
int h = drawable.getIntrinsicHeight();
|
||||
if (w <= 0) w = 10;
|
||||
if (h <= 0) h = 10;
|
||||
tileBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(tileBitmap);
|
||||
drawable.setBounds(0, 0, w, h);
|
||||
drawable.draw(c);
|
||||
tileShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setBackgroundType(int type) {
|
||||
if (bgType != type) {
|
||||
bgType = type;
|
||||
initTileBitmap();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public int getBackgroundType() {
|
||||
return bgType;
|
||||
}
|
||||
|
||||
private void drawBackground(Canvas canvas) {
|
||||
if (bgType == 0 && tileShader != null) {
|
||||
bgPaint.setShader(tileShader);
|
||||
canvas.drawRect(canvasBounds, bgPaint);
|
||||
} else if (bgType == 1) {
|
||||
bgPaint.setShader(null);
|
||||
bgPaint.setColor(Color.WHITE);
|
||||
canvas.drawRect(canvasBounds, bgPaint);
|
||||
} else {
|
||||
bgPaint.setShader(null);
|
||||
bgPaint.setColor(Color.BLACK);
|
||||
canvas.drawRect(canvasBounds, bgPaint);
|
||||
}
|
||||
}
|
||||
|
||||
public void setImageBitmap(Bitmap bitmap) {
|
||||
if (displayBitmap != null && displayBitmap != originalBitmap) {
|
||||
displayBitmap.recycle();
|
||||
}
|
||||
this.originalBitmap = bitmap;
|
||||
createDisplayBitmap();
|
||||
}
|
||||
|
||||
private void createDisplayBitmap() {
|
||||
if (displayBitmap != null && displayBitmap != originalBitmap) {
|
||||
displayBitmap.recycle();
|
||||
displayBitmap = null;
|
||||
}
|
||||
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
int w = originalBitmap.getWidth();
|
||||
int h = originalBitmap.getHeight();
|
||||
float scale = 1.0f;
|
||||
if (w > MAX_DISPLAY_SIZE || h > MAX_DISPLAY_SIZE) {
|
||||
scale = Math.min((float) MAX_DISPLAY_SIZE / w, (float) MAX_DISPLAY_SIZE / h);
|
||||
int newW = (int) (w * scale);
|
||||
int newH = (int) (h * scale);
|
||||
displayBitmap = Bitmap.createScaledBitmap(originalBitmap, newW, newH, true);
|
||||
} else {
|
||||
displayBitmap = originalBitmap;
|
||||
}
|
||||
displayBitmapScale = scale;
|
||||
}
|
||||
|
||||
private float displayBitmapScale = 1.0f;
|
||||
|
||||
public Bitmap getOriginalBitmap() {
|
||||
return originalBitmap;
|
||||
}
|
||||
|
||||
public float getDisplayBitmapScale() {
|
||||
return displayBitmapScale;
|
||||
}
|
||||
|
||||
public void onContainerScaled() {
|
||||
containerScale = 1.0f;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setContainerScale(float scale) {
|
||||
if (scale > 0 && scale != containerScale) {
|
||||
containerScale = scale;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public float getContainerScale() {
|
||||
return containerScale;
|
||||
}
|
||||
|
||||
public Bitmap getCanvasBitmap() {
|
||||
if (canvasWidth <= 0 || canvasHeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
Bitmap canvasBmp = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(canvasBmp);
|
||||
drawBackground(canvas);
|
||||
if (displayBitmap != null && !displayBitmap.isRecycled()) {
|
||||
if (displayBitmap == originalBitmap) {
|
||||
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||
} else {
|
||||
float invScale = 1f / displayBitmapScale;
|
||||
canvas.save();
|
||||
canvas.scale(invScale, invScale);
|
||||
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
|
||||
imageBounds.top * displayBitmapScale, imagePaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
return canvasBmp;
|
||||
}
|
||||
|
||||
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
|
||||
this.imageWidth = imgWidth;
|
||||
this.imageHeight = imgHeight;
|
||||
this.coverRatio = ratio;
|
||||
|
||||
float imgRatio = (float) imgWidth / imgHeight;
|
||||
|
||||
if (coverRatio >= imgRatio) {
|
||||
extendHeight = imgHeight;
|
||||
extendWidth = (int) (imgHeight * coverRatio);
|
||||
} else {
|
||||
extendWidth = imgWidth;
|
||||
extendHeight = (int) (imgWidth / coverRatio);
|
||||
}
|
||||
|
||||
canvasHeight = Math.max(imageHeight, extendHeight);
|
||||
canvasWidth = Math.max(imageWidth, extendWidth);
|
||||
|
||||
float left = (canvasWidth - imageWidth) / 2f;
|
||||
float top = (canvasHeight - imageHeight) / 2f;
|
||||
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
cropRect = new RectF(canvasBounds);
|
||||
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void getDisplayMatrix(Matrix matrix) {
|
||||
if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
|
||||
return;
|
||||
}
|
||||
matrix.reset();
|
||||
|
||||
float displayScaleX = (float) getWidth() / canvasWidth;
|
||||
float displayScaleY = (float) getHeight() / canvasHeight;
|
||||
displayScale = Math.min(displayScaleX, displayScaleY);
|
||||
|
||||
displayOffsetX = (getWidth() - canvasWidth * displayScale) / 2f;
|
||||
displayOffsetY = (getHeight() - canvasHeight * displayScale) / 2f;
|
||||
|
||||
matrix.postTranslate(displayOffsetX, displayOffsetY);
|
||||
matrix.postScale(displayScale, displayScale);
|
||||
}
|
||||
|
||||
public float screenToImageX(float screenX) {
|
||||
return (screenX - displayOffsetX) / displayScale;
|
||||
}
|
||||
|
||||
public float screenToImageY(float screenY) {
|
||||
return (screenY - displayOffsetY) / displayScale;
|
||||
}
|
||||
|
||||
public float imageToScreenX(float imageX) {
|
||||
return imageX * displayScale + displayOffsetX;
|
||||
}
|
||||
|
||||
public float imageToScreenY(float imageY) {
|
||||
return imageY * displayScale + displayOffsetY;
|
||||
}
|
||||
|
||||
public int getImageColorAt(float screenX, float screenY) {
|
||||
Bitmap bmp = (displayBitmap != null) ? displayBitmap : originalBitmap;
|
||||
if (bmp == null || bmp.isRecycled()) {
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
float imgX = screenToImageX(screenX);
|
||||
float imgY = screenToImageY(screenY);
|
||||
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||
if (bmpX >= 0 && bmpX < bmp.getWidth() &&
|
||||
bmpY >= 0 && bmpY < bmp.getHeight()) {
|
||||
return bmp.getPixel(bmpX, bmpY);
|
||||
}
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
|
||||
public void setBackgroundColor(int color) {
|
||||
this.backgroundColor = color;
|
||||
if (backgroundColorChangedListener != null) {
|
||||
backgroundColorChangedListener.onBackgroundColorChanged(color);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public int getPreviewColor() {
|
||||
return previewColor;
|
||||
}
|
||||
|
||||
public float getLastPickX() {
|
||||
return lastPickX;
|
||||
}
|
||||
|
||||
public float getLastPickY() {
|
||||
return lastPickY;
|
||||
}
|
||||
|
||||
public int getLastPickImageX() {
|
||||
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||
return 0;
|
||||
}
|
||||
float imgX = screenToImageX(lastPickX);
|
||||
float imgY = screenToImageY(lastPickY);
|
||||
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||
return Math.max(0, Math.min(bmpX, originalBitmap.getWidth() - 1));
|
||||
}
|
||||
|
||||
public int getLastPickImageY() {
|
||||
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||
return 0;
|
||||
}
|
||||
float imgX = screenToImageX(lastPickX);
|
||||
float imgY = screenToImageY(lastPickY);
|
||||
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||
return Math.max(0, Math.min(bmpY, originalBitmap.getHeight() - 1));
|
||||
}
|
||||
|
||||
public void setColorPickMode(boolean enable) {
|
||||
this.colorPickMode = enable;
|
||||
if (enable) {
|
||||
pickX = -1;
|
||||
pickY = -1;
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public boolean isColorPickMode() {
|
||||
return colorPickMode;
|
||||
}
|
||||
|
||||
public void scaleToView(int viewWidth, int viewHeight) {
|
||||
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
|
||||
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
|
||||
int oldCanvasW = canvasWidth;
|
||||
int oldCanvasH = canvasHeight;
|
||||
canvasWidth = (int) (canvasWidth * scale);
|
||||
canvasHeight = (int) (canvasHeight * scale);
|
||||
|
||||
float left = (canvasWidth - imageWidth) / 2f;
|
||||
float top = (canvasHeight - imageHeight) / 2f;
|
||||
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
if (cropRect != null) {
|
||||
float scaleX = (float) canvasWidth / oldCanvasW;
|
||||
float scaleY = (float) canvasHeight / oldCanvasH;
|
||||
cropRect.left *= scaleX;
|
||||
cropRect.top *= scaleY;
|
||||
cropRect.right *= scaleX;
|
||||
cropRect.bottom *= scaleY;
|
||||
}
|
||||
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public RectF getCropRect() {
|
||||
return new RectF(cropRect);
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
public int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
public RectF getCanvasBounds() {
|
||||
return new RectF(canvasBounds);
|
||||
}
|
||||
|
||||
public RectF getImageBounds() {
|
||||
return new RectF(imageBounds);
|
||||
}
|
||||
|
||||
public float getBitmapScale() {
|
||||
return displayScale;
|
||||
}
|
||||
|
||||
public float getDisplayScale() {
|
||||
return displayScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int w = canvasWidth > 0 ? canvasWidth : 0;
|
||||
int h = canvasHeight > 0 ? canvasHeight : 0;
|
||||
setMeasuredDimension(w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
getDisplayMatrix(new Matrix());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (cropRect == null) return;
|
||||
|
||||
if (displayBitmap != null && !displayBitmap.isRecycled()) {
|
||||
canvas.save();
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
getDisplayMatrix(matrix);
|
||||
canvas.concat(matrix);
|
||||
|
||||
drawBackground(canvas);
|
||||
|
||||
if (displayBitmap == originalBitmap) {
|
||||
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||
} else {
|
||||
float invScale = 1f / displayBitmapScale;
|
||||
canvas.save();
|
||||
canvas.scale(invScale, invScale);
|
||||
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
|
||||
imageBounds.top * displayBitmapScale, imagePaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
if (containerScale != 1.0f) {
|
||||
canvas.save();
|
||||
canvas.scale(containerScale, containerScale);
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
float cornerRadius = 12f / containerScale;
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||
}
|
||||
} else {
|
||||
if (containerScale != 1.0f) {
|
||||
canvas.save();
|
||||
canvas.scale(containerScale, containerScale);
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
float cornerRadius = 12f / containerScale;
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
|
||||
|
||||
if (event.getPointerCount() == 2) {
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
initialSpan = getSpan(event);
|
||||
return true;
|
||||
} else if (action == MotionEvent.ACTION_MOVE) {
|
||||
float span = getSpan(event);
|
||||
if (initialSpan > 0) {
|
||||
float scale = span / initialSpan;
|
||||
float centerX = cropRect.centerX();
|
||||
float centerY = cropRect.centerY();
|
||||
float newWidth = cropRect.width() * scale;
|
||||
float newHeight = newWidth / coverRatio;
|
||||
|
||||
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
|
||||
newHeight = newWidth / coverRatio;
|
||||
|
||||
float newLeft = centerX - newWidth / 2;
|
||||
float newTop = centerY - newHeight / 2;
|
||||
float newRight = centerX + newWidth / 2;
|
||||
float newBottom = centerY + newHeight / 2;
|
||||
|
||||
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right &&
|
||||
newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||
cropRect.set(newLeft, newTop, newRight, newBottom);
|
||||
}
|
||||
|
||||
initialSpan = span;
|
||||
invalidate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
RectF bounds = canvasBounds;
|
||||
if (containerScale != 1.0f) {
|
||||
x = x / containerScale;
|
||||
y = y / containerScale;
|
||||
bounds = new RectF(
|
||||
canvasBounds.left,
|
||||
canvasBounds.top,
|
||||
canvasBounds.right,
|
||||
canvasBounds.bottom
|
||||
);
|
||||
}
|
||||
|
||||
if (colorPickMode) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
float imgX = screenToImageX(x);
|
||||
float imgY = screenToImageY(y);
|
||||
if (imageBounds.contains(imgX, imgY)) {
|
||||
previewColor = getImageColorAt(x, y);
|
||||
} else if (canvasBounds.contains(x, y)) {
|
||||
previewColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
|
||||
} else {
|
||||
previewColor = Color.TRANSPARENT;
|
||||
}
|
||||
lastPickX = x;
|
||||
lastPickY = y;
|
||||
if (backgroundColorChangedListener != null) {
|
||||
backgroundColorChangedListener.onBackgroundColorChanged(previewColor);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (canvasBounds.contains(x, y)) {
|
||||
int pickedColor;
|
||||
float imgX = screenToImageX(x);
|
||||
float imgY = screenToImageY(y);
|
||||
if (imageBounds.contains(imgX, imgY)) {
|
||||
int colorAtPoint = getImageColorAt(x, y);
|
||||
pickedColor = colorAtPoint;
|
||||
backgroundColor = colorAtPoint;
|
||||
} else {
|
||||
pickedColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
|
||||
}
|
||||
if (colorPickedListener != null) {
|
||||
colorPickedListener.onColorPicked(pickedColor);
|
||||
}
|
||||
}
|
||||
if (colorPickEndListener != null) {
|
||||
colorPickEndListener.onColorPickEnd();
|
||||
}
|
||||
previewColor = 0;
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
activeCorner = getActiveCorner(x, y);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float dx = x - lastX;
|
||||
float dy = y - lastY;
|
||||
|
||||
if (activeCorner == CORNER_CENTER) {
|
||||
float newLeft = cropRect.left + dx;
|
||||
float newTop = cropRect.top + dy;
|
||||
float newRight = cropRect.right + dx;
|
||||
float newBottom = cropRect.bottom + dy;
|
||||
|
||||
if (newLeft >= bounds.left && newRight <= bounds.right) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newRight;
|
||||
}
|
||||
if (newTop >= bounds.top && newBottom <= bounds.bottom) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newBottom;
|
||||
}
|
||||
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
activeCorner = -1;
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||
float newWidth;
|
||||
float newLeft = cropRect.left;
|
||||
float newTop = cropRect.top;
|
||||
float newRight = cropRect.right;
|
||||
float newBottom = cropRect.bottom;
|
||||
|
||||
if (left) {
|
||||
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||
} else {
|
||||
newWidth = nx - cropRect.left;
|
||||
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||
newLeft = cropRect.left;
|
||||
}
|
||||
|
||||
float newHeight = newWidth / coverRatio;
|
||||
|
||||
if (top) {
|
||||
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||
newBottom = newTop + newHeight;
|
||||
} else {
|
||||
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||
newTop = newBottom - newHeight;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newLeft + newWidth;
|
||||
} else {
|
||||
cropRect.right = newRight;
|
||||
cropRect.left = newRight - newWidth;
|
||||
}
|
||||
|
||||
if (top) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newTop + newHeight;
|
||||
} else {
|
||||
cropRect.bottom = newBottom;
|
||||
cropRect.top = newBottom - newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private int getActiveCorner(float x, float y) {
|
||||
float dx = Math.abs(x - cropRect.left);
|
||||
float dy = Math.abs(y - cropRect.top);
|
||||
float dx2 = Math.abs(x - cropRect.right);
|
||||
float dy2 = Math.abs(y - cropRect.bottom);
|
||||
|
||||
if (dx <= touchArea && dy <= touchArea) {
|
||||
return CORNER_TOP_LEFT;
|
||||
}
|
||||
if (dx2 <= touchArea && dy <= touchArea) {
|
||||
return CORNER_TOP_RIGHT;
|
||||
}
|
||||
if (dx <= touchArea && dy2 <= touchArea) {
|
||||
return CORNER_BOTTOM_LEFT;
|
||||
}
|
||||
if (dx2 <= touchArea && dy2 <= touchArea) {
|
||||
return CORNER_BOTTOM_RIGHT;
|
||||
}
|
||||
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
|
||||
return CORNER_CENTER;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private float getSpan(MotionEvent event) {
|
||||
float x0 = event.getX(0);
|
||||
float y0 = event.getY(0);
|
||||
float x1 = event.getX(1);
|
||||
float y1 = event.getY(1);
|
||||
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
|
||||
}
|
||||
|
||||
public int getColorAt(float x, float y) {
|
||||
return getImageColorAt(x, y);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class CropOverlayView extends View {
|
||||
public interface OnCanvasSizeChangedListener {
|
||||
void onCanvasSizeChanged(RectF canvasBounds);
|
||||
}
|
||||
|
||||
private OnCanvasSizeChangedListener canvasSizeChangedListener;
|
||||
|
||||
private Paint borderPaint;
|
||||
private Paint cornerPaint;
|
||||
private RectF cropRect;
|
||||
private int touchArea = 50;
|
||||
|
||||
private float lastX, lastY;
|
||||
private int activeCorner = -1;
|
||||
private static final int CORNER_TOP_LEFT = 0;
|
||||
private static final int CORNER_TOP_RIGHT = 1;
|
||||
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||
private static final int CORNER_CENTER = 4;
|
||||
|
||||
private float targetRatio = 2.0f;
|
||||
private RectF canvasBounds = new RectF();
|
||||
private float minSize = 50;
|
||||
|
||||
public CropOverlayView(Context context) {
|
||||
super(context);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
public CropOverlayView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initPaints();
|
||||
}
|
||||
|
||||
private void initPaints() {
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(3);
|
||||
|
||||
cornerPaint = new Paint();
|
||||
cornerPaint.setColor(Color.WHITE);
|
||||
cornerPaint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
public void setTargetRatio(float ratio) {
|
||||
this.targetRatio = ratio;
|
||||
}
|
||||
|
||||
public float getTargetRatio() {
|
||||
return targetRatio;
|
||||
}
|
||||
|
||||
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
|
||||
this.canvasSizeChangedListener = listener;
|
||||
}
|
||||
|
||||
public void initCanvas(int canvasW, int canvasH) {
|
||||
canvasBounds.set(0, 0, canvasW, canvasH);
|
||||
|
||||
cropRect = new RectF(0, 0, canvasW, canvasH);
|
||||
|
||||
if (canvasSizeChangedListener != null) {
|
||||
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public RectF getCropRect() {
|
||||
return new RectF(cropRect);
|
||||
}
|
||||
|
||||
public RectF getCanvasBounds() {
|
||||
return new RectF(canvasBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
if (cropRect != null) {
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
|
||||
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (cropRect == null || canvasBounds.isEmpty()) return false;
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
activeCorner = getActiveCorner(x, y);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float dx = x - lastX;
|
||||
float dy = y - lastY;
|
||||
|
||||
if (activeCorner == CORNER_CENTER) {
|
||||
float newLeft = cropRect.left + dx;
|
||||
float newTop = cropRect.top + dy;
|
||||
float newRight = cropRect.right + dx;
|
||||
float newBottom = cropRect.bottom + dy;
|
||||
|
||||
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newRight;
|
||||
}
|
||||
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newBottom;
|
||||
}
|
||||
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
activeCorner = -1;
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||
float newWidth;
|
||||
float newLeft = cropRect.left;
|
||||
float newTop = cropRect.top;
|
||||
float newRight = cropRect.right;
|
||||
float newBottom = cropRect.bottom;
|
||||
|
||||
if (left) {
|
||||
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||
} else {
|
||||
newWidth = nx - cropRect.left;
|
||||
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||
newLeft = cropRect.left;
|
||||
}
|
||||
|
||||
float newHeight = newWidth / targetRatio;
|
||||
|
||||
if (top) {
|
||||
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||
newBottom = newTop + newHeight;
|
||||
} else {
|
||||
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||
newTop = newBottom - newHeight;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
cropRect.left = newLeft;
|
||||
cropRect.right = newLeft + newWidth;
|
||||
} else {
|
||||
cropRect.right = newRight;
|
||||
cropRect.left = newRight - newWidth;
|
||||
}
|
||||
|
||||
if (top) {
|
||||
cropRect.top = newTop;
|
||||
cropRect.bottom = newTop + newHeight;
|
||||
} else {
|
||||
cropRect.bottom = newBottom;
|
||||
cropRect.top = newBottom - newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private int getActiveCorner(float x, float y) {
|
||||
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||
return CORNER_TOP_LEFT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||
return CORNER_TOP_RIGHT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||
return CORNER_BOTTOM_LEFT;
|
||||
}
|
||||
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||
return CORNER_BOTTOM_RIGHT;
|
||||
}
|
||||
if (cropRect.contains(x, y)) {
|
||||
return CORNER_CENTER;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/04/24 15:23
|
||||
*/
|
||||
public class GlobalWinBoLLApplication extends GlobalApplication {
|
||||
|
||||
public static final String TAG = "GlobalWinBoLLApplication";
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
//setIsDebugging(false);
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.view_toast);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
//CrashHandler.getInstance().registerGlobal(this);
|
||||
//CrashHandler.getInstance().registerPart(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "onTerminate");
|
||||
ToastUtils.release();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
|
||||
public static final String TAG = "ImageAdapter";
|
||||
private ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
private ArrayList<String> imagePaths = new ArrayList<>();
|
||||
private OnImageClickListener listener;
|
||||
private int bgType = 0;
|
||||
private Preferences prefs;
|
||||
private PinnedImageDbHelper pinnedDbHelper;
|
||||
private AlbumCoverDbHelper coverDbHelper;
|
||||
private String albumPath;
|
||||
|
||||
private int getBgRes() {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
return R.drawable.bg_checkerboard;
|
||||
case 1:
|
||||
return R.drawable.bg_white;
|
||||
case 2:
|
||||
return R.drawable.bg_black;
|
||||
default:
|
||||
return R.drawable.bg_checkerboard;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnImageClickListener {
|
||||
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
|
||||
}
|
||||
|
||||
public void setOnImageClickListener(OnImageClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||
this.imageUrls = urls;
|
||||
this.imagePaths = paths;
|
||||
sortPinnedFirst();
|
||||
LogUtils.d(TAG, "setData: " + urls.size() + " images");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void sortPinnedFirst() {
|
||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayList<Uri> pinnedUrls = new ArrayList<>();
|
||||
ArrayList<String> pinnedPaths = new ArrayList<>();
|
||||
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
|
||||
ArrayList<String> unpinnedPaths = new ArrayList<>();
|
||||
for (int i = 0; i < imagePaths.size(); i++) {
|
||||
String path = imagePaths.get(i);
|
||||
if (pinnedDbHelper.isPinned(path)) {
|
||||
pinnedUrls.add(imageUrls.get(i));
|
||||
pinnedPaths.add(path);
|
||||
} else {
|
||||
unpinnedUrls.add(imageUrls.get(i));
|
||||
unpinnedPaths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
imageUrls.clear();
|
||||
imagePaths.clear();
|
||||
imageUrls.addAll(pinnedUrls);
|
||||
imageUrls.addAll(unpinnedUrls);
|
||||
imagePaths.addAll(pinnedPaths);
|
||||
imagePaths.addAll(unpinnedPaths);
|
||||
}
|
||||
|
||||
public void setContext(android.content.Context context) {
|
||||
prefs = new Preferences(context);
|
||||
bgType = prefs.getBgType();
|
||||
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
|
||||
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
|
||||
}
|
||||
|
||||
public void setAlbumPath(String albumPath) {
|
||||
this.albumPath = albumPath;
|
||||
}
|
||||
|
||||
public int getCropWidth() {
|
||||
return prefs != null ? prefs.getCoverWidth() : 240;
|
||||
}
|
||||
|
||||
public int getCropHeight() {
|
||||
return prefs != null ? prefs.getCoverHeight() : 120;
|
||||
}
|
||||
|
||||
public void refreshBg() {
|
||||
if (prefs != null) {
|
||||
bgType = prefs.getBgType();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshPinned() {
|
||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sortPinnedFirst();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void showContextMenu(final View view, final int position) {
|
||||
final String imagePath = imagePaths.get(position);
|
||||
final Uri imageUri = imageUrls.get(position);
|
||||
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
|
||||
final boolean[] isCover = {false};
|
||||
if (coverDbHelper != null && albumPath != null) {
|
||||
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
||||
isCover[0] = originalPath != null && originalPath.equals(imagePath);
|
||||
}
|
||||
|
||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||
builder.setTitle("Image");
|
||||
|
||||
String[] items;
|
||||
if (isPinned[0]) {
|
||||
if (isCover[0]) {
|
||||
items = new String[]{"取消置顶", "调整封面", "取消封面"};
|
||||
} else {
|
||||
items = new String[]{"取消置顶", "设置为封面"};
|
||||
}
|
||||
} else {
|
||||
if (isCover[0]) {
|
||||
items = new String[]{"置顶", "调整封面", "取消封面"};
|
||||
} else {
|
||||
items = new String[]{"置顶", "设置为封面"};
|
||||
}
|
||||
}
|
||||
|
||||
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
if (which == 0) {
|
||||
if (pinnedDbHelper != null) {
|
||||
if (isPinned[0]) {
|
||||
pinnedDbHelper.unpinImage(imagePath);
|
||||
} else {
|
||||
pinnedDbHelper.pinImage(imagePath);
|
||||
}
|
||||
refreshPinned();
|
||||
}
|
||||
} else if (which == 1) {
|
||||
if (coverDbHelper != null && albumPath != null) {
|
||||
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
|
||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
|
||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
|
||||
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
|
||||
}
|
||||
} else if (which == 2) {
|
||||
if (coverDbHelper != null && albumPath != null && isCover[0]) {
|
||||
coverDbHelper.deleteCover(albumPath);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_gallery, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class ImagePagerAdapter extends PagerAdapter {
|
||||
public static final String TAG = "ImagePagerAdapter";
|
||||
private ArrayList<Uri> imageUrls;
|
||||
private int bgType;
|
||||
|
||||
public ImagePagerAdapter(ArrayList<Uri> imageUrls, int bgType) {
|
||||
this.imageUrls = imageUrls;
|
||||
this.bgType = bgType;
|
||||
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images, bgType=" + bgType);
|
||||
}
|
||||
|
||||
private int getBgRes() {
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
return R.drawable.bg_checkerboard;
|
||||
case 1:
|
||||
return R.drawable.bg_white;
|
||||
case 2:
|
||||
return R.drawable.bg_black;
|
||||
default:
|
||||
return R.drawable.bg_checkerboard;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return imageUrls.size();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View view = LayoutInflater.from(container.getContext())
|
||||
.inflate(R.layout.item_image_pager, container, false);
|
||||
view.setBackgroundResource(getBgRes());
|
||||
ImageView imageView = view.findViewById(R.id.image);
|
||||
|
||||
Glide.with(imageView.getContext())
|
||||
.load(imageUrls.get(position))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(imageView);
|
||||
|
||||
container.addView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
container.removeView((View) object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener {
|
||||
public static final String TAG = "ImageViewerActivity";
|
||||
public static final String EXTRA_IMAGE_URLS = "image_urls";
|
||||
public static final String EXTRA_POSITIONS = "image_positions";
|
||||
public static final String EXTRA_POSITION = "position";
|
||||
|
||||
private ArrayList<Uri> imageUrls;
|
||||
private ArrayList<String> imagePaths;
|
||||
private int currentPosition;
|
||||
private ViewPager viewPager;
|
||||
private View toolbar;
|
||||
private ImageButton btnBack;
|
||||
private ImageButton btnDelete;
|
||||
private ImageButton btnShare;
|
||||
private ImageButton btnInfo;
|
||||
private ImageButton btnBg;
|
||||
private ImageButton btnGallery;
|
||||
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);
|
||||
|
||||
btnGallery = findViewById(R.id.btn_gallery);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
btnGallery.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
|
||||
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
||||
Uri imageUri = imageUrls.get(currentPosition);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(imageUri, "image/*");
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivity(Intent.createChooser(intent, "打开相册"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleToolbar() {
|
||||
if (toolbar.getVisibility() == View.VISIBLE) {
|
||||
toolbar.setVisibility(View.GONE);
|
||||
} else {
|
||||
toolbar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void 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 int[] bgResources = {R.drawable.bg_checkerboard, R.drawable.bg_white, R.drawable.bg_black};
|
||||
|
||||
BgSelectorDialog dialog = new BgSelectorDialog(this, bgType);
|
||||
dialog.setOnBgSelectedListener(new BgSelectorDialog.OnBgSelectedListener() {
|
||||
@Override
|
||||
public void onBgSelected(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.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();
|
||||
}
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.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);
|
||||
LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath);
|
||||
Uri coverUri = null;
|
||||
if (coverPath != null) {
|
||||
File coverFile = new File(coverPath);
|
||||
if (coverFile.exists()) {
|
||||
coverUri = Uri.fromFile(coverFile);
|
||||
LogUtils.d(TAG, "loadAlbums: cover from file=" + coverFile.getAbsolutePath());
|
||||
} else {
|
||||
coverUri = getUriFromPath(coverPath);
|
||||
LogUtils.d(TAG, "loadAlbums: cover from media store path=" + 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_mi_gallery) {
|
||||
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.action_settings) {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_about) {
|
||||
startActivity(new Intent(this, AboutActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_trash) {
|
||||
startActivity(new Intent(this, TrashActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_debug) {
|
||||
LogActivity.startLogActivity(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
|
||||
loadAlbums();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
|
||||
if (checkPermission()) {
|
||||
scanMediaStore();
|
||||
loadAlbums();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
adapter.refreshPinned();
|
||||
adapter.refreshCover();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterReceiver(coverUpdatedReceiver);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class PinnedAlbumDbHelper extends SQLiteOpenHelper {
|
||||
public static final String TAG = "PinnedAlbumDbHelper";
|
||||
private static final String DB_NAME = "pinned_album.db";
|
||||
private static final int DB_VERSION = 1;
|
||||
private static final String TABLE_NAME = "pinned_albums";
|
||||
private static final String COLUMN_PATH = "album_path";
|
||||
|
||||
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
|
||||
|
||||
private static PinnedAlbumDbHelper dbHelper;
|
||||
|
||||
public static PinnedAlbumDbHelper getInstance(Context context) {
|
||||
if (dbHelper == null) {
|
||||
dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext());
|
||||
}
|
||||
return dbHelper;
|
||||
}
|
||||
|
||||
public PinnedAlbumDbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public void pinAlbum(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_PATH, albumPath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
LogUtils.d(TAG, "pinAlbum: " + albumPath);
|
||||
}
|
||||
|
||||
public void unpinAlbum(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
|
||||
LogUtils.d(TAG, "unpinAlbum: " + albumPath);
|
||||
}
|
||||
|
||||
public boolean isPinned(String albumPath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
|
||||
new String[]{albumPath}, null, null, null);
|
||||
boolean pinned = cursor.getCount() > 0;
|
||||
cursor.close();
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public String[] getPinnedPaths() {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
|
||||
String[] paths = new String[cursor.getCount()];
|
||||
int i = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
paths[i++] = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class PinnedImageDbHelper extends SQLiteOpenHelper {
|
||||
public static final String TAG = "PinnedImageDbHelper";
|
||||
private static final String DB_NAME = "pinned_image.db";
|
||||
private static final int DB_VERSION = 1;
|
||||
private static final String TABLE_NAME = "pinned_images";
|
||||
private static final String COLUMN_PATH = "image_path";
|
||||
|
||||
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
|
||||
|
||||
private static PinnedImageDbHelper dbHelper;
|
||||
|
||||
public static PinnedImageDbHelper getInstance(Context context) {
|
||||
if (dbHelper == null) {
|
||||
dbHelper = new PinnedImageDbHelper(context.getApplicationContext());
|
||||
}
|
||||
return dbHelper;
|
||||
}
|
||||
|
||||
public PinnedImageDbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public void pinImage(String imagePath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_PATH, imagePath);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
LogUtils.d(TAG, "pinImage: " + imagePath);
|
||||
}
|
||||
|
||||
public void unpinImage(String imagePath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{imagePath});
|
||||
LogUtils.d(TAG, "unpinImage: " + imagePath);
|
||||
}
|
||||
|
||||
public boolean isPinned(String imagePath) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
|
||||
new String[]{imagePath}, null, null, null);
|
||||
boolean pinned = cursor.getCount() > 0;
|
||||
cursor.close();
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public String[] getPinnedPaths() {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
|
||||
String[] paths = new String[cursor.getCount()];
|
||||
int i = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
paths[i++] = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class Preferences {
|
||||
public static final String TAG = "Preferences";
|
||||
private static final String PREF_NAME = "gallery_prefs";
|
||||
private static final String KEY_FOLDER_PATH = "folder_path";
|
||||
private static final String KEY_BG_TYPE = "bg_type";
|
||||
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
|
||||
private static final String KEY_COVER_WIDTH = "cover_width";
|
||||
private static final String KEY_COVER_HEIGHT = "cover_height";
|
||||
private static final String KEY_COVER_RATIO = "cover_ratio";
|
||||
|
||||
public static final String ACTION_COVER_UPDATED = "cc.winboll.studio.gallery.COVER_UPDATED";
|
||||
|
||||
private static final int DEFAULT_BG_TYPE = 0;
|
||||
private static final int DEFAULT_SORT_MODE = 0;
|
||||
private static final int DEFAULT_COVER_WIDTH = 240;
|
||||
private static final int DEFAULT_COVER_HEIGHT = 120;
|
||||
private static final float DEFAULT_COVER_RATIO = 2.0f;
|
||||
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
|
||||
|
||||
public static final int SORT_TIME_DESC = 0;
|
||||
public static final int SORT_TIME_ASC = 1;
|
||||
public static final int SORT_NAME_DESC = 2;
|
||||
public static final int SORT_NAME_ASC = 3;
|
||||
|
||||
public static String getDefaultPath() {
|
||||
return DEFAULT_PATH;
|
||||
}
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getFolderPath() {
|
||||
String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH);
|
||||
LogUtils.d(TAG, "getFolderPath: " + path);
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setFolderPath(String path) {
|
||||
LogUtils.d(TAG, "setFolderPath: " + path);
|
||||
prefs.edit().putString(KEY_FOLDER_PATH, path).apply();
|
||||
}
|
||||
|
||||
public int getBgType() {
|
||||
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
|
||||
}
|
||||
|
||||
public void setBgType(int type) {
|
||||
LogUtils.d(TAG, "setBgType: " + type);
|
||||
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
|
||||
}
|
||||
|
||||
public int getAlbumSortMode() {
|
||||
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
|
||||
}
|
||||
|
||||
public void setAlbumSortMode(int mode) {
|
||||
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
|
||||
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
|
||||
}
|
||||
|
||||
public int getCoverWidth() {
|
||||
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
|
||||
}
|
||||
|
||||
public void setCoverWidth(int width) {
|
||||
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
|
||||
}
|
||||
|
||||
public int getCoverHeight() {
|
||||
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
|
||||
}
|
||||
|
||||
public void setCoverHeight(int height) {
|
||||
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
|
||||
}
|
||||
|
||||
public float getCoverRatio() {
|
||||
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
|
||||
}
|
||||
|
||||
public void setCoverRatio(float ratio) {
|
||||
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
public static final String TAG = "SettingsActivity";
|
||||
private Preferences prefs;
|
||||
private EditText editFolderPath;
|
||||
private TextView textCurrentPath;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
prefs = new Preferences(this);
|
||||
|
||||
editFolderPath = findViewById(R.id.edit_folder_path);
|
||||
textCurrentPath = findViewById(R.id.text_current_path);
|
||||
Button btnSave = findViewById(R.id.btn_save);
|
||||
|
||||
String currentPath = prefs.getFolderPath();
|
||||
editFolderPath.setText(currentPath);
|
||||
textCurrentPath.setText("Current: " + currentPath);
|
||||
|
||||
btnSave.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String newPath = editFolderPath.getText().toString().trim();
|
||||
if (!newPath.isEmpty()) {
|
||||
prefs.setFolderPath(newPath);
|
||||
textCurrentPath.setText("Current: " + newPath);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class TrashActivity extends AppCompatActivity {
|
||||
public static final String TAG = "TrashActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 102;
|
||||
private RecyclerView recyclerView;
|
||||
private TrashAdapter adapter;
|
||||
private TrashManager trashManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setTitle("Trash");
|
||||
|
||||
trashManager = new TrashManager(this);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||
adapter = new TrashAdapter();
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() {
|
||||
@Override
|
||||
public void onRestoreClick(int position) {
|
||||
restoreImage(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteClick(int position) {
|
||||
permanentlyDelete(position);
|
||||
}
|
||||
});
|
||||
|
||||
if (checkPermission()) {
|
||||
loadTrash();
|
||||
} else {
|
||||
requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestPermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadTrash();
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadTrash() {
|
||||
LogUtils.d(TAG, "loadTrash");
|
||||
Cursor cursor = trashManager.getTrashList();
|
||||
ArrayList<TrashItem> items = new ArrayList<TrashItem>();
|
||||
ArrayList<Uri> uris = new ArrayList<Uri>();
|
||||
|
||||
String trashPath = TrashDbHelper.getTrashPath();
|
||||
File trashDir = new File(trashPath);
|
||||
|
||||
if (cursor != null && cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
do {
|
||||
try {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
|
||||
String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path"));
|
||||
String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder"));
|
||||
|
||||
TrashItem item = new TrashItem();
|
||||
item.id = id;
|
||||
item.fileName = fileName;
|
||||
item.originalPath = originalPath;
|
||||
item.originalFolder = originalFolder;
|
||||
|
||||
File trashFile = new File(trashDir, fileName);
|
||||
if (trashFile.exists()) {
|
||||
items.add(item);
|
||||
uris.add(Uri.fromFile(trashFile));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
adapter.setData(items, uris);
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreImage(int position) {
|
||||
LogUtils.d(TAG, "restoreImage: " + position);
|
||||
long id = adapter.getItemId(position);
|
||||
String fileName = adapter.getFileName(position);
|
||||
String originalPath = adapter.getOriginalPath(position);
|
||||
|
||||
if (trashManager.restore(id, fileName, originalPath)) {
|
||||
Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.i(TAG, "Image restored");
|
||||
loadTrash();
|
||||
} else {
|
||||
Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void permanentlyDelete(int position) {
|
||||
long id = adapter.getItemId(position);
|
||||
String fileName = adapter.getFileName(position);
|
||||
|
||||
if (trashManager.deletePermanently(id, fileName)) {
|
||||
Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show();
|
||||
loadTrash();
|
||||
} else {
|
||||
Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_trash, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_clear_trash) {
|
||||
trashManager.clearTrash();
|
||||
Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show();
|
||||
loadTrash();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (checkPermission()) {
|
||||
loadTrash();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TrashItem {
|
||||
public long id;
|
||||
public String fileName;
|
||||
public String originalPath;
|
||||
public String originalFolder;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class TrashAdapter extends RecyclerView.Adapter<TrashAdapter.ViewHolder> {
|
||||
public static final String TAG = "TrashAdapter";
|
||||
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
|
||||
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
|
||||
private OnTrashClickListener listener;
|
||||
|
||||
public interface OnTrashClickListener {
|
||||
void onRestoreClick(int position);
|
||||
void onDeleteClick(int position);
|
||||
}
|
||||
|
||||
public void setOnTrashClickListener(OnTrashClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setData(ArrayList<TrashActivity.TrashItem> items, ArrayList<Uri> uris) {
|
||||
this.trashItems = items;
|
||||
this.imageUrls = uris;
|
||||
LogUtils.d(TAG, "setData: " + items.size() + " items");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public long getItemId(int position) {
|
||||
if (position >= 0 && position < trashItems.size()) {
|
||||
return trashItems.get(position).id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getFileName(int position) {
|
||||
if (position >= 0 && position < trashItems.size()) {
|
||||
return trashItems.get(position).fileName;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getOriginalPath(int position) {
|
||||
if (position >= 0 && position < trashItems.size()) {
|
||||
return trashItems.get(position).originalPath;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_trash, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||
if (position < imageUrls.size()) {
|
||||
Glide.with(holder.imageView.getContext())
|
||||
.load(imageUrls.get(position))
|
||||
.centerCrop()
|
||||
.into(holder.imageView);
|
||||
}
|
||||
|
||||
holder.btnRestore.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) {
|
||||
listener.onRestoreClick(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
holder.btnDelete.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) {
|
||||
listener.onDeleteClick(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return trashItems.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView imageView;
|
||||
ImageView btnRestore;
|
||||
ImageView btnDelete;
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
imageView = itemView.findViewById(R.id.image);
|
||||
btnRestore = itemView.findViewById(R.id.btn_restore);
|
||||
btnDelete = itemView.findViewById(R.id.btn_delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Environment;
|
||||
import java.io.File;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class TrashDbHelper extends SQLiteOpenHelper {
|
||||
public static final String TAG = "TrashDbHelper";
|
||||
private static final String DB_NAME = "trash.db";
|
||||
private static final int DB_VERSION = 1;
|
||||
private static final String TABLE_NAME = "trash_items";
|
||||
private static final String COL_ID = "_id";
|
||||
private static final String COL_FILE_NAME = "file_name";
|
||||
private static final String COL_ORIGINAL_PATH = "original_path";
|
||||
private static final String COL_ORIGINAL_FOLDER = "original_folder";
|
||||
private static final String COL_DELETE_TIME = "delete_time";
|
||||
|
||||
public TrashDbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +
|
||||
COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
COL_FILE_NAME + " TEXT, " +
|
||||
COL_ORIGINAL_PATH + " TEXT, " +
|
||||
COL_ORIGINAL_FOLDER + " TEXT, " +
|
||||
COL_DELETE_TIME + " INTEGER)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
LogUtils.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public long insert(String fileName, String originalPath, String originalFolder) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COL_FILE_NAME, fileName);
|
||||
values.put(COL_ORIGINAL_PATH, originalPath);
|
||||
values.put(COL_ORIGINAL_FOLDER, originalFolder);
|
||||
values.put(COL_DELETE_TIME, System.currentTimeMillis());
|
||||
return db.insert(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
public Cursor getAll() {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
return db.query(TABLE_NAME, null, null, null, null, null, COL_DELETE_TIME + " DESC");
|
||||
}
|
||||
|
||||
public int delete(long id) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
return db.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(id)});
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
public static String getTrashPath() {
|
||||
File trashDir = new File(Environment.getExternalStorageDirectory(), ".Trash");
|
||||
if (!trashDir.exists()) {
|
||||
trashDir.mkdirs();
|
||||
}
|
||||
return trashDir.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class TrashManager {
|
||||
public static final String TAG = "TrashManager";
|
||||
private final Context context;
|
||||
private final TrashDbHelper dbHelper;
|
||||
|
||||
public TrashManager(Context context) {
|
||||
LogUtils.d(TAG, "TrashManager created");
|
||||
this.context = context;
|
||||
this.dbHelper = new TrashDbHelper(context);
|
||||
}
|
||||
|
||||
public long addToTrash(String imagePath) {
|
||||
LogUtils.d(TAG, "addToTrash: " + imagePath);
|
||||
File sourceFile = new File(imagePath);
|
||||
if (!sourceFile.exists()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
String uniqueId = UUID.randomUUID().toString();
|
||||
String extension = getExtension(imagePath);
|
||||
String newFileName = uniqueId + extension;
|
||||
String trashPath = TrashDbHelper.getTrashPath();
|
||||
File destFile = new File(trashPath, newFileName);
|
||||
|
||||
if (sourceFile.renameTo(destFile)) {
|
||||
String originalFolder = sourceFile.getParent();
|
||||
long result = dbHelper.insert(newFileName, imagePath, originalFolder);
|
||||
LogUtils.i(TAG, "Added to trash: " + newFileName);
|
||||
return result;
|
||||
}
|
||||
LogUtils.e(TAG, "Failed to move to trash");
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Cursor getTrashList() {
|
||||
return dbHelper.getAll();
|
||||
}
|
||||
|
||||
public boolean restore(long id, String fileName, String originalPath) {
|
||||
LogUtils.i(TAG, "restore: " + fileName + " -> " + originalPath);
|
||||
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||
LogUtils.d(TAG, "trashFile exists: " + trashFile.exists() + ", path: " + trashFile.getAbsolutePath());
|
||||
if (!trashFile.exists()) {
|
||||
LogUtils.e(TAG, "trashFile not exists: " + trashFile.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
|
||||
File originalFolder = new File(originalPath).getParentFile();
|
||||
LogUtils.d(TAG, "originalFolder: " + originalFolder + ", exists: " + (originalFolder != null && originalFolder.exists()));
|
||||
if (originalFolder != null && !originalFolder.exists()) {
|
||||
boolean created = originalFolder.mkdirs();
|
||||
LogUtils.d(TAG, "mkdirs result: " + created + ", path: " + originalFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
File originalFile = new File(originalPath);
|
||||
String restoreName = originalFile.getName();
|
||||
File restoreFile = new File(originalFolder, restoreName);
|
||||
LogUtils.d(TAG, "restoreFile: " + restoreFile.getAbsolutePath() + ", exists: " + restoreFile.exists());
|
||||
|
||||
boolean renameResult = trashFile.renameTo(restoreFile);
|
||||
LogUtils.d(TAG, "renameTo result: " + renameResult);
|
||||
|
||||
if (renameResult) {
|
||||
dbHelper.delete(id);
|
||||
LogUtils.i(TAG, "Restored: " + fileName);
|
||||
scanMedia(restoreFile.getAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try copy + delete if rename failed
|
||||
LogUtils.i(TAG, "renameTo failed, trying copy + delete");
|
||||
try {
|
||||
java.io.InputStream in = new java.io.FileInputStream(trashFile);
|
||||
java.io.OutputStream out = new java.io.FileOutputStream(restoreFile);
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
boolean deleted = trashFile.delete();
|
||||
LogUtils.d(TAG, "copy+delete result: " + deleted);
|
||||
if (deleted) {
|
||||
dbHelper.delete(id);
|
||||
LogUtils.i(TAG, "Restored (copy): " + fileName);
|
||||
scanMedia(restoreFile.getAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "copy failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
LogUtils.e(TAG, "Failed to restore: " + fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deletePermanently(long id, String fileName) {
|
||||
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||
boolean deleted = trashFile.delete();
|
||||
if (deleted) {
|
||||
dbHelper.delete(id);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void clearTrash() {
|
||||
Cursor cursor = getTrashList();
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
int colIndex = cursor.getColumnIndexOrThrow("_id");
|
||||
if (!cursor.isNull(colIndex)) {
|
||||
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
|
||||
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||
trashFile.delete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
dbHelper.clear();
|
||||
}
|
||||
|
||||
private String getExtension(String path) {
|
||||
int lastDot = path.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
return path.substring(lastDot);
|
||||
}
|
||||
return ".jpg";
|
||||
}
|
||||
|
||||
private void scanMedia(String filePath) {
|
||||
LogUtils.d(TAG, "scanMedia: " + filePath);
|
||||
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new android.media.MediaScannerConnection.OnScanCompletedListener() {
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public class ZoomContainerView extends FrameLayout {
|
||||
private float scaleFactor = 1.0f;
|
||||
private float minScale = 0.1f;
|
||||
private float maxScale = 5.0f;
|
||||
private static final float ZOOM_STEP = 0.25f;
|
||||
|
||||
private ScaleGestureDetector scaleGestureDetector;
|
||||
private GestureDetector gestureDetector;
|
||||
private Paint borderPaint;
|
||||
|
||||
public ZoomContainerView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ZoomContainerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ZoomContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setWillNotDraw(false);
|
||||
setBackgroundColor(Color.YELLOW);
|
||||
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.parseColor("#333333"));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(2);
|
||||
|
||||
scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
scaleFactor *= detector.getScaleFactor();
|
||||
scaleFactor = Math.max(minScale, Math.min(scaleFactor, maxScale));
|
||||
invalidate();
|
||||
requestLayout();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (scaleFactor > 1.5f) {
|
||||
scaleFactor = 1.0f;
|
||||
} else {
|
||||
scaleFactor = 2.0f;
|
||||
}
|
||||
invalidate();
|
||||
requestLayout();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void zoomIn() {
|
||||
scaleFactor = Math.min(maxScale, scaleFactor + ZOOM_STEP);
|
||||
requestMeasure();
|
||||
}
|
||||
|
||||
public void zoomOut() {
|
||||
scaleFactor = Math.max(minScale, scaleFactor - ZOOM_STEP);
|
||||
requestMeasure();
|
||||
}
|
||||
|
||||
private void requestMeasure() {
|
||||
removeCallbacks(measureRunable);
|
||||
post(measureRunable);
|
||||
}
|
||||
|
||||
private Runnable measureRunable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
public float getScaleFactor() {
|
||||
return scaleFactor;
|
||||
}
|
||||
|
||||
public void setScaleFactor(float scale) {
|
||||
scaleFactor = Math.max(minScale, Math.min(scale, maxScale));
|
||||
int childCount = getChildCount();
|
||||
if (childCount > 0) {
|
||||
View child = getChildAt(0);
|
||||
if (child instanceof CropCanvasView) {
|
||||
((CropCanvasView) child).setContainerScale(scaleFactor);
|
||||
}
|
||||
}
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void resetZoom() {
|
||||
scaleFactor = 1.0f;
|
||||
invalidate();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int childCount = getChildCount();
|
||||
if (childCount > 0) {
|
||||
View child = getChildAt(0);
|
||||
child.measure(widthMeasureSpec, heightMeasureSpec);
|
||||
int childW = child.getMeasuredWidth();
|
||||
int childH = child.getMeasuredHeight();
|
||||
int scaledW = (int) (childW * scaleFactor);
|
||||
int scaledH = (int) (childH * scaleFactor);
|
||||
int widthSpec = MeasureSpec.makeMeasureSpec(scaledW, MeasureSpec.EXACTLY);
|
||||
int heightSpec = MeasureSpec.makeMeasureSpec(scaledH, MeasureSpec.EXACTLY);
|
||||
child.measure(widthSpec, heightSpec);
|
||||
setMeasuredDimension(scaledW, scaledH);
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
int childCount = getChildCount();
|
||||
if (childCount > 0) {
|
||||
View child = getChildAt(0);
|
||||
int childW = child.getMeasuredWidth();
|
||||
int childH = child.getMeasuredHeight();
|
||||
int scaledW = (int) (childW * scaleFactor);
|
||||
int scaledH = (int) (childH * scaleFactor);
|
||||
int parentW = right - left;
|
||||
int parentH = bottom - top;
|
||||
int childLeft = (parentW - scaledW) / 2;
|
||||
int childTop = (parentH - scaledH) / 2;
|
||||
child.layout(childLeft, childTop, childLeft + scaledW, childTop + scaledH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
boolean handled = scaleGestureDetector.onTouchEvent(event);
|
||||
if (!scaleGestureDetector.isInProgress()) {
|
||||
handled = gestureDetector.onTouchEvent(event) || handled;
|
||||
}
|
||||
return handled || super.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package cc.winboll.studio.gallery.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import cc.winboll.studio.gallery.R;
|
||||
|
||||
/**
|
||||
* 颜色表对话框
|
||||
* 继承于普通对话框类,使用视图文件
|
||||
*/
|
||||
public class ColorPaletteDialog extends Dialog {
|
||||
|
||||
public ColorPaletteDialog(@NonNull Context context) {
|
||||
super(context, R.style.ColorPaletteDialog);
|
||||
}
|
||||
|
||||
public ColorPaletteDialog(@NonNull Context context, int themeResId) {
|
||||
super(context, themeResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.dialog_color_palette);
|
||||
|
||||
TextView titleText = findViewById(R.id.title_text);
|
||||
|
||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||
params.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||
getWindow().setAttributes(params);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
|
||||
}
|
||||
|
||||
public interface OnColorItemClick {
|
||||
void onColorClick(int color);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#000000"/>
|
||||
</shape>
|
||||
@@ -1,30 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="10">
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
|
||||
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
|
||||
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
|
||||
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
|
||||
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
|
||||
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
|
||||
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
|
||||
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
|
||||
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
|
||||
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
|
||||
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
|
||||
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
|
||||
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
|
||||
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
|
||||
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
|
||||
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
|
||||
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
|
||||
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
|
||||
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
|
||||
</vector>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#CCFFFFFF"/>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#000000"/>
|
||||
</shape>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||
<size android:width="32dp" android:height="32dp"/>
|
||||
</shape>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||
<size android:width="36dp" android:height="36dp"/>
|
||||
</shape>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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/black"/>
|
||||
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||
</shape>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF"/>
|
||||
</shape>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
@@ -1,12 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
|
||||
</vector>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
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="#FFFFFF"
|
||||
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,-1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
</vector>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8s-3.59,8 -8,8z"/>
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3s-1.34,3 -3,3z"/>
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M8,12l1.5,-2l2,2.5l2.5,-3l2,2.5"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M1,3V21H23V3H1M21,5V14H3V5H21M11,16V19H8V16H11M3,16H6V19H3V16M13,19V16H16V19H13M18,19V16H21V19H18Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,13H5v-2h14v2z"/>
|
||||
</vector>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<cc.winboll.studio.libappbase.views.AboutView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/aboutview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,143 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/colorPrimary"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_close"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/ic_close"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textview_window_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="封面剪裁"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/color_icon_container"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<View
|
||||
android:id="@+id/color_view"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_color_circle"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_color_pick"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/ic_color_pick"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_info"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/ic_info"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_done"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/ic_done"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="56dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:fillViewport="true">
|
||||
|
||||
<cc.winboll.studio.gallery.ZoomContainerView
|
||||
android:id="@+id/zoom_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<cc.winboll.studio.gallery.CropCanvasView
|
||||
android:id="@+id/crop_canvas_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</cc.winboll.studio.gallery.ZoomContainerView>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/colorPrimary"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:layout_marginBottom="0dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="缩小"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar_zoom"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:min="0"
|
||||
android:max="100"
|
||||
android:progress="50"
|
||||
android:layout_marginHorizontal="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="放大"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="#CC000000"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back"
|
||||
android:contentDescription="Back"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_gallery"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_view_gallery_outline"
|
||||
android:contentDescription="Gallery"/>
|
||||
|
||||
<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>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_scroll_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_arrow_up"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/colorAccent"
|
||||
app:tint="@color/white"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/folder_path"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_folder_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/enter_folder_path"
|
||||
android:inputType="text"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_current_path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#888888"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/save"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择背景"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/radio_group_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_checkerboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="灰白相间"
|
||||
android:padding="12dp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_white"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="全白"
|
||||
android:padding="12dp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_black"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="全黑"
|
||||
android:padding="12dp" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_dialog"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/bg_dialog"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:textColor="@android:color/white"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/preview_image_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:background="#FFBBD505">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preview_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_margin="2dp"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#80000000"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="6dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="3dp"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/image_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:background="#80000000"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="10sp"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/color_view"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_color_circle"/>
|
||||
|
||||
<View
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_color_circle_border"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/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>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/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>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_done"
|
||||
android:title="完成"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_mi_gallery"
|
||||
android:title="@string/mi_gallery"
|
||||
android:icon="@drawable/ic_mi_gallery"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<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>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
<string name="system_gallery">系统相册</string>
|
||||
<string name="mi_gallery">小米相册</string>
|
||||
<string name="reset_gallery">重置</string>
|
||||
</resources>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
|
||||
<style name="ColorPaletteDialog" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
1
positions/.gitignore
vendored
Normal file
1
positions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -1,7 +1,8 @@
|
||||
# Gallery
|
||||
# Positions
|
||||
|
||||
#### 介绍
|
||||
云宝相册应用
|
||||
安卓位置应用,有关于地理位置的相关应用。
|
||||
PS:使用感言~~~『記低用唔到』。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh positions
|
||||
|
||||
#### 使用说明
|
||||
|
||||
@@ -18,43 +18,48 @@ def genVersionName(def versionName){
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
|
||||
compileSdkVersion 32
|
||||
|
||||
// 直接使用已安装的构建工具 33.0.3(无需修改)
|
||||
buildToolsVersion "33.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.gallery"
|
||||
minSdkVersion 23
|
||||
// 适配MIUI12
|
||||
applicationId "cc.winboll.studio.positions"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.0"
|
||||
versionName "15.12"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
// 米盟
|
||||
api 'com.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'
|
||||
|
||||
// 下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
// https://mvnrepository.com/artifact/com.jzxiang.pickerview/TimePickerDialog
|
||||
api 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
|
||||
|
||||
// 谷歌定位服务核心依赖(FusedLocationProviderClient所在库)
|
||||
api 'com.google.android.gms:play-services-location:21.0.1'
|
||||
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
@@ -65,55 +70,23 @@ dependencies {
|
||||
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 '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'
|
||||
*/
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libaes:15.15.9'
|
||||
api 'cc.winboll.studio:libappbase:15.15.21'
|
||||
api 'cc.winboll.studio:libaes:15.15.2'
|
||||
api 'cc.winboll.studio:libappbase:15.15.9'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
8
positions/build.properties
Normal file
8
positions/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Jan 24 13:10:53 HKT 2026
|
||||
stageCount=12
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.11
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.12
|
||||
@@ -67,6 +67,12 @@
|
||||
-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
|
||||
@@ -2,7 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
<application
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_launcher_beta">
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
5
positions/src/beta/res/values-zh/strings.xml
Normal file
5
positions/src/beta/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">悟空笔记#</string>
|
||||
<string name="appplus_name">时空任务#</string>
|
||||
</resources>
|
||||
7
positions/src/beta/res/values/strings.xml
Normal file
7
positions/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Positions</string>
|
||||
<string name="appplus_name">PositionsPlus+</string>
|
||||
|
||||
</resources>
|
||||
6
positions/src/beta/res/xml/file_provider.xml
Normal file
6
positions/src/beta/res/xml/file_provider.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-files-path
|
||||
name="BaseBean"
|
||||
path="BaseBean/" />
|
||||
</paths>
|
||||
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="open_appplus"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:shortcutShortLabel="@string/open_appplus"
|
||||
android:shortcutLongLabel="@string/open_appplus"
|
||||
android:shortcutDisabledMessage="@string/appplus_open_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.positions.App.ACTION_OPEN_APPPLUS"
|
||||
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||
android:data="open_appplus" />
|
||||
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="close_appplus"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:shortcutShortLabel="@string/close_appplus"
|
||||
android:shortcutLongLabel="@string/close_appplus"
|
||||
android:shortcutDisabledMessage="@string/appplus_close_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.positions.App.ACTION_CLOSE_APPPLUS"
|
||||
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||
android:data="close_appplus" />
|
||||
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
164
positions/src/main/AndroidManifest.xml
Normal file
164
positions/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,164 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.positions">
|
||||
|
||||
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<!-- 在后台使用位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 运行“location”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- 安装快捷方式 -->
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<action android:name="android.intent.action.EDIT"/>
|
||||
|
||||
<data android:mimeType="application/json"/>
|
||||
|
||||
<data android:mimeType="text/x-json"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityWukong"
|
||||
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/shortcutsmain"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityLaojun"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/appplus_name"
|
||||
android:icon="@drawable/ic_positions_plus"
|
||||
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/shortcutsplus"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.LocationActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
|
||||
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.DistanceRefreshService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.SettingsActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.AboutActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
@@ -0,0 +1,348 @@
|
||||
package cc.winboll.studio.positions;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buf = new byte[1024 * 8];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
output.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(File file, byte[] data) throws IOException {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
FileOutputStream output = new FileOutputStream(file);
|
||||
try {
|
||||
write(input, output);
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toString(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
write(input, output);
|
||||
try {
|
||||
return output.toString("UTF-8");
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeIO(Closeable... closeables) {
|
||||
for (Closeable closeable : closeables) {
|
||||
try {
|
||||
if (closeable != null) closeable.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CrashHandler {
|
||||
|
||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
private static CrashHandler sInstance;
|
||||
|
||||
private PartCrashHandler mPartCrashHandler;
|
||||
|
||||
public static CrashHandler getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new CrashHandler();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context) {
|
||||
registerGlobal(context, null);
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context, String crashDir) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||
}
|
||||
|
||||
public void registerPart(Context context) {
|
||||
unregisterPart(context);
|
||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||
}
|
||||
|
||||
public void unregisterPart(Context context) {
|
||||
if (mPartCrashHandler != null) {
|
||||
mPartCrashHandler.isRunning.set(false);
|
||||
mPartCrashHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PartCrashHandler implements Runnable {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||
|
||||
public PartCrashHandler(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (isRunning.get()) {
|
||||
try {
|
||||
Looper.loop();
|
||||
} catch (final Throwable e) {
|
||||
e.printStackTrace();
|
||||
if (isRunning.get()) {
|
||||
MAIN_HANDLER.post(new Runnable(){
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException)e;
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||
|
||||
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final File mCrashDir;
|
||||
|
||||
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||
this.mContext = context;
|
||||
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||
try {
|
||||
|
||||
String log = buildLog(throwable);
|
||||
writeLog(log);
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||
mContext.startActivity(intent);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
writeLog(e.toString());
|
||||
}
|
||||
|
||||
throwable.printStackTrace();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
|
||||
} catch (Throwable e) {
|
||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildLog(Throwable throwable) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
|
||||
String versionName = "unknown";
|
||||
long versionCode = 0;
|
||||
try {
|
||||
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||
versionName = packageInfo.versionName;
|
||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||
head.put("Time Of Crash", time);
|
||||
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||
head.put("Kernel", getKernel());
|
||||
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||
head.put("Fingerprint", Build.FINGERPRINT);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (String key : head.keySet()) {
|
||||
if (builder.length() != 0) builder.append("\n");
|
||||
builder.append(key);
|
||||
builder.append(" : ");
|
||||
builder.append(head.get(key));
|
||||
}
|
||||
|
||||
builder.append("\n\n");
|
||||
builder.append(Log.getStackTraceString(throwable));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void writeLog(String log) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||
try {
|
||||
write(file, log.getBytes("UTF-8"));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getKernel() {
|
||||
try {
|
||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||
} catch (Throwable e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CrashActivity extends Activity {
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setTheme(android.R.style.Theme_DeviceDefault);
|
||||
setTitle("App Crash");
|
||||
|
||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||
|
||||
TextView textView = new TextView(this);
|
||||
int padding = dp2px(16);
|
||||
textView.setPadding(padding, padding, padding, padding);
|
||||
textView.setText(mLog);
|
||||
textView.setTextIsSelectable(true);
|
||||
textView.setTypeface(Typeface.DEFAULT);
|
||||
textView.setLinksClickable(true);
|
||||
|
||||
horizontalScrollView.addView(textView);
|
||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
setContentView(contentView);
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static int dp2px(float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.copy:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package cc.winboll.studio.positions;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Switch;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.activities.AboutActivity;
|
||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||
import cc.winboll.studio.positions.activities.SettingsActivity;
|
||||
import cc.winboll.studio.positions.activities.WinBoLLActivity;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||
|
||||
/**
|
||||
* 主页面:仅负责
|
||||
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
||||
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
||||
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
||||
*/
|
||||
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public static final String TAG = "MainActivity";
|
||||
// 权限请求码(建议定义为类常量,避免魔法值)
|
||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||
|
||||
// UI 控件:服务控制开关、顶部工具栏
|
||||
private Switch mServiceSwitch;
|
||||
private Button mManagePositionsButton;
|
||||
private Toolbar mToolbar;
|
||||
// 服务相关:服务实例、绑定状态标记
|
||||
//private DistanceRefreshService mDistanceService;
|
||||
private boolean isServiceBound = false;
|
||||
ADsBannerView mADsBannerView;
|
||||
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
||||
// private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||
// /**
|
||||
// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
|
||||
// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||
// mDistanceService = binder.getService();
|
||||
// isServiceBound = true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceDisconnected(ComponentName name) {
|
||||
// mDistanceService = null;
|
||||
// isServiceBound = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ----------------------
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main); // 关联主页面布局
|
||||
|
||||
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
||||
initToolbar();
|
||||
// 2. 初始化其他控件
|
||||
initViews();
|
||||
// 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
|
||||
if (!checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
}
|
||||
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
||||
//bindDistanceService();
|
||||
|
||||
mADsBannerView = findViewById(R.id.adsbanner);
|
||||
|
||||
setLLMainBackgroundColor();
|
||||
}
|
||||
|
||||
// 在 Activity 的 onCreate() 或需要获取颜色的方法中调用
|
||||
private void setLLMainBackgroundColor() {
|
||||
// 1. 定义要解析的主题属性(这里是 colorAccent)
|
||||
TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
|
||||
// 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY)
|
||||
int colorAccent = a.getColor(0, Color.GRAY);
|
||||
// 3. 必须回收,避免内存泄漏
|
||||
a.recycle();
|
||||
|
||||
LinearLayout llmain = findViewById(R.id.llmain);
|
||||
llmain.setBackgroundColor(colorAccent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
}
|
||||
|
||||
// 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
|
||||
// if (isServiceBound) {
|
||||
// unbindService(mServiceConn);
|
||||
// isServiceBound = false;
|
||||
// mDistanceService = null;
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
||||
/**
|
||||
* 初始化顶部 Toolbar,设置页面标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
||||
setSupportActionBar(mToolbar);
|
||||
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
|
||||
*/
|
||||
private void initViews() {
|
||||
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
|
||||
mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
|
||||
|
||||
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
|
||||
|
||||
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
|
||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
// 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
|
||||
if (isChecked && !checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限就绪:执行服务启停逻辑
|
||||
if (isChecked) {
|
||||
LogUtils.d(TAG, "设置启动服务");
|
||||
ServiceUtil.startAutoService(MainActivity.this);
|
||||
} else {
|
||||
LogUtils.d(TAG, "设置关闭服务");
|
||||
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
}
|
||||
|
||||
mManagePositionsButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// 主题菜单
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
// 调试工具菜单
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
}
|
||||
// 应用其他菜单
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int menuItemId = item.getItemId();
|
||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
||||
} else if (menuItemId == R.id.item_settings) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (menuItemId == R.id.item_about) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绑定服务(仅用于获取服务状态,不启动服务)
|
||||
*/
|
||||
// private void bindDistanceService() {
|
||||
// Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制)
|
||||
// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
// }
|
||||
|
||||
// ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
|
||||
/**
|
||||
* 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
|
||||
* 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
|
||||
*/
|
||||
public void onPositions(View view) {
|
||||
//ToastUtils.show("onPositions");
|
||||
// 服务已启动:跳转到位置管理页
|
||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||
}
|
||||
|
||||
// ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ----------------------
|
||||
/**
|
||||
* 检查是否拥有「前台+后台」位置权限(适配Android版本差异)
|
||||
* Java7 特性:显式类型判断、无Lambda、兼容低版本API
|
||||
*/
|
||||
private boolean checkLocationPermissions() {
|
||||
// 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量)
|
||||
int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
// 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本)
|
||||
boolean hasBackgroundPerm = true;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
// 前台+后台权限均满足,才返回true
|
||||
return hasForegroundPerm && hasBackgroundPerm;
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
// 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限)
|
||||
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
// 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
|
||||
}
|
||||
} else {
|
||||
// 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// 2.1 检查后台权限是否未授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框)
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
// 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑
|
||||
// doLocationRelatedLogic();
|
||||
}
|
||||
}
|
||||
|
||||
// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑)
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
// 处理前台权限申请结果
|
||||
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 前台权限同意:自动尝试申请后台权限(如果是API 29+)
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
// 前台权限拒绝:提示用户(可选:引导跳转到应用设置页)
|
||||
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 后台权限同意:可执行后台定位逻辑
|
||||
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启)
|
||||
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||
import cc.winboll.studio.libappbase.views.AboutView;
|
||||
import cc.winboll.studio.positions.R;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/13 11:25
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
private Toolbar mToolbar;
|
||||
@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_about);
|
||||
|
||||
// 设置工具栏
|
||||
initToolbar();
|
||||
|
||||
AboutView aboutView = findViewById(R.id.aboutview);
|
||||
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
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 APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "positions";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("Positions");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Positions");
|
||||
appInfo.setAppAPKName("Positions");
|
||||
appInfo.setAppAPKFolderName("Positions");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:22
|
||||
* @Describe 位置列表页面(适配MainService GPS接口+规范服务交互+完善生命周期)
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
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.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.MainActivity;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Java 7 语法适配:
|
||||
* 1. 服务绑定用匿名内部类实现 ServiceConnection
|
||||
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
|
||||
* 3. 所有位置/任务操作通过 MainService 接口执行
|
||||
*/
|
||||
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public static final String TAG = "LocationActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
private RecyclerView mRvPosition;
|
||||
private PositionAdapter mPositionAdapter;
|
||||
|
||||
// MainService 引用+绑定状态(AtomicBoolean 确保多线程状态可见性)
|
||||
private MainService mMainService;
|
||||
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
|
||||
// 标记 Adapter 是否已初始化(避免重复初始化/销毁后初始化)
|
||||
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
|
||||
|
||||
// ---------------------- 新增:GPS监听核心变量 ----------------------
|
||||
private MainService.GpsUpdateListener mGpsUpdateListener; // GPS监听实例
|
||||
private PositionModel mCurrentGpsPos; // 缓存当前GPS位置(供页面使用)
|
||||
// 本地位置缓存(解决服务数据未同步时Adapter空数据问题)
|
||||
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
|
||||
|
||||
|
||||
// 服务连接(Java 7 匿名内部类实现,强化状态同步+数据预加载)
|
||||
private ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// 1. 安全获取服务实例(避免强转失败+服务未就绪)
|
||||
if (!(service instanceof MainService.LocalBinder)) {
|
||||
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配(非MainService.LocalBinder)");
|
||||
isServiceBound.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.LocalBinder binder = (MainService.LocalBinder) service;
|
||||
mMainService = binder.getService();
|
||||
// 2. 标记服务绑定成功(原子操作,确保多线程可见)
|
||||
isServiceBound.set(true);
|
||||
LogUtils.d(TAG, "MainService绑定成功,开始同步数据+初始化Adapter");
|
||||
|
||||
// 3. 同步服务数据到本地缓存(核心:先同步数据,再初始化Adapter)
|
||||
syncDataFromMainService();
|
||||
// 4. 注册GPS监听(确保监听在Adapter前初始化,数据不丢失)
|
||||
registerGpsListener();
|
||||
// 5. 初始化Adapter(传入本地缓存+服务实例,数据非空)
|
||||
initPositionAdapter();
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "服务绑定后初始化失败:" + e.getMessage());
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
showToast("服务初始化失败,无法加载数据");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MainService断开连接,清空引用+标记状态");
|
||||
// 1. 清空服务引用+标记绑定状态
|
||||
mMainService = null;
|
||||
isServiceBound.set(false);
|
||||
// 2. 标记Adapter未初始化(下次绑定需重新初始化)
|
||||
isAdapterInited.set(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_location);
|
||||
|
||||
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, "【导航栏】点击返回");
|
||||
startActivity(new Intent(LocationActivity.this, MainActivity.class));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 初始化视图(优先执行,避免Adapter初始化时视图为空)
|
||||
initView();
|
||||
// 2. 初始化GPS监听(提前创建,避免绑定服务后空指针)
|
||||
initGpsUpdateListener();
|
||||
// 3. 绑定MainService(最后执行,确保视图/监听已就绪)
|
||||
bindMainService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图(RecyclerView)- 确保视图先于Adapter初始化
|
||||
*/
|
||||
private void initView() {
|
||||
mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||
// 1. 显式设置布局管理器(避免Adapter设置时无布局管理器崩溃)
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
mRvPosition.setLayoutManager(layoutManager);
|
||||
// 2. 初始化本地缓存(避免首次加载时缓存为空)
|
||||
mLocalPosCache.clear();
|
||||
LogUtils.d(TAG, "视图初始化完成(布局管理器+本地缓存已就绪)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定MainService(Java 7 显式Intent,强化绑定安全性)
|
||||
*/
|
||||
private void bindMainService() {
|
||||
// 1. 避免重复绑定(快速重建Activity时防止多绑定)
|
||||
if (isServiceBound.get()) {
|
||||
LogUtils.w(TAG, "无需重复绑定:MainService已绑定");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// 2. 绑定服务(BIND_AUTO_CREATE:服务不存在时自动创建,增加绑定成功率)
|
||||
boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
|
||||
if (!bindSuccess) {
|
||||
LogUtils.e(TAG, "发起MainService绑定请求失败(服务未找到/系统限制)");
|
||||
showToast("服务绑定失败,无法加载位置数据");
|
||||
} else {
|
||||
LogUtils.d(TAG, "MainService绑定请求已发起");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MainService同步数据到本地缓存(核心解决Adapter空数据问题)
|
||||
* 作用:1. 服务数据优先同步到本地,Adapter基于本地缓存初始化
|
||||
* 2. 避免服务数据更新时直接操作Adapter,通过缓存中转
|
||||
*/
|
||||
private void syncDataFromMainService() {
|
||||
// 1. 安全校验(服务未绑定/服务空,用本地缓存兜底)
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "同步数据:服务未就绪,使用本地缓存(当前缓存量=" + mLocalPosCache.size() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 从服务获取最新位置数据(同步操作,确保数据拿到后再返回)
|
||||
ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
|
||||
// 3. 同步到本地缓存(清空旧数据+添加新数据,避免重复)
|
||||
synchronized (mLocalPosCache) { // 加锁避免多线程操作缓存冲突
|
||||
mLocalPosCache.clear();
|
||||
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||
mLocalPosCache.addAll(servicePosList);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
|
||||
+ ",本地缓存数=" + mLocalPosCache.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
|
||||
// 异常时保留本地缓存,避免Adapter无数据
|
||||
LogUtils.w(TAG, "同步失败,使用本地缓存兜底(缓存量=" + mLocalPosCache.size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化PositionAdapter(核心优化:基于本地缓存初始化,避免空数据)
|
||||
*/
|
||||
private void initPositionAdapter() {
|
||||
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
|
||||
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
|
||||
LogUtils.w(TAG, "Adapter初始化跳过:"
|
||||
+ "已初始化=" + isAdapterInited.get()
|
||||
+ ",服务绑定=" + isServiceBound.get()
|
||||
+ ",视图就绪=" + (mRvPosition != null));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 基于本地缓存初始化Adapter(缓存已同步服务数据,非空)
|
||||
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
|
||||
|
||||
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||
@Override
|
||||
public void onDeleteClick(int position) {
|
||||
// 安全校验(索引有效+服务绑定+缓存非空)
|
||||
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
PositionModel deletePos = mLocalPosCache.get(position);
|
||||
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
|
||||
// 步骤1:调用服务删除(确保服务数据一致性)
|
||||
mMainService.removePosition(deletePos.getPositionId());
|
||||
// 步骤2:删除本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.remove(position);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemRemoved(position);
|
||||
showToast("删除位置成功:" + deletePos.getMemo());
|
||||
LogUtils.d(TAG, "删除位置完成:ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||
@Override
|
||||
public void onSavePositionClick(int position, PositionModel updatedPos) {
|
||||
// 安全校验(索引有效+服务绑定+数据非空)
|
||||
if (!isServiceBound.get() || mMainService == null
|
||||
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
|
||||
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
|
||||
showToast("服务未就绪,保存失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤1:调用服务更新(确保服务数据一致性)
|
||||
mMainService.updatePosition(updatedPos);
|
||||
// 步骤2:更新本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.set(position, updatedPos);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemChanged(position);
|
||||
showToast("保存位置成功:" + updatedPos.getMemo());
|
||||
LogUtils.d(TAG, "保存位置完成:ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 设置Adapter到RecyclerView(最后一步,确保Adapter已配置完成)
|
||||
mRvPosition.setAdapter(mPositionAdapter);
|
||||
// 6. 标记Adapter已初始化(避免重复初始化)
|
||||
isAdapterInited.set(true);
|
||||
LogUtils.d(TAG, "PositionAdapter初始化完成(基于本地缓存,数据量=" + mLocalPosCache.size() + ")");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Adapter初始化失败:" + e.getMessage());
|
||||
isAdapterInited.set(false);
|
||||
mPositionAdapter = null;
|
||||
showToast("位置列表初始化失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast(Java 7 显式Toast.makeText,避免空Context)
|
||||
*/
|
||||
private void showToast(String content) {
|
||||
if (isFinishing() || isDestroyed()) { // 避免Activity销毁后弹Toast崩溃
|
||||
LogUtils.w(TAG, "Activity已销毁,跳过Toast:" + content);
|
||||
return;
|
||||
}
|
||||
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// ---------------------- 页面交互(新增位置逻辑保留,适配GPS数据) ----------------------
|
||||
/**
|
||||
* 新增位置(调用服务addPosition(),可选:用当前GPS位置初始化新位置)
|
||||
*/
|
||||
public void addNewPosition(View view) {
|
||||
// 1. 隐藏软键盘(避免软键盘遮挡操作)
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null && getCurrentFocus() != null) {
|
||||
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
// 2. 安全校验(服务未绑定,不允许新增)
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "新增位置失败:MainService未绑定");
|
||||
showToast("服务未就绪,无法新增位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 创建新位置模型(优化:优先用当前GPS位置初始化,无则用默认值)
|
||||
PositionModel newPos = new PositionModel();
|
||||
newPos.setPositionId(PositionModel.genPositionId()); // 生成唯一ID(需PositionModel实现)
|
||||
if (mCurrentGpsPos != null) {
|
||||
newPos.setLongitude(mCurrentGpsPos.getLongitude());
|
||||
newPos.setLatitude(mCurrentGpsPos.getLatitude());
|
||||
newPos.setMemo("当前GPS位置(可编辑)");
|
||||
} else {
|
||||
newPos.setLongitude(116.404267); // 北京经度(默认值)
|
||||
newPos.setLatitude(39.915119); // 北京纬度(默认值)
|
||||
newPos.setMemo("默认位置(可编辑备注)");
|
||||
}
|
||||
newPos.setIsSimpleView(true); // 默认简单视图
|
||||
newPos.setIsEnableRealPositionDistance(true); // 启用距离计算(依赖GPS)
|
||||
|
||||
// 4. 调用服务新增+同步本地缓存(确保缓存与服务一致)
|
||||
mMainService.addPosition(newPos);
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.add(newPos);
|
||||
}
|
||||
LogUtils.d(TAG, "通过服务新增位置:ID=" + newPos.getPositionId() + ",纬度=" + newPos.getLatitude() + "(缓存已同步)");
|
||||
|
||||
// 5. 刷新Adapter(基于缓存操作,确保数据立即显示)
|
||||
if (isAdapterInited.get() && mPositionAdapter != null) {
|
||||
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
|
||||
}
|
||||
showToast("新增位置成功(已启用GPS距离计算)");
|
||||
}
|
||||
|
||||
// ---------------------- 新增:GPS监听初始化+注册/反注册(核心适配逻辑) ----------------------
|
||||
/**
|
||||
* 初始化GPS监听:实现MainService.GpsUpdateListener,接收实时GPS数据
|
||||
*/
|
||||
private void initGpsUpdateListener() {
|
||||
LogUtils.d(TAG, "initGpsUpdateListener()");
|
||||
mGpsUpdateListener = new MainService.GpsUpdateListener() {
|
||||
@Override
|
||||
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
|
||||
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "GPS位置更新:数据为空或Activity已销毁");
|
||||
return;
|
||||
}
|
||||
// 缓存当前GPS位置(供页面其他逻辑使用)
|
||||
mCurrentGpsPos = currentGpsPos;
|
||||
LogUtils.d(TAG, String.format("收到GPS更新:纬度=%.4f,经度=%.4f"
|
||||
, currentGpsPos.getLatitude(), currentGpsPos.getLongitude()));
|
||||
// 安全更新UI(避免Activity销毁后操作视图崩溃)
|
||||
((TextView)findViewById(R.id.tv_latitude)).setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
|
||||
((TextView)findViewById(R.id.tv_longitude)).setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(String status) {
|
||||
if (status == null || isFinishing() || isDestroyed()) return;
|
||||
LogUtils.d(TAG, "GPS状态变化:" + status);
|
||||
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
|
||||
ToastUtils.show("GPS提示:" + status);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册GPS监听:调用MainService的PUBLIC方法,绑定监听
|
||||
*/
|
||||
private void registerGpsListener() {
|
||||
// 安全校验(避免Activity销毁/服务未绑定/监听为空时注册)
|
||||
if (isFinishing() || isDestroyed() || !isServiceBound.get() || mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听注册跳过:Activity状态异常/依赖未就绪");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已注册");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听注册失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反注册GPS监听:调用MainService的PUBLIC方法,解绑监听(核心防内存泄漏+数据异常)
|
||||
*/
|
||||
private void unregisterGpsListener() {
|
||||
// 避免Activity销毁后调用服务方法(防止空指针/服务已解绑)
|
||||
if (mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听反注册跳过:服务/监听未初始化");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已反注册");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听反注册失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面可见时同步数据(解决快速切回时数据未更新问题)
|
||||
* 场景:快速关闭再打开Activity,服务已绑定但数据未重新同步
|
||||
*/
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// 1. 服务已绑定但Adapter未初始化:重新同步数据+初始化Adapter
|
||||
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
|
||||
LogUtils.d(TAG, "onResume:服务已绑定但Adapter未初始化,重新同步数据");
|
||||
syncDataFromMainService();
|
||||
initPositionAdapter();
|
||||
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
|
||||
syncDataFromMainService();
|
||||
mPositionAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "onResume:刷新位置数据(与服务同步)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面不可见时暂停操作(避免后台操作导致数据异常)
|
||||
*/
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 避免后台时仍执行UI刷新(如GPS更新触发的视图操作)
|
||||
LogUtils.d(TAG, "onPause:页面不可见,暂停UI相关操作");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy:开始释放资源");
|
||||
|
||||
// 1. 反注册GPS监听(优先执行,避免服务持有Activity引用导致内存泄漏)
|
||||
unregisterGpsListener();
|
||||
|
||||
// 2. 释放Adapter资源(反注册可能的监听,避免内存泄漏)
|
||||
if (mPositionAdapter != null) {
|
||||
mPositionAdapter.release();
|
||||
mPositionAdapter = null; // 清空引用,帮助GC回收
|
||||
LogUtils.d(TAG, "Adapter资源已释放");
|
||||
}
|
||||
|
||||
// 3. 解绑MainService(最后执行,确保其他资源已释放)
|
||||
if (isServiceBound.get()) {
|
||||
try {
|
||||
unbindService(mServiceConnection);
|
||||
LogUtils.d(TAG, "MainService解绑完成");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 捕获“服务未绑定”异常(快速开关时可能出现,避免崩溃)
|
||||
LogUtils.d(TAG, "解绑MainService失败:服务未绑定(可能已提前解绑)");
|
||||
}
|
||||
// 重置绑定状态+服务引用
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 4. 清空本地缓存+GPS引用(帮助GC回收)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.clear();
|
||||
}
|
||||
mCurrentGpsPos = null;
|
||||
mGpsUpdateListener = null;
|
||||
isAdapterInited.set(false);
|
||||
LogUtils.d(TAG, "所有资源释放完成(onDestroy执行结束)");
|
||||
}
|
||||
|
||||
// ---------------------- 移除重复定义:LocalBinder 统一在 MainService 中定义 ----------------------
|
||||
// 说明:原LocationActivity中的LocalBinder是重复定义(MainService已实现),会导致类型强转失败
|
||||
// 此处删除该类,确保Activity绑定服务时强转的是MainService中的LocalBinder
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
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.positions.R;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/07 23:29
|
||||
* @Describe 应用设置活动窗口
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "SettingsActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
@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);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 00:11
|
||||
* @Describe WinBoLL 窗口基础类
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
void setThemeStyle() {
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, String.format("onResume %s", getTag()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
/*if (item.getItemId() == R.id.item_log) {
|
||||
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.item_home) {
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
return true;
|
||||
}*/
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
package cc.winboll.studio.positions.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 20:25
|
||||
* @Describe 位置数据适配器(修复视图复用资源加载,支持滚动后重新绑定数据,Java 7语法适配)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Java 7 语法严格适配 + 视图复用资源加载修复:
|
||||
* 1. 保留无Lambda/弱引用/线程安全集合等原有适配
|
||||
* 2. 修复核心问题:移除 onViewDetachedFromWindow 中关键资源释放,改为 onBind 时重新绑定
|
||||
* 3. 强化资源复用:任务视图/距离控件在复用后自动从服务同步最新数据,确保滚动后数据不丢失
|
||||
* 4. 优化缓存逻辑:仅清理脱离屏幕且无效的控件缓存,有效控件保留引用供局部更新(如距离)
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import cc.winboll.studio.positions.views.PositionTaskListView;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PositionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements MainService.TaskUpdateListener {
|
||||
public static final String TAG = "PositionAdapter";
|
||||
|
||||
// 视图类型常量(静态常量统一管理)
|
||||
private static final int VIEW_TYPE_SIMPLE = 0;
|
||||
private static final int VIEW_TYPE_EDIT = 1;
|
||||
|
||||
// 默认配置常量(避免魔法值)
|
||||
private static final String DEFAULT_MEMO = "无备注";
|
||||
private static final String DEFAULT_TASK_DESC = "新任务";
|
||||
private static final int DEFAULT_TASK_DISTANCE = 50; // 单位:米
|
||||
private static final String DISTANCE_FORMAT = "实时距离:%.1f 米";
|
||||
private static final String DISTANCE_DISABLED = "实时距离:未启用";
|
||||
private static final String DISTANCE_ERROR = "实时距离:计算失败";
|
||||
|
||||
// 核心依赖(弱引用+线程安全集合,适配Java 7)
|
||||
private final Context mContext;
|
||||
private final ArrayList<PositionModel> mCachedPositionList; // 位置缓存(与MainService同步)
|
||||
private final WeakReference<MainService> mMainServiceRef; // 弱引用MainService,防内存泄漏
|
||||
// 控件缓存:位置ID → 对应任务列表视图(分别缓存简单/编辑模式,支持复用后快速同步)
|
||||
private final ConcurrentHashMap<String, PositionTaskListView> mSimpleTaskViewMap;
|
||||
private final ConcurrentHashMap<String, PositionTaskListView> mEditTaskViewMap;
|
||||
// 距离控件缓存(用于局部更新距离UI,保留有效引用避免复用后更新失效)
|
||||
private final ConcurrentHashMap<String, TextView> mPosDistanceViewMap;
|
||||
|
||||
// 回调接口(仅处理位置逻辑,任务逻辑通过PositionTaskListView+MainService完成)
|
||||
public interface OnDeleteClickListener {
|
||||
void onDeleteClick(int position);
|
||||
}
|
||||
|
||||
public interface OnSavePositionClickListener {
|
||||
void onSavePositionClick(int position, PositionModel updatedPos);
|
||||
}
|
||||
|
||||
private OnDeleteClickListener mOnDeleteListener;
|
||||
private OnSavePositionClickListener mOnSavePosListener;
|
||||
|
||||
// =========================================================================
|
||||
// 构造函数(初始化依赖+注册服务监听+初始化控件缓存)
|
||||
// =========================================================================
|
||||
public PositionAdapter(Context context, ArrayList<PositionModel> cachedPositionList, MainService mainService) {
|
||||
this.mContext = context;
|
||||
// 容错处理:避免传入null导致空指针
|
||||
this.mCachedPositionList = (cachedPositionList != null) ? cachedPositionList : new ArrayList<PositionModel>();
|
||||
// 弱引用MainService:防止Adapter持有服务导致内存泄漏(Java 7 弱引用语法)
|
||||
this.mMainServiceRef = new WeakReference<MainService>(mainService);
|
||||
// 初始化控件缓存(线程安全集合,适配多线程更新场景)
|
||||
this.mSimpleTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||
this.mEditTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||
this.mPosDistanceViewMap = new ConcurrentHashMap<String, TextView>();
|
||||
|
||||
// 注册MainService任务监听:服务任务变化时同步刷新任务列表视图
|
||||
if (mainService != null) {
|
||||
mainService.registerTaskUpdateListener(this);
|
||||
LogUtils.d(TAG, "已注册MainService任务监听,确保任务数据与服务同步");
|
||||
} else {
|
||||
LogUtils.w(TAG, "构造函数:MainService为空,PositionTaskListView无法初始化");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "Adapter初始化完成:位置数量=" + mCachedPositionList.size());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// RecyclerView 核心方法(强化视图复用逻辑,复用后自动重新绑定资源)
|
||||
// =========================================================================
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
PositionModel posModel = getPositionByIndex(position);
|
||||
return (posModel != null && posModel.isSimpleView()) ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
if (viewType == VIEW_TYPE_SIMPLE) {
|
||||
View simpleView = inflater.inflate(R.layout.item_position_simple, parent, false);
|
||||
return new SimpleViewHolder(simpleView);
|
||||
} else {
|
||||
View editView = inflater.inflate(R.layout.item_position_edit, parent, false);
|
||||
return new EditViewHolder(editView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
PositionModel posModel = getPositionByIndex(position);
|
||||
if (posModel == null) {
|
||||
LogUtils.w(TAG, "onBindViewHolder:位置模型为空(索引=" + position + "),跳过绑定");
|
||||
return;
|
||||
}
|
||||
final String posId = posModel.getPositionId();
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
|
||||
// 按视图类型绑定数据(简单模式/编辑模式)—— 核心修复:复用后重新绑定所有资源
|
||||
if (holder instanceof SimpleViewHolder) {
|
||||
LogUtils.d(TAG, "instanceof SimpleViewHolder(复用/新创建):位置ID=" + posId);
|
||||
SimpleViewHolder simpleHolder = (SimpleViewHolder) holder;
|
||||
// 1. 重新绑定位置基础数据(经纬度/备注/距离,确保复用后显示最新数据)
|
||||
bindSimplePositionData(simpleHolder, posModel);
|
||||
// 2. 重新初始化+绑定简单模式任务视图(复用后从服务同步最新任务,避免数据丢失)
|
||||
initAndBindSimpleTaskView(simpleHolder.ptlvSimpleTasks, posId, mainService);
|
||||
// 3. 缓存/更新简单模式任务视图+距离控件(覆盖旧缓存,确保引用最新控件)
|
||||
mSimpleTaskViewMap.put(posId, simpleHolder.ptlvSimpleTasks);
|
||||
mPosDistanceViewMap.put(posId, simpleHolder.tvSimpleDistance);
|
||||
|
||||
} else if (holder instanceof EditViewHolder) {
|
||||
LogUtils.d(TAG, "instanceof EditViewHolder(复用/新创建):位置ID=" + posId);
|
||||
EditViewHolder editHolder = (EditViewHolder) holder;
|
||||
// 1. 重新绑定位置基础数据(经纬度/备注/距离/距离开关,复用后显示最新状态)
|
||||
bindEditPositionData(editHolder, posModel, position);
|
||||
// 2. 重新初始化+绑定编辑模式任务视图(复用后同步服务最新任务,支持增删改)
|
||||
initAndBindEditTaskView(editHolder.ptlvEditTasks, posId, mainService, editHolder.btnAddTask);
|
||||
// 3. 缓存/更新编辑模式任务视图+距离控件(覆盖旧缓存,确保引用最新控件)
|
||||
mEditTaskViewMap.put(posId, editHolder.ptlvEditTasks);
|
||||
mPosDistanceViewMap.put(posId, editHolder.tvEditDistance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
// 核心修改:仅清理「无效/已回收」的控件缓存,不释放关键资源(支持复用后重新绑定)
|
||||
PositionModel posModel = getPositionByIndex(holder.getAdapterPosition());
|
||||
if (posModel == null || TextUtils.isEmpty(posModel.getPositionId())) {
|
||||
return;
|
||||
}
|
||||
String posId = posModel.getPositionId();
|
||||
|
||||
// 1. 清理「已脱离视图树/无效」的距离控件缓存(保留有效控件供局部更新)
|
||||
if (mPosDistanceViewMap.containsKey(posId)) {
|
||||
TextView distanceView = mPosDistanceViewMap.get(posId);
|
||||
if (distanceView == null || !distanceView.isAttachedToWindow()) {
|
||||
mPosDistanceViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "视图脱离屏幕:移除无效距离控件缓存(位置ID=" + posId + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理「已脱离视图树/无效」的简单模式任务视图缓存(不解绑监听/不清空数据,复用后重新同步)
|
||||
if (holder instanceof SimpleViewHolder && mSimpleTaskViewMap.containsKey(posId)) {
|
||||
PositionTaskListView taskView = mSimpleTaskViewMap.get(posId);
|
||||
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||
mSimpleTaskViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "简单模式视图脱离屏幕:移除无效任务视图缓存(位置ID=" + posId + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清理「已脱离视图树/无效」的编辑模式任务视图缓存(同上,保留数据供复用)
|
||||
if (holder instanceof EditViewHolder && mEditTaskViewMap.containsKey(posId)) {
|
||||
PositionTaskListView taskView = mEditTaskViewMap.get(posId);
|
||||
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||
mEditTaskViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "编辑模式视图脱离屏幕:移除无效任务视图缓存(位置ID=" + posId + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mCachedPositionList.size();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 位置数据绑定(保留原有逻辑,确保复用后数据最新)
|
||||
// =========================================================================
|
||||
/**
|
||||
* 绑定简单模式位置数据(仅显示,无编辑操作)—— 复用后重新执行,显示最新数据
|
||||
*/
|
||||
private void bindSimplePositionData(SimpleViewHolder holder, final PositionModel posModel) {
|
||||
// 1. 经纬度显示(保留6位小数,格式统一)
|
||||
holder.tvSimpleLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||
holder.tvSimpleLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||
|
||||
// 2. 备注显示(无备注时显示默认文本)
|
||||
String memo = posModel.getMemo();
|
||||
holder.tvSimpleMemo.setText("备注:" + (TextUtils.isEmpty(memo) ? DEFAULT_MEMO : memo));
|
||||
|
||||
// 3. 实时距离显示(按状态区分文本+颜色——复用后重新计算显示最新距离)
|
||||
updateDistanceDisplay(holder.tvSimpleDistance, posModel);
|
||||
|
||||
// 4. 点击切换到编辑模式
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
posModel.setIsSimpleView(false);
|
||||
// 精准刷新当前项(避免全量刷新)
|
||||
notifyItemChanged(getPositionIndexById(posModel.getPositionId()));
|
||||
LogUtils.d(TAG, "简单视图点击:位置ID=" + posModel.getPositionId() + ",切换到编辑视图");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定编辑模式位置数据(支持备注修改/距离开关/删除/保存)—— 复用后重新执行,恢复编辑状态
|
||||
*/
|
||||
private void bindEditPositionData(final EditViewHolder holder, final PositionModel posModel, final int position) {
|
||||
final String posId = posModel.getPositionId();
|
||||
|
||||
// 1. 经纬度显示(不可编辑,仅展示)
|
||||
holder.tvEditLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||
holder.tvEditLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||
|
||||
// 2. 备注编辑(填充现有备注,光标定位到末尾——复用后恢复输入状态)
|
||||
String memo = posModel.getMemo();
|
||||
if (!TextUtils.isEmpty(memo)) {
|
||||
holder.etEditMemo.setText(memo);
|
||||
holder.etEditMemo.setSelection(memo.length());
|
||||
} else {
|
||||
holder.etEditMemo.setText("");
|
||||
}
|
||||
|
||||
// 3. 实时距离显示(与简单模式逻辑一致——复用后显示最新距离)
|
||||
updateDistanceDisplay(holder.tvEditDistance, posModel);
|
||||
|
||||
// 4. 距离开关状态(匹配位置模型中的启用状态——复用后恢复开关状态)
|
||||
holder.rgDistanceSwitch.check(posModel.isEnableRealPositionDistance()
|
||||
? R.id.rb_distance_enable
|
||||
: R.id.rb_distance_disable);
|
||||
|
||||
// 5. 取消编辑:切换回简单模式+隐藏软键盘
|
||||
holder.btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
posModel.setIsSimpleView(true);
|
||||
notifyItemChanged(position);
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "取消编辑:位置ID=" + posId + ",切换回简单视图");
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 删除位置:回调Activity处理(Adapter不直接操作数据)
|
||||
holder.btnDelete.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnDeleteListener != null) {
|
||||
mOnDeleteListener.onDeleteClick(position);
|
||||
}
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "触发删除:通知Activity处理位置ID=" + posId + "的删除逻辑");
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 保存位置:收集参数→更新模型→回调Activity→切换视图
|
||||
holder.btnSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 收集编辑后的数据
|
||||
String newMemo = holder.etEditMemo.getText().toString().trim();
|
||||
boolean isDistanceEnable = (holder.rgDistanceSwitch.getCheckedRadioButtonId() == R.id.rb_distance_enable);
|
||||
|
||||
// 构建更新后的位置模型(保留核心不可编辑字段)
|
||||
PositionModel updatedPos = new PositionModel();
|
||||
updatedPos.setPositionId(posId);
|
||||
updatedPos.setLongitude(posModel.getLongitude());
|
||||
updatedPos.setLatitude(posModel.getLatitude());
|
||||
updatedPos.setMemo(newMemo);
|
||||
updatedPos.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||
updatedPos.setIsSimpleView(true);
|
||||
|
||||
// 回调Activity保存(由Activity同步MainService)
|
||||
if (mOnSavePosListener != null) {
|
||||
mOnSavePosListener.onSavePositionClick(position, updatedPos);
|
||||
}
|
||||
|
||||
// 本地同步状态(避免刷新延迟)
|
||||
posModel.setMemo(newMemo);
|
||||
posModel.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||
posModel.setIsSimpleView(true);
|
||||
notifyItemChanged(position);
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "保存位置:ID=" + posId + ",新备注=" + newMemo + ",距离启用=" + isDistanceEnable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PositionTaskListView 集成(强化复用逻辑:复用后重新同步服务数据,确保数据不丢失)
|
||||
// =========================================================================
|
||||
/**
|
||||
* 初始化+绑定简单模式任务列表视图——复用后重新执行,从服务同步最新任务
|
||||
*/
|
||||
private void initAndBindSimpleTaskView(PositionTaskListView taskView, final String posId, MainService mainService) {
|
||||
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null) {
|
||||
LogUtils.w(TAG, "初始化简单模式任务视图失败:参数无效(posId=" + posId + ",service=" + mainService + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 初始化任务视图(绑定MainService+当前位置ID——复用后重新绑定,确保服务引用有效)
|
||||
taskView.init(mainService, posId);
|
||||
// 2. 设置为简单模式(仅展示,隐藏编辑按钮——复用后恢复视图模式)
|
||||
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_SIMPLE);
|
||||
// 3. 关键:复用后强制从服务同步最新任务(避免显示旧数据,核心修复点)
|
||||
taskView.syncTasksFromMainService();
|
||||
|
||||
// 4. 任务更新回调(服务任务变化时重新同步——复用后重新绑定回调,避免监听失效)
|
||||
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||
@Override
|
||||
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||
LogUtils.d(TAG, "简单模式任务更新:位置ID=" + positionId + ",已启用任务数=" + updatedTasks.size());
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "初始化/复用简单模式任务视图完成:位置ID=" + posId);
|
||||
}
|
||||
|
||||
/**
|
||||
- 初始化+绑定编辑模式任务列表视图——复用后重新执行,从服务同步最新任务+恢复编辑功能
|
||||
*/
|
||||
private void initAndBindEditTaskView(final PositionTaskListView taskView, final String posId,
|
||||
MainService mainService, Button btnAddTask) {
|
||||
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null || btnAddTask == null) {
|
||||
LogUtils.w(TAG, "初始化编辑模式任务视图失败:参数无效(posId=" + posId + ",btnAddTask=" + btnAddTask + ")");
|
||||
return;
|
||||
}
|
||||
// 1. 初始化任务视图(绑定MainService+当前位置ID——复用后重新绑定,确保服务引用有效)
|
||||
taskView.init(mainService, posId);
|
||||
// 2. 设置为编辑模式(显示编辑/删除按钮——复用后恢复视图模式)
|
||||
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT);
|
||||
// 3. 关键:复用后强制从服务同步最新任务(避免显示旧数据,核心修复点)
|
||||
taskView.syncTasksFromMainService();// 4. 绑定“新增任务”按钮逻辑(复用后重新绑定点击事件,避免按钮点击失效)
|
||||
btnAddTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 构建默认任务模型(关联当前位置ID,使用默认配置)
|
||||
PositionTaskModel newTask = new PositionTaskModel();
|
||||
newTask.setTaskId(PositionTaskModel.genTaskId()); // 生成唯一任务ID(需模型类实现)
|
||||
newTask.setPositionId(posId); // 绑定当前位置
|
||||
newTask.setTaskDescription(DEFAULT_TASK_DESC); // 默认描述
|
||||
newTask.setDiscussDistance(DEFAULT_TASK_DISTANCE); // 默认触发距离(50米)
|
||||
newTask.setIsEnable(true); // 默认启用
|
||||
newTask.setIsBingo(false); // 初始未触发// 调用任务视图的新增方法(假设视图已实现addNewTask,内部同步MainService)
|
||||
taskView.addNewTask(newTask);
|
||||
hideSoftKeyboard(v); // 隐藏软键盘,提升体验
|
||||
LogUtils.d(TAG, "编辑模式新增任务:位置ID=" + posId + ",任务ID=" + newTask.getTaskId());
|
||||
}
|
||||
});// 5. 任务更新回调(通知外部任务变化——复用后重新绑定回调,确保交互正常)
|
||||
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||
@Override
|
||||
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||
LogUtils.d(TAG, "编辑模式任务更新:位置ID=" + positionId + ",当前任务数=" + updatedTasks.size());
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "初始化/复用编辑模式任务视图完成:位置ID=" + posId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 工具方法(保留原有逻辑,适配资源复用场景)
|
||||
// =========================================================================
|
||||
/**
|
||||
|
||||
- 更新距离显示(复用后重新执行,确保显示最新距离+颜色——适配资源复用)
|
||||
*/
|
||||
private void updateDistanceDisplay(TextView distanceView, PositionModel posModel) {
|
||||
if (distanceView == null || posModel == null) {
|
||||
LogUtils.w(TAG, "更新距离显示失败:参数为空(控件/位置模型)");
|
||||
return;
|
||||
}// 场景1:距离未启用(灰色文本)
|
||||
if (!posModel.isEnableRealPositionDistance()) {
|
||||
distanceView.setText(DISTANCE_DISABLED);
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.gray));
|
||||
return;
|
||||
}// 场景2:距离计算失败(红色文本,用-1标记失败状态)
|
||||
double distance = posModel.getRealPositionDistance();
|
||||
if (distance < 0) {
|
||||
distanceView.setText(DISTANCE_ERROR);
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
|
||||
return;
|
||||
}// 场景3:正常显示距离(按距离范围设置颜色——复用后重新计算颜色)
|
||||
distanceView.setText(String.format(DISTANCE_FORMAT, distance));
|
||||
if (distance <= 100) {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.green)); // 近距离(≤100米)
|
||||
} else if (distance <= 500) {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.yellow));// 中距离(≤500米)
|
||||
} else {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.red)); // 远距离(>500米)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
- 根据索引获取位置模型(容错处理,避免越界/空指针——适配复用后索引变化场景)
|
||||
*/
|
||||
private PositionModel getPositionByIndex(int index) {
|
||||
if (mCachedPositionList == null || index < 0 || index >= mCachedPositionList.size()) {
|
||||
LogUtils.w(TAG, "获取位置模型失败:无效索引(" + index + ")或缓存为空");
|
||||
return null;
|
||||
}
|
||||
return mCachedPositionList.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
- 根据位置ID获取列表索引(用于精准刷新视图——适配复用后视图位置变化)
|
||||
*/
|
||||
private int getPositionIndexById(String positionId) {
|
||||
if (TextUtils.isEmpty(positionId) || mCachedPositionList == null || mCachedPositionList.isEmpty()) {
|
||||
LogUtils.w(TAG, "获取位置索引失败:参数无效(ID/缓存为空)");
|
||||
return -1;
|
||||
}// Java 7 增强for循环遍历(替代Lambda,适配语法)
|
||||
for (int i = 0; i < mCachedPositionList.size(); i++) {
|
||||
PositionModel pos = mCachedPositionList.get(i);
|
||||
if (positionId.equals(pos.getPositionId())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
LogUtils.w(TAG, "获取位置索引失败:未找到ID=" + positionId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
- 局部更新距离UI(仅更新指定位置的距离,避免全量刷新卡顿——适配复用后控件缓存有效场景)
|
||||
*/
|
||||
public void updateSinglePositionDistance(String positionId) {
|
||||
if (TextUtils.isEmpty(positionId) || mPosDistanceViewMap.isEmpty()) {
|
||||
LogUtils.w(TAG, "局部更新距离失败:ID无效或无控件缓存(ID=" + positionId + ")");
|
||||
return;
|
||||
}
|
||||
// 1. 获取服务端最新位置数据(带重试,避免服务临时回收)
|
||||
MainService mainService = getMainServiceWithRetry(2);
|
||||
if (mainService == null) {
|
||||
LogUtils.e(TAG, "局部更新距离失败:无法获取MainService");
|
||||
return;
|
||||
}
|
||||
PositionModel latestPos = null;
|
||||
try {
|
||||
ArrayList servicePosList = mainService.getPositionList();
|
||||
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||
// Java 7 迭代器遍历,避免ConcurrentModificationException
|
||||
Iterator iter = servicePosList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionModel pos = (PositionModel)iter.next();
|
||||
if (positionId.equals(pos.getPositionId())) {
|
||||
latestPos = pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "获取最新位置数据失败(ID=" + positionId + ")" + e);
|
||||
return;
|
||||
}if (latestPos == null) {
|
||||
LogUtils.w(TAG, "局部更新距离失败:未找到位置ID=" + positionId);
|
||||
return;
|
||||
}
|
||||
// 2. 更新距离控件(确保主线程操作,避免跨线程异常——适配复用后控件已重新绑定)
|
||||
final TextView distanceView = mPosDistanceViewMap.get(positionId);
|
||||
if (distanceView != null && distanceView.isAttachedToWindow()) {
|
||||
final PositionModel finalLatestPos = latestPos;
|
||||
distanceView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateDistanceDisplay(distanceView, finalLatestPos);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "局部更新距离完成:位置ID=" + positionId + ",距离=" + latestPos.getRealPositionDistance() + "米");
|
||||
} else {
|
||||
mPosDistanceViewMap.remove(positionId); // 移除无效控件缓存(如视图已被销毁)
|
||||
LogUtils.w(TAG, "局部更新距离失败:控件已回收/脱离视图树(ID=" + positionId + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- 全量更新位置数据(从服务同步最新数据,过滤无效/重复项——适配复用后全量刷新场景)
|
||||
*/
|
||||
public void updateAllPositionData(ArrayList<PositionModel> newPosList) {
|
||||
if (newPosList == null) {
|
||||
LogUtils.w(TAG, "全量更新位置数据失败:新列表为空");
|
||||
return;
|
||||
}
|
||||
// 1. 过滤无效位置(校验核心字段:ID/经纬度合法)
|
||||
ArrayList<PositionModel> validPosList = new ArrayList<PositionModel>();
|
||||
for (PositionModel pos : newPosList) {
|
||||
if (TextUtils.isEmpty(pos.getPositionId())
|
||||
|| pos.getLongitude() < -180 || pos.getLongitude() > 180
|
||||
|| pos.getLatitude() < -90 || pos.getLatitude() > 90) {
|
||||
LogUtils.w(TAG, "过滤无效位置:ID=" + pos.getPositionId() + "(经纬度/ID非法)");
|
||||
continue;
|
||||
}
|
||||
validPosList.add(pos);
|
||||
}
|
||||
// 2. 去重(按位置ID去重,保留服务端最新数据)
|
||||
ConcurrentHashMap<String, PositionModel> uniquePosMap = new ConcurrentHashMap<String, PositionModel>();
|
||||
for (PositionModel pos : validPosList) {
|
||||
uniquePosMap.put(pos.getPositionId(), pos); // 相同ID覆盖,保留最新
|
||||
}
|
||||
ArrayList uniquePosList = new ArrayList(uniquePosMap.values());// 3. 同步到本地缓存+刷新UI(刷新后触发onBind,自动重新绑定资源)
|
||||
this.mCachedPositionList.clear();
|
||||
this.mCachedPositionList.addAll(uniquePosList);
|
||||
// 清空旧控件缓存(避免引用失效数据,刷新后重新缓存新控件)
|
||||
mPosDistanceViewMap.clear();
|
||||
mSimpleTaskViewMap.clear();
|
||||
mEditTaskViewMap.clear();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "全量更新位置数据完成:原数量=" + newPosList.size() + ",过滤后=" + uniquePosList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
- 隐藏软键盘(编辑完成后调用,提升用户体验——适配复用后软键盘残留问题)
|
||||
*/
|
||||
private void hideSoftKeyboard(View view) {
|
||||
if (mContext == null || view == null) {
|
||||
LogUtils.w(TAG, "隐藏软键盘失败:参数为空(上下文/视图)");
|
||||
return;
|
||||
}InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0); // 强制隐藏
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- 带重试的服务获取(解决弱引用临时回收问题,最多重试2次——适配复用后服务引用失效场景)
|
||||
*/
|
||||
private MainService getMainServiceWithRetry(int retryCount) {
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
return mainService;
|
||||
}
|
||||
// 重试逻辑:每次间隔100ms,避免频繁重试
|
||||
for (int i = 0; i < retryCount; i++) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
LogUtils.d(TAG, "重试获取MainService成功(第" + (i + 1) + "次)");
|
||||
return mainService;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, "重试获取服务时线程被中断" + e);
|
||||
Thread.currentThread().interrupt(); // 恢复中断状态
|
||||
break;
|
||||
}
|
||||
}LogUtils.e(TAG, "重试" + retryCount + "次后仍未获取到MainService");
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 实现 MainService.TaskUpdateListener 接口(服务任务变化时回调——适配复用后任务同步)
|
||||
// =========================================================================
|
||||
@Override
|
||||
public void onTaskUpdated() {
|
||||
LogUtils.d(TAG, "收到MainService任务更新通知,同步所有任务列表视图(含复用视图)");
|
||||
|
||||
// 1. 同步简单模式任务视图(仅显示已启用任务——适配复用后视图已重新缓存)
|
||||
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||
PositionTaskListView taskView = entry.getValue();
|
||||
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||
taskView.syncTasksFromMainService(); // 从服务同步最新任务(复用后也能更新)
|
||||
} else {
|
||||
iter.remove(); // 移除无效视图缓存(如视图已脱离屏幕且未复用)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 同步编辑模式任务视图(支持编辑的全量任务——适配复用后视图已重新缓存)
|
||||
if (!mEditTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||
PositionTaskListView taskView = entry.getValue();
|
||||
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||
taskView.syncTasksFromMainService(); // 从服务同步最新任务(复用后也能更新)
|
||||
} else {
|
||||
iter.remove(); // 移除无效视图缓存
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 回调设置方法(供Activity绑定交互逻辑——适配复用后回调不失效)
|
||||
// =========================================================================
|
||||
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
|
||||
this.mOnDeleteListener = listener;
|
||||
}
|
||||
|
||||
public void setOnSavePositionClickListener(OnSavePositionClickListener listener) {
|
||||
this.mOnSavePosListener = listener;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 资源释放(Activity销毁时调用,彻底释放引用,避免内存泄漏——保留原有安全逻辑)
|
||||
// =========================================================================
|
||||
public void release() {
|
||||
// 1. 反注册MainService任务监听(解除服务绑定)
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
mainService.unregisterTaskUpdateListener(this);
|
||||
LogUtils.d(TAG, "已反注册MainService任务监听");
|
||||
}
|
||||
|
||||
// 2. 释放简单模式任务视图资源(清空数据+解绑监听——仅在Activity销毁时执行,不复用场景)
|
||||
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionTaskListView taskView = iter.next().getValue();
|
||||
if (taskView != null) {
|
||||
taskView.clearData();
|
||||
taskView.setOnTaskUpdatedListener(null);
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 释放编辑模式任务视图资源(清空数据+解绑监听——仅在Activity销毁时执行)
|
||||
if (!mEditTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionTaskListView taskView = iter.next().getValue();
|
||||
if (taskView != null) {
|
||||
taskView.clearData();
|
||||
taskView.setOnTaskUpdatedListener(null);
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 清空其他缓存+置空引用(彻底释放,避免内存泄漏)
|
||||
mPosDistanceViewMap.clear();
|
||||
if (mCachedPositionList != null) {
|
||||
mCachedPositionList.clear();
|
||||
}
|
||||
mOnDeleteListener = null;
|
||||
mOnSavePosListener = null;
|
||||
|
||||
LogUtils.d(TAG, "Adapter资源已完全释放(任务视图资源释放+缓存清空+引用置空)");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 静态内部类:ViewHolder(与布局严格对应,避免外部引用导致内存泄漏)
|
||||
// =========================================================================
|
||||
/**
|
||||
- 简单模式ViewHolder(对应 item_position_simple.xml,含简单模式任务列表视图)
|
||||
*/
|
||||
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvSimpleLon; // 经度显示
|
||||
TextView tvSimpleLat; // 纬度显示
|
||||
TextView tvSimpleMemo; // 备注显示
|
||||
TextView tvSimpleDistance; // 距离显示
|
||||
PositionTaskListView ptlvSimpleTasks; // 简单模式任务列表视图
|
||||
public SimpleViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// 绑定布局控件(与XML中ID严格一致,避免运行时空指针)
|
||||
tvSimpleLon = (TextView) itemView.findViewById(R.id.tv_simple_longitude);
|
||||
tvSimpleLat = (TextView) itemView.findViewById(R.id.tv_simple_latitude);
|
||||
tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo);
|
||||
tvSimpleDistance = (TextView) itemView.findViewById(R.id.tv_simple_distance);
|
||||
ptlvSimpleTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_simple_tasks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- 编辑模式ViewHolder(对应 item_position_edit.xml,含编辑模式任务列表视图)
|
||||
*/
|
||||
public static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvEditLon; // 经度显示(不可编辑)
|
||||
TextView tvEditLat; // 纬度显示(不可编辑)
|
||||
EditText etEditMemo; // 备注编辑
|
||||
TextView tvEditDistance; // 距离显示
|
||||
RadioGroup rgDistanceSwitch; // 距离启用/禁用开关
|
||||
Button btnCancel; // 取消编辑按钮
|
||||
Button btnDelete; // 删除位置按钮
|
||||
Button btnSave; // 保存位置按钮
|
||||
Button btnAddTask; // 新增任务按钮
|
||||
TextView tvTaskCount; // 任务数量显示(兼容旧布局,可隐藏)
|
||||
PositionTaskListView ptlvEditTasks; // 编辑模式任务列表视图
|
||||
public EditViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// 绑定布局控件(与XML中ID严格一致,避免运行时空指针)
|
||||
tvEditLon = (TextView) itemView.findViewById(R.id.tv_edit_longitude);
|
||||
tvEditLat = (TextView) itemView.findViewById(R.id.tv_edit_latitude);
|
||||
etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo);
|
||||
tvEditDistance = (TextView) itemView.findViewById(R.id.tv_edit_distance);
|
||||
rgDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_distance_switch);
|
||||
btnCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel);
|
||||
btnDelete = (Button) itemView.findViewById(R.id.btn_edit_delete);
|
||||
btnSave = (Button) itemView.findViewById(R.id.btn_edit_save);
|
||||
btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task);
|
||||
tvTaskCount = (TextView) itemView.findViewById(R.id.tv_task_count);
|
||||
ptlvEditTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_edit_tasks);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 补充:PositionTaskListView 必要方法适配(确保视图类已实现以下基础方法)
|
||||
// (若视图类未实现,需在 PositionTaskListView 中添加对应逻辑,否则复用后功能异常)
|
||||
// =========================================================================
|
||||
public static class PositionTaskListViewRequiredMethods {
|
||||
/**
|
||||
1. - init 方法:初始化任务视图(绑定服务+位置ID——复用后重新绑定,确保服务引用有效)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public void init(MainService mainService, String positionId) {
|
||||
- this.mMainServiceRef = new WeakReference(mainService); // 弱引用服务,防泄漏
|
||||
- this.mPositionId = positionId; // 绑定当前位置ID(复用后更新为当前位置ID)
|
||||
- }
|
||||
*/
|
||||
|
||||
/**
|
||||
2. - setViewStatus 方法:设置视图模式(简单/编辑——复用后恢复对应模式UI)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public static final int VIEW_MODE_SIMPLE = 0; // 仅显示(隐藏编辑/删除按钮)
|
||||
- public static final int VIEW_MODE_EDIT = 1; // 可编辑(显示编辑/删除按钮)
|
||||
- private int mViewMode;
|
||||
- public void setViewStatus(int viewMode) {
|
||||
- this.mViewMode = viewMode;
|
||||
- // 根据模式控制按钮显示:简单模式隐藏编辑按钮,编辑模式显示
|
||||
- if (mEditBtn != null) mEditBtn.setVisibility(viewMode == VIEW_MODE_EDIT ? View.VISIBLE : View.GONE);
|
||||
- if (mDeleteBtn != null) mDeleteBtn.setVisibility(viewMode == VIEW_MODE_EDIT ? View.VISIBLE : View.GONE);
|
||||
- }
|
||||
*/
|
||||
|
||||
/**
|
||||
3. - syncTasksFromMainService 方法:从服务同步任务数据(复用后核心方法,避免旧数据)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public void syncTasksFromMainService() {
|
||||
- MainService mainService = mMainServiceRef.get();
|
||||
- if (mainService == null || TextUtils.isEmpty(mPositionId)) return;
|
||||
- // 根据视图模式拉取对应任务:简单模式仅拉已启用,编辑模式拉全部
|
||||
- ArrayList tasks = (mViewMode == VIEW_MODE_SIMPLE)
|
||||
- ? mainService.getEnabledTasksByPositionId(mPositionId)
|
||||
- : mainService.getTasksByPositionId(mPositionId);
|
||||
- // 更新列表数据(刷新UI,确保复用后显示最新任务)
|
||||
- if (mTaskAdapter != null) {
|
||||
- mTaskAdapter.setTaskList(tasks);
|
||||
- mTaskAdapter.notifyDataSetChanged();
|
||||
- }
|
||||
- // 通知外部任务更新(如Adapter需要联动)
|
||||
- if (mTaskUpdateListener != null) mTaskUpdateListener.onTaskUpdated(mPositionId, tasks);
|
||||
- }
|
||||
*/
|
||||
|
||||
/**
|
||||
4. - addNewTask 方法:新增任务(同步服务+刷新列表——复用后功能正常)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public void addNewTask(PositionTaskModel newTask) {
|
||||
- if (newTask == null || TextUtils.isEmpty(newTask.getPositionId())) return;
|
||||
- MainService mainService = mMainServiceRef.get();
|
||||
- if (mainService != null) {
|
||||
- mainService.addTask(newTask); // 同步到服务(确保数据持久化)
|
||||
- syncTasksFromMainService(); // 新增后立即同步,刷新列表
|
||||
- Toast.makeText(getContext(), "任务新增成功", Toast.LENGTH_SHORT).show();
|
||||
- } else {
|
||||
- Toast.makeText(getContext(), "服务未就绪,新增失败", Toast.LENGTH_SHORT).show();
|
||||
- }
|
||||
- }
|
||||
*/
|
||||
|
||||
/**
|
||||
5. - clearData 方法:清空任务数据(仅在Activity销毁时调用,不复用场景)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public void clearData() {
|
||||
- if (mTaskList != null) mTaskList.clear(); // 清空本地任务列表
|
||||
- if (mTaskAdapter != null) mTaskAdapter.notifyDataSetChanged(); // 刷新空UI
|
||||
- }
|
||||
*/
|
||||
|
||||
/**
|
||||
6. - setOnTaskUpdatedListener 方法:设置任务更新回调(复用后重新绑定,避免回调失效)
|
||||
- 需在 PositionTaskListView 中添加:
|
||||
- public interface OnTaskUpdatedListener {
|
||||
- void onTaskUpdated(String positionId, ArrayList updatedTasks);
|
||||
- }
|
||||
- private OnTaskUpdatedListener mTaskUpdateListener;
|
||||
- public void setOnTaskUpdatedListener(OnTaskUpdatedListener listener) {
|
||||
- this.mTaskUpdateListener = listener;
|
||||
- }
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/01 04:50
|
||||
* @Describe AppConfigsModel
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import android.util.JsonWriter;
|
||||
import android.util.JsonReader;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AppConfigsModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "AppConfigsModel";
|
||||
|
||||
boolean isEnableMainService;
|
||||
|
||||
public AppConfigsModel(boolean isEnableMainService) {
|
||||
this.isEnableMainService = isEnableMainService;
|
||||
}
|
||||
|
||||
public AppConfigsModel() {
|
||||
this.isEnableMainService = false;
|
||||
}
|
||||
|
||||
public void setIsEnableMainService(boolean isEnableMainService) {
|
||||
this.isEnableMainService = isEnableMainService;
|
||||
}
|
||||
|
||||
public boolean isEnableMainService() {
|
||||
return isEnableMainService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigsModel.class.getName();
|
||||
}
|
||||
|
||||
// JSON序列化(保存位置数据)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载位置数据,校验字段)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("isEnableDistanceRefreshService")) {
|
||||
setIsEnableMainService(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取位置数据
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:57
|
||||
* @Describe 位置数据模型
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PositionModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PositionModel";
|
||||
// 位置唯一标识符(与任务的positionId绑定)
|
||||
String positionId;
|
||||
// 经度(范围:-180~180)
|
||||
double longitude;
|
||||
// 纬度(范围:-90~90)
|
||||
double latitude;
|
||||
// 位置备注(空值时显示“无备注”)
|
||||
String memo;
|
||||
// 定位点与指定点实时距离长度
|
||||
double realPositionDistance;
|
||||
// 是否启用实时距离计算
|
||||
boolean isEnableRealPositionDistance;
|
||||
// 是否显示简单视图(true=简单视图,false=编辑视图)
|
||||
boolean isSimpleView = true;
|
||||
|
||||
// 带参构造(强制初始化位置ID和经纬度)
|
||||
public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||
this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制
|
||||
this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制
|
||||
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
// 无参构造(默认值初始化,避免空指针)
|
||||
public PositionModel() {
|
||||
this.positionId = genPositionId();
|
||||
this.longitude = 0.0;
|
||||
this.latitude = 0.0;
|
||||
this.memo = "无备注";
|
||||
this.isEnableRealPositionDistance = false;
|
||||
}
|
||||
|
||||
public void setRealPositionDistance(double realPositionDistance) {
|
||||
this.realPositionDistance = realPositionDistance;
|
||||
}
|
||||
|
||||
public double getRealPositionDistance() {
|
||||
return realPositionDistance;
|
||||
}
|
||||
|
||||
// ---------------------- Getter/Setter(确保字段有效性) ----------------------
|
||||
public void setPositionId(String positionId) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||
}
|
||||
|
||||
public String getPositionId() {
|
||||
return positionId;
|
||||
}
|
||||
|
||||
public void setIsEnableRealPositionDistance(boolean isEnableRealPositionDistance) {
|
||||
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
public boolean isEnableRealPositionDistance() {
|
||||
return isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setMemo(String memo) {
|
||||
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||
}
|
||||
|
||||
public String getMemo() {
|
||||
return memo;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
// ---------------------- 父类方法重写 ----------------------
|
||||
@Override
|
||||
public String getName() {
|
||||
return PositionModel.class.getName();
|
||||
}
|
||||
|
||||
// 生成唯一位置ID(与任务ID格式一致,确保关联匹配)
|
||||
public static String genPositionId() {
|
||||
UUID uniqueUuid = UUID.randomUUID();
|
||||
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一)
|
||||
}
|
||||
|
||||
// JSON序列化(保存位置数据)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("positionId").value(getPositionId());
|
||||
jsonWriter.name("longitude").value(getLongitude());
|
||||
jsonWriter.name("latitude").value(getLatitude());
|
||||
jsonWriter.name("memo").value(getMemo());
|
||||
jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载位置数据,校验字段)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("positionId")) {
|
||||
setPositionId(jsonReader.nextString());
|
||||
} else if (name.equals("longitude")) {
|
||||
setLongitude(jsonReader.nextDouble());
|
||||
} else if (name.equals("latitude")) {
|
||||
setLatitude(jsonReader.nextDouble());
|
||||
} else if (name.equals("memo")) {
|
||||
setMemo(jsonReader.nextString());
|
||||
} else if (name.equals("isEnableRealPositionDistance")) {
|
||||
setIsEnableRealPositionDistance(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取位置数据
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
// ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ----------------------
|
||||
/**
|
||||
* 计算两个位置之间的直线距离(地球表面最短距离)
|
||||
* @param position1 第一个位置(非null)
|
||||
* @param position2 第二个位置(非null)
|
||||
* @param isKilometer 是否返回千米单位:true→千米,false→米
|
||||
* @return 距离(保留1位小数,符合显示需求)
|
||||
* @throws IllegalArgumentException 位置为null或经纬度无效时抛出
|
||||
*/
|
||||
public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) {
|
||||
// 1. 校验参数有效性(避免计算异常)
|
||||
if (position1 == null || position2 == null) {
|
||||
throw new IllegalArgumentException("位置对象不能为null");
|
||||
}
|
||||
double lon1 = position1.getLongitude();
|
||||
double lat1 = position1.getLatitude();
|
||||
double lon2 = position2.getLongitude();
|
||||
double lat2 = position2.getLatitude();
|
||||
// 经纬度范围二次校验(确保有效)
|
||||
if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90
|
||||
|| lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {
|
||||
throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180)");
|
||||
}
|
||||
|
||||
// 2. Haversine公式计算(地球半径取6371km,行业标准)
|
||||
final double EARTH_RADIUS_KM = 6371;
|
||||
double radLat1 = Math.toRadians(lat1); // 角度转弧度
|
||||
double radLat2 = Math.toRadians(lat2);
|
||||
double radLon1 = Math.toRadians(lon1);
|
||||
double radLon2 = Math.toRadians(lon2);
|
||||
|
||||
double deltaLat = radLat2 - radLat1; // 纬度差
|
||||
double deltaLon = radLon2 - radLon1; // 经度差
|
||||
|
||||
// 核心公式
|
||||
double a = Math.pow(Math.sin(deltaLat / 2), 2)
|
||||
+ Math.cos(radLat1) * Math.cos(radLat2)
|
||||
* Math.pow(Math.sin(deltaLon / 2), 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米)
|
||||
|
||||
// 3. 单位转换+精度处理(保留1位小数,符合显示需求)
|
||||
double distance;
|
||||
if (isKilometer) {
|
||||
distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数
|
||||
} else {
|
||||
distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/30 02:48
|
||||
* @Describe 位置任务数据模型
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PositionTaskModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PositionTaskModel";
|
||||
// 任务标识符(唯一)
|
||||
String taskId;
|
||||
// 绑定的位置标识符(与PositionModel的positionId一一对应)
|
||||
String positionId;
|
||||
// 任务描述
|
||||
String taskDescription;
|
||||
// 任务距离条件:是否大于设定距离
|
||||
boolean isGreaterThan;
|
||||
// 任务距离条件:是否小于设定距离(与isGreaterThan互斥)
|
||||
boolean isLessThan;
|
||||
// 任务条件距离(单位:米)
|
||||
int discussDistance;
|
||||
// 任务开始启用时间
|
||||
long startTime;
|
||||
// 任务是否已触发
|
||||
boolean isBingo = false;
|
||||
// 是否启用任务
|
||||
boolean isEnable;
|
||||
|
||||
// 带参构造(强制传入positionId,确保任务与位置绑定)
|
||||
public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, long startTime, boolean isEnable) {
|
||||
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成
|
||||
this.positionId = positionId; // 强制绑定位置ID
|
||||
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||
this.isGreaterThan = isGreaterThan;
|
||||
this.isLessThan = !isGreaterThan; // 确保互斥
|
||||
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值
|
||||
this.startTime = startTime;
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
// 无参构造(初始化默认值,positionId需后续设置)
|
||||
public PositionTaskModel() {
|
||||
this.taskId = genTaskId();
|
||||
this.positionId = "";
|
||||
this.taskDescription = "新任务";
|
||||
this.isGreaterThan = true;
|
||||
this.isLessThan = false; // 初始互斥
|
||||
this.discussDistance = 100; // 默认100米
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.isEnable = true;
|
||||
}
|
||||
|
||||
public void setStartTime(long startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setIsBingo(boolean isBingo) {
|
||||
this.isBingo = isBingo;
|
||||
}
|
||||
|
||||
public boolean isBingo() {
|
||||
return isBingo;
|
||||
}
|
||||
|
||||
// ---------------------- Getter/Setter(确保positionId不为空,距离有效) ----------------------
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setPositionId(String positionId) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护
|
||||
}
|
||||
|
||||
public String getPositionId() {
|
||||
return positionId;
|
||||
}
|
||||
|
||||
public void setTaskDescription(String taskDescription) {
|
||||
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||
}
|
||||
|
||||
public String getTaskDescription() {
|
||||
return taskDescription;
|
||||
}
|
||||
|
||||
// 修复:确保isGreaterThan和isLessThan互斥
|
||||
public void setIsGreaterThan(boolean isGreaterThan) {
|
||||
this.isGreaterThan = isGreaterThan;
|
||||
this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于
|
||||
}
|
||||
|
||||
public boolean isGreaterThan() {
|
||||
return isGreaterThan;
|
||||
}
|
||||
|
||||
// 修复:确保isLessThan和isGreaterThan互斥
|
||||
public void setIsLessThan(boolean isLessThan) {
|
||||
this.isLessThan = isLessThan;
|
||||
this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于
|
||||
}
|
||||
|
||||
public boolean isLessThan() {
|
||||
return isLessThan;
|
||||
}
|
||||
|
||||
public void setDiscussDistance(int discussDistance) {
|
||||
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数
|
||||
}
|
||||
|
||||
public int getDiscussDistance() {
|
||||
return discussDistance;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
// ---------------------- 父类方法重写 ----------------------
|
||||
@Override
|
||||
public String getName() {
|
||||
return PositionTaskModel.class.getName();
|
||||
}
|
||||
|
||||
// 生成唯一任务ID(与PositionModel保持一致格式)
|
||||
public static String genTaskId() {
|
||||
UUID uniqueUuid = UUID.randomUUID();
|
||||
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一)
|
||||
}
|
||||
|
||||
// JSON序列化(保存任务数据,包含所有字段)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("taskId").value(getTaskId());
|
||||
jsonWriter.name("positionId").value(getPositionId());
|
||||
jsonWriter.name("taskDescription").value(getTaskDescription());
|
||||
jsonWriter.name("isGreaterThan").value(isGreaterThan());
|
||||
jsonWriter.name("isLessThan").value(isLessThan());
|
||||
jsonWriter.name("discussDistance").value(getDiscussDistance());
|
||||
jsonWriter.name("startTime").value(getStartTime());
|
||||
jsonWriter.name("isEnable").value(isEnable());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载任务数据,校验字段有效性)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("taskId")) {
|
||||
setTaskId(jsonReader.nextString());
|
||||
} else if (name.equals("positionId")) {
|
||||
setPositionId(jsonReader.nextString());
|
||||
} else if (name.equals("taskDescription")) {
|
||||
setTaskDescription(jsonReader.nextString());
|
||||
} else if (name.equals("isGreaterThan")) {
|
||||
setIsGreaterThan(jsonReader.nextBoolean());
|
||||
} else if (name.equals("isLessThan")) {
|
||||
setIsLessThan(jsonReader.nextBoolean());
|
||||
} else if (name.equals("discussDistance")) {
|
||||
setDiscussDistance(jsonReader.nextInt());
|
||||
} else if (name.equals("startTime")) {
|
||||
setStartTime(jsonReader.nextLong());
|
||||
} else if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取任务数据(确保反序列化完整)
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段,避免崩溃
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package cc.winboll.studio.positions.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/19 14:30:57
|
||||
* @Describe 应用主要服务组件类守护进程服务组件类
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||
|
||||
public class AssistantService extends Service {
|
||||
|
||||
public final static String TAG = "AssistantService";
|
||||
public static final String EXTRA_IS_SETTING_TO_ENABLE = "EXTRA_IS_SETTING_TO_ENABLE";
|
||||
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
volatile boolean mIsServiceRunning;
|
||||
AppConfigsUtil mAppConfigsUtil;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mAppConfigsUtil = AppConfigsUtil.getInstance(this);
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
mIsServiceRunning = false;
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
run();
|
||||
}
|
||||
|
||||
return mAppConfigsUtil.isEnableMainService(true) ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mIsServiceRunning = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
//
|
||||
// 运行服务内容
|
||||
//
|
||||
void run() {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
if (mIsServiceRunning == false) {
|
||||
// 设置运行状态
|
||||
mIsServiceRunning = true;
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) {
|
||||
startForegroundService(new Intent(AssistantService.this, MainService.class));
|
||||
}
|
||||
|
||||
bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
//
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
//package cc.winboll.studio.positions.services;
|
||||
//
|
||||
///**
|
||||
// * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
// * @Date 2025/09/30 19:53
|
||||
// * @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)+ GPS信号加载
|
||||
// */
|
||||
//import android.app.Service;
|
||||
//import android.content.Context;
|
||||
//import android.content.Intent;
|
||||
//import android.content.pm.PackageManager;
|
||||
//import android.location.Location;
|
||||
//import android.location.LocationListener;
|
||||
//import android.location.LocationManager;
|
||||
//import android.os.Binder;
|
||||
//import android.os.Build;
|
||||
//import android.os.Bundle;
|
||||
//import android.os.IBinder;
|
||||
//import android.os.Looper;
|
||||
//import android.widget.Toast;
|
||||
//import cc.winboll.studio.libappbase.LogUtils;
|
||||
//import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
//import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||
//import cc.winboll.studio.positions.models.PositionModel;
|
||||
//import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
//import cc.winboll.studio.positions.utils.NotificationUtil;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Iterator;
|
||||
//import java.util.Set;
|
||||
//import java.util.concurrent.Executors;
|
||||
//import java.util.concurrent.ScheduledExecutorService;
|
||||
//import java.util.concurrent.TimeUnit;
|
||||
//
|
||||
///**
|
||||
// * 核心职责:
|
||||
// * 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
|
||||
// * 2. 单例式管理位置/任务数据,提供安全增删改查接口
|
||||
// * 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
|
||||
// * 4. 内置GPS信号加载(通过LocationManager实时获取位置,解决“等待GPS信号”问题)
|
||||
// * 5. 服务启动时启动前台通知(保活后台GPS功能,符合系统规范)
|
||||
// * 6. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
|
||||
// */
|
||||
//public class DistanceRefreshService extends Service {
|
||||
// public static final String TAG = "DistanceRefreshService";
|
||||
//
|
||||
//
|
||||
// // 服务状态与配置
|
||||
// private boolean isServiceRunning = false;
|
||||
//
|
||||
// private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
|
||||
// // 前台通知相关:记录是否已启动前台服务(避免重复调用startForeground)
|
||||
// private boolean isForegroundServiceStarted = false;
|
||||
//
|
||||
//
|
||||
//
|
||||
// // 服务绑定与UI回调
|
||||
// private final IBinder mBinder = new DistanceBinder();
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 在主线程显示Toast(避免子线程无法显示Toast的问题)
|
||||
// */
|
||||
// private void showToastOnMainThread(final String message) {
|
||||
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
// } else {
|
||||
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// Toast.makeText(DistanceRefreshService.this, message, Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
|
||||
// public class DistanceBinder extends Binder {
|
||||
// /**
|
||||
// * 外部绑定后获取服务实例(安全暴露服务引用)
|
||||
// */
|
||||
// public DistanceRefreshService getService() {
|
||||
// return DistanceRefreshService.this;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// @Override
|
||||
// public void onCreate() {
|
||||
// super.onCreate();
|
||||
//
|
||||
// // 初始化GPS管理器(提前获取系统服务,避免启动时延迟)
|
||||
// mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
// LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
|
||||
// run();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// run();
|
||||
// AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
|
||||
// boolean isEnableService = (bean == null) ? false : bean.isEnableMainService();
|
||||
// // 服务启用时返回START_STICKY(被杀死后尝试重启),禁用时返回默认值
|
||||
// return isEnableService ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
// }
|
||||
//
|
||||
// public void run() {
|
||||
// // 仅服务未运行时启动(避免重复启动)
|
||||
// if (!isServiceRunning) {
|
||||
// isServiceRunning = true;
|
||||
//
|
||||
//
|
||||
//
|
||||
// startDistanceRefreshTask(); // 启动定时距离计算
|
||||
// startForegroundNotification(); // 启动前台通知
|
||||
//
|
||||
// LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒,前台通知+GPS已启动");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动(前台通知:" + (isForegroundServiceStarted ? "已启动" : "未启动") + " | GPS:" + (isGpsEnabled ? "已开启" : "未开启") + ")");
|
||||
// // 异常场景恢复:补全未启动的组件
|
||||
// if (!isForegroundServiceStarted) {
|
||||
// startForegroundNotification();
|
||||
// LogUtils.d(TAG, "服务 run:前台通知未启动,已恢复");
|
||||
// }
|
||||
// if (isServiceRunning && !isGpsEnabled) {
|
||||
// startGpsLocation();
|
||||
// LogUtils.d(TAG, "服务 run:GPS未启动,已恢复");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public IBinder onBind(Intent intent) {
|
||||
// return null; // 按你的业务逻辑返回,无绑定需求则保留null
|
||||
// //LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + " | GPS状态:" + (isGpsEnabled ? "可用" : "不可用") + ")");
|
||||
// //return mBinder; // 返回Binder实例,供外部获取服务
|
||||
// }
|
||||
//
|
||||
// /*@Override
|
||||
// public boolean onUnbind(Intent intent) {
|
||||
// LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
|
||||
// // 解绑后清理资源,避免内存泄漏
|
||||
// mDistanceReceiver = null;
|
||||
// mVisiblePositionIds.clear();
|
||||
// // 解绑时不停止GPS(服务仍在后台运行,需持续获取位置)
|
||||
// return super.onUnbind(intent);
|
||||
// }*/
|
||||
//
|
||||
// @Override
|
||||
// public void onDestroy() {
|
||||
// super.onDestroy();
|
||||
//
|
||||
// LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放(GPS+前台通知+线程池)");
|
||||
// }
|
||||
//
|
||||
// // ---------------------- 前台服务通知管理(与GPS状态联动优化) ----------------------
|
||||
// /**
|
||||
// * 启动前台服务通知(调用NotificationUtils创建通知,确保仅启动一次)
|
||||
// */
|
||||
// private void startForegroundNotification() {
|
||||
// // 1. 校验:避免重复调用startForeground(系统不允许重复启动)
|
||||
// if (isForegroundServiceStarted) {
|
||||
// LogUtils.w(TAG, "startForegroundNotification:前台通知已启动,无需重复执行");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
//// 2. 初始化通知状态文本(根据GPS初始状态动态显示,避免固定“等待”)
|
||||
// String initialStatus;
|
||||
// if (isGpsPermissionGranted && isGpsEnabled) {
|
||||
// initialStatus = "GPS已就绪,正在获取位置(刷新间隔" + REFRESH_INTERVAL + "秒)";
|
||||
// } else if (!isGpsPermissionGranted) {
|
||||
// initialStatus = "缺少定位权限,无法获取GPS位置";
|
||||
// } else {
|
||||
// initialStatus = "GPS未开启,请在设置中打开";
|
||||
// }
|
||||
//
|
||||
//
|
||||
//// 5. 标记前台服务已启动
|
||||
// isForegroundServiceStarted = true;
|
||||
// LogUtils.d(TAG, "startForegroundNotification:前台服务通知启动成功,初始状态:" + initialStatus);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
//// 捕获异常(如上下文失效、通知渠道未创建)
|
||||
// isForegroundServiceStarted = false;
|
||||
// LogUtils.d(TAG, "startForegroundNotification:前台通知启动失败" + e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 主线程回调Adapter更新UI(避免跨线程操作UI异常)
|
||||
// */
|
||||
// /*private void notifyDistanceUpdateToUI(final String positionId) {
|
||||
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// if (mDistanceReceiver != null) {
|
||||
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
// }
|
||||
// } else {
|
||||
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// if (mDistanceReceiver != null) {
|
||||
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }*/
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
|
||||
//
|
||||
// public ArrayList getPositionList() {
|
||||
// if (!isServiceRunning) {
|
||||
// LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
|
||||
// return new ArrayList();
|
||||
// }
|
||||
// return new ArrayList(mPositionList);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public ArrayList getPositionTasksList() {
|
||||
// if (!isServiceRunning) {
|
||||
// LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
|
||||
// return new ArrayList();
|
||||
// }
|
||||
// return new ArrayList(mTaskList);
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// /*public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
|
||||
// this.mDistanceReceiver = receiver;
|
||||
// LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + ")");
|
||||
// }*/
|
||||
//
|
||||
// public void addVisibleDistanceView(String positionId) {
|
||||
// if (!isServiceRunning || positionId == null) {
|
||||
// LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
|
||||
// return;
|
||||
// }
|
||||
// if (mVisiblePositionIds.add(positionId)) {
|
||||
// LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
|
||||
//// 新增:添加可见位置后,立即更新通知(显示最新可见数量)
|
||||
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||
// syncGpsStatusToNotification();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void removeVisibleDistanceView(String positionId) {
|
||||
// if (positionId == null) {
|
||||
// LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
|
||||
// return;
|
||||
// }
|
||||
// if (mVisiblePositionIds.remove(positionId)) {
|
||||
// int remainingCount = mVisiblePositionIds.size();
|
||||
// LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + remainingCount + ")");
|
||||
//// 新增:移除可见位置后,更新通知(同步数量变化)
|
||||
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||
// syncGpsStatusToNotification();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void clearVisibleDistanceViews() {
|
||||
// mVisiblePositionIds.clear();
|
||||
// LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
|
||||
//// 新增:清空可见位置后,更新通知(提示计算暂停)
|
||||
// if (isForegroundServiceStarted) {
|
||||
// updateNotificationGpsStatus("无可见位置,距离计算暂停");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//// ---------------------- 数据管理接口(修复原有语法错误+优化逻辑) ----------------------
|
||||
// /**
|
||||
//
|
||||
// - 获取服务运行状态
|
||||
// */
|
||||
// public boolean isServiceRunning() {
|
||||
// return isServiceRunning;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 添加位置(修复迭代器泛型缺失问题)
|
||||
// */
|
||||
// public void addPosition(PositionModel position) {
|
||||
// if (!isServiceRunning || position == null || position.getPositionId() == null) {
|
||||
// LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
|
||||
// return;
|
||||
// }// 修复:显式声明PositionModel泛型,避免类型转换警告
|
||||
// boolean isDuplicate = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel existingPos = (PositionModel)posIter.next();
|
||||
// if (position.getPositionId().equals(existingPos.getPositionId())) {
|
||||
// isDuplicate = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (!isDuplicate) {
|
||||
// mPositionList.add(position);
|
||||
// LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 删除位置(修复任务删除时的类型转换错误)
|
||||
// */
|
||||
// public void removePosition(String positionId) {
|
||||
// if (!isServiceRunning || positionId == null) {
|
||||
// LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
|
||||
// return;
|
||||
// }// 1. 删除位置
|
||||
// boolean isRemoved = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel pos = (PositionModel)posIter.next();
|
||||
// if (positionId.equals(pos.getPositionId())) {
|
||||
// posIter.remove();
|
||||
// isRemoved = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (isRemoved) {
|
||||
//// 修复:任务列表迭代时用PositionTaskModel泛型(原错误用PositionModel导致转换失败)
|
||||
// Iterator taskIter = mTaskList.iterator();
|
||||
// while (taskIter.hasNext()) {
|
||||
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||
// if (positionId.equals(task.getPositionId())) {
|
||||
// taskIter.remove();
|
||||
// }
|
||||
// }// 3. 移除可见位置
|
||||
// mVisiblePositionIds.remove(positionId);
|
||||
// LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ",剩余任务数=" + mTaskList.size() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 更新位置信息(修复代码格式+迭代器泛型)
|
||||
// */
|
||||
// public void updatePosition(PositionModel updatedPosition) {
|
||||
// if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
|
||||
// LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
|
||||
// return;
|
||||
// }boolean isUpdated = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel pos = (PositionModel)posIter.next();
|
||||
// if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
|
||||
// pos.setMemo(updatedPosition.getMemo());
|
||||
// pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
|
||||
// if (!updatedPosition.isEnableRealPositionDistance()) {
|
||||
// pos.setRealPositionDistance(-1);
|
||||
// //notifyDistanceUpdateToUI(pos.getPositionId());
|
||||
// }
|
||||
// isUpdated = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (isUpdated) {
|
||||
// LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 同步任务列表(修复泛型缺失+代码格式)
|
||||
// */
|
||||
// public void syncAllPositionTasks(ArrayList tasks) {
|
||||
// if (!isServiceRunning || tasks == null) {
|
||||
// LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
|
||||
// return;
|
||||
// }// 1. 清空旧任务
|
||||
// mTaskList.clear();
|
||||
//// 2. 添加新任务(修复泛型+去重逻辑)
|
||||
// Set taskIdSet = new HashSet();
|
||||
// Iterator taskIter = tasks.iterator();
|
||||
// while (taskIter.hasNext()) {
|
||||
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||
// if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
|
||||
// taskIdSet.add(task.getTaskId());
|
||||
// mTaskList.add(task);
|
||||
// }
|
||||
// }LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + ")");
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// ---------------------- 补充:修复LocationProvider引用缺失问题(避免编译错误) ----------------------
|
||||
//// 注:原代码中onStatusChanged使用LocationProvider枚举,需补充静态导入或显式声明
|
||||
//// 此处通过内部静态类定义,解决系统API引用问题(兼容Java 7语法)
|
||||
// private static class LocationProvider {
|
||||
// public static final int AVAILABLE = 2;
|
||||
// public static final int OUT_OF_SERVICE = 0;
|
||||
// public static final int TEMPORARILY_UNAVAILABLE = 1;
|
||||
// }
|
||||
//
|
||||
//// ---------------------- 补充:Context引用工具(避免服务销毁后Context失效) ----------------------
|
||||
// /*private Context getSafeContext() {
|
||||
// // 服务未销毁时返回自身Context,已销毁时返回应用Context(避免内存泄漏)
|
||||
// if (isDestroyed()) {
|
||||
// return getApplicationContext();
|
||||
// }
|
||||
// return this;
|
||||
// }*/
|
||||
//
|
||||
//// 注:isDestroyed()为API 17+方法,若需兼容更低版本,可添加版本判断
|
||||
// /*private boolean isDestroyed() {
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
// return super.isDestroyed();
|
||||
// }
|
||||
// // 低版本通过状态标记间接判断(服务销毁时会置为false)
|
||||
// return !isServiceRunning && !isForegroundServiceStarted;
|
||||
// }*/
|
||||
//}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user