Compare commits
162 Commits
contacts-v
...
powerbell-
| Author | SHA1 | Date | |
|---|---|---|---|
| 16e581802f | |||
| 3048963fa9 | |||
| 34082c49e9 | |||
| eff1822ee5 | |||
| 4e84ff493b | |||
| c2def0bb3b | |||
| a086a47b2d | |||
| c38b392e39 | |||
| 0f736c2007 | |||
| 26b86cd715 | |||
| 7888696e65 | |||
| 8609c2f784 | |||
| 863b743330 | |||
| 61b7afa4b5 | |||
| 8e4c7a6832 | |||
| d29068b029 | |||
| 51963f8e0f | |||
| 18a5762c15 | |||
| 60de16ab45 | |||
| 6d60d71991 | |||
| 0cfbc43acb | |||
| 20227e29ef | |||
| 2b7007f478 | |||
| 212b8185c8 | |||
| 2ef09e020e | |||
| 6de9b7379b | |||
| 7fba2c8812 | |||
| 20e3a5f974 | |||
| c8333e1e81 | |||
| 6beb56efae | |||
| eb51b2c8f4 | |||
| 36bdd059b0 | |||
| bf051dcc9f | |||
| b38a8df462 | |||
| a8dbe43d4b | |||
| 05a8dc5205 | |||
| 280fc1abd6 | |||
| fe2084f5ff | |||
| 2a75aa140e | |||
| 04b597cbe7 | |||
| 35d210c378 | |||
| 79ae6de6fc | |||
| 328f559d2e | |||
| 28d340a772 | |||
| 65acbfcd04 | |||
| 6af2096b30 | |||
| e539922478 | |||
| 1d9a03554c | |||
| 23beabe99b | |||
| 9c1e9dc75b | |||
| 76c1dee625 | |||
| e967ce5511 | |||
| bea77409a5 | |||
| e584e824c0 | |||
| bc6a82af41 | |||
| 9bc71bb3f6 | |||
| b0dee5e98e | |||
| 7d796b5c3f | |||
| d43ba4bff2 | |||
| 796d826331 | |||
| b3f4571b57 | |||
| 5e6de91430 | |||
| d61d1da5d1 | |||
| 5cf47172f4 | |||
| 683dc6791e | |||
| 1e9a6adc88 | |||
| dbcb5259d9 | |||
| 740ab932a4 | |||
| 29b9f3c82b | |||
| 47601ef542 | |||
| 7b17fae798 | |||
| 405b914f02 | |||
| 1c7ceebb78 | |||
| 493b7e433c | |||
| d2ddfedc96 | |||
| bba48a4458 | |||
| b7ae6ce190 | |||
| ba5470ebcb | |||
| 78c7763212 | |||
| d051b1f737 | |||
| d6323bc1ed | |||
| dffcc0f8a0 | |||
| 9426618b59 | |||
| 68d98d4be3 | |||
| 4db458dda8 | |||
| 83a8f5dada | |||
| 8e1d6ba197 | |||
| 70a004d9e3 | |||
| c7f8aea1ce | |||
| 6d4381d78a | |||
| ddcd9a450e | |||
| ca2323f534 | |||
| 851800e39a | |||
| f17624048c | |||
| 724fce895f | |||
| 5ece532dd4 | |||
| 8b20bc84c8 | |||
| 634c71dfd4 | |||
| 947df2e9b4 | |||
| 08a33365b3 | |||
| 7cffe5c0a5 | |||
| 5a0c429131 | |||
| cff26b3d11 | |||
| e59034e48d | |||
| 3d3301064c | |||
| 2d12397f5e | |||
| f09bb17cbc | |||
| 28d8a5679f | |||
| b4d9bdf3b3 | |||
| 111cf01f9a | |||
| e51d46186a | |||
| 8fc6855066 | |||
| 4ceaf1e46a | |||
| e669bbb04b | |||
| 6bf3ebe2fd | |||
| a44f7fe6d4 | |||
| 35a34b5b53 | |||
| d35d0d0291 | |||
| 03212c0554 | |||
| 0c963213df | |||
| 10ddca4f73 | |||
| f240d9c057 | |||
| 2c77bf775b | |||
| 1db7c9bf80 | |||
| fd556fd06f | |||
| 220aa9dbfb | |||
| ecafd2026f | |||
| 6ed9bc0d8e | |||
| bcb5db0a17 | |||
| 6b69e04706 | |||
| a2a61bbf0b | |||
| 9f4211c83e | |||
| 447a786632 | |||
| ff0f239ffc | |||
| 7c59a982fc | |||
| 895cc4630d | |||
| 851a539364 | |||
| d79f2937ba | |||
| c524a21429 | |||
| a148de2ab8 | |||
| de34e823b6 | |||
| fd0833476e | |||
| 361b533b0d | |||
| f277f76468 | |||
| ec54865a8e | |||
| 6b3682994e | |||
| 7145bff552 | |||
| 4c3b60128f | |||
| 29d30f3831 | |||
| c7b0b1bc80 | |||
| 9082b67bbd | |||
| 2b5447e65f | |||
| 6a5c2dbbe8 | |||
| 8cad25bb11 | |||
| e440943992 | |||
| 7c5f8c3cc2 | |||
| 016b3b5e48 | |||
| 489b72b582 | |||
| 918df3dfbe | |||
| 8fd955af73 | |||
| 1db438e231 | |||
| 8cceac1d03 |
@@ -5,11 +5,10 @@
|
||||
## ☁ ☁ ☁ 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 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
|
||||
## WinBoLL 提问
|
||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
||||
能否实现手机编译与电脑编译的源码同步。
|
||||
|
||||
@@ -25,33 +25,30 @@ android {
|
||||
applicationId "cc.winboll.studio.contacts"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 2
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.14"
|
||||
versionName "15.3"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// 米盟
|
||||
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 fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'cc.winboll.studio:libaes:15.9.3'
|
||||
api 'cc.winboll.studio:libapputils:15.8.5'
|
||||
api 'cc.winboll.studio:libappbase:15.9.5'
|
||||
|
||||
// 权限请求框架:https://github.com/getActivity/XXPermissions
|
||||
api 'com.github.getActivity:XXPermissions:18.63'
|
||||
// 下拉控件
|
||||
@@ -68,6 +65,8 @@ dependencies {
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 吐司类库
|
||||
//api 'com.github.getActivity:ToastUtils:10.5'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
|
||||
@@ -85,15 +84,4 @@ dependencies {
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Dec 15 20:54:20 HKT 2025
|
||||
stageCount=2
|
||||
#Mon Nov 03 12:01:02 HKT 2025
|
||||
stageCount=22
|
||||
libraryProject=
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.1
|
||||
baseVersion=15.3
|
||||
publishVersion=15.3.21
|
||||
buildCount=0
|
||||
baseBetaVersion=15.14.2
|
||||
baseBetaVersion=15.3.22
|
||||
|
||||
138
contacts/proguard-rules.pro
vendored
138
contacts/proguard-rules.pro
vendored
@@ -9,135 +9,9 @@
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep class cc.winboll.studio.** { *; }
|
||||
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||
|
||||
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||
-keepclassmembers class * {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# 保留 R 文件(避免资源ID混淆)
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# 保留 native 方法(避免JNI调用失败)
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# 保留注解和泛型(避免反射/序列化异常)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
|
||||
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||
-dontwarn java.lang.invoke.*
|
||||
-dontwarn android.support.v8.renderscript.*
|
||||
-dontwarn java.util.function.**
|
||||
|
||||
# ============================== 第三方框架专项规则 ==============================
|
||||
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-keep class okhttp3.internal.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
# ============================== 必要补充规则 ==============================
|
||||
# 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
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||
<init>();
|
||||
}
|
||||
-dontwarn com.bumptech.glide.**
|
||||
|
||||
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep interface com.google.gson.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||
-keep class com.miui.zeus.** { *; }
|
||||
-keep interface com.miui.zeus.** { *; }
|
||||
# 保留米盟日志字段(便于广告加载失败排查)
|
||||
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||
-keep class androidx.recyclerview.** { *; }
|
||||
-keep interface androidx.recyclerview.** { *; }
|
||||
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||
public *;
|
||||
}
|
||||
|
||||
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||
# XXPermissions 18.63
|
||||
-keep class com.hjq.permissions.** { *; }
|
||||
-keep interface com.hjq.permissions.** { *; }
|
||||
|
||||
# ZXing 二维码(核心解析组件)
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-keep class com.journeyapps.zxing.** { *; }
|
||||
|
||||
# Jsoup HTML解析
|
||||
-keep class org.jsoup.** { *; }
|
||||
|
||||
# Pinyin4j 拼音搜索
|
||||
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||
|
||||
# JSch SSH组件
|
||||
-keep class com.jcraft.jsch.** { *; }
|
||||
|
||||
# AndroidX 基础组件
|
||||
-keep class androidx.appcompat.** { *; }
|
||||
-keep interface androidx.appcompat.** { *; }
|
||||
|
||||
# ============================== 优化与调试配置 ==============================
|
||||
# 优化级别(平衡混淆效果与性能)
|
||||
-optimizationpasses 5
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
|
||||
# 调试辅助(保留行号便于崩溃定位)
|
||||
-verbose
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
@@ -9,41 +9,33 @@
|
||||
<!-- 拨打电话 -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||
|
||||
<!-- 读取手机状态和身份(API 30+ 需细化权限) -->
|
||||
<!-- 读取手机状态和身份 -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
||||
|
||||
<!-- 修改系统设置(移除无效的 protectionLevel 声明,该属性由系统定义) -->
|
||||
<!-- 修改系统设置 -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
|
||||
<!-- 联系人权限(适配 Android 13+ 细分权限) -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.GET_CONTACTS"/>
|
||||
<!-- 重新设置外拨电话的路径 -->
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
|
||||
|
||||
<!-- 悬浮窗权限(需动态申请) -->
|
||||
<!-- 读取联系人 -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
|
||||
<!-- 修改您的通讯录 -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 更改音频设置 -->
|
||||
<!-- 更改您的音频设置 -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
|
||||
<!-- 通话记录权限(适配 Android 13+ 细分权限) -->
|
||||
<!-- 读取通话记录 -->
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
|
||||
|
||||
<!-- 录音权限 -->
|
||||
<!-- 录音 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
|
||||
<!-- 前台服务权限(按业务类型声明) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||
|
||||
<!-- API 30+ 通话筛选服务权限(替代 PROCESS_OUTGOING_CALLS) -->
|
||||
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
@@ -59,8 +51,11 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
@@ -70,6 +65,7 @@
|
||||
android:label="CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -78,92 +74,89 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="tel"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
|
||||
|
||||
<!-- 主服务:仅 dataSync 类型(与代码中 0x00000008 匹配) -->
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
android:name="cc.winboll.studio.contacts.services.MainService"
|
||||
android:exported="true"/>
|
||||
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
<service android:name="cc.winboll.studio.contacts.services.AssistantService"/>
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
android:exported="false">
|
||||
|
||||
<meta-data
|
||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||
android:value="true"/>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.telecom.InCallService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- 通话监听服务:phoneCall 类型(与代码中 0x00000020 匹配) -->
|
||||
<service
|
||||
android:name=".listenphonecall.CallListenerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
android:exported="false">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name=".service.CallShowService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver android:name=".receivers.MainReceiver">
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.APPStatusWidget"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
@@ -172,11 +165,14 @@
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
@@ -187,7 +183,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
android:resource="@xml/studio_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
@@ -198,4 +194,3 @@
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -1,313 +1,59 @@
|
||||
package cc.winboll.studio.contacts;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe Activity 栈管理工具,统一管理应用内 Activity 生命周期
|
||||
* 适配:Java7 + Android API29-30 + 小米机型,优化并发安全与通话场景稳定性
|
||||
*/
|
||||
|
||||
public class ActivityStack {
|
||||
// 常量定义(核心标识+版本兼容常量)
|
||||
public static final String TAG = "ActivityStack";
|
||||
private static final int API_VERSION_O = 26; // Android 8.0 API26(isDestroyed适配用)
|
||||
|
||||
// 单例与核心成员变量(按优先级排序)
|
||||
private static final ActivityStack INSTANCE = new ActivityStack();
|
||||
// 替换为ArrayList+同步锁:解决CopyOnWriteArrayList迭代器不能删除的崩溃,兼顾并发安全
|
||||
private final List<Activity> mActivityList = new ArrayList<Activity>();
|
||||
private final Handler mMainHandler = new Handler(Looper.getMainLooper()); // 复用主线程Handler,避免内存泄漏
|
||||
|
||||
// 单例对外暴露方法
|
||||
private List<Activity> activities = new ArrayList<>();
|
||||
|
||||
public static ActivityStack getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// 私有构造,禁止外部实例化
|
||||
private ActivityStack() {
|
||||
LogUtils.d(TAG, "ActivityStack 初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 栈基础操作(添加/移除) ======================
|
||||
/**
|
||||
* 添加Activity到栈中,避免重复入栈
|
||||
* @param activity 待添加的Activity
|
||||
*/
|
||||
public void addActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "addActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
// 同步锁:解决多线程并发添加冲突(小米机型多线程场景适配)
|
||||
synchronized (mActivityList) {
|
||||
if (!mActivityList.contains(activity)) {
|
||||
mActivityList.add(activity);
|
||||
LogUtils.d(TAG, "addActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
activities.add(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Activity(不销毁,用于正常退出场景)
|
||||
* @param activity 待移除的Activity
|
||||
*/
|
||||
public void removeActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "removeActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.remove(activity)) {
|
||||
LogUtils.d(TAG, "removeActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== Activity状态查询(获取/判断存活) ======================
|
||||
/**
|
||||
* 获取栈顶有效Activity(迭代遍历替代递归,避免栈溢出,适配小米多页面场景)
|
||||
* @return 栈顶有效Activity,无则返回null
|
||||
*/
|
||||
public Activity getTopActivity() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getTopActivity: stack is empty, return null");
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity validTopActivity = null;
|
||||
// 倒序遍历,优先取最顶层有效Activity,同时清理无效残留
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
// 版本兼容校验:API26+才支持isDestroyed
|
||||
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
validTopActivity = activity;
|
||||
break;
|
||||
} else {
|
||||
mActivityList.remove(i);
|
||||
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||
LogUtils.w(TAG, "getTopActivity: remove invalid activity: " + className);
|
||||
}
|
||||
}
|
||||
|
||||
if (validTopActivity != null) {
|
||||
LogUtils.d(TAG, "getTopActivity: top activity: " + validTopActivity.getClass().getSimpleName());
|
||||
}
|
||||
return validTopActivity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类的有效Activity实例(通话场景核心方法,判断页面是否存活)
|
||||
* @param activityClass 目标Activity类
|
||||
* @return 有效实例,无则返回null
|
||||
*/
|
||||
public Activity getActivity(Class<?> activityClass) {
|
||||
if (activityClass == null) {
|
||||
LogUtils.w(TAG, "getActivity: activityClass is null, return null");
|
||||
return null;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getActivity: stack empty, return null");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Activity activity : mActivityList) {
|
||||
if (activity != null && activity.getClass().equals(activityClass) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
LogUtils.d(TAG, "getActivity: find valid activity: " + activityClass.getSimpleName());
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
LogUtils.w(TAG, "getActivity: no valid activity: " + activityClass.getSimpleName());
|
||||
if (activities.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return activities.get(activities.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定Activity是否存活(简化通话场景调用,避免重复判空)
|
||||
* @param activityClass 目标Activity类
|
||||
* @return true:存活,false:未存活
|
||||
*/
|
||||
public boolean isActivityAlive(Class<?> activityClass) {
|
||||
boolean isAlive = getActivity(activityClass) != null;
|
||||
LogUtils.d(TAG, "isActivityAlive: " + activityClass.getSimpleName() + ", result: " + isAlive);
|
||||
return isAlive;
|
||||
}
|
||||
|
||||
// ====================== Activity销毁操作(单/批量/全部) ======================
|
||||
/**
|
||||
* 销毁栈顶Activity(主线程执行,适配小米机型线程限制)
|
||||
*/
|
||||
public void finishTopActivity() {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishTopActivity: stack is empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先移除再校验,避免并发冲突(小米多线程场景适配)
|
||||
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
||||
if (topActivity == null) {
|
||||
LogUtils.w(TAG, "finishTopActivity: top activity is null, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!topActivity.isFinishing() && (getSdkVersion() < API_VERSION_O || !topActivity.isDestroyed())) {
|
||||
topActivity.finish();
|
||||
LogUtils.d(TAG, "finishTopActivity: destroy top activity: " + topActivity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!activities.isEmpty()) {
|
||||
activities.remove(activities.size() - 1).finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指定Activity(主线程执行,避免跨线程异常)
|
||||
* @param activity 待销毁的Activity
|
||||
*/
|
||||
public void finishActivity(final Activity activity) {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "finishActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.contains(activity) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
mActivityList.remove(activity);
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: destroy activity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
public void finishActivity(Activity activity) {
|
||||
if (activity != null) {
|
||||
activities.remove(activity);
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指定类的所有Activity(核心修复:迭代器删除崩溃,通话场景核心)
|
||||
* @param activityClass 目标Activity类
|
||||
*/
|
||||
public void finishActivity(final Class<?> activityClass) {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activityClass == null) {
|
||||
LogUtils.w(TAG, "finishActivity: activityClass is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishActivity: stack empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 核心修复:用索引遍历+倒序删除,替代迭代器删除(避免UnsupportedOperationException)
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
if (activity != null && activity.getClass().equals(activityClass)) {
|
||||
if (!activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
mActivityList.remove(i); // 索引删除,支持ArrayList
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: destroy class activity: " + activityClass.getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
} else {
|
||||
mActivityList.remove(i); // 清理无效残留
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
public void finishActivity(Class activityClass) {
|
||||
for (Activity activity : activities) {
|
||||
if (activity.getClass().equals(activityClass)) {
|
||||
finishActivity(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁栈中所有Activity(退出应用/清空栈场景用)
|
||||
*/
|
||||
public void finishAllActivity() {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishAllActivity: stack is empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历销毁所有有效Activity,逐个状态校验(小米机型稳定性适配)
|
||||
for (Activity activity : mActivityList) {
|
||||
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishAllActivity: destroy activity: " + activity.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
mActivityList.clear();
|
||||
LogUtils.d(TAG, "finishAllActivity: all activity destroyed, stack cleared");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 栈优化与工具方法 ======================
|
||||
/**
|
||||
* 清理栈中所有无效Activity(null/已销毁/已结束),优化小米机型内存占用
|
||||
*/
|
||||
public void clearInvalidActivities() {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 倒序索引删除,避免遍历过程中索引错乱
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
if (activity == null || activity.isFinishing() || (getSdkVersion() >= API_VERSION_O && activity.isDestroyed())) {
|
||||
mActivityList.remove(i);
|
||||
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "clearInvalidActivities: remove invalid activity: " + className);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "clearInvalidActivities: done, stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保任务在主线程执行(Activity操作必须主线程,小米机型严格限制)
|
||||
* @param runnable 待执行任务
|
||||
*/
|
||||
private void runOnMainThread(Runnable runnable) {
|
||||
if (runnable == null) {
|
||||
return;
|
||||
if (!activities.isEmpty()) {
|
||||
for (Activity activity : activities) {
|
||||
activity.finish();
|
||||
activities.remove(activity);
|
||||
}
|
||||
}
|
||||
// 避免不必要的线程切换,优化性能(小米机型流畅度适配)
|
||||
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
mMainHandler.post(runnable);
|
||||
LogUtils.d(TAG, "runOnMainThread: post task to main thread");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:获取当前系统SDK版本(简化版本判断逻辑,统一调用)
|
||||
* @return SDK版本号
|
||||
*/
|
||||
private int getSdkVersion() {
|
||||
return android.os.Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ package cc.winboll.studio.contacts;
|
||||
* @Date 2024/12/08 15:10:51
|
||||
* @Describe 全局应用类
|
||||
*/
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.WinBoLLActivityManager;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
@@ -15,19 +16,22 @@ public class App extends GlobalApplication {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// 必须在调用基类前设置应用调试标志,
|
||||
// 这样可以预先设置日志与数据的存储根目录。
|
||||
//setIsDebuging(BuildConfig.DEBUG);
|
||||
super.onCreate();
|
||||
// 设置应用调试标志
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
// 初始化窗口管理类
|
||||
WinBoLLActivityManager.init(this);
|
||||
// 设置 WinBoLL 应用 UI 类型
|
||||
getWinBoLLActivityManager().setWinBoLLUI_TYPE(WinBoLLActivityManager.WinBoLLUI_TYPE.Aplication);
|
||||
|
||||
//LogUtils.d(TAG, "onCreate");
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.toast_custom_view);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package cc.winboll.studio.contacts;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 主窗口
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -21,79 +25,64 @@ import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||
import cc.winboll.studio.contacts.activities.WinBollActivity;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||
import cc.winboll.studio.contacts.fragments.ContactsFragment;
|
||||
import cc.winboll.studio.contacts.fragments.LogFragment;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.utils.PermissionUtils;
|
||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.contacts.utils.AppGoToSettingsUtil;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe Contacts 主窗口(完全适配 API 30 + Java 7 语法)
|
||||
* 核心优化:1. 移除电话状态监听 2. 移除通话筛选服务 3. 移除 MainService 所有相关逻辑 4. ViewPager 实现 Fragment 懒加载(仅首屏初始化)
|
||||
* 问题修复:解决首屏 Fragment 空白问题(删除 setPrimaryItem 冲突逻辑+延迟首屏初始化)
|
||||
*/
|
||||
public final class MainActivity extends WinBollActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||
|
||||
// ====================== 1. 常量定义区(硬编码API版本,避免高版本依赖) ======================
|
||||
public static final String TAG = "MainActivity";
|
||||
public static final int REQUEST_HOME_ACTIVITY = 0;
|
||||
public static final int REQUEST_ABOUT_ACTIVITY = 1;
|
||||
public static final int REQUEST_APP_SETTINGS = 2;
|
||||
public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS";
|
||||
private static final int DIALER_REQUEST_CODE = 1;
|
||||
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
|
||||
private static final int REQUEST_OVERLAY_PERMISSION = 1003;
|
||||
|
||||
// API版本硬编码常量(Java 7兼容,杜绝Build.VERSION_CODES高版本引用)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
private static final int ANDROID_8_API = 26;
|
||||
private static final int ANDROID_10_API = 29;
|
||||
private static final int ANDROID_14_API = 34;
|
||||
|
||||
// ====================== 2. 静态成员区 ======================
|
||||
static MainActivity _MainActivity;
|
||||
|
||||
// ====================== 3. 权限常量区 ======================
|
||||
private final String[] REQUIRED_PERMISSIONS = PermissionUtils.BASE_PERMISSIONS;
|
||||
|
||||
// ====================== 4. UI控件成员区 ======================
|
||||
private ADsBannerView mADsBannerView;
|
||||
private LogView mLogView;
|
||||
private Toolbar mToolbar;
|
||||
private CheckBox cbMainService;
|
||||
LogView mLogView;
|
||||
Toolbar mToolbar;
|
||||
CheckBox cbMainService;
|
||||
MainServiceBean mMainServiceBean;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private List<View> views;
|
||||
private ImageView[] imageViews;
|
||||
private LinearLayout linearLayout;
|
||||
private List<View> views;
|
||||
ImageView[] imageViews;
|
||||
LinearLayout linearLayout;
|
||||
int currentPoint = 0;
|
||||
|
||||
private TelephonyManager telephonyManager;
|
||||
private MyPhoneStateListener phoneStateListener;
|
||||
List<Fragment> fragmentList;
|
||||
List<String> tabTitleList;
|
||||
|
||||
private static final int DIALER_REQUEST_CODE = 1;
|
||||
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
|
||||
// 关键修改1:新增 READ_CALL_LOG 权限到必需权限列表(解决通话记录读取崩溃)
|
||||
private String[] REQUIRED_PERMISSIONS = new String[]{
|
||||
Manifest.permission.READ_CONTACTS, // 通讯录读取(原)
|
||||
Manifest.permission.CALL_PHONE, // 电话拨号(原)
|
||||
Manifest.permission.READ_CALL_LOG // 通话记录读取(新增,核心修复)
|
||||
};
|
||||
|
||||
// ====================== 5. 业务逻辑成员区 ======================
|
||||
private int currentPoint = 0;
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
// 记录已初始化的Fragment位置(避免重复初始化)
|
||||
private boolean[] isFragmentInit;
|
||||
|
||||
// ====================== 6. 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -104,235 +93,102 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 7. 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "===== onCreate: 主Activity开始创建 =====");
|
||||
_MainActivity = this;
|
||||
|
||||
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
|
||||
initUIAndLogic(savedInstanceState);
|
||||
|
||||
MainServiceBean mainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mainServiceBean != null && mainServiceBean.isEnable()) {
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate: 主Activity创建完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
LogUtils.d(TAG, "onResume: 广告栏资源已恢复");
|
||||
// 优先检查所有必需权限(含新增的 READ_CALL_LOG)
|
||||
if (!checkAllRequiredPermissions()) {
|
||||
requestAllRequiredPermissions();
|
||||
} else {
|
||||
initUIAndLogic(savedInstanceState);
|
||||
}
|
||||
|
||||
//ToastUtils.show("onCreate");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 权限检查方法(无需修改,自动包含新增的 READ_CALL_LOG)
|
||||
private boolean checkAllRequiredPermissions() {
|
||||
for (String permission : REQUIRED_PERMISSIONS) {
|
||||
if (ActivityCompat.checkSelfPermission(this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 主Activity开始销毁 =====");
|
||||
// 释放广告资源
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
LogUtils.d(TAG, "onDestroy: 广告栏资源已释放");
|
||||
}
|
||||
// 清空Fragment相关引用,避免内存泄漏
|
||||
if (fragmentList != null) {
|
||||
fragmentList.clear();
|
||||
fragmentList = null;
|
||||
}
|
||||
if (tabTitleList != null) {
|
||||
tabTitleList.clear();
|
||||
tabTitleList = null;
|
||||
}
|
||||
isFragmentInit = null;
|
||||
LogUtils.d(TAG, "===== onDestroy: 主Activity销毁完成 =====");
|
||||
// 权限申请方法(无需修改,自动申请新增的 READ_CALL_LOG)
|
||||
private void requestAllRequiredPermissions() {
|
||||
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS);
|
||||
}
|
||||
|
||||
// ====================== 8. 权限相关回调函数区 ======================
|
||||
// 权限结果回调(无需修改,确保所有权限(含 READ_CALL_LOG)都通过才加载UI)
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
|
||||
if (requestCode == REQUEST_REQUIRED_PERMISSIONS) {
|
||||
String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions);
|
||||
if (deniedPerms.length() == 0) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功");
|
||||
checkAndRequestRemainingPermissions();
|
||||
boolean allPermissionsGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allPermissionsGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPermissionsGranted) {
|
||||
initUIAndLogic(null);
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 被拒权限:" + deniedPerms);
|
||||
showPermissionDeniedDialogAndExit("应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。");
|
||||
// 关键修改2:更新提示文案,告知用户新增的“通话记录权限”
|
||||
showPermissionDeniedDialogAndExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "onActivityResult: 页面回调触发,requestCode=" + requestCode + ",resultCode=" + resultCode);
|
||||
|
||||
switch (requestCode) {
|
||||
case DIALER_REQUEST_CODE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
LogUtils.d(TAG, "onActivityResult: 设为默认拨号应用成功");
|
||||
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
break;
|
||||
case REQUEST_APP_SETTINGS:
|
||||
LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity");
|
||||
recreate();
|
||||
break;
|
||||
case REQUEST_OVERLAY_PERMISSION:
|
||||
handleOverlayPermissionResult();
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onActivityResult: 未知requestCode=" + requestCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理悬浮窗权限申请结果
|
||||
*/
|
||||
private void handleOverlayPermissionResult() {
|
||||
if (PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||
LogUtils.d(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请成功");
|
||||
LogUtils.d(TAG, "handleOverlayPermissionResult: 所有权限已授予");
|
||||
initUIAndLogic(null);
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请失败");
|
||||
showPermissionDeniedDialogAndExit("应用需要悬浮窗权限才能展示来电弹窗,请授予后重新打开应用。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并申请剩余权限(仅保留悬浮窗)
|
||||
*/
|
||||
private void checkAndRequestRemainingPermissions() {
|
||||
if (!PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 悬浮窗权限未授予,跳转设置页");
|
||||
PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION);
|
||||
} else {
|
||||
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 所有权限已授予");
|
||||
initUIAndLogic(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限拒绝提示对话框(Java 7 匿名内部类实现,禁止Lambda)
|
||||
*/
|
||||
private void showPermissionDeniedDialogAndExit(String tip) {
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 弹出权限不足提示框");
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle("权限不足,无法使用");
|
||||
builder.setMessage(tip);
|
||||
builder.setCancelable(false);
|
||||
|
||||
builder.setNegativeButton("去设置", new DialogInterface.OnClickListener() {
|
||||
// 核心修改:新增“设置权限”按钮,点击调用 AppGoToSettingsUtil 跳转设置页
|
||||
private void showPermissionDeniedDialogAndExit() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("权限不足,无法使用")
|
||||
// 文案修改:明确新增“通话记录读取”权限
|
||||
.setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。")
|
||||
.setCancelable(false)
|
||||
// 新增:左侧“设置权限”按钮(先添加的按钮在左侧)
|
||||
.setNegativeButton("设置权限", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择去设置权限");
|
||||
PermissionUtils.goAppDetailsSettings(MainActivity.this);
|
||||
// 调用工具类跳转应用设置页(按需求实现)
|
||||
AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil();
|
||||
appGoToSettingsUtil.GoToSetting(MainActivity.this);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() {
|
||||
})
|
||||
// 原有:右侧“确定退出”按钮(后添加的按钮在右侧)
|
||||
.setPositiveButton("确定退出", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择退出应用");
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
// ====================== 9. UI与业务逻辑初始化区 ======================
|
||||
// 初始化UI和逻辑(无需修改,权限通过后才加载 CallLogFragment)
|
||||
private void initUIAndLogic(Bundle savedInstanceState) {
|
||||
if (mToolbar != null) {
|
||||
LogUtils.d(TAG, "initUIAndLogic: UI已初始化,无需重复执行");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "===== initUIAndLogic: 开始初始化UI与业务逻辑 =====");
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// 1. 工具栏初始化
|
||||
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||
setSupportActionBar(mToolbar);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 工具栏初始化完成");
|
||||
|
||||
// 2. TabLayout与ViewPager初始化
|
||||
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
|
||||
viewPager = (ViewPager) findViewById(R.id.viewPager);
|
||||
initViewPagerAndTabs();
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
LogUtils.d(TAG, "initUIAndLogic: ViewPager与TabLayout初始化完成");
|
||||
|
||||
// 3. 广告栏初始化
|
||||
mADsBannerView = (ADsBannerView) findViewById(R.id.adsbanner);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 广告栏控件初始化完成");
|
||||
|
||||
// 左边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||
DunTemperatureView tempViewLeft = (DunTemperatureView) findViewById(R.id.dun_temp_view_left);
|
||||
tempViewLeft.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||
tempViewLeft.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||
|
||||
int[] customColors = new int[2];
|
||||
customColors[0] = Color.parseColor("#FF3366FF");
|
||||
customColors[1] = Color.parseColor("#FF9900CC");
|
||||
float[] positions = new float[2];
|
||||
positions[0] = 0.0f;
|
||||
positions[1] = 1.0f;
|
||||
tempViewLeft.setGradientColors(customColors, positions);
|
||||
// 文本放在温度条右侧(默认,可省略)
|
||||
tempViewLeft.setTextPosition(true);
|
||||
// 右边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||
DunTemperatureView tempViewRight = (DunTemperatureView) findViewById(R.id.dun_temp_view_right);
|
||||
tempViewRight.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||
tempViewRight.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||
|
||||
tempViewRight.setGradientColors(customColors, positions);
|
||||
// 文本放在温度条左侧
|
||||
tempViewRight.setTextPosition(false);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 盾值视图初始化完成");
|
||||
LogUtils.d(TAG, "===== initUIAndLogic: 初始化流程全部结束 =====");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ViewPager与Tab数据(Java7规范,泛型完整声明),添加懒加载标记
|
||||
* 关键修改:延迟50ms初始化首屏,确保Fragment控件就绪;删除setPrimaryItem冲突逻辑
|
||||
*/
|
||||
private void initViewPagerAndTabs() {
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: 开始初始化ViewPager数据");
|
||||
fragmentList = new ArrayList<Fragment>();
|
||||
tabTitleList = new ArrayList<String>();
|
||||
|
||||
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
|
||||
// CallLogFragment 仅在权限通过后才实例化(避免提前触发读取)
|
||||
fragmentList.add(CallLogFragment.newInstance(0));
|
||||
fragmentList.add(ContactsFragment.newInstance(1));
|
||||
fragmentList.add(LogFragment.newInstance(2));
|
||||
@@ -340,172 +196,41 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
|
||||
tabTitleList.add("联系人");
|
||||
tabTitleList.add("应用日志");
|
||||
|
||||
// 初始化懒加载标记数组(默认均未初始化)
|
||||
int fragmentCount = fragmentList.size();
|
||||
isFragmentInit = new boolean[fragmentCount];
|
||||
for (int i = 0; i < fragmentCount; i++) {
|
||||
isFragmentInit[i] = false;
|
||||
}
|
||||
|
||||
// 设置自定义适配器(已删除setPrimaryItem,避免初始化冲突)
|
||||
LazyLoadPagerAdapter adapter = new LazyLoadPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||
viewPager.setAdapter(adapter);
|
||||
// 关闭预加载(设为0仅加载当前页,关键)
|
||||
viewPager.setOffscreenPageLimit(0);
|
||||
viewPager.addOnPageChangeListener(this);
|
||||
viewPager.setOffscreenPageLimit(0); // 关闭预加载,避免提前初始化 CallLogFragment
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
// 关键优化:延迟50ms初始化首屏(确保Fragment已完成onCreateView,控件绑定就绪)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initFragmentByPosition(0);
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: 延迟初始化首屏Fragment,位置=0");
|
||||
}
|
||||
}, 50);
|
||||
// 原有服务启动、电话监听等逻辑...
|
||||
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean == null) {
|
||||
mMainServiceBean = new MainServiceBean();
|
||||
MainServiceBean.saveBean(this, mMainServiceBean);
|
||||
}
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MainService.startMainService(MainActivity.this);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: ViewPager初始化完成,等待延迟初始化首屏");
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据位置初始化Fragment(调用Fragment的初始化逻辑,避免重复执行)
|
||||
* 优化:添加isAdded判断,确保Fragment已附加到Activity,防止上下文空指针
|
||||
*/
|
||||
private void initFragmentByPosition(int position) {
|
||||
// 校验位置合法性 + 避免重复初始化 + 确保Fragment已附加到Activity
|
||||
if (position < 0 || position >= fragmentList.size() || isFragmentInit[position]) {
|
||||
return;
|
||||
}
|
||||
Fragment targetFragment = fragmentList.get(position);
|
||||
if (targetFragment != null && targetFragment.isAdded()) {
|
||||
// 触发Fragment初始化(调用各Fragment的initData方法)
|
||||
if (targetFragment instanceof CallLogFragment) {
|
||||
((CallLogFragment) targetFragment).initData();
|
||||
} else if (targetFragment instanceof ContactsFragment) {
|
||||
((ContactsFragment) targetFragment).initData();
|
||||
} else if (targetFragment instanceof LogFragment) {
|
||||
((LogFragment) targetFragment).initData();
|
||||
}
|
||||
// 标记为已初始化
|
||||
isFragmentInit[position] = true;
|
||||
LogUtils.d(TAG, "initFragmentByPosition: 初始化Fragment,位置=" + position + ",标题=" + tabTitleList.get(position));
|
||||
} else {
|
||||
LogUtils.w(TAG, "initFragmentByPosition: Fragment未附加到Activity/实例为空,位置=" + position);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 10. 菜单相关函数区 ======================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu: 菜单加载完成");
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
// 以下为原有代码(无需修改)
|
||||
private class MyPagerAdapter extends FragmentPagerAdapter {
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.item_settings) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected: 用户点击设置菜单");
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
// ====================== 11. ViewPager页面回调区(切换时初始化对应Fragment) ======================
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
currentPoint = position;
|
||||
LogUtils.d(TAG, "onPageSelected: 页面切换至[" + position + "],标题=" + tabTitleList.get(position));
|
||||
// 切换页面时,初始化当前页Fragment(未初始化过才执行)
|
||||
initFragmentByPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {}
|
||||
|
||||
// ====================== 12. 工具函数区 ======================
|
||||
/**
|
||||
* 拨号工具方法(添加空指针防护)
|
||||
*/
|
||||
public static void dialPhoneNumber(String phoneNumber) {
|
||||
if (_MainActivity == null) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: MainActivity实例为空,无法拨号");
|
||||
return;
|
||||
}
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨号号码为空");
|
||||
return;
|
||||
}
|
||||
if (PermissionUtils.checkPermission(_MainActivity, Manifest.permission.CALL_PHONE)) {
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL);
|
||||
intent.setData(Uri.parse("tel:" + phoneNumber));
|
||||
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||
_MainActivity.startActivity(intent);
|
||||
} else {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨号权限不足,无法发起拨号");
|
||||
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为默认拨号应用(适配API30,硬编码版本判断)
|
||||
*/
|
||||
public boolean isDefaultPhoneCallApp() {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
TelecomManager manager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
|
||||
if (manager != null && manager.getDefaultDialerPackage() != null) {
|
||||
boolean isDefault = manager.getDefaultDialerPackage().equals(getPackageName());
|
||||
LogUtils.d(TAG, "isDefaultPhoneCallApp: 是否为默认拨号应用=" + isDefault);
|
||||
return isDefault;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isDefaultPhoneCallApp: 系统版本低于Android 6,无法判断");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否正在运行(通用工具方法,添加空指针防护)
|
||||
*/
|
||||
public boolean isServiceRunning(Class<?> serviceClass) {
|
||||
if (serviceClass == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning: 服务类参数为null");
|
||||
return false;
|
||||
}
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (manager == null) {
|
||||
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(service.service.getClassName())) {
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]正在运行");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]未运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================== 13. 内部类定义区(Java 7 规范,禁止Lambda) ======================
|
||||
/**
|
||||
* 自定义懒加载ViewPager适配器(删除setPrimaryItem方法,解决首屏初始化冲突)
|
||||
*/
|
||||
private class LazyLoadPagerAdapter extends FragmentPagerAdapter {
|
||||
private final List<Fragment> fragmentList;
|
||||
private final List<String> tabTitleList;
|
||||
|
||||
public LazyLoadPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
|
||||
public MyPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
|
||||
super(fm);
|
||||
this.fragmentList = fragmentList;
|
||||
this.tabTitleList = tabTitleList;
|
||||
LogUtils.d(MainActivity.TAG, "LazyLoadPagerAdapter: 初始化完成,Fragment数量=" + fragmentList.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -522,8 +247,108 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return tabTitleList.get(position);
|
||||
}
|
||||
}
|
||||
|
||||
// 【已删除】移除setPrimaryItem方法,避免与手动初始化+onPageSelected回调冲突
|
||||
public static void dialPhoneNumber(String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(_MainActivity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
_MainActivity.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
@Override
|
||||
public void onPageSelected(int position) {}
|
||||
@Override
|
||||
public void onClick(View v) {}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
LogUtils.d(TAG, "电话已挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
LogUtils.d(TAG, "正在通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
LogUtils.d(TAG, "来电: " + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy() SOS");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.item_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
public boolean isDefaultPhoneCallApp() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
|
||||
if (manger != null && manger.getDefaultDialerPackage() != null) {
|
||||
return manger.getDefaultDialerPackage().equals(getPackageName());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isServiceRunning(Class<?> serviceClass) {
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (manager == null) return false;
|
||||
|
||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
|
||||
Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(service.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DIALER_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_APP_SETTINGS) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:15:54
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libaes.views.AboutView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libaes.winboll.APPInfo;
|
||||
import cc.winboll.studio.libaes.winboll.AboutView;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:15:54
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AboutActivity";
|
||||
private static final String BRANCH_NAME = "contacts";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Toolbar mToolbar;
|
||||
Context mContext;
|
||||
Toolbar mToolbar;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -42,75 +35,58 @@ public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 关于页面开始创建");
|
||||
|
||||
mContext = this;
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 初始化关于页面视图
|
||||
initAboutView();
|
||||
// 注册Activity管理
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
|
||||
AboutView aboutView = CreateAboutView();
|
||||
// 在 Activity 的 onCreate 或其他生命周期方法中调用
|
||||
// LinearLayout layout = new LinearLayout(this);
|
||||
// layout.setOrientation(LinearLayout.VERTICAL);
|
||||
// // 创建布局参数(宽度和高度)
|
||||
// ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT
|
||||
// );
|
||||
// addContentView(aboutView, params);
|
||||
|
||||
LinearLayout layout = findViewById(R.id.aboutviewroot_ll);
|
||||
// 创建布局参数(宽度和高度)
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layout.addView(aboutView, params);
|
||||
|
||||
GlobalApplication.getWinBoLLActivityManager().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面开始销毁");
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面销毁完成");
|
||||
GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||
// Java7 适配:添加强制类型转换
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
// 非空判断,避免空指针异常
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initAboutView() {
|
||||
LogUtils.d(TAG, "initAboutView: 初始化关于页面内容视图");
|
||||
AboutView aboutView = createAboutView();
|
||||
LinearLayout layout = (LinearLayout) findViewById(R.id.aboutviewroot_ll);
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layout.addView(aboutView, params);
|
||||
LogUtils.d(TAG, "initAboutView: AboutView已添加到布局");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑函数区 ======================
|
||||
private AboutView createAboutView() {
|
||||
LogUtils.d(TAG, "createAboutView: 构建APP信息并创建AboutView");
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "contacts";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("Contacts");
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(BRANCH_NAME);
|
||||
appInfo.setAppGitAPPSubProjectFolder(BRANCH_NAME);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=4&extra=page%3D1");
|
||||
appInfo.setAppAPKName("Contacts");
|
||||
appInfo.setAppAPKFolderName("Contacts");
|
||||
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 17:15:46
|
||||
* @Describe 拨号窗口
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -15,145 +20,100 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 17:15:46
|
||||
* @Describe 拨号窗口
|
||||
*/
|
||||
public class CallActivity extends AppCompatActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallActivity";
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
private EditText phoneNumberEditText;
|
||||
private TextView callStatusTextView;
|
||||
private Button dialButton;
|
||||
|
||||
// ====================== 业务成员区 ======================
|
||||
private TelephonyManager telephonyManager;
|
||||
private MyPhoneStateListener phoneStateListener;
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面开始创建");
|
||||
//setContentView(R.layout.activity_main);
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
// 初始化控件
|
||||
initViews();
|
||||
// 初始化电话状态监听
|
||||
initPhoneStateListener();
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面初始化完成");
|
||||
}
|
||||
phoneNumberEditText = findViewById(R.id.phone_number);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
callStatusTextView = findViewById(R.id.call_status);
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 拨号页面开始销毁");
|
||||
// 取消电话状态监听,避免内存泄漏
|
||||
if (telephonyManager != null && phoneStateListener != null) {
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
LogUtils.d(TAG, "onDestroy: 电话状态监听已取消");
|
||||
}
|
||||
LogUtils.d(TAG, "onDestroy: 拨号页面销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 权限回调函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_CALL_PHONE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 拨打电话权限授予成功");
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
dialPhoneNumber(phoneNumber);
|
||||
} else {
|
||||
LogUtils.w(TAG, "onRequestPermissionsResult: 拨打电话权限被拒绝");
|
||||
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumberEditText = (EditText) findViewById(R.id.phone_number);
|
||||
dialButton = (Button) findViewById(R.id.dial_button);
|
||||
callStatusTextView = (TextView) findViewById(R.id.call_status);
|
||||
|
||||
// 设置拨号按钮点击事件
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "initViews: 拨号按钮点击,号码=" + phoneNumber);
|
||||
if (phoneNumber.isEmpty()) {
|
||||
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
if (!phoneNumber.isEmpty()) {
|
||||
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(CallActivity.this,
|
||||
new String[]{Manifest.permission.CALL_PHONE},
|
||||
REQUEST_CALL_PHONE);
|
||||
} else {
|
||||
dialPhoneNumber(phoneNumber);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 权限检查
|
||||
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "initViews: 拨打电话权限未授予,发起权限申请");
|
||||
ActivityCompat.requestPermissions(CallActivity.this,
|
||||
new String[]{Manifest.permission.CALL_PHONE},
|
||||
REQUEST_CALL_PHONE);
|
||||
} else {
|
||||
dialPhoneNumber(phoneNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 电话状态监听初始化函数区 ======================
|
||||
private void initPhoneStateListener() {
|
||||
LogUtils.d(TAG, "initPhoneStateListener: 初始化电话状态监听");
|
||||
// 初始化TelephonyManager和PhoneStateListener
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
// ====================== 核心业务函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CALL_PHONE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
dialPhoneNumber(phoneNumber);
|
||||
} else {
|
||||
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dialPhoneNumber(String phoneNumber) {
|
||||
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨打电话权限缺失,拨号失败");
|
||||
return;
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
// ====================== 内部电话状态监听类 ======================
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
callStatusTextView.setText("电话已挂断");
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
callStatusTextView.setText("正在通话中");
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
callStatusTextView.setText("来电: " + incomingNumber);
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-来电,号码=" + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 取消监听
|
||||
if (telephonyManager != null) {
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +1,40 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 20:18:26
|
||||
*/
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 20:18:26
|
||||
* @Describe 拨号盘窗口(跳转到系统拨号界面)
|
||||
*/
|
||||
public class DialerActivity extends AppCompatActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DialerActivity";
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private EditText phoneNumberEditText;
|
||||
private Button dialButton;
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 拨号盘页面开始创建");
|
||||
setContentView(R.layout.activity_dialer);
|
||||
|
||||
// 初始化UI控件与点击事件
|
||||
initViews();
|
||||
LogUtils.d(TAG, "onCreate: 拨号盘页面初始化完成");
|
||||
}
|
||||
phoneNumberEditText = findViewById(R.id.phone_number_edit_text);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 拨号盘页面已销毁");
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumberEditText = (EditText) findViewById(R.id.phone_number_edit_text);
|
||||
dialButton = (Button) findViewById(R.id.dial_button);
|
||||
|
||||
// 设置拨号按钮点击事件
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "initViews: 拨号按钮点击,输入号码=" + phoneNumber);
|
||||
|
||||
// 空号码校验
|
||||
if (phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "initViews: 拨号失败,号码为空");
|
||||
Toast.makeText(DialerActivity.this, "请输入有效电话号码", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳转到系统拨号界面
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||
if (intent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(intent);
|
||||
LogUtils.d(TAG, "initViews: 成功跳转到系统拨号界面");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initViews: 跳转失败,无可用拨号应用");
|
||||
Toast.makeText(DialerActivity.this, "未找到可用拨号应用", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString();
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 05:37:42
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@@ -20,59 +24,49 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.App;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.PhoneConnectRuleAdapter;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.views.DuInfoTextView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 05:37:42
|
||||
* @Describe Contacts 设置页面(完全适配 API 30 + Java 7 语法)
|
||||
* 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 5. LogUtils统一日志管理
|
||||
*/
|
||||
public class SettingsActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
public class SettingsActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区(置顶,统一管理) ======================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
// API版本硬编码(替代Build.VERSION_CODES,适配Java7)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
|
||||
// ====================== 静态成员属性区 ======================
|
||||
private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀
|
||||
Toolbar mToolbar;
|
||||
Switch swSilent;
|
||||
SeekBar msbVolume;
|
||||
TextView mtvVolume;
|
||||
int mnStreamMaxVolume;
|
||||
int mnStreamVolume;
|
||||
Switch mswMainService;
|
||||
static DuInfoTextView _DuInfoTextView;
|
||||
|
||||
// ====================== 数据业务属性区 ======================
|
||||
private int mStreamMaxVolume; // 铃音最大音量
|
||||
private int mStreamVolume; // 当前铃音音量
|
||||
private List<PhoneConnectRuleBean> mRuleList; // 通话规则列表
|
||||
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
|
||||
// 云盾防御层数量
|
||||
EditText etDunTotalCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
EditText etDunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
EditText etDunResumeCount;
|
||||
// 是否启用云盾
|
||||
Switch swIsEnableDun;
|
||||
|
||||
// ====================== UI控件属性区(统一归类,规范命名) ======================
|
||||
private Toolbar mToolbar; // 顶部工具栏
|
||||
private Switch mSwMainService; // 主服务开关
|
||||
private SeekBar mSbVolume; // 音量调节条
|
||||
private TextView mTvVolume; // 音量显示文本
|
||||
private Switch mSwEnableDun; // 云盾功能开关
|
||||
private EditText mEtDunTotalCount; // 云盾总次数输入框
|
||||
private EditText mEtDunResumeSecondCount; // 云盾恢复秒数输入框
|
||||
private EditText mEtDunResumeCount; // 云盾恢复次数输入框
|
||||
private RecyclerView mRvRuleList; // 规则列表RecyclerView
|
||||
private EditText mEtBoBullToonUrl; // BoBullToon地址输入框
|
||||
private EditText mEtSearchPhone; // 号码查询输入框
|
||||
private RecyclerView recyclerView;
|
||||
private PhoneConnectRuleAdapter adapter;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
|
||||
// ====================== 接口实现区(IWinBoLLActivity规范实现) ======================
|
||||
@Override
|
||||
public AppCompatActivity getActivity() {
|
||||
return this;
|
||||
@@ -83,531 +77,268 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区(按执行顺序排列) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 设置页面启动");
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
// 初始化核心流程(按优先级执行)
|
||||
initToolbar(); // 工具栏初始化(优先)
|
||||
initMainServiceSwitch();// 主服务开关初始化
|
||||
initVolumeControl(); // 音量控制初始化
|
||||
initRuleRecyclerView(); // 规则列表初始化
|
||||
initDunSettings(); // 云盾设置初始化
|
||||
initBoBullToonViews(); // BoBullToon功能初始化
|
||||
|
||||
LogUtils.d(TAG, "onCreate: 设置页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 设置页面销毁");
|
||||
// 内存泄漏防护:清空所有引用(静态+成员+UI)
|
||||
sDuInfoTextView = null;
|
||||
mRuleList = null;
|
||||
mRuleAdapter = null;
|
||||
mToolbar = null;
|
||||
mSwMainService = null;
|
||||
mSbVolume = null;
|
||||
mTvVolume = null;
|
||||
mSwEnableDun = null;
|
||||
mEtDunTotalCount = null;
|
||||
mEtDunResumeSecondCount = null;
|
||||
mEtDunResumeCount = null;
|
||||
mRvRuleList = null;
|
||||
mEtBoBullToonUrl = null;
|
||||
mEtSearchPhone = null;
|
||||
LogUtils.d(TAG, "onDestroy: 设置页面资源清理完成");
|
||||
}
|
||||
|
||||
// ====================== 初始化函数区(按功能模块归类) ======================
|
||||
/**
|
||||
* 初始化顶部工具栏(后退按钮+标题)
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||
// 初始化工具栏
|
||||
mToolbar = findViewById(R.id.activitymainToolbar1);
|
||||
setSupportActionBar(mToolbar);
|
||||
// 显示后退按钮
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getTag());
|
||||
|
||||
// 显示后退按钮(空指针防护)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
}
|
||||
mswMainService = findViewById(R.id.sw_mainservice);
|
||||
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
mswMainService.setChecked(mMainServiceBean.isEnable());
|
||||
mswMainService.setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View arg0) {
|
||||
LogUtils.d(TAG, "mswMainService onClick");
|
||||
// TODO: Implement this method
|
||||
if (mswMainService.isChecked()) {
|
||||
//ToastUtils.show("Is Checked");
|
||||
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
} else {
|
||||
//ToastUtils.show("Not Checked");
|
||||
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 后退按钮点击事件(Java7匿名内部类)
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主服务开关(联动MainService启停)
|
||||
*/
|
||||
private void initMainServiceSwitch() {
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 初始化主服务开关");
|
||||
mSwMainService = (Switch) findViewById(R.id.sw_mainservice);
|
||||
MainServiceBean serviceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
|
||||
// 加载开关状态(空指针防护)
|
||||
boolean isServiceEnable = serviceBean != null && serviceBean.isEnable();
|
||||
mSwMainService.setChecked(isServiceEnable);
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 主服务当前状态:" + (isServiceEnable ? "启用" : "禁用"));
|
||||
|
||||
// 开关点击事件
|
||||
mSwMainService.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isChecked = mSwMainService.isChecked();
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||
if (isChecked) {
|
||||
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
} else {
|
||||
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化音量控制(SeekBar+音量显示+配置保存)
|
||||
*/
|
||||
private void initVolumeControl() {
|
||||
LogUtils.d(TAG, "initVolumeControl: 初始化音量控制");
|
||||
mSbVolume = (SeekBar) findViewById(R.id.bellvolume);
|
||||
mTvVolume = (TextView) findViewById(R.id.tv_volume);
|
||||
msbVolume = findViewById(R.id.bellvolume);
|
||||
mtvVolume = findViewById(R.id.tv_volume);
|
||||
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// 空指针防护:AudioManager获取失败直接返回
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败");
|
||||
return;
|
||||
}
|
||||
// 设置SeekBar的最大值为系统铃声音量的最大刻度
|
||||
mnStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setMax(mnStreamMaxVolume);
|
||||
// 获取当前铃声音量并设置为SeekBar的初始进度
|
||||
mnStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setProgress(mnStreamVolume);
|
||||
|
||||
// 初始化音量参数
|
||||
mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
mSbVolume.setMax(mStreamMaxVolume);
|
||||
mSbVolume.setProgress(mStreamVolume);
|
||||
updateVolumeDisplay(); // 更新音量文本显示
|
||||
updateStreamVolumeTextView();
|
||||
|
||||
// 音量调节监听
|
||||
mSbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
LogUtils.d(TAG, "initVolumeControl: 音量调节至:" + progress + "/" + mStreamMaxVolume);
|
||||
// 实时更新系统音量+保存配置
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
|
||||
RingTongBean ringBean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
|
||||
if (ringBean == null) {
|
||||
ringBean = new RingTongBean();
|
||||
}
|
||||
ringBean.setStreamVolume(progress);
|
||||
RingTongBean.saveBean(SettingsActivity.this, ringBean);
|
||||
mStreamVolume = progress;
|
||||
updateVolumeDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话规则列表(加载黑白名单规则)
|
||||
*/
|
||||
private void initRuleRecyclerView() {
|
||||
LogUtils.d(TAG, "initRuleRecyclerView: 初始化规则列表");
|
||||
mRvRuleList = (RecyclerView) findViewById(R.id.recycler_view);
|
||||
mRvRuleList.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
// 加载规则数据
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "initRuleRecyclerView: Rules实例获取失败,列表初始化失败");
|
||||
return;
|
||||
}
|
||||
mRuleList = rules.getPhoneBlacRuleBeanList();
|
||||
mRuleAdapter = new PhoneConnectRuleAdapter(this, mRuleList);
|
||||
mRvRuleList.setAdapter(mRuleAdapter);
|
||||
LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + mRuleList.size() + "条规则");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化云盾设置(参数加载+开关联动)
|
||||
*/
|
||||
private void initDunSettings() {
|
||||
LogUtils.d(TAG, "initDunSettings: 初始化云盾设置");
|
||||
sDuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo);
|
||||
mSwEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun);
|
||||
mEtDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount);
|
||||
mEtDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount);
|
||||
mEtDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount);
|
||||
|
||||
// 加载云盾配置
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "initDunSettings: Rules实例获取失败,云盾初始化失败");
|
||||
return;
|
||||
}
|
||||
SettingsBean dunSettings = rules.getSettingsModel();
|
||||
if (dunSettings == null) {
|
||||
LogUtils.e(TAG, "initDunSettings: 云盾配置获取失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充配置参数
|
||||
mEtDunTotalCount.setText(String.valueOf(dunSettings.getDunTotalCount()));
|
||||
mEtDunResumeSecondCount.setText(String.valueOf(dunSettings.getDunResumeSecondCount()));
|
||||
mEtDunResumeCount.setText(String.valueOf(dunSettings.getDunResumeCount()));
|
||||
mSwEnableDun.setChecked(dunSettings.isEnableDun());
|
||||
|
||||
// 开关联动:启用云盾时禁用参数编辑
|
||||
boolean isDunEnable = dunSettings.isEnableDun();
|
||||
mEtDunTotalCount.setEnabled(!isDunEnable);
|
||||
mEtDunResumeSecondCount.setEnabled(!isDunEnable);
|
||||
mEtDunResumeCount.setEnabled(!isDunEnable);
|
||||
LogUtils.d(TAG, "initDunSettings: 云盾当前状态:" + (isDunEnable ? "启用" : "禁用"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化BoBullToon功能(地址配置+号码查询)
|
||||
*/
|
||||
private void initBoBullToonViews() {
|
||||
LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon功能");
|
||||
mEtBoBullToonUrl = (EditText) findViewById(R.id.bobulltoonurl_et);
|
||||
mEtSearchPhone = (EditText) findViewById(R.id.activitysettingsEditText1);
|
||||
|
||||
// 加载保存的地址
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules != null) {
|
||||
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||
LogUtils.d(TAG, "initBoBullToonViews: 加载BoBullToon地址完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initBoBullToonViews: Rules实例获取失败,地址加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 点击事件回调区(按功能模块归类) ======================
|
||||
/**
|
||||
* 云盾开关点击事件(联动参数编辑权限+配置保存)
|
||||
*/
|
||||
public void onSW_IsEnableDun(View view) {
|
||||
boolean isChecked = mSwEnableDun.isChecked();
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||
|
||||
// 联动参数编辑权限
|
||||
mEtDunTotalCount.setEnabled(!isChecked);
|
||||
mEtDunResumeSecondCount.setEnabled(!isChecked);
|
||||
mEtDunResumeCount.setEnabled(!isChecked);
|
||||
|
||||
// 保存配置
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: Rules实例获取失败,配置保存失败");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
SettingsBean dunSettings = rules.getSettingsModel();
|
||||
if (dunSettings == null) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾配置获取失败,保存失败");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启用云盾时校验参数合法性
|
||||
if (isChecked) {
|
||||
try {
|
||||
String totalCountStr = mEtDunTotalCount.getText().toString().trim();
|
||||
String resumeSecStr = mEtDunResumeSecondCount.getText().toString().trim();
|
||||
String resumeCountStr = mEtDunResumeCount.getText().toString().trim();
|
||||
|
||||
// 空参数校验
|
||||
if (totalCountStr.isEmpty() || resumeSecStr.isEmpty() || resumeCountStr.isEmpty()) {
|
||||
throw new NumberFormatException("参数不能为空");
|
||||
msbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
// 设置铃声音量
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
|
||||
RingTongBean bean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
bean = new RingTongBean();
|
||||
}
|
||||
bean.setStreamVolume(progress);
|
||||
RingTongBean.saveBean(SettingsActivity.this, bean);
|
||||
mnStreamVolume = progress;
|
||||
updateStreamVolumeTextView();
|
||||
//Toast.makeText(SettingsActivity.this, "音量设置为: " + progress, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
// 转换参数并保存
|
||||
int totalCount = Integer.parseInt(totalCountStr);
|
||||
int resumeSec = Integer.parseInt(resumeSecStr);
|
||||
int resumeCount = Integer.parseInt(resumeCountStr);
|
||||
dunSettings.setDunTotalCount(totalCount);
|
||||
dunSettings.setDunResumeSecondCount(resumeSec);
|
||||
dunSettings.setDunResumeCount(resumeCount);
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数保存完成,总次数:" + totalCount + ",恢复秒数:" + resumeSec);
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// 当开始拖动SeekBar时的操作
|
||||
}
|
||||
|
||||
// 提示信息
|
||||
String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话";
|
||||
ToastUtils.show(toastMsg);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e);
|
||||
ToastUtils.show("参数格式错误,请输入整数");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// 当停止拖动SeekBar时的操作
|
||||
}
|
||||
});
|
||||
|
||||
// 保存开关状态并刷新配置
|
||||
dunSettings.setIsEnableDun(isChecked);
|
||||
rules.saveDun();
|
||||
rules.reload();
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成");
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
ruleList = Rules.getInstance(this).getPhoneBlacRuleBeanList();
|
||||
|
||||
adapter = new PhoneConnectRuleAdapter(this, ruleList);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
// 设置参数云盾
|
||||
_DuInfoTextView = findViewById(R.id.tv_DunInfo);
|
||||
etDunTotalCount = findViewById(R.id.et_DunTotalCount);
|
||||
etDunResumeSecondCount = findViewById(R.id.et_DunResumeSecondCount);
|
||||
etDunResumeCount = findViewById(R.id.et_DunResumeCount);
|
||||
swIsEnableDun = findViewById(R.id.sw_IsEnableDun);
|
||||
SettingsModel settingsModel = Rules.getInstance(this).getSettingsModel();
|
||||
|
||||
etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount()));
|
||||
etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount()));
|
||||
etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount()));
|
||||
swIsEnableDun.setChecked(settingsModel.isEnableDun());
|
||||
|
||||
boolean isEnableDun = settingsModel.isEnableDun();
|
||||
etDunTotalCount.setEnabled(!isEnableDun);
|
||||
etDunResumeSecondCount.setEnabled(!isEnableDun);
|
||||
etDunResumeCount.setEnabled(!isEnableDun);
|
||||
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
etBoBullToonURL.setText(Rules.getInstance(this).getBoBullToonURL());
|
||||
}
|
||||
|
||||
public static void notifyDunInfoUpdate() {
|
||||
if (_DuInfoTextView != null) {
|
||||
_DuInfoTextView.notifyInfoUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSW_IsEnableDun(View view) {
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun");
|
||||
boolean isEnableDun = swIsEnableDun.isChecked();
|
||||
etDunTotalCount.setEnabled(!isEnableDun);
|
||||
etDunResumeSecondCount.setEnabled(!isEnableDun);
|
||||
etDunResumeCount.setEnabled(!isEnableDun);
|
||||
|
||||
SettingsModel settingsModel = Rules.getInstance(this).getSettingsModel();
|
||||
if (isEnableDun) {
|
||||
settingsModel.setDunTotalCount(Integer.parseInt(etDunTotalCount.getText().toString()));
|
||||
settingsModel.setDunResumeSecondCount(Integer.parseInt(etDunResumeSecondCount.getText().toString()));
|
||||
settingsModel.setDunResumeCount(Integer.parseInt(etDunResumeCount.getText().toString()));
|
||||
|
||||
// 应用效果提示
|
||||
ToastUtils.show((settingsModel.getDunTotalCount() == 1)?"电话骚扰防御力几乎为0。":String.format("以下设置将在连拨%d次后接通电话。", settingsModel.getDunTotalCount()));
|
||||
}
|
||||
settingsModel.setIsEnableDun(isEnableDun);
|
||||
Rules.getInstance(this).saveDun();
|
||||
Rules.getInstance(this).reload();
|
||||
|
||||
// 重新加载盾牌参数
|
||||
etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount()));
|
||||
etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount()));
|
||||
etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount()));
|
||||
|
||||
}
|
||||
|
||||
void updateStreamVolumeTextView() {
|
||||
mtvVolume.setText(String.format("%d/%d", mnStreamVolume, mnStreamMaxVolume));
|
||||
}
|
||||
|
||||
public void onUnitTest(View view) {
|
||||
Intent intent = new Intent(this, UnitTestActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新通话规则(黑白名单)
|
||||
*/
|
||||
public void onAddNewConnectionRule(View view) {
|
||||
LogUtils.d(TAG, "onAddNewConnectionRule: 添加新通话规则");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例获取失败,添加失败");
|
||||
return;
|
||||
}
|
||||
mRuleList.add(new PhoneConnectRuleBean());
|
||||
rules.saveRules();
|
||||
mRuleAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则");
|
||||
Rules.getInstance(this).getPhoneBlacRuleBeanList().add(new PhoneConnectRuleModel());
|
||||
Rules.getInstance(this).saveRules();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转默认电话应用设置
|
||||
*/
|
||||
public void onDefaultPhone(View view) {
|
||||
LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置");
|
||||
startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS));
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮窗权限检查与请求
|
||||
*/
|
||||
public void onCanDrawOverlays(View view) {
|
||||
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
|
||||
// API6.0+校验权限
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !Settings.canDrawOverlays(this)) {
|
||||
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
|
||||
showDrawOverlayRequestDialog();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !Settings.canDrawOverlays(this)) {
|
||||
// 请求 悬浮框 权限
|
||||
askForDrawOverlay();
|
||||
} else {
|
||||
ToastUtils.show("悬浮窗权限已开启");
|
||||
ToastUtils.show("悬浮窗已开启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理BoBullToon本地数据
|
||||
*/
|
||||
public void onCleanBoBullToonData(View view) {
|
||||
LogUtils.d(TAG, "onCleanBoBullToonData: 清理BoBullToon数据");
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
if (tomCat != null) {
|
||||
tomCat.cleanBoBullToon();
|
||||
ToastUtils.show("BoBullToon数据已清理");
|
||||
LogUtils.d(TAG, "onCleanBoBullToonData: 数据清理完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "onCleanBoBullToonData: TomCat实例获取失败,清理失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置BoBullToon默认地址
|
||||
*/
|
||||
public void onResetBoBullToonURL(View view) {
|
||||
LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon地址");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onResetBoBullToonURL: Rules实例获取失败,重置失败");
|
||||
return;
|
||||
}
|
||||
rules.resetDefaultBoBullToonURL();
|
||||
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||
ToastUtils.show("BoBullToon地址已重置为默认");
|
||||
LogUtils.d(TAG, "onResetBoBullToonURL: 地址重置完成");
|
||||
Rules.getInstance(this).resetDefaultBoBullToonURL();
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
etBoBullToonURL.setText(Rules.getInstance(this).getBoBullToonURL());
|
||||
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
tomCat.cleanBoBullToon();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载BoBullToon数据(子线程执行,避免阻塞UI)
|
||||
*/
|
||||
public void onDownloadBoBullToon(View view) {
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: 开始下载BoBullToon数据");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例获取失败,下载失败");
|
||||
return;
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
if (!etBoBullToonURL.getText().toString().trim().equals(Rules.getInstance(this).getBoBullToonURL())) {
|
||||
Rules.getInstance(this).setBoBullToonURL(etBoBullToonURL.getText().toString().trim());
|
||||
}
|
||||
|
||||
// 校验并更新地址
|
||||
String inputUrl = mEtBoBullToonUrl.getText().toString().trim();
|
||||
String savedUrl = rules.getBoBullToonURL();
|
||||
if (!inputUrl.equals(savedUrl)) {
|
||||
rules.setBoBullToonURL(inputUrl);
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon地址更新为:" + inputUrl);
|
||||
}
|
||||
|
||||
// 子线程下载(Java7匿名内部类)
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean downloadSuccess = tomCat != null && tomCat.downloadBoBullToon();
|
||||
if (downloadSuccess) {
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: 数据下载成功");
|
||||
// 主线程更新UI
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("BoBullToon下载成功");
|
||||
}
|
||||
});
|
||||
// 重启主服务+刷新配置
|
||||
MainService.restartMainService(SettingsActivity.this);
|
||||
Rules.getInstance(SettingsActivity.this).reload();
|
||||
} else {
|
||||
LogUtils.e(TAG, "onDownloadBoBullToon: 数据下载失败");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("BoBullToon下载失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
@Override
|
||||
public void run() {
|
||||
if (tomCat.downloadBoBullToon()) {
|
||||
LogUtils.d(TAG, "BoBullToon downlaod OK!");
|
||||
MainService.restartMainService(SettingsActivity.this);
|
||||
Rules.getInstance(SettingsActivity.this).reload();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询号码是否为BoBullToon号码
|
||||
*/
|
||||
public void onSearchBoBullToonPhone(View view) {
|
||||
LogUtils.d(TAG, "onSearchBoBullToonPhone: 执行号码查询");
|
||||
String phone = mEtSearchPhone.getText().toString().trim();
|
||||
// 空号码校验
|
||||
if (phone.isEmpty()) {
|
||||
LogUtils.w(TAG, "onSearchBoBullToonPhone: 查询号码为空,取消查询");
|
||||
ToastUtils.show("请输入查询号码");
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
if (tomCat == null || !tomCat.loadPhoneBoBullToon()) {
|
||||
LogUtils.w(TAG, "onSearchBoBullToonPhone: BoBullToon数据未加载,查询失败");
|
||||
ToastUtils.show("请先下载BoBullToon数据");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone);
|
||||
String resultMsg = isBoBullToon ? "是BoBullToon号码" : "非BoBullToon号码";
|
||||
ToastUtils.show(resultMsg);
|
||||
LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + resultMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转单元测试页面
|
||||
*/
|
||||
public void onUnitTest(View view) {
|
||||
LogUtils.d(TAG, "onUnitTest: 跳转单元测试页面");
|
||||
startActivity(new Intent(this, UnitTestActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转关于页面
|
||||
*/
|
||||
public void onAbout(View view) {
|
||||
LogUtils.d(TAG, "onAbout: 跳转关于页面");
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转日志查看页面
|
||||
*/
|
||||
public void onLogView(View view) {
|
||||
LogUtils.d(TAG, "onLogView: 跳转日志页面");
|
||||
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||
}
|
||||
|
||||
// ====================== 工具方法区(通用功能+权限相关) ======================
|
||||
/**
|
||||
* 更新音量显示文本(当前音量/最大音量)
|
||||
*/
|
||||
private void updateVolumeDisplay() {
|
||||
mTvVolume.setText(mStreamVolume + "/" + mStreamMaxVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示悬浮窗权限请求对话框
|
||||
*/
|
||||
private void showDrawOverlayRequestDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("权限请求")
|
||||
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
jumpToDrawOverlaySettings();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
// 解决对话框焦点问题
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗权限设置页面(反射适配低版本)
|
||||
*/
|
||||
private void jumpToDrawOverlaySettings() {
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
|
||||
try {
|
||||
// 反射获取设置页面Action(避免高版本API依赖)
|
||||
Class<?> settingsClazz = Settings.class;
|
||||
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
String action = (String) actionField.get(null);
|
||||
|
||||
// 跳转当前应用权限设置页
|
||||
Intent intent = new Intent(action);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
|
||||
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 静态通知方法区(云盾信息更新) ======================
|
||||
/**
|
||||
* 通知云盾信息刷新(外部调用)
|
||||
*/
|
||||
public static void notifyDunInfoUpdate() {
|
||||
if (sDuInfoTextView != null) {
|
||||
LogUtils.d(TAG, "notifyDunInfoUpdate: 刷新云盾信息显示");
|
||||
sDuInfoTextView.notifyInfoUpdate();
|
||||
EditText etPhone = findViewById(R.id.activitysettingsEditText1);
|
||||
String phone = etPhone.getText().toString().trim();
|
||||
if (tomCat.loadPhoneBoBullToon()) {
|
||||
if (tomCat.isPhoneBoBullToon(phone)) {
|
||||
ToastUtils.show("It is a BoBullToon Phone!");
|
||||
} else {
|
||||
ToastUtils.show("Not in BoBullToon.");
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败");
|
||||
ToastUtils.show("没有下载 BoBullToon。");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void askForDrawOverlay() {
|
||||
AlertDialog alertDialog = new AlertDialog.Builder(this)
|
||||
.setTitle("允许显示悬浮框")
|
||||
.setMessage("为了使电话监听服务正常工作,请允许这项权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
openDrawOverlaySettings();
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后再说", new DialogInterface.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
alertDialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗管理设置界面
|
||||
*/
|
||||
private void openDrawOverlaySettings() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Android M 以上引导用户去系统设置中打开允许悬浮窗
|
||||
// 使用反射是为了用尽可能少的代码保证在大部分机型上都可用
|
||||
try {
|
||||
Context context = this;
|
||||
Class clazz = Settings.class;
|
||||
Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
Intent intent = new Intent(field.get(null).toString());
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "请在悬浮窗管理中打开权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onAbout(View view) {
|
||||
App.getWinBoLLActivityManager().startWinBoLLActivity(this, AboutActivity.class);
|
||||
}
|
||||
|
||||
public void onLogView(View view) {
|
||||
App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,145 +1,94 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 16:07:04
|
||||
* @Describe 规则单元测试页面
|
||||
*/
|
||||
public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
public class UnitTestActivity extends Activity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "UnitTestActivity";
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private LogView logView;
|
||||
private EditText etPhone;
|
||||
LogView logView;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public AppCompatActivity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 单元测试页面开始创建");
|
||||
setContentView(R.layout.activity_unittest);
|
||||
|
||||
// 初始化控件
|
||||
initViews();
|
||||
LogUtils.d(TAG, "onCreate: 单元测试页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 单元测试页面开始销毁");
|
||||
if (logView != null) {
|
||||
// 若LogView有停止方法,建议调用避免资源泄漏
|
||||
// logView.stop();
|
||||
LogUtils.d(TAG, "onDestroy: LogView资源已处理");
|
||||
}
|
||||
LogUtils.d(TAG, "onDestroy: 单元测试页面销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
logView = (LogView) findViewById(R.id.logview);
|
||||
etPhone = (EditText) findViewById(R.id.phone_et);
|
||||
|
||||
// 启动日志视图
|
||||
logView = findViewById(R.id.logview);
|
||||
logView.start();
|
||||
LogUtils.d(TAG, "initViews: LogView已启动");
|
||||
}
|
||||
|
||||
// ====================== 点击事件测试函数区 ======================
|
||||
/**
|
||||
* 测试单个号码匹配规则
|
||||
*/
|
||||
public void onTestPhone(View view) {
|
||||
LogUtils.d(TAG, "onTestPhone: 开始测试单个号码规则匹配");
|
||||
// 开始测试数据
|
||||
EditText etPhone = findViewById(R.id.phone_et);
|
||||
Rules rules = Rules.getInstance(this);
|
||||
String phone = etPhone.getText().toString().trim();
|
||||
if (phone.isEmpty()) {
|
||||
LogUtils.w(TAG, "onTestPhone: 测试号码为空,跳过匹配");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
Rules rules = Rules.getInstance(this);
|
||||
boolean isAllowed = rules.isAllowed(phone);
|
||||
LogUtils.d(TAG, String.format("onTestPhone: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量测试预设号码规则匹配
|
||||
*/
|
||||
public void onTestMain(View view) {
|
||||
LogUtils.d(TAG, "onTestMain: 开始批量测试号码规则匹配");
|
||||
// 测试IntUtils工具类方法
|
||||
LogUtils.d(TAG, "onTestMain: 执行 IntUtils.unittest_getIntInRange() 测试");
|
||||
LogUtils.d(TAG, "IntUtils.unittest_getIntInRange();");
|
||||
IntUtils.unittest_getIntInRange();
|
||||
|
||||
// 初始化规则实例
|
||||
|
||||
Rules rules = Rules.getInstance(this);
|
||||
// 无规则时添加测试规则集
|
||||
initTestRulesIfEmpty(rules);
|
||||
|
||||
// 预设测试号码列表
|
||||
String[] testPhones = {
|
||||
"16769764848", "16856582777", "17519703124",
|
||||
"0205658955", "0108965253", "+8616769764848",
|
||||
"4005816769764848", "95566"
|
||||
};
|
||||
|
||||
// 遍历测试号码并输出结果
|
||||
for (String phone : testPhones) {
|
||||
boolean isAllowed = rules.isAllowed(phone);
|
||||
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
|
||||
}
|
||||
|
||||
// ====================== 私有工具函数区 ======================
|
||||
/**
|
||||
* 规则集为空时初始化测试规则
|
||||
*/
|
||||
private void initTestRulesIfEmpty(Rules rules) {
|
||||
// 如果没有规则就添加测试规则
|
||||
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
|
||||
// 规则1:中国手机号允许
|
||||
rules.add("^1[3-9]\\d{9}$", true, true);
|
||||
// 规则2:0660区号号码允许
|
||||
rules.add("^0660\\d+$", true, true);
|
||||
// 规则3:020区号号码允许
|
||||
rules.add("^020\\d+$", true, true);
|
||||
// 规则4:默认拒接所有号码
|
||||
rules.add(".*", false, true);
|
||||
// 手机号码允许
|
||||
// 中国手机号码正则表达式,以1开头,第二位可以是3、4、5、6、7、8、9,后面跟9位数字
|
||||
String regex = "^1[3-9]\\d{9}$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 保存规则到本地
|
||||
// 指定区号号码允许
|
||||
regex = "^0660\\d+$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 指定区号号码允许
|
||||
regex = "^020\\d+$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 添加默认拒接规则
|
||||
regex = ".*";
|
||||
rules.add(regex, false, true);
|
||||
|
||||
// 保存规则到文件
|
||||
rules.saveRules();
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
|
||||
}
|
||||
|
||||
// 开始测试数据
|
||||
String phone = "16769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "16856582777";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "17519703124";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "0205658955";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "0108965253";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "+8616769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "4005816769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "95566";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:16:45
|
||||
* @Describe 应用窗口基类
|
||||
*/
|
||||
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.beans.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:16:45
|
||||
* @Describe 应用窗口基类,统一处理主题设置与导航返回
|
||||
*/
|
||||
public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "WinBollActivity";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -33,24 +29,14 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
//LogUtils.d(TAG, "onCreate: 基类页面开始创建");
|
||||
// 优先设置主题,再执行父类初始化
|
||||
// mThemeType = getThemeType();
|
||||
// setThemeStyle();
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
//LogUtils.d(TAG, "onCreate: 基类主题设置完成,当前主题类型=" + mThemeType);
|
||||
}
|
||||
|
||||
// ====================== 主题相关函数区 ======================
|
||||
/**
|
||||
* 获取当前应用主题类型
|
||||
*/
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
LogUtils.d(TAG, "getThemeType: 获取应用主题类型");
|
||||
// 注释的SharedPreferences逻辑保留,便于后续扩展
|
||||
/*SharedPreferences sharedPreferences = getSharedPreferences(
|
||||
SHAREDPREFERENCES_NAME, MODE_PRIVATE);
|
||||
return AESThemeBean.ThemeType.values()[((sharedPreferences.getInt(DRAWER_THEME_TYPE, AESThemeBean.ThemeType.DEFAULT.ordinal())))];
|
||||
@@ -58,27 +44,17 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用当前主题样式
|
||||
*/
|
||||
void setThemeStyle() {
|
||||
LogUtils.d(TAG, "setThemeStyle: 开始设置应用主题");
|
||||
// 替换原注释逻辑,使用AESThemeUtil获取的主题ID
|
||||
//setTheme(AESThemeBean.getThemeStyle(getThemeType()));
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
LogUtils.d(TAG, "setThemeStyle: 主题设置完成");
|
||||
}
|
||||
|
||||
// ====================== 菜单与导航函数区 ======================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected: 菜单选项点击,itemId=" + item.getItemId());
|
||||
// 处理导航栏返回按钮点击事件
|
||||
// if (item.getItemId() == android.R.id.home) {
|
||||
// LogUtils.d(TAG, "onOptionsItemSelected: 点击导航返回按钮,关闭当前页面");
|
||||
// finish();
|
||||
// return true;
|
||||
// }
|
||||
if(item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:09:32
|
||||
* @Describe CallLogAdapter
|
||||
*/
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -8,167 +13,125 @@ import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.contacts.beans.CallLogModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:09:32
|
||||
* @Describe 通话记录列表适配器
|
||||
*/
|
||||
public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogAdapter";
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<CallLogModel> callLogList;
|
||||
private ContactUtils mContactUtils;
|
||||
ContactUtils mContactUtils;
|
||||
Context mContext;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public CallLogAdapter(Context context, List<CallLogModel> callLogList) {
|
||||
LogUtils.d(TAG, "CallLogAdapter: 初始化适配器,数据量=" + callLogList.size());
|
||||
this.mContext = context;
|
||||
this.callLogList = callLogList;
|
||||
mContext = context;
|
||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
||||
this.callLogList = callLogList;
|
||||
}
|
||||
|
||||
public void relaodContacts() {
|
||||
this.mContactUtils.relaodContacts();
|
||||
}
|
||||
|
||||
// ====================== 公共方法区 ======================
|
||||
/**
|
||||
* 重新加载联系人数据
|
||||
*/
|
||||
public void relaodContacts() {
|
||||
LogUtils.d(TAG, "relaodContacts: 开始重新加载联系人数据");
|
||||
this.mContactUtils.reloadContacts();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "relaodContacts: 联系人数据加载完成,列表已刷新");
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建列表项ViewHolder");
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_call_log, parent, false);
|
||||
return new CallLogViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定列表项数据,position=" + position);
|
||||
final CallLogModel callLog = callLogList.get(position);
|
||||
|
||||
// 绑定通话号码与联系人名称
|
||||
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
|
||||
String phoneText = callLog.getPhoneNumber() + "☎" + (contactName == null ? "" : contactName);
|
||||
holder.phoneNumber.setText(phoneText);
|
||||
|
||||
// 号码长按弹出菜单事件
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
holder.phoneNumber.setText(callLog.getPhoneNumber() + "☎" + mContactUtils.getContactsName(callLog.getPhoneNumber()));
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View p1) {
|
||||
showPhonePopupMenu(holder.phoneNumber, callLog);
|
||||
// 弹出复制菜单
|
||||
PopupMenu menu = new PopupMenu(mContext, holder.phoneNumber);
|
||||
//加载菜单资源
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
|
||||
//设置点击事件的响应
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int nItemId = menuItem.getItemId();
|
||||
if (nItemId == R.id.item_calllog_phonenumber_copy) {
|
||||
// Gets a handle to the clipboard service.
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
// Creates a new text clip to put on the clipboard
|
||||
ClipData clip = ClipData.newPlainText("simple text", callLog.getPhoneNumber());
|
||||
// Set the clipboard's primary clip.
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
} else if (nItemId == R.id.item_calllog_phonenumber_add_contact) {
|
||||
//ToastUtils.show(callLog.getPhoneNumber());
|
||||
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
//一定要调用show()来显示弹出式菜单
|
||||
menu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 绑定通话状态与时间
|
||||
|
||||
holder.callStatus.setText(callLog.getCallStatus());
|
||||
holder.callDate.setText(DATE_FORMAT.format(callLog.getCallDate()));
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
holder.callDate.setText(dateFormat.format(callLog.getCallDate()));
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, callLog);
|
||||
// 初始化拉动后拨号控件
|
||||
holder.dialAOHPCTCSeekBar.setThumb(holder.itemView.getContext().getDrawable(R.drawable.ic_call));
|
||||
holder.dialAOHPCTCSeekBar.setBlurRightDP(80);
|
||||
holder.dialAOHPCTCSeekBar.setThumbOffset(0);
|
||||
holder.dialAOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
|
||||
ToastUtils.show(phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
holder.itemView.getContext().startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return callLogList == null ? 0 : callLogList.size();
|
||||
return callLogList.size();
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 显示号码操作弹窗菜单
|
||||
*/
|
||||
private void showPhonePopupMenu(View anchorView, final CallLogModel callLog) {
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 弹出号码操作菜单");
|
||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
|
||||
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.item_calllog_phonenumber_copy) {
|
||||
// 复制号码到剪贴板
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("call_log_phone", callLog.getPhoneNumber());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 号码" + callLog.getPhoneNumber() + "已复制到剪贴板");
|
||||
} else if (itemId == R.id.item_calllog_phonenumber_add_contact) {
|
||||
// 跳转到添加联系人页面
|
||||
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 跳转添加联系人页面,号码=" + callLog.getPhoneNumber());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动拨号SeekBar
|
||||
*/
|
||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final CallLogModel callLog) {
|
||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
||||
seekBar.setBlurRightDP(80);
|
||||
seekBar.setThumbOffset(0);
|
||||
|
||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
|
||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
||||
ToastUtils.show(phoneNumber);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类 ======================
|
||||
public class CallLogViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView phoneNumber;
|
||||
TextView callStatus;
|
||||
TextView callDate;
|
||||
TextView phoneNumber, callStatus, callDate;
|
||||
Button dialButton;
|
||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
||||
|
||||
public CallLogViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumber = (TextView) itemView.findViewById(R.id.phone_number);
|
||||
callStatus = (TextView) itemView.findViewById(R.id.call_status);
|
||||
callDate = (TextView) itemView.findViewById(R.id.call_date);
|
||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
phoneNumber = itemView.findViewById(R.id.phone_number);
|
||||
callStatus = itemView.findViewById(R.id.call_status);
|
||||
callDate = itemView.findViewById(R.id.call_date);
|
||||
dialAOHPCTCSeekBar = itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:35:44
|
||||
* @Describe ContactAdapter
|
||||
*/
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -15,142 +20,111 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.contacts.beans.ContactModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:35:44
|
||||
* @Describe 联系人列表适配器
|
||||
*/
|
||||
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactAdapter";
|
||||
// 移除未使用的 REQUEST_CALL_PHONE 常量,精简冗余代码
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
private List<ContactModel> contactList;
|
||||
Context mContext;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public ContactAdapter(Context context, List<ContactModel> contactList) {
|
||||
LogUtils.d(TAG, "ContactAdapter: 初始化适配器,联系人数量=" + contactList.size());
|
||||
this.mContext = context;
|
||||
mContext = context;
|
||||
this.contactList = contactList;
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建联系人列表项ViewHolder");
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false);
|
||||
return new ContactViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定联系人列表项数据,position=" + position);
|
||||
final ContactModel contact = contactList.get(position);
|
||||
|
||||
// 绑定联系人名称与号码
|
||||
holder.contactName.setText(contact.getName());
|
||||
holder.contactNumber.setText(contact.getNumber());
|
||||
|
||||
// 长按联系人条目弹出操作菜单
|
||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
showContactPopupMenu(holder.llPhoneNumberMain, contact);
|
||||
public boolean onLongClick(View p1) {
|
||||
// 弹出复制菜单
|
||||
PopupMenu menu = new PopupMenu(mContext, holder.llPhoneNumberMain);
|
||||
//加载菜单资源
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
|
||||
//设置点击事件的响应
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int nItemId = menuItem.getItemId();
|
||||
if (nItemId == R.id.item_contact_phonenumber_copy) {
|
||||
// Gets a handle to the clipboard service.
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
// Creates a new text clip to put on the clipboard
|
||||
ClipData clip = ClipData.newPlainText("simple text", contact.getNumber());
|
||||
// Set the clipboard's primary clip.
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
} else if (nItemId == R.id.item_calllog_phonenumber_edit_contact) {
|
||||
//ToastUtils.show("Test");
|
||||
Long nContactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
|
||||
//ToastUtils.show(String.format("%d", nContactId));
|
||||
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), nContactId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
//一定要调用show()来显示弹出式菜单
|
||||
menu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
holder.contactName.setText(contact.getName());
|
||||
holder.contactNumber.setText(contact.getNumber());
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, contact);
|
||||
// 初始化拉动后拨号控件
|
||||
holder.dialAOHPCTCSeekBar.setThumb(holder.itemView.getContext().getDrawable(R.drawable.ic_call));
|
||||
holder.dialAOHPCTCSeekBar.setBlurRightDP(80);
|
||||
holder.dialAOHPCTCSeekBar.setThumbOffset(0);
|
||||
holder.dialAOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
|
||||
ToastUtils.show(phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
holder.itemView.getContext().startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
// 增加空指针判断,避免空列表崩溃
|
||||
return contactList == null ? 0 : contactList.size();
|
||||
return contactList.size();
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 显示联系人操作弹窗菜单
|
||||
*/
|
||||
private void showContactPopupMenu(View anchorView, final ContactModel contact) {
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 弹出联系人操作菜单");
|
||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
|
||||
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.item_contact_phonenumber_copy) {
|
||||
// 复制联系人号码到剪贴板
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("contact_phone", contact.getNumber());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 联系人号码" + contact.getNumber() + "已复制到剪贴板");
|
||||
} else if (itemId == R.id.item_calllog_phonenumber_edit_contact) {
|
||||
// 跳转到编辑联系人页面
|
||||
Long contactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
|
||||
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), contactId);
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 跳转编辑联系人页面,号码=" + contact.getNumber() + ",ID=" + contactId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动拨号SeekBar
|
||||
*/
|
||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final ContactModel contact) {
|
||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
||||
seekBar.setBlurRightDP(80);
|
||||
seekBar.setThumbOffset(0);
|
||||
|
||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
|
||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
||||
ToastUtils.show(phoneNumber);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类 ======================
|
||||
public class ContactViewHolder extends RecyclerView.ViewHolder {
|
||||
LinearLayout llPhoneNumberMain;
|
||||
LinearLayout llPhoneNumberMain;
|
||||
TextView contactName;
|
||||
TextView contactNumber;
|
||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
||||
|
||||
public ContactViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
// Java7 适配:添加强制类型转换
|
||||
llPhoneNumberMain = (LinearLayout) itemView.findViewById(R.id.itemcontactLinearLayout1);
|
||||
contactName = (TextView) itemView.findViewById(R.id.contact_name);
|
||||
contactNumber = (TextView) itemView.findViewById(R.id.contact_number);
|
||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
llPhoneNumberMain = itemView.findViewById(R.id.itemcontactLinearLayout1);
|
||||
contactName = itemView.findViewById(R.id.contact_name);
|
||||
contactNumber = itemView.findViewById(R.id.contact_number);
|
||||
dialAOHPCTCSeekBar = itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 14:20:38
|
||||
* @Describe ImagePagerAdapter
|
||||
*/
|
||||
public class ImagePagerAdapter {
|
||||
|
||||
public static final String TAG = "ImagePagerAdapter";
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 17:27:41
|
||||
* @Describe PhoneConnectRuleAdapter
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -12,230 +17,226 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.views.LeftScrollView;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 17:27:41
|
||||
* @Describe 通话规则列表适配器,支持简单查看/编辑两种视图切换
|
||||
*/
|
||||
public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneConnectRuleAdapter";
|
||||
|
||||
private static final int VIEW_TYPE_SIMPLE = 0;
|
||||
private static final int VIEW_TYPE_EDIT = 1;
|
||||
private static final String NULL_RULE_TEXT = "[NULL]";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<PhoneConnectRuleBean> mRuleList;
|
||||
private Context context;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleBean> ruleList) {
|
||||
LogUtils.d(TAG, "PhoneConnectRuleAdapter: 初始化适配器,规则数量=" + ruleList.size());
|
||||
this.mContext = context;
|
||||
this.mRuleList = ruleList;
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleModel> ruleList) {
|
||||
this.context = context;
|
||||
this.ruleList = ruleList;
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
if (viewType == VIEW_TYPE_SIMPLE) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建简单视图ViewHolder");
|
||||
View view = inflater.inflate(R.layout.view_phone_connect_rule_simple, parent, false);
|
||||
return new SimpleViewHolder(parent, view);
|
||||
} else {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建编辑视图ViewHolder");
|
||||
View view = inflater.inflate(R.layout.view_phone_connect_rule, parent, false);
|
||||
return new EditViewHolder(view);
|
||||
return new EditViewHolder(parent, view);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
|
||||
final PhoneConnectRuleBean model = mRuleList.get(position);
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定规则数据,position=" + position + ",视图类型=" + getItemViewType(position));
|
||||
|
||||
final PhoneConnectRuleModel model = ruleList.get(position);
|
||||
if (holder instanceof SimpleViewHolder) {
|
||||
bindSimpleViewHolder((SimpleViewHolder) holder, model, position);
|
||||
final SimpleViewHolder simpleViewHolder = (SimpleViewHolder) holder;
|
||||
String szView = model.getRuleText().trim().equals("") ?"[NULL]": model.getRuleText();
|
||||
simpleViewHolder.tvRuleText.setText(szView);
|
||||
simpleViewHolder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
simpleViewHolder.checkBoxAllow.setEnabled(false);
|
||||
simpleViewHolder.checkBoxEnable.setChecked(model.isEnable());
|
||||
simpleViewHolder.checkBoxEnable.setEnabled(false);
|
||||
simpleViewHolder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener(){
|
||||
|
||||
@Override
|
||||
public void onUp() {
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
if (position > 0) {
|
||||
ToastUtils.show("onUp");
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
// PhoneConnectRuleModel newBean = new PhoneConnectRuleModel();
|
||||
// newBean.setRuleText(list.get(position).getRuleText());
|
||||
// newBean.setIsAllowConnection(list.get(position).isAllowConnection());
|
||||
// newBean.setIsEnable(list.get(position).isEnable());
|
||||
// newBean.setIsSimpleView(list.get(position).isSimpleView());
|
||||
list.add(position - 1, list.get(position));
|
||||
list.remove(position + 1);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown() {
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
if (position < list.size() - 1) {
|
||||
ToastUtils.show("onDown");
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
// PhoneConnectRuleModel newBean = new PhoneConnectRuleModel();
|
||||
// newBean.setRuleText(list.get(position).getRuleText());
|
||||
// newBean.setIsAllowConnection(list.get(position).isAllowConnection());
|
||||
// newBean.setIsEnable(list.get(position).isEnable());
|
||||
// newBean.setIsSimpleView(list.get(position).isSimpleView());
|
||||
list.add(position + 2, list.get(position));
|
||||
list.remove(position);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEdit() {
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
model.setIsSimpleView(false);
|
||||
notifyDataSetChanged();
|
||||
//notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
YesNoAlertDialog.show(simpleViewHolder.scrollView.getContext(), "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
model.setIsSimpleView(true);
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
list.remove(position);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
//notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
// simpleViewHolder.editButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View v) {
|
||||
// model.setIsSimpleView(false);
|
||||
// notifyItemChanged(position);
|
||||
// }
|
||||
// });
|
||||
// simpleViewHolder.deleteButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View v) {
|
||||
// model.setIsSimpleView(false);
|
||||
// ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
// list.remove(position);
|
||||
// Rules.getInstance(context).saveRules();
|
||||
// notifyItemChanged(position);
|
||||
// }
|
||||
// });
|
||||
// // 触摸事件处理
|
||||
// simpleViewHolder.contentLayout.setOnTouchListener(new View.OnTouchListener() {
|
||||
// @Override
|
||||
// public boolean onTouch(View v, MotionEvent event) {
|
||||
// switch (event.getAction()) {
|
||||
// case MotionEvent.ACTION_DOWN:
|
||||
// simpleViewHolder.startX = event.getX();
|
||||
// simpleViewHolder.isSwiping = true;
|
||||
// break;
|
||||
// case MotionEvent.ACTION_MOVE:
|
||||
// if (simpleViewHolder.isSwiping) {
|
||||
// float deltaX = simpleViewHolder.startX - event.getX();
|
||||
// if (deltaX > 0) { // 左滑
|
||||
// float translationX = Math.max(-simpleViewHolder.actionLayout.getWidth(), -deltaX);
|
||||
// simpleViewHolder.contentLayout.setTranslationX(translationX);
|
||||
// simpleViewHolder.actionLayout.setVisibility(View.VISIBLE);
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// case MotionEvent.ACTION_UP:
|
||||
// simpleViewHolder.isSwiping = false;
|
||||
// if (simpleViewHolder.contentLayout.getTranslationX() < -simpleViewHolder.actionLayout.getWidth() / 2) {
|
||||
// // 保持按钮显示
|
||||
// simpleViewHolder.contentLayout.setTranslationX(-actionLayout.getWidth());
|
||||
// } else {
|
||||
// // 恢复原状
|
||||
// simpleViewHolder.contentLayout.animate().translationX(0).setDuration(200).start();
|
||||
// simpleViewHolder.actionLayout.setVisibility(View.INVISIBLE);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// });
|
||||
} else if (holder instanceof EditViewHolder) {
|
||||
bindEditViewHolder((EditViewHolder) holder, model, position);
|
||||
final EditViewHolder editViewHolder = (EditViewHolder) holder;
|
||||
editViewHolder.editText.setText(model.getRuleText());
|
||||
editViewHolder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
editViewHolder.checkBoxEnable.setChecked(model.isEnable());
|
||||
editViewHolder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
model.setRuleText(editViewHolder.editText.getText().toString());
|
||||
model.setIsAllowConnection(editViewHolder.checkBoxAllow.isChecked());
|
||||
model.setIsEnable(editViewHolder.checkBoxEnable.isChecked());
|
||||
model.setIsSimpleView(true);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyItemChanged(position);
|
||||
Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mRuleList == null ? 0 : mRuleList.size();
|
||||
return ruleList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return mRuleList.get(position).isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
PhoneConnectRuleModel model = ruleList.get(position);
|
||||
// 这里可以根据模型的状态来决定视图类型,简单起见,假设点击按钮后进入编辑视图
|
||||
return model.isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
}
|
||||
|
||||
// ====================== 私有视图绑定方法区 ======================
|
||||
/**
|
||||
* 绑定简单视图数据
|
||||
*/
|
||||
private void bindSimpleViewHolder(final SimpleViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
||||
// 绑定规则文本,空值显示[NULL]
|
||||
String ruleText = model.getRuleText().trim().isEmpty() ? NULL_RULE_TEXT : model.getRuleText().trim();
|
||||
holder.tvRuleText.setText(ruleText);
|
||||
// 设置复选框状态并禁用编辑
|
||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
holder.checkBoxAllow.setEnabled(false);
|
||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
||||
holder.checkBoxEnable.setEnabled(false);
|
||||
|
||||
// 设置左滑操作监听
|
||||
holder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener() {
|
||||
@Override
|
||||
public void onUp() {
|
||||
LogUtils.d(TAG, "onUp: 规则上移,position=" + position);
|
||||
moveRuleUp(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown() {
|
||||
LogUtils.d(TAG, "onDown: 规则下移,position=" + position);
|
||||
moveRuleDown(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEdit() {
|
||||
LogUtils.d(TAG, "onEdit: 切换到编辑视图,position=" + position);
|
||||
model.setIsSimpleView(false);
|
||||
notifyItemChanged(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
LogUtils.d(TAG, "onDelete: 触发规则删除确认,position=" + position);
|
||||
showDeleteConfirmDialog(holder.scrollView.getContext(), model, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定编辑视图数据
|
||||
*/
|
||||
private void bindEditViewHolder(final EditViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
||||
// 绑定规则文本到输入框
|
||||
holder.editText.setText(model.getRuleText());
|
||||
// 绑定复选框状态
|
||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
||||
|
||||
// 确认按钮点击事件
|
||||
holder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String newRuleText = holder.editText.getText().toString().trim();
|
||||
model.setRuleText(newRuleText);
|
||||
model.setIsAllowConnection(holder.checkBoxAllow.isChecked());
|
||||
model.setIsEnable(holder.checkBoxEnable.isChecked());
|
||||
model.setIsSimpleView(true);
|
||||
|
||||
// 保存规则并刷新视图
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyItemChanged(position);
|
||||
Toast.makeText(mContext, "保存成功", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "bindEditViewHolder: 规则保存成功,position=" + position + ",规则内容=" + newRuleText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 私有业务工具方法区 ======================
|
||||
/**
|
||||
* 规则上移
|
||||
*/
|
||||
private void moveRuleUp(int position) {
|
||||
if (position <= 0) {
|
||||
ToastUtils.show("已到顶部,无法上移");
|
||||
return;
|
||||
}
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
swapRulePosition(ruleList, position, position - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则下移
|
||||
*/
|
||||
private void moveRuleDown(int position) {
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
if (position >= ruleList.size() - 1) {
|
||||
ToastUtils.show("已到底部,无法下移");
|
||||
return;
|
||||
}
|
||||
swapRulePosition(ruleList, position, position + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换规则位置
|
||||
*/
|
||||
private void swapRulePosition(ArrayList<PhoneConnectRuleBean> list, int fromPos, int toPos) {
|
||||
PhoneConnectRuleBean temp = list.get(fromPos);
|
||||
list.set(fromPos, list.get(toPos));
|
||||
list.set(toPos, temp);
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "swapRulePosition: 规则位置交换完成,from=" + fromPos + ",to=" + toPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示删除确认弹窗
|
||||
*/
|
||||
private void showDeleteConfirmDialog(Context dialogContext, final PhoneConnectRuleBean model, final int position) {
|
||||
YesNoAlertDialog.show(dialogContext, "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
ruleList.remove(position);
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 规则删除成功,position=" + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 用户取消删除规则,position=" + position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类区 ======================
|
||||
static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||
LeftScrollView scrollView;
|
||||
TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
|
||||
private final LeftScrollView scrollView;
|
||||
private final TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
CheckBox checkBoxEnable;
|
||||
|
||||
|
||||
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
scrollView = (LeftScrollView) itemView.findViewById(R.id.scrollView);
|
||||
// 初始化简单视图内容布局
|
||||
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
|
||||
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
|
||||
tvRuleText = (TextView) viewContent.findViewById(R.id.ruletext_tv);
|
||||
checkBoxAllow = (CheckBox) viewContent.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = (CheckBox) viewContent.findViewById(R.id.checkbox_enable);
|
||||
// 设置内容宽度并添加到滚动视图
|
||||
scrollView = itemView.findViewById(R.id.scrollView);
|
||||
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
|
||||
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
|
||||
tvRuleText = viewContent.findViewById(R.id.ruletext_tv);
|
||||
checkBoxAllow = viewContent.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = viewContent.findViewById(R.id.checkbox_enable);
|
||||
//tvRuleText = new TextView(itemView.getContext());
|
||||
scrollView.setContentWidth(parent.getWidth());
|
||||
//scrollView.setContentWidth(600);
|
||||
scrollView.addContentLayout(viewContent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||
@@ -244,14 +245,17 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
|
||||
CheckBox checkBoxEnable;
|
||||
Button buttonConfirm;
|
||||
|
||||
public EditViewHolder(@NonNull View itemView) {
|
||||
public EditViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
// Java7 适配:添加强制类型转换
|
||||
editText = (EditText) itemView.findViewById(R.id.edit_text);
|
||||
checkBoxAllow = (CheckBox) itemView.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = (CheckBox) itemView.findViewById(R.id.checkbox_enable);
|
||||
buttonConfirm = (Button) itemView.findViewById(R.id.button_confirm);
|
||||
editText = itemView.findViewById(R.id.edit_text);
|
||||
checkBoxAllow = itemView.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = itemView.findViewById(R.id.checkbox_enable);
|
||||
buttonConfirm = itemView.findViewById(R.id.button_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCheckBoxTouchListener(CheckBox checkBox) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:10:57
|
||||
* @Describe CallLogModel
|
||||
*/
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class CallLogModel {
|
||||
public static final String TAG = "CallLogModel";
|
||||
|
||||
private String phoneNumber;
|
||||
private String callStatus;
|
||||
private Date callDate;
|
||||
|
||||
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
|
||||
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
|
||||
this.callStatus = callStatus;
|
||||
this.callDate = callDate;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getCallStatus() {
|
||||
return callStatus;
|
||||
}
|
||||
|
||||
public Date getCallDate() {
|
||||
return callDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人信息数据模型
|
||||
*/
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
||||
|
||||
public class ContactModel {
|
||||
|
||||
public static final String TAG = "ContactModel";
|
||||
|
||||
private String name;
|
||||
private String number;
|
||||
private String pinyin;
|
||||
// 新增:存储姓名的拼音首字母(如"啊牛"→"an")
|
||||
private String pinyinFirstLetter;
|
||||
|
||||
public ContactModel(String name, String number) {
|
||||
this.name = name;
|
||||
this.number = number.replaceAll("\\s", "");
|
||||
this.pinyin = convertToPinyin(name);
|
||||
// 初始化时生成拼音首字母
|
||||
this.pinyinFirstLetter = convertToPinyinFirstLetter(name);
|
||||
}
|
||||
|
||||
// 原方法:转换为全拼(如"啊牛"→"aniu")
|
||||
private String convertToPinyin(String chinese) {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
|
||||
StringBuilder pinyin = new StringBuilder();
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认首选项)
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
pinyin.append(ch); // 非汉字直接拼接(如字母、数字、符号)
|
||||
}
|
||||
}
|
||||
return pinyin.toString();
|
||||
}
|
||||
|
||||
// 新增:转换为拼音首字母(如"啊牛"→"an")
|
||||
private String convertToPinyinFirstLetter(String chinese) {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
|
||||
StringBuilder firstLetters = new StringBuilder();
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
// 取拼音的第一个字母(如"a"、"niu"→"a"、"n")
|
||||
firstLetters.append(pinyinArray[0].charAt(0));
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// 非汉字可根据需求处理:此处保留原字符(如"李3"→"l3","张A"→"za")
|
||||
firstLetters.append(ch);
|
||||
}
|
||||
}
|
||||
return firstLetters.toString();
|
||||
}
|
||||
|
||||
// 新增:获取拼音首字母
|
||||
public String getPinyinFirstLetter() {
|
||||
return pinyinFirstLetter;
|
||||
}
|
||||
|
||||
// 原有getter方法
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getPinyin() {
|
||||
return pinyin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 07:06:13
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class MainServiceBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "MainServiceBean";
|
||||
|
||||
boolean isEnable;
|
||||
|
||||
public MainServiceBean() {
|
||||
this.isEnable = false;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MainServiceBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
MainServiceBean bean = this;
|
||||
jsonWriter.name("isEnable").value(bean.isEnable());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 09:52:10
|
||||
* @Describe 电话黑名单规则
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PhoneConnectRuleModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PhoneConnectRuleModel";
|
||||
|
||||
String ruleText;
|
||||
boolean isAllowConnection;
|
||||
boolean isEnable;
|
||||
boolean isSimpleView;
|
||||
|
||||
public PhoneConnectRuleModel() {
|
||||
this.ruleText = "";
|
||||
this.isAllowConnection = false;
|
||||
this.isEnable = false;
|
||||
this.isSimpleView = true;
|
||||
}
|
||||
|
||||
public PhoneConnectRuleModel(String ruleText, boolean isAllowConnection, boolean isEnable) {
|
||||
this.ruleText = ruleText;
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
this.isEnable = isEnable;
|
||||
this.isSimpleView = true;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setRuleText(String ruleText) {
|
||||
this.ruleText = ruleText;
|
||||
}
|
||||
|
||||
public String getRuleText() {
|
||||
return ruleText;
|
||||
}
|
||||
|
||||
public void setIsAllowConnection(boolean isAllowConnection) {
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
}
|
||||
|
||||
public boolean isAllowConnection() {
|
||||
return isAllowConnection;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return PhoneConnectRuleModel.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("ruleText").value(getRuleText());
|
||||
jsonWriter.name("isAllowConnection").value(isAllowConnection());
|
||||
jsonWriter.name("isEnable").value(isEnable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("ruleText")) {
|
||||
setRuleText(jsonReader.nextString());
|
||||
} else if (name.equals("isAllowConnection")) {
|
||||
setIsAllowConnection(jsonReader.nextBoolean());
|
||||
} else if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/24 18:47:11
|
||||
* @Describe 手机铃声设置参数类
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import android.util.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import android.media.AudioManager;
|
||||
import android.util.JsonReader;
|
||||
|
||||
public class RingTongBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "AudioRingTongBean";
|
||||
|
||||
// 铃声音量
|
||||
int streamVolume;
|
||||
|
||||
public RingTongBean() {
|
||||
this.streamVolume = 100;
|
||||
}
|
||||
|
||||
public RingTongBean(int streamVolume) {
|
||||
this.streamVolume = streamVolume;
|
||||
}
|
||||
|
||||
public void setStreamVolume(int streamVolume) {
|
||||
this.streamVolume = streamVolume;
|
||||
}
|
||||
|
||||
public int getStreamVolume() {
|
||||
return streamVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return RingTongBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("streamVolume").value(getStreamVolume());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("streamVolume")) {
|
||||
setStreamVolume(jsonReader.nextInt());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 19:51:40
|
||||
* @Describe SettingsModel
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
|
||||
public class SettingsModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "SettingsModel";
|
||||
|
||||
public static final int MAX_INTRANGE = 666666;
|
||||
public static final int MIN_INTRANGE = 1;
|
||||
|
||||
// 云盾防御层数量
|
||||
int dunTotalCount;
|
||||
// 当前云盾防御层
|
||||
int dunCurrentCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
int dunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
int dunResumeCount;
|
||||
// 是否启用云盾
|
||||
boolean isEnableDun;
|
||||
// BoBullToon 应用模块数据请求地址
|
||||
String szBoBullToon_URL;
|
||||
|
||||
public SettingsModel() {
|
||||
this.dunTotalCount = 6;
|
||||
this.dunCurrentCount = 6;
|
||||
this.dunResumeSecondCount = 60;
|
||||
this.dunResumeCount = 1;
|
||||
this.isEnableDun = false;
|
||||
this.szBoBullToon_URL = "";
|
||||
}
|
||||
|
||||
public SettingsModel(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount, int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
this.isEnableDun = isEnableDun;
|
||||
this.szBoBullToon_URL = szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setBoBullToon_URL(String boBullToon_URL) {
|
||||
this.szBoBullToon_URL = boBullToon_URL;
|
||||
}
|
||||
|
||||
public String getBoBullToon_URL() {
|
||||
return szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setDunTotalCount(int dunTotalCount) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
}
|
||||
|
||||
public int getDunTotalCount() {
|
||||
return dunTotalCount;
|
||||
}
|
||||
|
||||
public void setDunCurrentCount(int dunCurrentCount) {
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
}
|
||||
|
||||
public int getDunCurrentCount() {
|
||||
return dunCurrentCount;
|
||||
}
|
||||
|
||||
public void setDunResumeSecondCount(int dunResumeSecondCount) {
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
}
|
||||
|
||||
public int getDunResumeSecondCount() {
|
||||
return dunResumeSecondCount;
|
||||
}
|
||||
|
||||
public void setDunResumeCount(int dunResumeCount) {
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
}
|
||||
|
||||
public int getDunResumeCount() {
|
||||
return dunResumeCount;
|
||||
}
|
||||
|
||||
public void setIsEnableDun(boolean isEnableDun) {
|
||||
this.isEnableDun = isEnableDun;
|
||||
}
|
||||
|
||||
public boolean isEnableDun() {
|
||||
return isEnableDun;
|
||||
}
|
||||
|
||||
int getSettingsModelRangeInt(int origin) {
|
||||
return IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return SettingsModel.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("dunTotalCount").value(getDunTotalCount());
|
||||
jsonWriter.name("dunCurrentCount").value(getDunCurrentCount());
|
||||
jsonWriter.name("dunResumeSecondCount").value(getDunResumeSecondCount());
|
||||
jsonWriter.name("dunResumeCount").value(getDunResumeCount());
|
||||
jsonWriter.name("isEnableDun").value(isEnableDun());
|
||||
jsonWriter.name("szBoBullToon_URL").value(getBoBullToon_URL());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("dunTotalCount")) {
|
||||
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunCurrentCount")) {
|
||||
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunResumeSecondCount")) {
|
||||
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunResumeCount")) {
|
||||
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("isEnableDun")) {
|
||||
setIsEnableDun(jsonReader.nextBoolean());
|
||||
} else if (name.equals("szBoBullToon_URL")) {
|
||||
setBoBullToon_URL(jsonReader.nextString());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import android.content.Context;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
@@ -1,66 +1,46 @@
|
||||
package cc.winboll.studio.contacts.dun;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 06:15:10
|
||||
* @Describe 云盾防御规则
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.contacts.utils.RegexPPiUtils;
|
||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.regex.Pattern;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 06:15:10
|
||||
* @Describe 云盾防御规则(双重校验锁单例模式)
|
||||
*/
|
||||
public class Rules {
|
||||
|
||||
public static final String TAG = "Rules";
|
||||
|
||||
// 单例核心:volatile 保证多线程可见性,禁止指令重排
|
||||
private static volatile Rules sInstance;
|
||||
// 上下文需使用 ApplicationContext 避免内存泄漏
|
||||
private static Context sApplicationContext;
|
||||
|
||||
ArrayList<PhoneConnectRuleBean> _PhoneConnectRuleModelList;
|
||||
ArrayList<PhoneConnectRuleModel> _PhoneConnectRuleModelList;
|
||||
static volatile Rules _Rules;
|
||||
Context mContext;
|
||||
SettingsBean mSettingsModel;
|
||||
SettingsModel mSettingsModel;
|
||||
Timer mDunResumeTimer;
|
||||
|
||||
/**
|
||||
* 私有化构造方法,禁止外部 new 实例
|
||||
*/
|
||||
private Rules(Context context) {
|
||||
mContext = context.getApplicationContext();
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleBean>();
|
||||
Rules(Context context) {
|
||||
mContext = context;
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleModel>();
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,线程安全)
|
||||
* @param context 上下文,建议传入 ApplicationContext
|
||||
* @return Rules 唯一实例
|
||||
*/
|
||||
public static Rules getInstance(Context context) {
|
||||
// 第一次校验:无锁,提高性能
|
||||
if (sInstance == null) {
|
||||
// 加锁:保证多线程下仅初始化一次
|
||||
synchronized (Rules.class) {
|
||||
// 第二次校验:防止多线程并发时重复创建
|
||||
if (sInstance == null) {
|
||||
sInstance = new Rules(context);
|
||||
}
|
||||
}
|
||||
public static synchronized Rules getInstance(Context context) {
|
||||
if (_Rules == null) {
|
||||
_Rules = new Rules(context);
|
||||
}
|
||||
return sInstance;
|
||||
return _Rules;
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
@@ -77,35 +57,32 @@ public class Rules {
|
||||
|
||||
// 盾牌恢复定时器
|
||||
mDunResumeTimer = new Timer();
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsBean.MIN_INTRANGE, SettingsBean.MAX_INTRANGE);
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsModel.MIN_INTRANGE, SettingsModel.MAX_INTRANGE);
|
||||
mDunResumeTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
|
||||
LogUtils.d(TAG, String.format("当前防御值为%d,最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
|
||||
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
|
||||
// 设置盾值在[0,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
||||
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
|
||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
||||
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
|
||||
saveDun();
|
||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
||||
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
}
|
||||
}
|
||||
}, 1000, ss);
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
|
||||
LogUtils.d(TAG, String.format("当前防御值为%d,最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
|
||||
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
|
||||
// 设置盾值在[0,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
||||
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
|
||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
||||
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
}
|
||||
}
|
||||
}, 1000, ss);
|
||||
}
|
||||
|
||||
public void loadRules() {
|
||||
_PhoneConnectRuleModelList.clear();
|
||||
PhoneConnectRuleBean.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
PhoneConnectRuleModel.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
}
|
||||
|
||||
public void saveRules() {
|
||||
LogUtils.d(TAG, String.format("saveRules()"));
|
||||
PhoneConnectRuleBean.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
PhoneConnectRuleModel.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
}
|
||||
|
||||
public void resetDefaultBoBullToonURL() {
|
||||
@@ -123,16 +100,16 @@ public class Rules {
|
||||
}
|
||||
|
||||
public void loadDun() {
|
||||
mSettingsModel = SettingsBean.loadBean(mContext, SettingsBean.class);
|
||||
mSettingsModel = SettingsModel.loadBean(mContext, SettingsModel.class);
|
||||
if (mSettingsModel == null) {
|
||||
mSettingsModel = new SettingsBean();
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
mSettingsModel = new SettingsModel();
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveDun() {
|
||||
LogUtils.d(TAG, String.format("saveDun()"));
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
|
||||
public boolean isAllowed(String phoneNumber) {
|
||||
@@ -142,7 +119,8 @@ public class Rules {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 云盾防御体系
|
||||
//
|
||||
// 以下是云盾防御体系
|
||||
boolean isDefend = false; // 盾牌是否生效
|
||||
boolean isConnect = true; // 防御结果是否连接
|
||||
|
||||
@@ -211,7 +189,10 @@ public class Rules {
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
} else if (isDefend) {
|
||||
// 如果触发了以上某个防御模块,减少防御盾牌层数
|
||||
// 如果触发了以上某个防御模块,
|
||||
// 就减少防御盾牌层数。
|
||||
// 每校验一次规则,云盾防御层数减1
|
||||
// 当云盾防御层数为0时,再次进行以下程序段则恢复满值防御。
|
||||
int newDunCount = nDunCurrentCount;
|
||||
LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount));
|
||||
|
||||
@@ -222,7 +203,7 @@ public class Rules {
|
||||
} else {
|
||||
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
|
||||
LogUtils.d(TAG, String.format("盾值不在[0,%d]区间,恢复防御最大值%d", mSettingsModel.getDunTotalCount(), mSettingsModel.getDunTotalCount()));
|
||||
}
|
||||
}
|
||||
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
@@ -230,35 +211,18 @@ public class Rules {
|
||||
|
||||
// 返回校验结果
|
||||
LogUtils.d(TAG, String.format("返回校验结果 isConnect == %s", isConnect));
|
||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
||||
|
||||
return isConnect;
|
||||
}
|
||||
|
||||
public void add(String szPhoneConnectRule, boolean isAllowConnection, boolean isEnable) {
|
||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleBean(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleModel(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
}
|
||||
|
||||
public ArrayList<PhoneConnectRuleBean> getPhoneBlacRuleBeanList() {
|
||||
public ArrayList<PhoneConnectRuleModel> getPhoneBlacRuleBeanList() {
|
||||
return _PhoneConnectRuleModelList;
|
||||
}
|
||||
|
||||
public SettingsBean getSettingsModel() {
|
||||
public SettingsModel getSettingsModel() {
|
||||
return mSettingsModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选:释放单例资源(如退出应用时调用)
|
||||
*/
|
||||
public static void releaseInstance() {
|
||||
if (sInstance != null) {
|
||||
sInstance.mDunResumeTimer.cancel();
|
||||
sInstance._PhoneConnectRuleModelList.clear();
|
||||
sInstance.mSettingsModel = null;
|
||||
sInstance.mContext = null;
|
||||
sInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:57:00
|
||||
* @Describe 拨号
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
@@ -19,48 +24,42 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.CallLogAdapter;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.contacts.beans.CallLogModel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:57:00
|
||||
* @Describe 通话记录区域视图(支持懒加载,仅切换到当前页才加载数据)
|
||||
*/
|
||||
public class CallLogFragment extends Fragment {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogFragment";
|
||||
public static final int MSG_UPDATE = 1;
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
||||
public static final String TAG = "CallFragment";
|
||||
|
||||
// ====================== 静态成员区 ======================
|
||||
static volatile CallLogFragment _CallLogFragment;
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
public static final int MSG_UPDATE = 1; // 添加消息常量
|
||||
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private int mPage;
|
||||
|
||||
// ====================== UI控件与适配器区 ======================
|
||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
||||
private RecyclerView recyclerView;
|
||||
private CallLogAdapter callLogAdapter;
|
||||
private List<CallLogModel> callLogList = new ArrayList<CallLogModel>();
|
||||
private List<CallLogModel> callLogList = new ArrayList<>();
|
||||
|
||||
// ====================== 业务逻辑成员区 ======================
|
||||
private Handler mHandler;
|
||||
// 懒加载标记:记录当前Fragment是否已初始化数据(避免重复加载)
|
||||
private boolean isDataInited = false;
|
||||
// 添加Handler
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
readCallLog(); // 接收到消息时更新通话记录
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 单例与实例化函数区 ======================
|
||||
CallLogFragment() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static CallLogFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建通话记录Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
CallLogFragment fragment = new CallLogFragment();
|
||||
@@ -69,159 +68,67 @@ public class CallLogFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
// Java7 兼容:移除Lambda,使用匿名内部类初始化Handler
|
||||
mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到更新消息,开始读取通话记录");
|
||||
readCallLog();
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
return inflater.inflate(R.layout.fragment_call_log, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
LogUtils.d(TAG, "onViewCreated: 视图创建完成,仅初始化控件(不加载数据)");
|
||||
// 初始化RecyclerView(仅绑定控件、设置布局管理器,不设置数据/发起请求)
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
|
||||
recyclerView = view.findViewById(R.id.recyclerView);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
// 初始化适配器(传入空列表,后续懒加载时更新数据)
|
||||
callLogAdapter = new CallLogAdapter(getContext(), callLogList);
|
||||
recyclerView.setAdapter(callLogAdapter);
|
||||
LogUtils.d(TAG, "onViewCreated: RecyclerView控件初始化完成(未加载数据)");
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
|
||||
} else {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE); // 通过Handler触发更新
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已初始化过数据 → 仅刷新(避免重复初始化,优化性能)
|
||||
if (isDataInited && callLogAdapter != null) {
|
||||
LogUtils.d(TAG, "onResume: 数据已初始化,仅刷新列表");
|
||||
callLogAdapter.relaodContacts();
|
||||
readCallLog(); // 刷新最新通话记录
|
||||
LogUtils.d(TAG, "onResume: 通话记录数据刷新完成");
|
||||
}
|
||||
// 未初始化 → 不操作(等待MainActivity调用initData触发初始化)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
if (mHandler != null) {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDestroy: Handler消息已清空");
|
||||
}
|
||||
// 释放资源,避免内存泄漏
|
||||
if (callLogList != null) {
|
||||
callLogList.clear();
|
||||
callLogList = null;
|
||||
}
|
||||
callLogAdapter = null;
|
||||
recyclerView = null;
|
||||
_CallLogFragment = null;
|
||||
isDataInited = false;
|
||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 权限回调函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_READ_CALL_LOG) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 通话记录权限授予成功,开始加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 通话记录权限被拒绝,无法加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE); // 通过Handler触发更新
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 避免重复初始化(双重防护:标记+判断)
|
||||
if (isDataInited || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 数据已初始化/上下文为空,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化通话记录数据");
|
||||
// 权限检查与数据加载(原onViewCreated中的核心逻辑迁移至此)
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "initData: 读取通话记录权限未授予,发起权限申请");
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
|
||||
} else {
|
||||
LogUtils.d(TAG, "initData: 权限已授予,发送更新消息加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
// 标记为已初始化(后续仅刷新,不重复初始化)
|
||||
isDataInited = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化流程完成");
|
||||
}
|
||||
|
||||
// ====================== 业务核心函数区 ======================
|
||||
private void readCallLog() {
|
||||
LogUtils.d(TAG, "readCallLog: 开始读取系统通话记录");
|
||||
// 避免空指针(懒加载场景下,控件可能未初始化完成)
|
||||
if (callLogList == null || callLogAdapter == null || getContext() == null) {
|
||||
LogUtils.w(TAG, "readCallLog: 控件/列表为空,跳过读取");
|
||||
return;
|
||||
}
|
||||
callLogList.clear();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = requireContext().getContentResolver().query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC"
|
||||
);
|
||||
if (cursor != null) {
|
||||
LogUtils.d(TAG, "readCallLog: 成功获取通话记录游标,数据条数=" + cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
|
||||
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
|
||||
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
|
||||
Date callDate = new Date(callDateLong);
|
||||
String callStatus = getCallStatus(callType);
|
||||
callLogList.clear(); // 清空原有数据
|
||||
Cursor cursor = requireContext().getContentResolver().query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC");
|
||||
|
||||
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
|
||||
}
|
||||
callLogAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "readCallLog: 通话记录数据解析完成,共" + callLogList.size() + "条");
|
||||
} else {
|
||||
LogUtils.w(TAG, "readCallLog: 通话记录游标为空");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readCallLog: 读取通话记录异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, "readCallLog: 游标已关闭");
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
|
||||
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
|
||||
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
|
||||
Date callDate = new Date(callDateLong);
|
||||
|
||||
String callStatus = getCallStatus(callType);
|
||||
|
||||
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
|
||||
}
|
||||
cursor.close();
|
||||
callLogAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,21 +145,27 @@ public class CallLogFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 外部调用函数区 ======================
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mHandler.removeCallbacksAndMessages(null); // 清理Handler防止内存泄漏
|
||||
}
|
||||
|
||||
public void triggerUpdate() {
|
||||
LogUtils.d(TAG, "triggerUpdate: 外部触发通话记录更新");
|
||||
if (isDataInited) { // 已初始化才触发更新(避免未加载时调用)
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
|
||||
public static void updateCallLogFragment() {
|
||||
if (_CallLogFragment != null) {
|
||||
LogUtils.d(TAG, "updateCallLogFragment: 静态方法触发Fragment更新");
|
||||
_CallLogFragment.triggerUpdate();
|
||||
} else {
|
||||
LogUtils.w(TAG, "updateCallLogFragment: Fragment实例为空,无法更新");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//ToastUtils.show("onResume");
|
||||
callLogAdapter.relaodContacts();
|
||||
readCallLog(); // 窗口回显时更新通话记录
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人视图
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -23,55 +29,42 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.ContactAdapter;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.contacts.beans.ContactModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人区域视图(支持懒加载,仅切换到当前页才加载数据)
|
||||
*/
|
||||
public class ContactsFragment extends Fragment {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactsFragment";
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private static final int REQUEST_READ_CONTACTS = 1;
|
||||
private static final long DEBOUNCE_DELAY = 300; // 搜索防抖延迟
|
||||
|
||||
// ====================== 静态缓存区 ======================
|
||||
// 全局复用联系人数据,减少重复查询
|
||||
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
|
||||
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
private boolean isViewInitialized = false; // 视图初始化标记(控件绑定完成)
|
||||
private boolean isDataLoaded = false; // 数据加载标记(数据+功能初始化完成)
|
||||
private boolean isLazyInitCompleted = false; // 懒加载总标记(供MainActivity判断)
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private RecyclerView recyclerView;
|
||||
private ContactAdapter contactAdapter;
|
||||
private EditText searchEditText;
|
||||
private Button btnDial;
|
||||
private boolean isViewInitialized = false; // 标记视图是否已初始化
|
||||
|
||||
// ====================== 数据容器区 ======================
|
||||
// 静态缓存:全局复用联系人数据
|
||||
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
|
||||
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
|
||||
|
||||
// 当前页面数据容器
|
||||
private List<ContactModel> contactList = new ArrayList<ContactModel>();
|
||||
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
|
||||
|
||||
// ====================== 异步工具区 ======================
|
||||
// 异步工具
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean isDataLoaded = false;
|
||||
|
||||
|
||||
// ====================== 实例化函数区 ======================
|
||||
public static ContactsFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建联系人Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
ContactsFragment fragment = new ContactsFragment();
|
||||
@@ -79,154 +72,61 @@ public class ContactsFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
return inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
// 加载布局(已移除进度条相关代码)
|
||||
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
LogUtils.d(TAG, "onViewCreated: 开始初始化UI控件(仅绑定,不加载数据/功能)");
|
||||
// 初始化RecyclerView(仅绑定控件、设适配器,隐藏列表)
|
||||
// 初始化RecyclerView
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
contactList = new ArrayList<ContactModel>();
|
||||
contactAdapter = new ContactAdapter(getActivity(), contactList);
|
||||
recyclerView.setAdapter(contactAdapter);
|
||||
// 初始隐藏列表,数据加载后显示
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
|
||||
// 绑定搜索框和拨号按钮(仅赋值,不显示、不绑定事件)
|
||||
// 绑定搜索框和拨号按钮
|
||||
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
|
||||
btnDial = (Button) view.findViewById(R.id.btn_dial);
|
||||
// 初始隐藏搜索相关控件,延迟到首次可见时显示
|
||||
searchEditText.setVisibility(View.GONE);
|
||||
btnDial.setVisibility(View.GONE);
|
||||
|
||||
// 标记视图控件绑定完成
|
||||
isViewInitialized = true;
|
||||
LogUtils.d(TAG, "onViewCreated: UI控件初始化完成(未加载数据/功能)");
|
||||
}
|
||||
|
||||
// 首次可见时初始化资源
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已完成懒加载 → 仅恢复缓存数据(切回页面时刷新)
|
||||
if (isLazyInitCompleted && isDataLoaded) {
|
||||
LogUtils.d(TAG, "onResume: 懒加载已完成,恢复缓存数据");
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
executor.shutdown(); // 关闭线程池
|
||||
mainHandler.removeCallbacksAndMessages(null); // 清空Handler任务
|
||||
// 释放本地数据引用(保留静态缓存,全局复用)
|
||||
if (contactList != null) {
|
||||
contactList.clear();
|
||||
contactList = null;
|
||||
}
|
||||
if (originalContactList != null) {
|
||||
originalContactList.clear();
|
||||
originalContactList = null;
|
||||
}
|
||||
// 重置标记
|
||||
isViewInitialized = false;
|
||||
isDataLoaded = false;
|
||||
isLazyInitCompleted = false;
|
||||
LogUtils.d(TAG, "onDestroy: 异步工具+本地资源已释放");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
LogUtils.d(TAG, "onHiddenChanged: Fragment隐藏状态变更,hidden=" + hidden);
|
||||
// 已完成懒加载+显示状态 → 恢复缓存数据(兼容Tab切换场景)
|
||||
if (!hidden && isLazyInitCompleted && isDataLoaded) {
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "onHiddenChanged: 恢复缓存数据,列表已显示");
|
||||
if (!isViewInitialized) {
|
||||
initSearchAndDial(); // 初始化搜索和拨号功能
|
||||
checkContactPermission(); // 检查权限并加载数据
|
||||
isViewInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 权限相关函数区 ======================
|
||||
private void checkContactPermission() {
|
||||
LogUtils.d(TAG, "checkContactPermission: 检查联系人读取权限");
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "checkContactPermission: 权限未授予,发起申请");
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||
} else {
|
||||
LogUtils.d(TAG, "checkContactPermission: 权限已授予,开始加载数据");
|
||||
loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限回调触发,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_READ_CONTACTS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 联系人权限授予成功");
|
||||
loadContacts();
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 联系人权限被拒绝");
|
||||
ToastUtils.show("请授予联系人权限以查看联系人列表");
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
// 权限拒绝也标记懒加载完成(避免重复触发)
|
||||
isLazyInitCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 双重防护:避免重复初始化(标记+视图就绪判断)
|
||||
if (isLazyInitCompleted || !isViewInitialized || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化(功能+数据)");
|
||||
// 1. 初始化搜索、拨号功能(原onResume首次进入逻辑迁移至此)
|
||||
initSearchAndDial();
|
||||
// 2. 检查权限+加载数据(原onResume首次进入逻辑迁移至此)
|
||||
checkContactPermission();
|
||||
// 标记懒加载总流程完成(无论权限是否授予,仅执行一次)
|
||||
isLazyInitCompleted = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化流程启动完成");
|
||||
}
|
||||
|
||||
// ====================== UI功能初始化区 ======================
|
||||
// 初始化搜索框和拨号按钮
|
||||
private void initSearchAndDial() {
|
||||
LogUtils.d(TAG, "initSearchAndDial: 初始化搜索和拨号功能");
|
||||
// 显示控件
|
||||
// 显示搜索相关控件
|
||||
searchEditText.setVisibility(View.VISIBLE);
|
||||
btnDial.setVisibility(View.VISIBLE);
|
||||
|
||||
// 搜索防抖监听
|
||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(DEBOUNCE_DELAY) {
|
||||
// 搜索框防抖监听
|
||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(300) {
|
||||
@Override
|
||||
public void onDebounceTextChanged(String query) {
|
||||
filterContacts(query);
|
||||
@@ -242,58 +142,68 @@ public class ContactsFragment extends Fragment {
|
||||
ToastUtils.show("请输入号码");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initSearchAndDial: 发起拨号,号码=" + phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initSearchAndDial: 功能初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 数据加载与处理区 ======================
|
||||
// 权限检查
|
||||
private void checkContactPermission() {
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||
} else {
|
||||
loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载联系人(延迟到首次可见时)
|
||||
private void loadContacts() {
|
||||
// 优先使用缓存数据(保留原有缓存逻辑,提升性能)
|
||||
// 若有缓存,直接复用
|
||||
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
|
||||
LogUtils.d(TAG, "loadContacts: 存在缓存数据,直接复用");
|
||||
originalContactList.clear();
|
||||
originalContactList.addAll(sCachedOriginalList);
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
recyclerView.setVisibility(View.VISIBLE); // 显示列表
|
||||
isDataLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 无缓存时异步加载(保留原有异步逻辑,避免主线程阻塞)
|
||||
// 无缓存时异步加载
|
||||
if (!isDataLoaded) {
|
||||
LogUtils.d(TAG, "loadContacts: 无缓存,异步读取联系人数据");
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
recyclerView.setVisibility(View.GONE); // 加载中隐藏列表
|
||||
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 子线程读取联系人
|
||||
final List<ContactModel> tempList = readContactsInBackground();
|
||||
// 主线程更新UI和缓存
|
||||
|
||||
// 主线程更新UI
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 更新缓存
|
||||
sCachedOriginalList.clear();
|
||||
sCachedOriginalList.addAll(tempList);
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(tempList);
|
||||
|
||||
// 更新当前列表
|
||||
originalContactList.clear();
|
||||
originalContactList.addAll(sCachedOriginalList);
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, String.format("联系人加载完成,共%d条数据", contactList.size()));
|
||||
|
||||
// 数据加载后显示列表
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
isDataLoaded = true;
|
||||
|
||||
LogUtils.d(TAG, "loadContacts: 联系人数据加载完成,共" + contactList.size() + "条");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -301,11 +211,12 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// 子线程读取联系人
|
||||
private List<ContactModel> readContactsInBackground() {
|
||||
LogUtils.d(TAG, "readContactsInBackground: 子线程读取联系人");
|
||||
List<ContactModel> tempList = new ArrayList<ContactModel>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
// 查询联系人姓名和号码
|
||||
cursor = requireContext().getContentResolver().query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
new String[]{
|
||||
@@ -320,52 +231,66 @@ public class ContactsFragment extends Fragment {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
|
||||
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
|
||||
|
||||
do {
|
||||
String name = cursor.getString(nameIndex);
|
||||
String number = cursor.getString(numberIndex).replaceAll("\\s", "");
|
||||
String number = cursor.getString(numberIndex).replaceAll("\\s", ""); // 去除空格
|
||||
tempList.add(new ContactModel(name, number));
|
||||
} while (cursor.moveToNext());
|
||||
LogUtils.d(TAG, "readContactsInBackground: 成功读取" + tempList.size() + "条联系人数据");
|
||||
} else {
|
||||
LogUtils.w(TAG, "readContactsInBackground: 未读取到联系人数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readContactsInBackground: 读取联系人异常", e);
|
||||
LogUtils.d(TAG, "读取联系人失败:" + e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, "readContactsInBackground: 游标已关闭");
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
}
|
||||
}
|
||||
return tempList;
|
||||
}
|
||||
|
||||
// 过滤联系人
|
||||
private void filterContacts(String query) {
|
||||
LogUtils.d(TAG, "filterContacts: 搜索过滤,关键词=" + query);
|
||||
contactList.clear();
|
||||
sCachedFilteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
contactList.addAll(originalContactList);
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(originalContactList);
|
||||
} else {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
for (ContactModel contact : originalContactList) {
|
||||
// 匹配姓名、全拼、简拼、号码
|
||||
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
|
||||
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
|
||||
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
|
||||
boolean matchNumber = contact.getNumber().contains(lowerQuery);
|
||||
|
||||
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
|
||||
contactList.add(contact);
|
||||
}
|
||||
}
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(contactList);
|
||||
}
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
// 过滤后确保列表可见
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "filterContacts: 过滤完成,显示" + contactList.size() + "条数据");
|
||||
}
|
||||
|
||||
// ====================== 内部防抖监听类 ======================
|
||||
// 权限回调
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_READ_CONTACTS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadContacts(); // 授权后加载联系人
|
||||
} else {
|
||||
ToastUtils.show("请授予联系人权限以查看联系人列表");
|
||||
recyclerView.setVisibility(View.VISIBLE); // 显示空列表
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖TextWatcher(Java 7实现)
|
||||
public abstract static class DebounceTextWatcher implements TextWatcher {
|
||||
private final long debounceDelay;
|
||||
private Handler handler = new Handler(Looper.getMainLooper());
|
||||
@@ -376,13 +301,17 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// 无需处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, int start, int before, int count) {
|
||||
// 移除之前的延迟任务
|
||||
if (pendingRunnable != null) {
|
||||
handler.removeCallbacks(pendingRunnable);
|
||||
}
|
||||
// 延迟执行过滤
|
||||
pendingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -393,9 +322,33 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 无需处理
|
||||
}
|
||||
|
||||
// 抽象方法:防抖后的回调
|
||||
public abstract void onDebounceTextChanged(String query);
|
||||
}
|
||||
|
||||
// 资源释放
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
executor.shutdown(); // 关闭线程池
|
||||
mainHandler.removeCallbacksAndMessages(null); // 清除未执行任务
|
||||
}
|
||||
|
||||
// Fragment隐藏/显示时的处理
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden && isDataLoaded) {
|
||||
// 复用缓存数据并显示列表
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:58:15
|
||||
* @Describe 应用日志
|
||||
*/
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -8,34 +13,18 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:58:15
|
||||
* @Describe 应用日志区域视图(支持懒加载,仅切换到当前页才启动日志)
|
||||
*/
|
||||
public class LogFragment extends Fragment {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "LogFragment";
|
||||
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
|
||||
LogView mLogView;
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private LogView mLogView;
|
||||
|
||||
// ====================== 懒加载标记区 ======================
|
||||
private boolean isViewInitialized = false; // 视图控件绑定完成标记
|
||||
private boolean isLazyInitCompleted = false; // 懒加载总流程完成标记
|
||||
private boolean isLogViewStarted = false; // LogView启动状态标记
|
||||
|
||||
// ====================== 实例化函数区 ======================
|
||||
public static LogFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建日志Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
LogFragment fragment = new LogFragment();
|
||||
@@ -43,76 +32,30 @@ public class LogFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
View view = inflater.inflate(R.layout.fragment_log, container, false);
|
||||
// Java7 适配:添加强制类型转换,仅初始化LogView控件(不启动)
|
||||
mLogView = (LogView) view.findViewById(R.id.logview);
|
||||
LogUtils.d(TAG, "onCreateView: LogView控件初始化完成(未启动)");
|
||||
// 标记视图控件绑定完成
|
||||
isViewInitialized = true;
|
||||
mLogView = view.findViewById(R.id.logview);
|
||||
mLogView.start();
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已完成懒加载 → 仅重启LogView(切回页面时恢复日志显示)
|
||||
if (isLazyInitCompleted && mLogView != null && !isLogViewStarted) {
|
||||
mLogView.start();
|
||||
isLogViewStarted = true;
|
||||
LogUtils.d(TAG, "onResume: LogView已重启,恢复日志显示");
|
||||
}
|
||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
if (mLogView != null) {
|
||||
// 若LogView有停止方法,必须调用(避免后台持续占用资源,根据实际API调整)
|
||||
// mLogView.stop(); // 关键:释放LogView资源,防止内存泄漏
|
||||
LogUtils.d(TAG, "onDestroy: LogView资源已释放");
|
||||
}
|
||||
// 重置所有标记,避免重建时状态异常
|
||||
mLogView = null;
|
||||
isViewInitialized = false;
|
||||
isLazyInitCompleted = false;
|
||||
isLogViewStarted = false;
|
||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 双重防护:避免重复初始化(标记+视图就绪+控件非空)
|
||||
if (isLazyInitCompleted || !isViewInitialized || mLogView == null || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化,启动LogView");
|
||||
// 核心:启动LogView(原onCreateView中的start逻辑迁移至此)
|
||||
//ToastUtils.show("onResume");
|
||||
mLogView.start();
|
||||
isLogViewStarted = true;
|
||||
// 标记懒加载总流程完成(仅执行一次)
|
||||
isLazyInitCompleted = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化完成,LogView正常启动");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
@@ -16,377 +14,198 @@ import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.Nullable;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity;
|
||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Describe 通话监听服务(无前台服务),负责监听通话状态、显示通话悬浮窗、跳转通话界面
|
||||
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 避免内存泄漏
|
||||
*/
|
||||
|
||||
public class CallListenerService extends Service {
|
||||
|
||||
// ====================== 常量定义区(精准适配API29-30,无冗余) ======================
|
||||
public static final String TAG = "CallListenerService";
|
||||
private View phoneCallView;
|
||||
private TextView tvCallNumber;
|
||||
private Button btnOpenApp;
|
||||
|
||||
// Android版本常量(仅保留适配必需版本,精简无用定义)
|
||||
private static final int ANDROID_8_API = 26; // 悬浮窗类型适配(API26+必需)
|
||||
private static final int ANDROID_10_API = 29; // API29+ 悬浮窗权限/参数适配
|
||||
private static final int ANDROID_19_API = 19; // 透明状态栏/导航栏适配
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams params;
|
||||
|
||||
// 延迟初始化参数(让出主线程,避免启动阻塞)
|
||||
private static final long DELAY_INIT_MS = 100L;
|
||||
private PhoneStateListener phoneStateListener;
|
||||
private TelephonyManager telephonyManager;
|
||||
|
||||
// ====================== 成员属性区(按功能归类,命名规范) ======================
|
||||
// 延迟初始化核心
|
||||
private Handler mDelayHandler; // 延迟处理器(避免onCreate阻塞)
|
||||
private String callNumber;
|
||||
private boolean hasShown;
|
||||
private boolean isCallingIn;
|
||||
|
||||
// 通话监听核心
|
||||
private TelephonyManager mTelephonyManager; // 电话管理器(监听通话状态)
|
||||
private PhoneStateListener mPhoneStateListener;// 通话状态监听回调
|
||||
private String mCallNumber; // 当前通话号码
|
||||
private boolean mIsCallingIn; // 是否为来电(true=来电,false=去电)
|
||||
|
||||
// 悬浮窗核心
|
||||
private WindowManager mWindowManager; // 窗口管理器(添加/移除悬浮窗)
|
||||
private WindowManager.LayoutParams mWindowParams;// 悬浮窗参数配置
|
||||
private View mPhoneCallView; // 通话悬浮窗根视图
|
||||
private TextView mTvCallNumber; // 悬浮窗号码显示控件
|
||||
private Button mBtnOpenApp; // 悬浮窗跳转APP按钮
|
||||
private boolean mHasShown; // 悬浮窗显示状态标记(避免重复操作)
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 =====");
|
||||
|
||||
// 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度)
|
||||
initDelayHandlerAndLogic();
|
||||
initPhoneStateListener();
|
||||
|
||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动完成 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId);
|
||||
|
||||
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
|
||||
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
|
||||
return startMode;
|
||||
initPhoneCallView();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务无需绑定,返回null");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化来电状态监听器
|
||||
*/
|
||||
private void initPhoneStateListener() {
|
||||
phoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
|
||||
callNumber = incomingNumber;
|
||||
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE: // 待机,即无电话时,挂断时触发
|
||||
dismiss();
|
||||
break;
|
||||
|
||||
case TelephonyManager.CALL_STATE_RINGING: // 响铃,来电时触发
|
||||
isCallingIn = true;
|
||||
updateUI();
|
||||
show();
|
||||
break;
|
||||
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK: // 摘机,接听或拨出电话时触发
|
||||
updateUI();
|
||||
show();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 设置来电监听器
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
if (telephonyManager != null) {
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void initPhoneCallView() {
|
||||
windowManager = (WindowManager) getApplicationContext()
|
||||
.getSystemService(Context.WINDOW_SERVICE);
|
||||
int width = windowManager.getDefaultDisplay().getWidth();
|
||||
int height = windowManager.getDefaultDisplay().getHeight();
|
||||
|
||||
params = new WindowManager.LayoutParams();
|
||||
params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
|
||||
params.width = width;
|
||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
|
||||
// 设置图片格式,效果为背景透明
|
||||
params.format = PixelFormat.TRANSLUCENT;
|
||||
// 设置 Window flag 为系统级弹框 | 覆盖表层
|
||||
params.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
|
||||
WindowManager.LayoutParams.TYPE_PHONE;
|
||||
|
||||
// 不可聚集(不响应返回键)| 全屏
|
||||
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
// API 19 以上则还可以开启透明状态栏与导航栏
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
params.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
|
||||
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
}
|
||||
|
||||
FrameLayout interceptorLayout = new FrameLayout(this) {
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
phoneCallView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.view_phone_call, interceptorLayout);
|
||||
tvCallNumber = phoneCallView.findViewById(R.id.tv_call_number);
|
||||
btnOpenApp = phoneCallView.findViewById(R.id.btn_open_app);
|
||||
btnOpenApp.setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// CallListenerService.this.startActivity(intent);
|
||||
|
||||
PhoneCallService.CallType callType = isCallingIn ? PhoneCallService.CallType.CALL_IN: PhoneCallService.CallType.CALL_OUT;
|
||||
PhoneCallActivity.actionStart(CallListenerService.this, callNumber, callType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示顶级弹框展示通话信息
|
||||
*/
|
||||
private void show() {
|
||||
if (!hasShown) {
|
||||
windowManager.addView(phoneCallView, params);
|
||||
hasShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消显示
|
||||
*/
|
||||
private void dismiss() {
|
||||
if (hasShown) {
|
||||
windowManager.removeView(phoneCallView);
|
||||
isCallingIn = false;
|
||||
hasShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
tvCallNumber.setText(formatPhoneNumber(callNumber));
|
||||
|
||||
int callTypeDrawable = isCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
|
||||
tvCallNumber.setCompoundDrawablesWithIntrinsicBounds(null, null,
|
||||
getResources().getDrawable(callTypeDrawable), null);
|
||||
}
|
||||
|
||||
public static String formatPhoneNumber(String phoneNum) {
|
||||
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
|
||||
return phoneNum.substring(0, 3) + "-"
|
||||
+ phoneNum.substring(3, 7) + "-"
|
||||
+ phoneNum.substring(7);
|
||||
}
|
||||
return phoneNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务开始销毁 =====");
|
||||
|
||||
// 全量清理资源,彻底避免内存泄漏
|
||||
dismissFloatWindow(); // 移除悬浮窗
|
||||
unregisterPhoneStateListener();// 注销通话监听
|
||||
clearDelayHandler(); // 清空延迟任务
|
||||
resetAllReferences(); // 置空所有成员属性
|
||||
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务销毁完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 延迟初始化方法区(非阻塞启动,提升稳定性) ======================
|
||||
/**
|
||||
* 初始化延迟处理器,执行核心逻辑(通话监听+悬浮窗)
|
||||
*/
|
||||
private void initDelayHandlerAndLogic() {
|
||||
mDelayHandler = new Handler(Looper.getMainLooper());
|
||||
mDelayHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 开始延迟初始化核心逻辑");
|
||||
initPhoneStateListener(); // 初始化通话状态监听
|
||||
initFloatWindow(); // 初始化通话悬浮窗
|
||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 延迟初始化完成,服务就绪");
|
||||
}
|
||||
}, DELAY_INIT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话状态监听(注册TelephonyManager,响应通话状态变化)
|
||||
*/
|
||||
private void initPhoneStateListener() {
|
||||
// 1. 创建通话状态监听回调
|
||||
mPhoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int callState, String incomingNumber) {
|
||||
super.onCallStateChanged(callState, incomingNumber);
|
||||
mCallNumber = incomingNumber;
|
||||
LogUtils.d(TAG, "onCallStateChanged: 通话状态变化,状态=" + getCallStateDesc(callState) + ",号码=" + incomingNumber);
|
||||
|
||||
// 响应不同通话状态
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// 通话空闲(挂断/未通话):隐藏悬浮窗
|
||||
dismissFloatWindow();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
// 来电响铃:标记来电状态,更新UI并显示悬浮窗
|
||||
mIsCallingIn = true;
|
||||
updateFloatWindowUI();
|
||||
showFloatWindow();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
// 通话中(接听/拨号):更新UI并显示悬浮窗
|
||||
updateFloatWindowUI();
|
||||
showFloatWindow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 注册通话监听(非空校验,避免崩溃)
|
||||
mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
if (mTelephonyManager != null) {
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
LogUtils.d(TAG, "initPhoneStateListener: 通话状态监听注册成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败,监听注册失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话悬浮窗(配置参数+加载布局+绑定事件,适配API29-30)
|
||||
*/
|
||||
private void initFloatWindow() {
|
||||
// 1. 获取窗口管理器(非空校验,避免后续崩溃)
|
||||
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
|
||||
if (mWindowManager == null) {
|
||||
LogUtils.e(TAG, "initFloatWindow: WindowManager获取失败,悬浮窗初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 配置悬浮窗参数(精准适配API29+,兼容悬浮窗权限)
|
||||
initFloatWindowParams();
|
||||
|
||||
// 3. 加载悬浮窗布局(添加返回键拦截,避免误关闭)
|
||||
FrameLayout keyInterceptorLayout = new FrameLayout(this) {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// 拦截返回键,保障通话时悬浮窗正常显示
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件");
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
mPhoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, keyInterceptorLayout);
|
||||
|
||||
// 4. 绑定悬浮窗控件,设置跳转按钮事件
|
||||
bindFloatWindowViews();
|
||||
|
||||
LogUtils.d(TAG, "initFloatWindow: 悬浮窗初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置悬浮窗参数(适配API29+窗口类型,确保正常显示)
|
||||
*/
|
||||
private void initFloatWindowParams() {
|
||||
mWindowParams = new WindowManager.LayoutParams();
|
||||
// 窗口位置:顶部居中
|
||||
mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
|
||||
// 窗口大小:宽度全屏,高度自适应
|
||||
mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 固定竖屏显示
|
||||
mWindowParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
// 窗口格式:半透明
|
||||
mWindowParams.format = PixelFormat.TRANSLUCENT;
|
||||
|
||||
// 窗口类型(API29+ 强制用 TYPE_APPLICATION_OVERLAY,需悬浮窗权限)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
LogUtils.d(TAG, "initFloatWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY(需开启悬浮窗权限)");
|
||||
} else if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
} else {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
|
||||
}
|
||||
|
||||
// 窗口标志:无焦点(不抢占输入)、全屏布局、兼容透明状态栏/导航栏
|
||||
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_19_API) {
|
||||
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定悬浮窗控件,设置跳转通话详情页事件
|
||||
*/
|
||||
private void bindFloatWindowViews() {
|
||||
mTvCallNumber = (TextView) mPhoneCallView.findViewById(R.id.tv_call_number);
|
||||
mBtnOpenApp = (Button) mPhoneCallView.findViewById(R.id.btn_open_app);
|
||||
|
||||
// 跳转按钮点击事件
|
||||
mBtnOpenApp.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (TextUtils.isEmpty(mCallNumber)) {
|
||||
LogUtils.w(TAG, "bindFloatWindowViews: 通话号码为空,跳过跳转");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "bindFloatWindowViews: 点击跳转通话详情页,号码=" + mCallNumber);
|
||||
PhoneCallService.CallType callType = mIsCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT;
|
||||
PhoneCallActivity.actionStart(CallListenerService.this, mCallNumber, callType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 悬浮窗功能逻辑区(显示/隐藏/更新UI) ======================
|
||||
/**
|
||||
* 显示通话悬浮窗(避免重复添加,防止窗口泄露)
|
||||
*/
|
||||
private void showFloatWindow() {
|
||||
if (!mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
||||
try {
|
||||
mWindowManager.addView(mPhoneCallView, mWindowParams);
|
||||
mHasShown = true;
|
||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗显示成功");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示失败(无悬浮窗权限,需引导用户开启)", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示异常", e);
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗已显示/组件未初始化,跳过显示");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏通话悬浮窗(避免重复移除,防止崩溃)
|
||||
*/
|
||||
private void dismissFloatWindow() {
|
||||
if (mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
||||
try {
|
||||
mWindowManager.removeView(mPhoneCallView);
|
||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗隐藏成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "dismissFloatWindow: 悬浮窗隐藏异常", e);
|
||||
} finally {
|
||||
mHasShown = false;
|
||||
mIsCallingIn = false; // 重置来电状态标记
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗已隐藏/组件未初始化,跳过隐藏");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新悬浮窗UI(显示格式化号码+通话类型图标)
|
||||
*/
|
||||
private void updateFloatWindowUI() {
|
||||
if (mTvCallNumber == null || TextUtils.isEmpty(mCallNumber)) {
|
||||
LogUtils.w(TAG, "updateFloatWindowUI: 控件未初始化/号码为空,更新失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化11位手机号(3-4-4分隔,提升可读性)
|
||||
String formattedNumber = formatPhoneNumber(mCallNumber);
|
||||
mTvCallNumber.setText(formattedNumber);
|
||||
|
||||
// 设置通话类型图标(来电/去电区分)
|
||||
int iconResId = mIsCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
|
||||
mTvCallNumber.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null, null, getResources().getDrawable(iconResId), null
|
||||
);
|
||||
LogUtils.d(TAG, "updateFloatWindowUI: 悬浮窗UI更新完成,号码=" + formattedNumber + ",类型=" + (mIsCallingIn ? "来电" : "去电"));
|
||||
}
|
||||
|
||||
// ====================== 资源清理方法区(服务销毁时全量释放) ======================
|
||||
/**
|
||||
* 注销通话状态监听(释放TelephonyManager资源)
|
||||
*/
|
||||
private void unregisterPhoneStateListener() {
|
||||
if (mTelephonyManager != null && mPhoneStateListener != null) {
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
LogUtils.d(TAG, "unregisterPhoneStateListener: 通话监听已注销");
|
||||
}
|
||||
mTelephonyManager = null;
|
||||
mPhoneStateListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空延迟处理器(移除未执行任务,避免内存泄漏)
|
||||
*/
|
||||
private void clearDelayHandler() {
|
||||
if (mDelayHandler != null) {
|
||||
mDelayHandler.removeCallbacksAndMessages(null);
|
||||
mDelayHandler = null;
|
||||
LogUtils.d(TAG, "clearDelayHandler: 延迟处理器已清空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 置空所有成员属性(彻底释放引用,避免内存泄漏)
|
||||
*/
|
||||
private void resetAllReferences() {
|
||||
mCallNumber = null;
|
||||
mPhoneCallView = null;
|
||||
mWindowParams = null;
|
||||
mWindowManager = null;
|
||||
mTvCallNumber = null;
|
||||
mBtnOpenApp = null;
|
||||
}
|
||||
|
||||
// ====================== 工具方法区(通用辅助功能,独立归类) ======================
|
||||
/**
|
||||
* 格式化手机号(11位手机号:3-4-4分隔,非11位保持原格式)
|
||||
* @param phoneNum 待格式化的手机号
|
||||
* @return 格式化后的号码
|
||||
*/
|
||||
public static String formatPhoneNumber(String phoneNum) {
|
||||
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
|
||||
String formatted = phoneNum.substring(0, 3) + "-"
|
||||
+ phoneNum.substring(3, 7) + "-"
|
||||
+ phoneNum.substring(7);
|
||||
LogUtils.d(TAG, "formatPhoneNumber: 号码格式化,原=" + phoneNum + ",新=" + formatted);
|
||||
return formatted;
|
||||
}
|
||||
LogUtils.d(TAG, "formatPhoneNumber: 非11位号码,无需格式化,号码=" + phoneNum);
|
||||
return phoneNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换通话状态为文字描述(便于日志查看,快速定位问题)
|
||||
* @param callState 通话状态(TelephonyManager常量)
|
||||
* @return 状态描述文字
|
||||
*/
|
||||
private String getCallStateDesc(int callState) {
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲(挂断/未通话)";
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃(来电)";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中(接听/拨号)";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:10:57
|
||||
* @Describe 通话记录数据模型
|
||||
*/
|
||||
public class CallLogModel {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogModel";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String phoneNumber;
|
||||
private String callStatus;
|
||||
private Date callDate;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
|
||||
// 去除号码中的空格并初始化
|
||||
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
|
||||
this.callStatus = callStatus;
|
||||
this.callDate = callDate;
|
||||
|
||||
LogUtils.d(TAG, "CallLogModel: 初始化通话记录模型 | 号码=" + this.phoneNumber
|
||||
+ " | 状态=" + this.callStatus + " | 时间=" + this.callDate);
|
||||
}
|
||||
|
||||
// ====================== Getter 方法区 ======================
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getCallStatus() {
|
||||
return callStatus;
|
||||
}
|
||||
|
||||
public Date getCallDate() {
|
||||
return callDate;
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人信息数据模型,支持姓名转全拼和拼音首字母
|
||||
*/
|
||||
public class ContactModel {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactModel";
|
||||
// 汉字匹配正则常量,避免重复创建
|
||||
private static final String CHINESE_CHAR_REGEX = "[\\u4e00-\\u9fa5]";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String name;
|
||||
private String number;
|
||||
private String pinyin;
|
||||
private String pinyinFirstLetter;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public ContactModel(String name, String number) {
|
||||
LogUtils.d(TAG, "ContactModel: 开始初始化联系人模型");
|
||||
this.name = name == null ? "" : name;
|
||||
// 去除号码空格,空值处理为""
|
||||
this.number = number == null ? "" : number.replaceAll("\\s", "");
|
||||
// 初始化拼音和拼音首字母
|
||||
this.pinyin = convertToPinyin(this.name);
|
||||
this.pinyinFirstLetter = convertToPinyinFirstLetter(this.name);
|
||||
|
||||
LogUtils.d(TAG, "ContactModel: 联系人初始化完成 | 姓名=" + this.name
|
||||
+ " | 号码=" + this.number + " | 全拼=" + this.pinyin
|
||||
+ " | 拼音首字母=" + this.pinyinFirstLetter);
|
||||
}
|
||||
|
||||
// ====================== 拼音转换工具方法区 ======================
|
||||
/**
|
||||
* 姓名转为全拼(多音字默认取首个拼音)
|
||||
*/
|
||||
private String convertToPinyin(String chinese) {
|
||||
LogUtils.d(TAG, "convertToPinyin: 开始转换姓名为全拼,姓名=" + chinese);
|
||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
||||
StringBuilder pinyinSb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
// 仅处理汉字
|
||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
pinyinSb.append(pinyinArray[0]);
|
||||
LogUtils.v(TAG, "convertToPinyin: 字符[" + ch + "]转为拼音[" + pinyinArray[0] + "]");
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "convertToPinyin: 拼音转换异常,字符=" + ch, e);
|
||||
}
|
||||
} else {
|
||||
pinyinSb.append(ch);
|
||||
LogUtils.v(TAG, "convertToPinyin: 非汉字字符直接拼接,字符=" + ch);
|
||||
}
|
||||
}
|
||||
|
||||
String result = pinyinSb.toString();
|
||||
LogUtils.d(TAG, "convertToPinyin: 全拼转换完成,结果=" + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名转为拼音首字母(多音字默认取首个拼音首字母)
|
||||
*/
|
||||
private String convertToPinyinFirstLetter(String chinese) {
|
||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 开始转换姓名为拼音首字母,姓名=" + chinese);
|
||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
||||
StringBuilder firstLetterSb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
char firstChar = pinyinArray[0].charAt(0);
|
||||
firstLetterSb.append(firstChar);
|
||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 字符[" + ch + "]转为首字母[" + firstChar + "]");
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "convertToPinyinFirstLetter: 拼音首字母转换异常,字符=" + ch, e);
|
||||
}
|
||||
} else {
|
||||
firstLetterSb.append(ch);
|
||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 非汉字字符直接拼接,字符=" + ch);
|
||||
}
|
||||
}
|
||||
|
||||
String result = firstLetterSb.toString();
|
||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 拼音首字母转换完成,结果=" + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统一的拼音输出格式(小写、无音调)
|
||||
* 抽离为公共方法,避免重复创建对象
|
||||
*/
|
||||
private HanyuPinyinOutputFormat getPinyinOutputFormat() {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
return format;
|
||||
}
|
||||
|
||||
// ====================== Getter 方法区 ======================
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getPinyin() {
|
||||
return pinyin;
|
||||
}
|
||||
|
||||
public String getPinyinFirstLetter() {
|
||||
return pinyinFirstLetter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 07:06:13
|
||||
* @Describe 主服务配置实体类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class MainServiceBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainServiceBean";
|
||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private boolean isEnable;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainServiceBean() {
|
||||
this.isEnable = false;
|
||||
LogUtils.d(TAG, "MainServiceBean: 初始化实体类,默认状态为禁用");
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
LogUtils.d(TAG, "setIsEnable: 服务状态设置为" + isEnable);
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = MainServiceBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将实体类写入JSON");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入服务启用状态字段
|
||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(this.isEnable);
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON写入完成,isEnable=" + this.isEnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类方法处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_IS_ENABLE.equals(name)) {
|
||||
this.isEnable = jsonReader.nextBoolean();
|
||||
LogUtils.d(TAG, "initObjectsFromJsonReader: 读取字段[" + name + "]值=" + this.isEnable);
|
||||
return true;
|
||||
}
|
||||
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别字段=" + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON读取实体类数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + name);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON读取完成,当前实体状态=" + this.isEnable);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 09:52:10
|
||||
* @Describe 电话黑名单规则实体类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class PhoneConnectRuleBean extends BaseBean {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneConnectRuleModel";
|
||||
// JSON字段名常量,避免硬编码错误
|
||||
private static final String JSON_KEY_RULE_TEXT = "ruleText";
|
||||
private static final String JSON_KEY_ALLOW_CONNECTION = "isAllowConnection";
|
||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String ruleText;
|
||||
private boolean isAllowConnection;
|
||||
private boolean isEnable;
|
||||
private boolean isSimpleView;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,初始化默认值
|
||||
*/
|
||||
public PhoneConnectRuleBean() {
|
||||
this.ruleText = "";
|
||||
this.isAllowConnection = false;
|
||||
this.isEnable = false;
|
||||
this.isSimpleView = true;
|
||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 默认构造初始化完成 | 规则文本空串,默认禁用状态");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化核心规则参数
|
||||
*/
|
||||
public PhoneConnectRuleBean(String ruleText, boolean isAllowConnection, boolean isEnable) {
|
||||
this.ruleText = ruleText == null ? "" : ruleText;
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
this.isEnable = isEnable;
|
||||
this.isSimpleView = true;
|
||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 带参构造初始化完成 | 规则文本=" + this.ruleText
|
||||
+ " | 允许连接=" + this.isAllowConnection + " | 规则启用=" + this.isEnable);
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public String getRuleText() {
|
||||
return ruleText;
|
||||
}
|
||||
|
||||
public void setRuleText(String ruleText) {
|
||||
String oldValue = this.ruleText;
|
||||
this.ruleText = ruleText == null ? "" : ruleText;
|
||||
LogUtils.d(TAG, "setRuleText: 规则文本更新 | 旧值=" + oldValue + " | 新值=" + this.ruleText);
|
||||
}
|
||||
|
||||
public boolean isAllowConnection() {
|
||||
return isAllowConnection;
|
||||
}
|
||||
|
||||
public void setIsAllowConnection(boolean isAllowConnection) {
|
||||
LogUtils.d(TAG, "setIsAllowConnection: 允许连接状态更新为" + isAllowConnection);
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
LogUtils.d(TAG, "setIsEnable: 规则启用状态更新为" + isEnable);
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
LogUtils.d(TAG, "setIsSimpleView: 视图模式更新 | 简洁模式=" + isSimpleView);
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = PhoneConnectRuleBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化规则数据");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 序列化核心字段
|
||||
jsonWriter.name(JSON_KEY_RULE_TEXT).value(getRuleText());
|
||||
jsonWriter.name(JSON_KEY_ALLOW_CONNECTION).value(isAllowConnection());
|
||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(isEnable());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先让父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_RULE_TEXT.equals(name)) {
|
||||
setRuleText(jsonReader.nextString());
|
||||
} else if (JSON_KEY_ALLOW_CONNECTION.equals(name)) {
|
||||
setIsAllowConnection(jsonReader.nextBoolean());
|
||||
} else if (JSON_KEY_IS_ENABLE.equals(name)) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析规则数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 解析后规则=" + getRuleText());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/24 18:47:11
|
||||
* @Describe 手机铃声设置参数类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class RingTongBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AudioRingTongBean";
|
||||
private static final String JSON_KEY_STREAM_VOLUME = "streamVolume";
|
||||
// 铃声音量范围常量(参考AudioManager标准)
|
||||
private static final int VOLUME_MIN = 0;
|
||||
private static final int VOLUME_MAX = 100;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private int streamVolume;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,铃声音量初始化为最大值
|
||||
*/
|
||||
public RingTongBean() {
|
||||
this.streamVolume = VOLUME_MAX;
|
||||
LogUtils.d(TAG, "RingTongBean: 默认构造初始化 | 铃声音量=" + this.streamVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化指定铃声音量
|
||||
*/
|
||||
public RingTongBean(int streamVolume) {
|
||||
// 音量值范围校验,避免非法值
|
||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
||||
LogUtils.d(TAG, "RingTongBean: 带参构造初始化 | 原始音量=" + streamVolume + " | 校正后=" + this.streamVolume);
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public int getStreamVolume() {
|
||||
return streamVolume;
|
||||
}
|
||||
|
||||
public void setStreamVolume(int streamVolume) {
|
||||
int oldVolume = this.streamVolume;
|
||||
// 音量值范围校验
|
||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
||||
LogUtils.d(TAG, "setStreamVolume: 铃声音量更新 | 旧值=" + oldVolume + " | 新值=" + this.streamVolume);
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = RingTongBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化铃声音量参数");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name(JSON_KEY_STREAM_VOLUME).value(getStreamVolume());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成 | 音量值=" + getStreamVolume());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_STREAM_VOLUME.equals(name)) {
|
||||
setStreamVolume(jsonReader.nextInt());
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 解析字段[" + name + "]值=" + this.streamVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析铃声音量参数");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 最终音量值=" + this.streamVolume);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 19:51:40
|
||||
* @Describe 应用设置数据模型,支持云盾防御配置与JSON序列化
|
||||
*/
|
||||
public class SettingsBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "SettingsModel";
|
||||
// 数值范围常量
|
||||
public static final int MAX_INTRANGE = 666666;
|
||||
public static final int MIN_INTRANGE = 1;
|
||||
// JSON字段名常量,消除硬编码
|
||||
private static final String JSON_KEY_DUN_TOTAL = "dunTotalCount";
|
||||
private static final String JSON_KEY_DUN_CURRENT = "dunCurrentCount";
|
||||
private static final String JSON_KEY_DUN_RESUME_SECOND = "dunResumeSecondCount";
|
||||
private static final String JSON_KEY_DUN_RESUME_COUNT = "dunResumeCount";
|
||||
private static final String JSON_KEY_DUN_ENABLE = "isEnableDun";
|
||||
private static final String JSON_KEY_URL = "szBoBullToon_URL";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 云盾防御层数量
|
||||
private int dunTotalCount;
|
||||
// 当前云盾防御层
|
||||
private int dunCurrentCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
private int dunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
private int dunResumeCount;
|
||||
// 是否启用云盾
|
||||
private boolean isEnableDun;
|
||||
// BoBullToon 应用模块数据请求地址
|
||||
private String szBoBullToon_URL;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,初始化默认配置
|
||||
*/
|
||||
public SettingsBean() {
|
||||
this.dunTotalCount = 6;
|
||||
this.dunCurrentCount = 6;
|
||||
this.dunResumeSecondCount = 60;
|
||||
this.dunResumeCount = 1;
|
||||
this.isEnableDun = false;
|
||||
this.szBoBullToon_URL = "";
|
||||
LogUtils.d(TAG, "SettingsModel: 默认构造初始化完成 | 云盾默认配置加载完毕");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化自定义配置并校验数值范围
|
||||
*/
|
||||
public SettingsBean(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount,
|
||||
int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
this.isEnableDun = isEnableDun;
|
||||
this.szBoBullToon_URL = szBoBullToon_URL == null ? "" : szBoBullToon_URL;
|
||||
|
||||
LogUtils.d(TAG, "SettingsModel: 带参构造初始化完成 | 总层数=" + this.dunTotalCount
|
||||
+ " | 当前层数=" + this.dunCurrentCount + " | 恢复间隔=" + this.dunResumeSecondCount
|
||||
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun);
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 数值范围校验,确保参数在 MIN~MAX 区间内
|
||||
*/
|
||||
private int getSettingsModelRangeInt(int origin) {
|
||||
int result = IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
|
||||
if (result != origin) {
|
||||
LogUtils.w(TAG, "getSettingsModelRangeInt: 数值校正 | 原始值=" + origin + " | 校正后=" + result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public int getDunTotalCount() {
|
||||
return dunTotalCount;
|
||||
}
|
||||
|
||||
public void setDunTotalCount(int dunTotalCount) {
|
||||
int oldValue = this.dunTotalCount;
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
LogUtils.d(TAG, "setDunTotalCount: 总防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunTotalCount);
|
||||
}
|
||||
|
||||
public int getDunCurrentCount() {
|
||||
return dunCurrentCount;
|
||||
}
|
||||
|
||||
public void setDunCurrentCount(int dunCurrentCount) {
|
||||
int oldValue = this.dunCurrentCount;
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
LogUtils.d(TAG, "setDunCurrentCount: 当前防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunCurrentCount);
|
||||
}
|
||||
|
||||
public int getDunResumeSecondCount() {
|
||||
return dunResumeSecondCount;
|
||||
}
|
||||
|
||||
public void setDunResumeSecondCount(int dunResumeSecondCount) {
|
||||
int oldValue = this.dunResumeSecondCount;
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
LogUtils.d(TAG, "setDunResumeSecondCount: 恢复间隔更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeSecondCount);
|
||||
}
|
||||
|
||||
public int getDunResumeCount() {
|
||||
return dunResumeCount;
|
||||
}
|
||||
|
||||
public void setDunResumeCount(int dunResumeCount) {
|
||||
int oldValue = this.dunResumeCount;
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
LogUtils.d(TAG, "setDunResumeCount: 恢复层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeCount);
|
||||
}
|
||||
|
||||
public boolean isEnableDun() {
|
||||
return isEnableDun;
|
||||
}
|
||||
|
||||
public void setIsEnableDun(boolean isEnableDun) {
|
||||
LogUtils.d(TAG, "setIsEnableDun: 云盾启用状态更新为" + isEnableDun);
|
||||
this.isEnableDun = isEnableDun;
|
||||
}
|
||||
|
||||
public String getBoBullToon_URL() {
|
||||
return szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setBoBullToon_URL(String boBullToon_URL) {
|
||||
String oldValue = this.szBoBullToon_URL;
|
||||
this.szBoBullToon_URL = boBullToon_URL == null ? "" : boBullToon_URL;
|
||||
LogUtils.d(TAG, "setBoBullToon_URL: 请求地址更新 | 旧值=" + oldValue + " | 新值=" + this.szBoBullToon_URL);
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = SettingsBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化设置数据");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入所有配置字段
|
||||
jsonWriter.name(JSON_KEY_DUN_TOTAL).value(getDunTotalCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_CURRENT).value(getDunCurrentCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_RESUME_SECOND).value(getDunResumeSecondCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_RESUME_COUNT).value(getDunResumeCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_ENABLE).value(isEnableDun());
|
||||
jsonWriter.name(JSON_KEY_URL).value(getBoBullToon_URL());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属配置字段
|
||||
if (JSON_KEY_DUN_TOTAL.equals(name)) {
|
||||
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_CURRENT.equals(name)) {
|
||||
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_RESUME_SECOND.equals(name)) {
|
||||
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_RESUME_COUNT.equals(name)) {
|
||||
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_ENABLE.equals(name)) {
|
||||
setIsEnableDun(jsonReader.nextBoolean());
|
||||
} else if (JSON_KEY_URL.equals(name)) {
|
||||
setBoBullToon_URL(jsonReader.nextString());
|
||||
} else {
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析设置数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 云盾配置加载完毕");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,362 +1,161 @@
|
||||
package cc.winboll.studio.contacts.phonecallui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
||||
|
||||
|
||||
/**
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/14 21:01
|
||||
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
|
||||
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
|
||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
||||
*
|
||||
* @author aJIEw
|
||||
*/
|
||||
public class PhoneCallActivity extends Activity implements View.OnClickListener {
|
||||
// 常量定义区(核心常量+小米适配标识)
|
||||
public static final String TAG = "PhoneCallActivity";
|
||||
private static final int MSG_CLOSE_ACTIVITY = 0x001;
|
||||
private static final String MI_ADAPT_TAG = "MiAdapt";
|
||||
private static final String TOAST_CALLING = "通话进行中,无法重复创建通话窗口";
|
||||
private static final long CLOSE_DELAY_MS = 100; // 小米机型关闭延迟时间
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
|
||||
// 静态属性区(单例核心+全局工具对象)
|
||||
private static volatile boolean sIsActivityAlive = false;
|
||||
private static Handler sCloseHandler;
|
||||
private TextView tvCallNumberLabel;
|
||||
private TextView tvCallNumber;
|
||||
private TextView tvPickUp;
|
||||
private TextView tvCallingTime;
|
||||
private TextView tvHangUp;
|
||||
|
||||
// 控件属性区(按界面布局顺序排列)
|
||||
private TextView mTvCallNumberLabel;
|
||||
private TextView mTvCallNumber;
|
||||
private TextView mTvPickUp;
|
||||
private TextView mTvCallingTime;
|
||||
private TextView mTvHangUp;
|
||||
private PhoneCallManager phoneCallManager;
|
||||
private PhoneCallService.CallType callType;
|
||||
private String phoneNumber;
|
||||
|
||||
// 业务属性区(按依赖优先级排列)
|
||||
private PhoneCallManager mPhoneCallManager;
|
||||
private PhoneCallService.CallType mCallType;
|
||||
private String mPhoneNumber;
|
||||
private Timer mOnGoingCallTimer;
|
||||
private int mCallingTime;
|
||||
private boolean isClosing = false; // 新增:避免重复关闭页面
|
||||
private Timer onGoingCallTimer;
|
||||
private int callingTime;
|
||||
|
||||
// 对外静态接口(单例启动+外部关闭)
|
||||
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
|
||||
if (context == null || phoneNumber == null || callType == null) {
|
||||
LogUtils.e(TAG, "actionStart: 入参为空,启动失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sIsActivityAlive) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 已有活跃通话窗口,拒绝重复启动");
|
||||
Toast.makeText(context, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 启动通话界面,号码=" + phoneNumber + ",类型=" + callType.name());
|
||||
public static void actionStart(Context context, String phoneNumber,
|
||||
PhoneCallService.CallType callType) {
|
||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||
intent.putExtra("call_type", callType);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void closePhoneCallActivity() {
|
||||
LogUtils.d(TAG, "closePhoneCallActivity: 收到外部关闭指令");
|
||||
if (sIsActivityAlive && sCloseHandler != null) {
|
||||
sCloseHandler.sendEmptyMessage(MSG_CLOSE_ACTIVITY);
|
||||
LogUtils.d(TAG, "closePhoneCallActivity: 关闭消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "closePhoneCallActivity: 页面已销毁或Handler未初始化,关闭跳过");
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期方法区(按执行流程排序)
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始创建,SDK版本=" + Build.VERSION.SDK_INT);
|
||||
|
||||
// 单例双重校验,防止异常场景多实例
|
||||
if (sIsActivityAlive) {
|
||||
Toast.makeText(this, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 拦截重复创建,即将关闭当前实例");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
sIsActivityAlive = false;
|
||||
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
|
||||
ActivityStack.getInstance().addActivity(this);
|
||||
adaptLockScreenAndXiaomi();
|
||||
initHandler();
|
||||
initData();
|
||||
initView();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
|
||||
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
phoneCallManager = new PhoneCallManager(this);
|
||||
onGoingCallTimer = new Timer();
|
||||
if (getIntent() != null) {
|
||||
phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
|
||||
}
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
|
||||
tvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||
tvCallNumber = findViewById(R.id.tv_call_number);
|
||||
tvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||
tvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||
tvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||
|
||||
tvCallNumber.setText(formatPhoneNumber(phoneNumber));
|
||||
tvPickUp.setOnClickListener(this);
|
||||
tvHangUp.setOnClickListener(this);
|
||||
|
||||
// 打进的电话
|
||||
if (callType == PhoneCallService.CallType.CALL_IN) {
|
||||
tvCallNumberLabel.setText("来电号码");
|
||||
tvPickUp.setVisibility(View.VISIBLE);
|
||||
} else if (callType == PhoneCallService.CallType.CALL_OUT) {
|
||||
tvCallNumberLabel.setText("呼叫号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
phoneCallManager.openSpeaker();
|
||||
}
|
||||
|
||||
showOnLockScreen();
|
||||
}
|
||||
|
||||
public void showOnLockScreen() {
|
||||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.tv_phone_pick_up) {
|
||||
phoneCallManager.answer();
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
tvCallingTime.setVisibility(View.VISIBLE);
|
||||
onGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void run() {
|
||||
callingTime++;
|
||||
tvCallingTime.setText("通话中:" + getCallingTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
} else if (v.getId() == R.id.tv_phone_hang_up) {
|
||||
phoneCallManager.disconnect();
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private String getCallingTime() {
|
||||
int minute = callingTime / 60;
|
||||
int second = callingTime % 60;
|
||||
return (minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
if (onGoingCallTimer != null) {
|
||||
onGoingCallTimer.cancel();
|
||||
}
|
||||
|
||||
callingTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始销毁");
|
||||
|
||||
sIsActivityAlive = false;
|
||||
isClosing = false;
|
||||
stopTimer();
|
||||
// 销毁通话管理器
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.destroy();
|
||||
mPhoneCallManager = null;
|
||||
LogUtils.d(TAG, "销毁通话管理器资源");
|
||||
}
|
||||
// 销毁Handler避免内存泄漏
|
||||
if (sCloseHandler != null) {
|
||||
sCloseHandler.removeCallbacksAndMessages(null);
|
||||
sCloseHandler = null;
|
||||
LogUtils.d(TAG, "销毁关闭Handler");
|
||||
}
|
||||
ActivityStack.getInstance().removeActivity(this);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面销毁完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (isFinishing()) {
|
||||
sIsActivityAlive = false;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 页面即将关闭,重置单例标记");
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件回调
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == null) {
|
||||
LogUtils.w(TAG, "onClick: 点击控件为空,忽略操作");
|
||||
return;
|
||||
}
|
||||
switch (v.getId()) {
|
||||
case R.id.tv_phone_pick_up:
|
||||
LogUtils.d(TAG, "onClick: 触发接听操作");
|
||||
answerCall();
|
||||
break;
|
||||
case R.id.tv_phone_hang_up:
|
||||
LogUtils.d(TAG, "onClick: 触发挂断操作,当前通话时长=" + mCallingTime + "秒");
|
||||
hangUpCall();
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onClick: 未知点击事件,控件ID=" + v.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化方法区(按初始化顺序排列)
|
||||
private void initHandler() {
|
||||
sCloseHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_CLOSE_ACTIVITY) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到关闭消息,执行挂断逻辑");
|
||||
hangUpCall();
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initHandler: 关闭Handler初始化完成");
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
LogUtils.d(TAG, "initData: 开始初始化业务数据");
|
||||
mPhoneCallManager = PhoneCallManager.getInstance(this);
|
||||
Intent intent = getIntent();
|
||||
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "initData: 启动Intent为空,终止初始化");
|
||||
removeFromRecentsAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
mPhoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
mCallType = (PhoneCallService.CallType) intent.getSerializableExtra("call_type");
|
||||
if (mPhoneNumber == null || mCallType == null) {
|
||||
LogUtils.e(TAG, "initData: 通话号码或类型解析失败");
|
||||
removeFromRecentsAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
mOnGoingCallTimer = new Timer();
|
||||
mCallingTime = 0;
|
||||
LogUtils.d(TAG, "initData: 业务数据初始化完成,号码=" + mPhoneNumber);
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
LogUtils.d(TAG, "initView: 开始初始化界面控件");
|
||||
// 修复沉浸式导航栏语法,适配小米全面屏
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||
|
||||
// 绑定控件
|
||||
mTvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||
mTvCallNumber = findViewById(R.id.tv_call_number);
|
||||
mTvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||
mTvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||
mTvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||
|
||||
// 设置控件属性
|
||||
mTvCallNumber.setText(formatPhoneNumber(mPhoneNumber));
|
||||
mTvPickUp.setOnClickListener(this);
|
||||
mTvHangUp.setOnClickListener(this);
|
||||
|
||||
// 区分来电/去电UI样式
|
||||
if (PhoneCallService.CallType.CALL_IN == mCallType) {
|
||||
mTvCallNumberLabel.setText("来电号码");
|
||||
mTvPickUp.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setVisibility(View.GONE);
|
||||
} else if (PhoneCallService.CallType.CALL_OUT == mCallType) {
|
||||
mTvCallNumberLabel.setText("呼叫号码");
|
||||
mTvPickUp.setVisibility(View.GONE);
|
||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setText("通话中:00:00");
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.openSpeaker();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 去电模式自动开启免提");
|
||||
}
|
||||
startCallTimer();
|
||||
}
|
||||
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
||||
}
|
||||
|
||||
// 小米机型专属适配方法
|
||||
private void adaptLockScreenAndXiaomi() {
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 执行锁屏适配逻辑");
|
||||
Window window = getWindow();
|
||||
if (window == null) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " Window对象为空,适配失败");
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
|
||||
|
||||
// 小米机型额外添加解锁屏标志,解决MIUI锁屏拦截问题
|
||||
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
|
||||
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 已添加小米机型专属锁屏适配标志");
|
||||
}
|
||||
window.addFlags(flags);
|
||||
|
||||
// 适配API29+锁屏新接口
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setShowWhenLocked(true);
|
||||
setTurnScreenOn(true);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 适配API29+锁屏接口完成");
|
||||
}
|
||||
}
|
||||
|
||||
// 通话核心业务方法
|
||||
private void answerCall() {
|
||||
LogUtils.d(TAG, "answerCall: 执行接听操作");
|
||||
if (mPhoneCallManager == null) {
|
||||
LogUtils.e(TAG, "answerCall: 通话管理器为空,接听失败");
|
||||
return;
|
||||
}
|
||||
mPhoneCallManager.answer();
|
||||
mTvPickUp.setVisibility(View.GONE);
|
||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setText("通话中:00:00");
|
||||
startCallTimer();
|
||||
LogUtils.d(TAG, "answerCall: 接听操作完成,启动通话计时");
|
||||
}
|
||||
|
||||
private void hangUpCall() {
|
||||
if (isClosing) {
|
||||
LogUtils.w(TAG, "hangUpCall: 挂断操作已执行,无需重复调用");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "hangUpCall: 执行挂断操作,当前时长=" + mCallingTime + "秒");
|
||||
isClosing = true;
|
||||
stopTimer();
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.disconnect();
|
||||
LogUtils.d(TAG, "hangUpCall: 通话连接已断开");
|
||||
}
|
||||
// 延迟关闭页面,适配小米机型通话时序
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
removeFromRecentsAndFinish();
|
||||
}
|
||||
}, CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
// 任务栈清理方法
|
||||
private void removeFromRecentsAndFinish() {
|
||||
if (isFinishing()) {
|
||||
LogUtils.d(TAG, "removeFromRecentsAndFinish: 页面已在关闭中,无需重复操作");
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 移除任务栈并关闭页面");
|
||||
} else {
|
||||
finish();
|
||||
LogUtils.d(TAG, "兼容低版本,关闭页面");
|
||||
}
|
||||
}
|
||||
|
||||
// 计时工具方法
|
||||
private void startCallTimer() {
|
||||
LogUtils.d(TAG, "startCallTimer: 启动通话计时器");
|
||||
if (mOnGoingCallTimer == null) {
|
||||
mOnGoingCallTimer = new Timer();
|
||||
}
|
||||
mOnGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallingTime++;
|
||||
mTvCallingTime.setText("通话中:" + formatCallingTime(mCallingTime));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
LogUtils.d(TAG, "stopTimer: 停止通话计时器");
|
||||
if (mOnGoingCallTimer != null) {
|
||||
mOnGoingCallTimer.cancel();
|
||||
mOnGoingCallTimer = null;
|
||||
}
|
||||
mCallingTime = 0;
|
||||
}
|
||||
|
||||
// 辅助工具方法:格式化通话时长
|
||||
private String formatCallingTime(int seconds) {
|
||||
int minute = seconds / 60;
|
||||
int second = seconds % 60;
|
||||
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
|
||||
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
|
||||
return minuteStr + ":" + secondStr;
|
||||
//MainActivity.updateCallLogFragment();
|
||||
phoneCallManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,199 +6,57 @@ import android.os.Build;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.VideoProfile;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/15 20:11
|
||||
* @Describe 通话核心管理类
|
||||
* 功能:接听/挂断通话、免提控制、资源释放,适配API29-30及小米机型
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallManager {
|
||||
// 常量定义区
|
||||
public static final String TAG = "PhoneCallManager";
|
||||
private static final String MI_ADAPT_TAG = "MiDeviceAdapt"; // 小米适配标识
|
||||
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
|
||||
private static final int AUDIO_MODE_BACKUP = -1; // 音频模式备份默认值
|
||||
|
||||
// 成员属性区(按依赖优先级排序,移除静态call避免跨组件冲突)
|
||||
private Context mContext;
|
||||
private AudioManager mAudioManager;
|
||||
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
|
||||
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
|
||||
public static Call call;
|
||||
|
||||
// 构造方法(单例化改造,避免多实例冲突)
|
||||
private static volatile PhoneCallManager sInstance;
|
||||
public static PhoneCallManager getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为空,初始化失败");
|
||||
return null;
|
||||
}
|
||||
if (sInstance == null) {
|
||||
synchronized (PhoneCallManager.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PhoneCallManager(context.getApplicationContext()); // 用应用上下文,避免内存泄漏
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
private Context context;
|
||||
private AudioManager audioManager;
|
||||
|
||||
public PhoneCallManager(Context context) {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
// 私有构造,禁止外部实例化
|
||||
private PhoneCallManager(Context context) {
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 初始化通话管理类");
|
||||
this.mContext = context;
|
||||
this.mAudioModeBackup = AUDIO_MODE_BACKUP;
|
||||
this.mIsSpeakerOpened = false;
|
||||
initAudioManager();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理类初始化完成");
|
||||
}
|
||||
|
||||
// 初始化辅助方法
|
||||
private void initAudioManager() {
|
||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (mAudioManager != null) {
|
||||
// 备份原始音频模式(小米机型切换后需恢复,避免外放异常)
|
||||
mAudioModeBackup = mAudioManager.getMode();
|
||||
LogUtils.d(TAG, "音频管理器初始化成功,原始模式备份:" + mAudioModeBackup);
|
||||
} else {
|
||||
LogUtils.e(TAG, "音频管理器初始化失败,将影响通话音频控制");
|
||||
}
|
||||
}
|
||||
|
||||
// 核心业务方法(按使用场景排序,强化小米适配+容错)
|
||||
/**
|
||||
* 接听电话,默认音频通话模式
|
||||
* 接听电话
|
||||
*/
|
||||
public void answer() {
|
||||
LogUtils.d(TAG, "执行接听通话操作");
|
||||
// 从PhoneCallService的静态管理器获取通话对象,统一数据源
|
||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||
if (currentCall == null) {
|
||||
LogUtils.e(TAG, "接听失败:通话对象为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验通话状态,避免重复接听(小米机型状态变更延迟)
|
||||
if (currentCall.getState() != Call.STATE_RINGING) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 非响铃状态,无需接听,当前状态:" + currentCall.getState());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
|
||||
openSpeaker(); // 接听后自动开免提
|
||||
LogUtils.d(TAG, "通话接听成功,自动开启免提");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 接听权限不足(需android.permission.ANSWER_PHONE_CALLS)", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法接听", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "接听通话异常", e);
|
||||
if (call != null) {
|
||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
||||
openSpeaker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开通话(支持来电拒接、通话中挂断)
|
||||
* 断开电话,包括来电时的拒接以及接听后的挂断
|
||||
*/
|
||||
public void disconnect() {
|
||||
LogUtils.d(TAG, "执行断开通话操作");
|
||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||
if (currentCall == null) {
|
||||
LogUtils.e(TAG, "挂断失败:通话对象为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验通话状态,避免重复挂断
|
||||
if (currentCall.getState() == Call.STATE_DISCONNECTED) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 通话已断开,无需重复操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentCall.disconnect();
|
||||
closeSpeaker(); // 挂断后关闭免提+恢复音频模式
|
||||
LogUtils.d(TAG, "通话断开成功");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 挂断权限不足(需android.permission.CALL_PHONE)", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法挂断", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "断开通话异常", e);
|
||||
if (call != null) {
|
||||
call.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开免提,适配小米机型音频通道切换(解决MIUI音频混乱)
|
||||
* 打开免提
|
||||
*/
|
||||
public void openSpeaker() {
|
||||
LogUtils.d(TAG, "执行打开免提操作");
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, "打开免提失败:音频管理器未初始化");
|
||||
return;
|
||||
}
|
||||
if (mIsSpeakerOpened) {
|
||||
LogUtils.w(TAG, "免提已开启,无需重复操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 小米机型适配步骤:1. 设置通话模式 2. 关闭静音 3. 开启免提(固定顺序)
|
||||
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
mAudioManager.setStreamMute(AudioManager.STREAM_VOICE_CALL, false); // 确保通话音频不静音
|
||||
mAudioManager.setSpeakerphoneOn(true);
|
||||
|
||||
mIsSpeakerOpened = true;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 免提开启成功,当前模式:" + mAudioManager.getMode());
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 音频控制权限不足", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "打开免提异常", e);
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
|
||||
*/
|
||||
public void closeSpeaker() {
|
||||
LogUtils.d(TAG, "执行关闭免提操作");
|
||||
if (mAudioManager == null || !mIsSpeakerOpened) {
|
||||
LogUtils.w(TAG, "免提未开启或音频管理器为空,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mAudioManager.setSpeakerphoneOn(false);
|
||||
// 恢复原始音频模式(关键:小米机型不恢复会导致其他应用外放异常)
|
||||
if (mAudioModeBackup != AUDIO_MODE_BACKUP) {
|
||||
mAudioManager.setMode(mAudioModeBackup);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 恢复原始音频模式:" + mAudioModeBackup);
|
||||
}
|
||||
mIsSpeakerOpened = false;
|
||||
LogUtils.d(TAG, "免提关闭成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 关闭免提异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源,避免内存泄漏+音频残留(适配小米内存管理)
|
||||
* 销毁资源
|
||||
*/
|
||||
public void destroy() {
|
||||
LogUtils.d(TAG, "开始销毁通话管理资源");
|
||||
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
|
||||
// 释放资源(应用上下文无需主动置空,避免空指针)
|
||||
mAudioManager = null;
|
||||
sInstance = null; // 单例置空,下次重新初始化
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:获取当前免提状态(供UI层同步显示)
|
||||
*/
|
||||
public boolean isSpeakerOpened() {
|
||||
return mIsSpeakerOpened;
|
||||
call = null;
|
||||
context = null;
|
||||
audioManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,284 +1,215 @@
|
||||
package cc.winboll.studio.contacts.phonecallui;
|
||||
|
||||
/**
|
||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
||||
*
|
||||
* @author aJIEw
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaRecorder;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.CallLog;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.InCallService;
|
||||
import android.telephony.TelephonyManager;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
||||
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
* 适配:Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
|
||||
*/
|
||||
@RequiresApi(api = 29)
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallService extends InCallService {
|
||||
// 常量定义区
|
||||
|
||||
public static final String TAG = "PhoneCallService";
|
||||
// 小米设备适配标识,便于日志区分
|
||||
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
|
||||
|
||||
// 成员属性区(按依赖顺序排列)
|
||||
private Call.Callback mCallCallback;
|
||||
private AudioManager mAudioManager;
|
||||
MediaRecorder mediaRecorder;
|
||||
|
||||
// 内部枚举类(通话类型定义)
|
||||
public enum CallType {
|
||||
CALL_IN, // 来电
|
||||
CALL_OUT // 去电
|
||||
}
|
||||
private final Call.Callback callback = new Call.Callback() {
|
||||
@Override
|
||||
public void onStateChanged(Call call, int state) {
|
||||
super.onStateChanged(call, state);
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
{
|
||||
long callId = getCurrentCallId();
|
||||
if (callId != -1) {
|
||||
// 在这里可以对获取到的通话记录ID进行处理
|
||||
//System.out.println("当前通话记录ID: " + callId);
|
||||
|
||||
// Service生命周期方法区(按执行流程排序)
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
|
||||
initAudioManager();
|
||||
initCallCallback();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
|
||||
}
|
||||
// 电话接通,开始录音
|
||||
startRecording(callId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// 电话挂断,停止录音
|
||||
stopRecording();
|
||||
break;
|
||||
case Call.STATE_ACTIVE: {
|
||||
break;
|
||||
}
|
||||
|
||||
case Call.STATE_DISCONNECTED: {
|
||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCallAdded(Call call) {
|
||||
super.onCallAdded(call);
|
||||
LogUtils.d(TAG, "检测到新通话");
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "通话对象为空,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重校验回调,避免重复注册
|
||||
if (mCallCallback != null) {
|
||||
call.registerCallback(mCallCallback);
|
||||
}
|
||||
// 绑定通话对象到管理器,供UI层调用
|
||||
call.registerCallback(callback);
|
||||
PhoneCallManager.call = call;
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
|
||||
CallType callType = null;
|
||||
|
||||
if (call.getState() == Call.STATE_RINGING) {
|
||||
callType = CallType.CALL_IN;
|
||||
} else if (call.getState() == Call.STATE_CONNECTING) {
|
||||
callType = CallType.CALL_OUT;
|
||||
}
|
||||
|
||||
CallType callType = judgeCallType(call);
|
||||
if (callType != null) {
|
||||
handleValidCall(call, callType);
|
||||
} else {
|
||||
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
|
||||
Call.Details details = call.getDetails();
|
||||
String phoneNumber = details.getHandle().getSchemeSpecificPart();
|
||||
|
||||
// 记录原始铃声音量
|
||||
//
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
// 恢复铃声音量,预防其他意外条件导致的音量变化问题
|
||||
//
|
||||
|
||||
// 读取应用配置,未配置就初始化配置文件
|
||||
RingTongBean bean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
// 初始化配置
|
||||
bean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, bean);
|
||||
}
|
||||
// 如果当前音量和应用保存的不一致就恢复为应用设定值
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
if (ringerVolume != bean.getStreamVolume()) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
}
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
// 检查电话接收规则
|
||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||
// 调低音量
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_SILENT);
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
// 断开电话
|
||||
call.disconnect();
|
||||
// 停顿1秒,预防第一声铃声响动
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, "");
|
||||
}
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
// 屏蔽电话结束
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常接听电话
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallRemoved(Call call) {
|
||||
super.onCallRemoved(call);
|
||||
LogUtils.d(TAG, "通话结束,开始清理资源");
|
||||
if (call != null && mCallCallback != null) {
|
||||
call.unregisterCallback(mCallCallback);
|
||||
LogUtils.d(TAG, "通话回调已注销");
|
||||
}
|
||||
|
||||
// 延迟置空通话对象,避免UI层挂断时对象已被释放(适配小米机型时序)
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 延迟200ms,确保PhoneCallActivity挂断逻辑执行完成
|
||||
Thread.sleep(200);
|
||||
PhoneCallManager.call = null;
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 延迟置空通话对象异常", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
PhoneCallActivity.closePhoneCallActivity();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话资源清理完成");
|
||||
call.unregisterCallback(callback);
|
||||
PhoneCallManager.call = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "服务开始销毁");
|
||||
CallLogFragment.updateCallLogFragment();
|
||||
// 释放资源,适配小米设备内存管理,避免内存泄漏
|
||||
mCallCallback = null;
|
||||
mAudioManager = null;
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务销毁完成");
|
||||
}
|
||||
|
||||
// 初始化方法区
|
||||
private void initAudioManager() {
|
||||
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 音频管理器初始化成功");
|
||||
public enum CallType {
|
||||
CALL_IN,
|
||||
CALL_OUT,
|
||||
}
|
||||
|
||||
|
||||
private void startRecording(long callId) {
|
||||
LogUtils.d(TAG, "startRecording(...)");
|
||||
mediaRecorder = new MediaRecorder();
|
||||
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
|
||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||
mediaRecorder.setOutputFile(getOutputFilePath(callId));
|
||||
try {
|
||||
mediaRecorder.prepare();
|
||||
mediaRecorder.start();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
private void initCallCallback() {
|
||||
mCallCallback = new Call.Callback() {
|
||||
@Override
|
||||
public void onStateChanged(Call call, int state) {
|
||||
super.onStateChanged(call, state);
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "onStateChanged: 通话对象为空");
|
||||
return;
|
||||
}
|
||||
String stateDesc = getCallStateDesc(state);
|
||||
LogUtils.d(TAG, "通话状态变更:" + stateDesc + "(状态码:" + state + ")");
|
||||
|
||||
switch (state) {
|
||||
case Call.STATE_DISCONNECTED:
|
||||
// 双重校验,避免重复关闭页面
|
||||
if (ActivityStack.getInstance().getActivity(PhoneCallActivity.class) != null) {
|
||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||
LogUtils.d(TAG, "通话界面已关闭");
|
||||
}
|
||||
break;
|
||||
case Call.STATE_ACTIVE:
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话进入活跃状态,适配音频通道");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "通话状态回调初始化完成");
|
||||
private String getOutputFilePath(long callId) {
|
||||
LogUtils.d(TAG, "getOutputFilePath(...)");
|
||||
// 设置录音文件的保存路径
|
||||
File file = new File(getExternalFilesDir(TAG), String.format("call_%d.mp4", callId));
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
// 核心业务处理方法区
|
||||
private CallType judgeCallType(Call call) {
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
|
||||
return null;
|
||||
private void stopRecording() {
|
||||
LogUtils.d(TAG, "stopRecording()");
|
||||
if (mediaRecorder != null) {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.release();
|
||||
mediaRecorder = null;
|
||||
}
|
||||
int callState = call.getState();
|
||||
if (callState == Call.STATE_RINGING) {
|
||||
LogUtils.d(TAG, "识别为来电");
|
||||
return CallType.CALL_IN;
|
||||
} else if (callState == Call.STATE_CONNECTING) {
|
||||
LogUtils.d(TAG, "识别为去电");
|
||||
return CallType.CALL_OUT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean handleValidCall(Call call, CallType callType) {
|
||||
if (call == null || callType == null) {
|
||||
LogUtils.e(TAG, "handleValidCall: 通话对象或类型为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
Call.Details callDetails = call.getDetails();
|
||||
if (callDetails == null || callDetails.getHandle() == null) {
|
||||
LogUtils.e(TAG, "通话详情缺失,处理终止");
|
||||
return false;
|
||||
}
|
||||
|
||||
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
|
||||
LogUtils.d(TAG, "处理通话:号码=" + phoneNumber + ",类型=" + callType.name());
|
||||
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, "音频管理器未初始化");
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkRulesAndHandleRingerVolumeControl(phoneNumber, call)) {
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话界面启动成功");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkRulesAndHandleRingerVolumeControl(String phoneNumber, Call call) {
|
||||
if (mAudioManager == null || phoneNumber == null || call == null) {
|
||||
LogUtils.e(TAG, "checkRulesAndHandleRingerVolumeControl: 入参为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
LogUtils.d(TAG, "当前铃声音量:" + currentVolume);
|
||||
|
||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringTongBean == null) {
|
||||
ringTongBean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringTongBean);
|
||||
LogUtils.d(TAG, "初始化默认铃音配置");
|
||||
}
|
||||
final int configVolume = ringTongBean.getStreamVolume();
|
||||
private long getCurrentCallId() {
|
||||
LogUtils.d(TAG, "getCurrentCallId()");
|
||||
ContentResolver contentResolver = getApplicationContext().getContentResolver();
|
||||
Uri callLogUri = Uri.parse("content://call_log/calls");
|
||||
String[] projection = {"_id", "number", "call_type", "date"};
|
||||
String selection = "call_type = " + CallLog.Calls.OUTGOING_TYPE + " OR call_type = " + CallLog.Calls.INCOMING_TYPE;
|
||||
String sortOrder = "date DESC";
|
||||
|
||||
try {
|
||||
// 小米机型适配:调整音量时添加权限校验
|
||||
if (currentVolume != configVolume) {
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
|
||||
Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex("_id"));
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "音量调整失败,权限不足", e);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
// 校验拦截规则
|
||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||
LogUtils.d(TAG, "号码" + phoneNumber + "命中拦截规则");
|
||||
try {
|
||||
// 拦截时静音并挂断
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||
call.disconnect();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 拦截通话已挂断并静音");
|
||||
|
||||
// 延迟恢复音量,适配小米机型音频通道延迟
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
if (mAudioManager != null) {
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 延迟恢复铃音配置");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "恢复音量线程中断", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "拦截静音失败", e);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 辅助工具方法区:解析通话状态描述
|
||||
private String getCallStateDesc(int state) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃中";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中";
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
case Call.STATE_ACTIVE:
|
||||
return "通话活跃";
|
||||
case Call.STATE_CONNECTING:
|
||||
return "连接中";
|
||||
case Call.STATE_DISCONNECTED:
|
||||
return "已断开";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
// 静态内部类:统一管理通话对象,避免跨组件对象混乱
|
||||
public static class PhoneCallManager {
|
||||
public static Call call;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,98 +1,45 @@
|
||||
package cc.winboll.studio.contacts.receivers;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 主要广播接收器
|
||||
*/
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 主要广播接收器,监听系统开机广播并自动启动主服务
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "MainReceiver";
|
||||
// 监听的系统广播 Action
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
WeakReference<MainService> mwrService;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 使用弱引用关联 MainService,避免内存泄漏
|
||||
private WeakReference<MainService> mMainServiceWeakRef;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainReceiver(MainService service) {
|
||||
this.mMainServiceWeakRef = new WeakReference<>(service);
|
||||
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
|
||||
mwrService = new WeakReference<MainService>(service);
|
||||
}
|
||||
|
||||
// ====================== 重写 BroadcastReceiver 核心方法 ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "onReceive: Context 为 null,无法处理广播");
|
||||
return;
|
||||
}
|
||||
if (intent == null || intent.getAction() == null) {
|
||||
LogUtils.w(TAG, "onReceive: 接收到空 Intent 或空 Action");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, "onReceive: 接收到广播 | Action=" + action);
|
||||
|
||||
// 处理开机完成广播
|
||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
LogUtils.i(TAG, "onReceive: 监听到开机完成广播,自动启动 MainService");
|
||||
ToastUtils.show("设备开机,启动拨号主服务");
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
ToastUtils.show("ACTION_BOOT_COMPLETED");
|
||||
MainService.startMainService(context);
|
||||
} else {
|
||||
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
|
||||
ToastUtils.show("收到广播:" + action);
|
||||
ToastUtils.show(szAction);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 广播注册/注销方法区 ======================
|
||||
/**
|
||||
* 注册广播接收器,监听指定系统广播
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
// 注册 Receiver
|
||||
//
|
||||
public void registerAction(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "registerAction: Context 为 null,注册失败");
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_BOOT_COMPLETED);
|
||||
// 可按需添加其他监听的 Action
|
||||
// intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
|
||||
context.registerReceiver(this, intentFilter);
|
||||
LogUtils.d(TAG, "registerAction: 广播接收器注册成功 | 监听 Action=" + ACTION_BOOT_COMPLETED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器,释放资源(解决 mMainReceiver.unregisterAction(this) 调用缺失问题)
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "unregisterAction: Context 为 null,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播接收器注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播接收器未注册,无需注销", e);
|
||||
}
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(ACTION_BOOT_COMPLETED);
|
||||
//filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
context.registerReceiver(this, filter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,271 +1,137 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务,用于监控并保活主服务 MainService
|
||||
* 适配 Android 12+ 后台服务启动限制,支持前台服务运行
|
||||
* 兼容 Java 7 语法 & 低版本 SDK 编译
|
||||
* 移除无关的 microphone 类型配置,修复前台服务类型不匹配崩溃
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "AssistantService";
|
||||
// 前台服务通知配置
|
||||
private static final String FOREGROUND_CHANNEL_ID = "assistant_service_foreground_channel";
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1002;
|
||||
// 修复:前台服务类型改为 dataSync(0x00000001),与 Manifest 保持一致,移除 microphone 类型
|
||||
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001;
|
||||
// Android 版本常量硬编码(Java 7 兼容)
|
||||
private static final int ANDROID_8_API = 26; // 通知渠道最低版本
|
||||
private static final int ANDROID_10_API = 29; // 前台服务类型最低支持版本
|
||||
private static final int ANDROID_12_API = 31; // 后台启动限制最低版本
|
||||
// 重试延迟时间(避免频繁触发后台启动限制)
|
||||
private static final long RETRY_DELAY_MS = 3000L;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private MainServiceBean mMainServiceBean;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private MainService mMainService;
|
||||
private boolean mIsBound = false;
|
||||
private volatile boolean mIsThreadAlive = false;
|
||||
MainServiceBean mMainServiceBean;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
MainService mMainService;
|
||||
boolean isBound = false;
|
||||
volatile boolean isThreadAlive = false;
|
||||
|
||||
// ====================== Binder 内部类 ======================
|
||||
/**
|
||||
* 对外暴露服务实例的 Binder
|
||||
*/
|
||||
public class MyBinder extends Binder {
|
||||
public AssistantService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 获取 AssistantService 实例");
|
||||
return AssistantService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== ServiceConnection 内部类 ======================
|
||||
/**
|
||||
* 主服务连接状态监听回调
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
|
||||
mIsBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
mIsBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 主服务绑定成功 | MainService=" + mMainService);
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
|
||||
mIsBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务连接断开");
|
||||
mMainService = null;
|
||||
mIsBound = false;
|
||||
|
||||
// 尝试重新绑定主服务(如果配置为启用)
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务");
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外方法区 ======================
|
||||
/**
|
||||
* 设置线程存活状态
|
||||
*/
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
this.mIsThreadAlive = isThreadAlive;
|
||||
LogUtils.d(TAG, "setIsThreadAlive: 线程存活状态变更 | " + isThreadAlive);
|
||||
LogUtils.d(TAG, "setIsThreadAlive(...)");
|
||||
LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive));
|
||||
this.isThreadAlive = isThreadAlive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程存活状态
|
||||
*/
|
||||
public boolean isThreadAlive() {
|
||||
return mIsThreadAlive;
|
||||
}
|
||||
|
||||
// ====================== 前台服务辅助方法 ======================
|
||||
/**
|
||||
* 创建前台服务通知(Android 8.0+ 必须配置渠道)
|
||||
*/
|
||||
private Notification createForegroundNotification() {
|
||||
// 1. 创建通知渠道(API 26+ 必需)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
FOREGROUND_CHANNEL_ID,
|
||||
"守护服务",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("守护服务后台运行,保障主服务存活");
|
||||
// 空指针防护
|
||||
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建通知(Java 7 分步设置,取消链式调用简化)
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
|
||||
} else {
|
||||
builder = new Notification.Builder(this);
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_launcher);
|
||||
builder.setContentTitle("守护服务运行中");
|
||||
builder.setContentText("正在监控主服务状态");
|
||||
builder.setPriority(Notification.PRIORITY_LOW);
|
||||
builder.setOngoing(true); // 不可手动取消
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// ====================== Service 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
||||
|
||||
// 适配 Android 12+ 后台启动限制:应用后台时启动为前台服务
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_12_API) {
|
||||
Notification notification = createForegroundNotification();
|
||||
// 修复:使用 dataSync 类型,添加异常捕获防止崩溃
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
} else {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, notification);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: 守护服务已启动为前台服务(dataSync 类型)");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "onCreate: 启动前台服务失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主服务连接回调
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
LogUtils.d(TAG, "onCreate: 初始化 MyServiceConnection 完成");
|
||||
}
|
||||
|
||||
// 初始化运行状态
|
||||
setIsThreadAlive(false);
|
||||
// 启动守护逻辑
|
||||
assistantService();
|
||||
return isThreadAlive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
||||
// 每次启动都执行守护逻辑,确保主服务存活
|
||||
public void onCreate() {
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
super.onCreate();
|
||||
|
||||
//mMyBinder = new MyBinder();
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
setIsThreadAlive(false);
|
||||
assistantService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "call onStartCommand(...)");
|
||||
assistantService();
|
||||
// START_STICKY:服务被杀死后系统尝试重启
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁");
|
||||
|
||||
// 停止线程并解除主服务绑定
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
setIsThreadAlive(false);
|
||||
if (mIsBound && mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 解除主服务绑定成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
|
||||
}
|
||||
mIsBound = false;
|
||||
// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
mMainService = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// ====================== 核心守护逻辑方法区 ======================
|
||||
/**
|
||||
* 守护服务核心逻辑:检查配置并保活主服务
|
||||
*/
|
||||
private void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService: 执行守护逻辑");
|
||||
|
||||
// 加载主服务配置
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean == null) {
|
||||
LogUtils.e(TAG, "assistantService: 主服务配置加载失败,终止守护逻辑");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "assistantService: 主服务启用状态 | " + mMainServiceBean.isEnable());
|
||||
// 配置启用且线程未存活时,唤醒并绑定主服务
|
||||
if (mMainServiceBean.isEnable() && !isThreadAlive()) {
|
||||
setIsThreadAlive(true);
|
||||
wakeupAndBindMain();
|
||||
} else if (!mMainServiceBean.isEnable()) {
|
||||
setIsThreadAlive(false);
|
||||
LogUtils.d(TAG, "assistantService: 主服务已禁用,停止保活");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒并绑定主服务 MainService(适配后台启动限制)
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
if (mMyServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||
startForegroundService(intent);
|
||||
|
||||
// BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
|
||||
}
|
||||
|
||||
// ====================== 辅助方法区 ======================
|
||||
/**
|
||||
* 重新加载主服务配置
|
||||
*/
|
||||
private void reloadMainServiceConfig() {
|
||||
// 运行服务内容
|
||||
//
|
||||
void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
|
||||
LogUtils.d(TAG, String.format("mMainServiceBean.isEnable() %s", mMainServiceBean.isEnable()));
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, String.format("mIsThreadAlive %s", isThreadAlive()));
|
||||
if (isThreadAlive() == false) {
|
||||
// 设置运行状态
|
||||
setIsThreadAlive(true);
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain()");
|
||||
// 绑定服务的Intent
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
startService(new Intent(this, MainService.class));
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// startService(new Intent(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) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
mMainServiceBean = MainServiceBean.loadBean(AssistantService.this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
isBound = false;
|
||||
mMainService = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
public class MyBinder extends Binder {
|
||||
AssistantService getService() {
|
||||
LogUtils.d(TAG, "AssistantService MyBinder getService()");
|
||||
return AssistantService.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务
|
||||
* 参考:
|
||||
* 进程保活-双进程守护的正确姿势
|
||||
* https://blog.csdn.net/sinat_35159441/article/details/75267380
|
||||
* Android Service之onStartCommand方法研究
|
||||
* https://blog.csdn.net/cyp331203/article/details/38920491
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -11,586 +17,309 @@ import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.App;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.receivers.MainReceiver;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.threads.MainServiceThread;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.sos.SOS;
|
||||
import cc.winboll.studio.libappbase.winboll.WinBoLL;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
|
||||
* 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃
|
||||
* 核心优化:1. 移除延迟启动逻辑 2. 标准化日志管理 3. 强化资源清理 4. 结构分层重构
|
||||
*/
|
||||
public class MainService extends Service {
|
||||
|
||||
// ====================== 常量定义区(全硬编码,无高版本API依赖) ======================
|
||||
public static final String TAG = "MainService";
|
||||
|
||||
public static final int MSG_UPDATE_STATUS = 0;
|
||||
|
||||
// 铃声音量监控参数(定时检查+恢复)
|
||||
private static final long VOLUME_CHECK_DELAY = 1000L; // 首次检查延迟1s
|
||||
private static final long VOLUME_CHECK_PERIOD = 60000L; // 后续每60s检查一次
|
||||
static MainService _mControlCenterService;
|
||||
|
||||
// 前台服务配置(固定ID+渠道,避免重复创建)
|
||||
private static final String FOREGROUND_CHANNEL_ID = "main_service_foreground_channel";
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1001;
|
||||
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; // dataSync类型硬编码
|
||||
volatile boolean isServiceRunning;
|
||||
|
||||
// Android版本常量(替代Build.VERSION_CODES,适配Java7)
|
||||
private static final int ANDROID_8_API = 26; // Android 8.0
|
||||
private static final int ANDROID_10_API = 29; // Android 10
|
||||
private static final int ANDROID_12_API = 31; // Android 12
|
||||
|
||||
// 守护服务重绑定延迟(仅保留核心重试逻辑)
|
||||
private static final long RETRY_DELAY_MS = 3000L;
|
||||
|
||||
// ====================== 静态成员属性区(全局共享实例,统一前缀s) ======================
|
||||
private static MainService sMainServiceInstance; // 主服务全局实例
|
||||
private static volatile TomCat sTomCatInstance; // 号码识别核心实例(volatile保证可见性)
|
||||
|
||||
// ====================== 成员属性区(业务+UI+资源,统一前缀m) ======================
|
||||
private volatile boolean mIsServiceRunning; // 服务运行状态标记(volatile防指令重排)
|
||||
private MainServiceBean mMainServiceBean; // 服务配置实体(启用状态存储)
|
||||
private MainServiceHandler mMainServiceHandler; // 服务消息处理器(主线程通信)
|
||||
private MyServiceConnection mServiceConnection; // 守护服务连接实例
|
||||
private AssistantService mAssistantService; // 绑定的守护服务实例
|
||||
private boolean mIsAssistantBound; // 守护服务绑定状态标记
|
||||
private MainReceiver mMainReceiver; // 全局广播接收器(监听系统事件)
|
||||
private Timer mVolumeCheckTimer; // 铃声音量检查定时器(定时恢复配置)
|
||||
|
||||
// ====================== 内部类:Binder(服务绑定通信,优先定义) ======================
|
||||
public class MyBinder extends Binder {
|
||||
/**
|
||||
* 外部组件绑定服务时,获取主服务实例
|
||||
* @return MainService 主服务实例
|
||||
*/
|
||||
public MainService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 外部获取主服务实例");
|
||||
return MainService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类:ServiceConnection(守护服务绑定回调) ======================
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的IBinder为空,绑定失败");
|
||||
mIsAssistantBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 类型转换获取守护服务实例
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
mIsAssistantBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功");
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder类型转换失败", e);
|
||||
mIsAssistantBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开");
|
||||
mAssistantService = null;
|
||||
mIsAssistantBound = false;
|
||||
|
||||
// 服务启用状态下,重试绑定守护服务(主服务存活核心保障)
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: " + RETRY_DELAY_MS + "ms后重试绑定守护服务");
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
wakeupAndBindAssistantService();
|
||||
}
|
||||
}, RETRY_DELAY_MS);
|
||||
} else {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务已禁用,跳过重试绑定");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外静态方法区(服务启停/重启/状态查询,全局调用) ======================
|
||||
/**
|
||||
* 检查号码是否在BoBullToon库中(外部组件调用,静态入口)
|
||||
* @param phone 待查询号码
|
||||
* @return true=是BoBullToon号码,false=否/初始化失败
|
||||
*/
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (sTomCatInstance != null && phone != null && !phone.isEmpty()) {
|
||||
boolean result = sTomCatInstance.isPhoneBoBullToon(phone);
|
||||
LogUtils.d(TAG, "isPhoneInBoBullToon: 号码" + phone + "查询结果:" + (result ? "是" : "否"));
|
||||
return result;
|
||||
}
|
||||
LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat未初始化或号码为空,查询失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止主服务(仅停止,不修改配置)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void stopMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainService: 上下文为空,无法停止服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainService: 执行停止主服务操作");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动主服务(仅启动,不修改配置)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void startMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainService: 上下文为空,无法启动服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainService: 执行启动主服务操作(前台服务模式)");
|
||||
Intent intent = new Intent(context, MainService.class);
|
||||
context.startForegroundService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启主服务(先停后启,需服务已启用)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void restartMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "restartMainService: 上下文为空,无法重启服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "restartMainService: 执行主服务重启流程");
|
||||
|
||||
MainServiceBean config = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (config != null && config.isEnable()) {
|
||||
stopMainService(context);
|
||||
startMainService(context);
|
||||
LogUtils.i(TAG, "restartMainService: 主服务重启完成");
|
||||
} else {
|
||||
LogUtils.w(TAG, "restartMainService: 服务未启用或配置为空,跳过重启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务并保存禁用状态(更新配置+停止服务)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainServiceAndSaveStatus: 上下文为空,操作失败");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 保存禁用状态并停止服务");
|
||||
MainServiceBean config = new MainServiceBean();
|
||||
config.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, config);
|
||||
stopMainService(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务并保存启用状态(更新配置+启动服务,先停后启避免重复)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void startMainServiceAndSaveStatus(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainServiceAndSaveStatus: 上下文为空,操作失败");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus: 保存启用状态并启动服务");
|
||||
MainServiceBean config = new MainServiceBean();
|
||||
config.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, config);
|
||||
stopMainService(context); // 先停止旧服务,避免冲突
|
||||
startMainService(context);
|
||||
}
|
||||
|
||||
// ====================== 核心工具方法区(服务状态检查+前台通知创建,通用功能) ======================
|
||||
/**
|
||||
* 补充消息追加方法(外部组件向服务发送消息)
|
||||
* @param message 待追加消息(空值防护)
|
||||
*/
|
||||
public void appenMessage(String message) {
|
||||
String msg = message == null ? "null" : message;
|
||||
LogUtils.d(TAG, "appenMessage: 接收外部消息:" + msg);
|
||||
|
||||
if (mMainServiceHandler != null) {
|
||||
android.os.Message handlerMsg = android.os.Message.obtain();
|
||||
handlerMsg.what = MSG_UPDATE_STATUS;
|
||||
handlerMsg.obj = msg;
|
||||
mMainServiceHandler.sendMessage(handlerMsg);
|
||||
LogUtils.d(TAG, "appenMessage: 消息已发送至Handler处理");
|
||||
} else {
|
||||
LogUtils.w(TAG, "appenMessage: MainServiceHandler未初始化,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建前台服务通知(Android8.0+需渠道,低版本兼容)
|
||||
* @return Notification 前台服务通知实例
|
||||
*/
|
||||
private Notification createForegroundNotification() {
|
||||
// 1. Android8.0+创建通知渠道(必需,否则通知不显示)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
FOREGROUND_CHANNEL_ID,
|
||||
"拨号主服务",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("主服务后台运行,保障通话监听与号码识别功能正常");
|
||||
channel.setSound(null, null); // 关闭通知声音
|
||||
channel.enableVibration(false); // 关闭振动
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "createForegroundNotification: Android8.0+通知渠道创建成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "createForegroundNotification: NotificationManager获取失败,渠道创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建通知实例(分版本兼容Builder)
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
|
||||
} else {
|
||||
builder = new Notification.Builder(this);
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_launcher);
|
||||
builder.setContentTitle("拨号服务运行中");
|
||||
builder.setContentText("后台保障通话监听与号码识别,请勿手动关闭");
|
||||
builder.setPriority(Notification.PRIORITY_LOW); // 低优先级,不打扰用户
|
||||
builder.setOngoing(true); // 不可手动清除,保障服务存活
|
||||
|
||||
LogUtils.d(TAG, "createForegroundNotification: 前台服务通知构建完成");
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定服务是否正在运行(通过ActivityManager查询)
|
||||
* @param serviceClass 待检查服务类
|
||||
* @return true=运行中,false=未运行/查询失败
|
||||
*/
|
||||
private boolean isServiceRunning(Class<?> serviceClass) {
|
||||
if (serviceClass == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning: 服务类为空,检查失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager == null) {
|
||||
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败,检查失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 遍历运行中服务,匹配类名
|
||||
for (ActivityManager.RunningServiceInfo serviceInfo : activityManager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "正在运行");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "未运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序:创建→绑定→启动→销毁) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "===== onCreate: 主服务开始创建 =====");
|
||||
sMainServiceInstance = this;
|
||||
mIsServiceRunning = false;
|
||||
|
||||
// 初始化核心组件(无延迟,直接初始化)
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
mServiceConnection = new MyServiceConnection();
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 初始化音量监控定时器(服务启动即开启,保障音量配置)
|
||||
initVolumeCheckTimer();
|
||||
|
||||
// 执行核心业务启动逻辑(无延迟,优先启动)
|
||||
startCoreBusiness();
|
||||
|
||||
LogUtils.d(TAG, "===== onCreate: 主服务创建完成 =====");
|
||||
}
|
||||
MainServiceBean mMainServiceBean;
|
||||
MainServiceThread mMainServiceThread;
|
||||
MainServiceHandler mMainServiceHandler;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
AssistantService mAssistantService;
|
||||
boolean isBound = false;
|
||||
MainReceiver mMainReceiver;
|
||||
Timer mStreamVolumeCheckTimer;
|
||||
static volatile TomCat _TomCat;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 主服务被外部组件绑定,Intent=" + (intent != null ? intent.getAction() : "null"));
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
public MainServiceThread getRemindThread() {
|
||||
return mMainServiceThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate()");
|
||||
_mControlCenterService = MainService.this;
|
||||
isServiceRunning = false;
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 铃声检查定时器
|
||||
mStreamVolumeCheckTimer = new Timer();
|
||||
mStreamVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
// 恢复铃声音量,预防其他意外条件导致的音量变化问题
|
||||
//
|
||||
|
||||
// 读取应用配置,未配置就初始化配置文件
|
||||
RingTongBean bean = RingTongBean.loadBean(MainService.this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
// 初始化配置
|
||||
bean = new RingTongBean();
|
||||
RingTongBean.saveBean(MainService.this, bean);
|
||||
}
|
||||
// 如果当前音量和应用保存的不一致就恢复为应用设定值
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
if (ringerVolume != bean.getStreamVolume()) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
}
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
}, 1000, 60000);
|
||||
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 主服务被启动,startId=" + startId);
|
||||
// 重复启动时再次执行核心业务(避免服务被杀死后重启失败)
|
||||
startCoreBusiness();
|
||||
LogUtils.d(TAG, "onStartCommand(...)");
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
// 服务启用则返回START_STICKY(系统杀死后自动重启),否则默认行为
|
||||
int result = (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (result == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
return result;
|
||||
// 运行服务内容
|
||||
//
|
||||
void mainService() {
|
||||
LogUtils.d(TAG, "mainService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable() && isServiceRunning == false) {
|
||||
LogUtils.d(TAG, "mainService() start running");
|
||||
isServiceRunning = true;
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
// 召唤 WinBoLL APP 绑定本服务
|
||||
if (App.isDebuging()) {
|
||||
WinBoLL.bindToAPPBaseBeta(this, MainService.class.getName());
|
||||
} else {
|
||||
WinBoLL.bindToAPPBase(this, MainService.class.getName());
|
||||
}
|
||||
|
||||
// 初始化服务运行参数
|
||||
_TomCat = TomCat.getInstance(this);
|
||||
if (!_TomCat.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "没有下载 BoBullToon 数据。BoBullToon 参数无法加载。");
|
||||
}
|
||||
|
||||
if (mMainReceiver == null) {
|
||||
// 注册广播接收器
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
}
|
||||
|
||||
Rules.getInstance(this).loadRules();
|
||||
|
||||
startPhoneCallListener();
|
||||
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).start();
|
||||
|
||||
LogUtils.i(TAG, "Main Service Is Start.");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (_TomCat != null) {
|
||||
return _TomCat.isPhoneBoBullToon(phone);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 唤醒和绑定守护进程
|
||||
//
|
||||
void wakeupAndBindAssistant() {
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistant()");
|
||||
// if (ServiceUtils.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) {
|
||||
// startService(new Intent(MainService.this, AssistantService.class));
|
||||
// //LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService");
|
||||
// bindService(new Intent(MainService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
// }
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
startService(intent);
|
||||
// 绑定服务的Intent
|
||||
//Intent intent = new Intent(this, AssistantService.class);
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// Intent intent = new Intent(this, AssistantService.class);
|
||||
// startService(intent);
|
||||
// LogUtils.d(TAG, "startService(intent)");
|
||||
// bindService(new Intent(this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
void startPhoneCallListener() {
|
||||
Intent callListener = new Intent(this, CallListenerService.class);
|
||||
startService(callListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁,全量清理资源 =====");
|
||||
|
||||
// 1. 停止音量监控定时器(释放线程资源)
|
||||
cancelVolumeCheckTimer();
|
||||
|
||||
// 2. 解除守护服务绑定(避免内存泄漏)
|
||||
if (mIsAssistantBound && mServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务绑定已解除");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除守护服务绑定失败(服务未绑定)", e);
|
||||
}
|
||||
mIsAssistantBound = false;
|
||||
mServiceConnection = null;
|
||||
}
|
||||
|
||||
// 3. 注销广播接收器(释放系统资源)
|
||||
if (mMainReceiver != null) {
|
||||
mMainReceiver.unregisterAction(this);
|
||||
mMainReceiver = null;
|
||||
LogUtils.d(TAG, "onDestroy: 全局广播接收器已注销");
|
||||
}
|
||||
|
||||
// 4. 停止所有子服务(强制停止,避免子服务残留)
|
||||
stopService(new Intent(this, AssistantService.class));
|
||||
stopService(new Intent(this, CallListenerService.class));
|
||||
stopService(new Intent(this, MyCallScreeningService.class));
|
||||
LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止");
|
||||
|
||||
// 5. 清空所有引用(静态+成员,彻底避免内存泄漏,不修改配置Bean)
|
||||
sMainServiceInstance = null;
|
||||
sTomCatInstance = null;
|
||||
mMainServiceHandler = null;
|
||||
mAssistantService = null;
|
||||
mMainServiceBean = null;
|
||||
|
||||
// 标记服务为未运行
|
||||
mIsServiceRunning = false;
|
||||
LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成,资源全清理 =====");
|
||||
}
|
||||
|
||||
// ====================== 核心业务逻辑区(服务启动核心流程,无延迟) ======================
|
||||
/**
|
||||
* 启动核心业务(主服务核心入口,避免重复启动)
|
||||
*/
|
||||
private synchronized void startCoreBusiness() {
|
||||
// 服务已运行则直接返回,避免重复执行启动流程
|
||||
if (mIsServiceRunning) {
|
||||
LogUtils.d(TAG, "startCoreBusiness: 主服务已运行,跳过重复启动");
|
||||
return;
|
||||
}
|
||||
mIsServiceRunning = true;
|
||||
|
||||
// 重新加载最新配置(避免配置修改后未生效)
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean == null || !mMainServiceBean.isEnable()) {
|
||||
LogUtils.w(TAG, "startCoreBusiness: 服务配置未启用或配置为空,启动流程终止");
|
||||
mIsServiceRunning = false;
|
||||
stopSelf(); // 未启用则主动停止服务
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.i(TAG, "startCoreBusiness: 服务配置已启用,启动核心流程");
|
||||
|
||||
// 1. 优先启动前台服务(避免前台服务启动超时崩溃,核心优先级)
|
||||
try {
|
||||
Notification foregroundNotification = createForegroundNotification();
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
LogUtils.i(TAG, "startCoreBusiness: 前台服务启动成功,通知ID=" + FOREGROUND_NOTIFICATION_ID);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动失败(服务类型不匹配)", e);
|
||||
mIsServiceRunning = false;
|
||||
stopSelf();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动异常", e);
|
||||
mIsServiceRunning = false;
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 绑定守护服务(保障主服务存活,防系统杀死)
|
||||
wakeupAndBindAssistantService();
|
||||
|
||||
// 3. 初始化核心业务组件(号码识别+黑白名单规则)
|
||||
initTomCatComponent();
|
||||
initRulesConfig();
|
||||
|
||||
// 4. 启动通话监听相关服务(无延迟,直接启动,保障功能生效)
|
||||
startCallRelatedServices();
|
||||
|
||||
LogUtils.i(TAG, "startCoreBusiness: 主服务核心流程启动完成,服务正常运行");
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒并绑定守护服务(分版本适配启动方式,Android10+前台服务启动)
|
||||
*/
|
||||
private void wakeupAndBindAssistantService() {
|
||||
if (mServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindAssistantService: 服务连接实例未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent assistantIntent = new Intent(this, AssistantService.class);
|
||||
// Android10+应用后台启动服务需用前台服务模式
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistantService: Android10+,前台服务模式启动守护服务");
|
||||
startForegroundService(assistantIntent);
|
||||
|
||||
// 绑定守护服务(BIND_IMPORTANT:高优先级绑定,断开时回调)
|
||||
bindService(assistantIntent, mServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistantService: 守护服务启动并发起绑定请求");
|
||||
}
|
||||
|
||||
// ====================== 铃声音量监控方法区(定时检查+恢复配置) ======================
|
||||
/**
|
||||
* 初始化音量检查定时器(先取消旧定时器,避免重复创建)
|
||||
*/
|
||||
private void initVolumeCheckTimer() {
|
||||
cancelVolumeCheckTimer();
|
||||
mVolumeCheckTimer = new Timer();
|
||||
mVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkAndRestoreRingerVolume();
|
||||
}
|
||||
}, VOLUME_CHECK_DELAY, VOLUME_CHECK_PERIOD);
|
||||
LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量监控定时器启动,周期" + VOLUME_CHECK_PERIOD + "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并恢复铃声音量(对比配置值,不一致则恢复,保障用户配置生效)
|
||||
*/
|
||||
private void checkAndRestoreRingerVolume() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: AudioManager获取失败,音量检查终止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载音量配置(无配置则初始化默认值)
|
||||
RingTongBean ringConfig = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringConfig == null) {
|
||||
ringConfig = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringConfig);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 音量配置未存在,初始化默认配置");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
int configVolume = ringConfig.getStreamVolume();
|
||||
// 音量不一致则恢复配置值
|
||||
if (currentVolume != configVolume) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复,配置值=" + configVolume + ",原当前值=" + currentVolume);
|
||||
} else {
|
||||
LogUtils.v(TAG, "checkAndRestoreRingerVolume: 铃声音量与配置一致,无需调整");
|
||||
//LogUtils.d(TAG, "onDestroy done");
|
||||
if (mMainServiceBean.isEnable() == false) {
|
||||
// 设置运行状态
|
||||
isServiceRunning = false;// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足,恢复失败", e);
|
||||
// 停止守护进程
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
stopService(intent);
|
||||
// 停止Receiver
|
||||
if (mMainReceiver != null) {
|
||||
unregisterReceiver(mMainReceiver);
|
||||
mMainReceiver = null;
|
||||
}
|
||||
// 停止前台通知栏
|
||||
stopForeground(true);
|
||||
|
||||
// 停止主要进程
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).setIsExit(true);
|
||||
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
if (App.isDebuging()) {
|
||||
SOS.sosToAppBase(getApplicationContext(), MainService.class.getName());
|
||||
} else {
|
||||
SOS.sosToAppBaseBeta(getApplicationContext(), MainService.class.getName());
|
||||
}
|
||||
}
|
||||
isBound = false;
|
||||
mAssistantService = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
public class MyBinder extends Binder {
|
||||
MainService getService() {
|
||||
LogUtils.d(TAG, "MainService MyBinder getService()");
|
||||
return MainService.this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消音量检查定时器(释放Timer资源,避免内存泄漏)
|
||||
*/
|
||||
private void cancelVolumeCheckTimer() {
|
||||
if (mVolumeCheckTimer != null) {
|
||||
mVolumeCheckTimer.cancel();
|
||||
mVolumeCheckTimer = null;
|
||||
LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量监控定时器已取消");
|
||||
// //
|
||||
// // 启动服务
|
||||
// //
|
||||
// public static void startControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.startForegroundService(intent);
|
||||
// }
|
||||
//
|
||||
// //
|
||||
// // 停止服务
|
||||
// //
|
||||
// public static void stopControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.stopService(intent);
|
||||
// }
|
||||
|
||||
public void appenMessage(String message) {
|
||||
LogUtils.d(TAG, String.format("Message : %s", message));
|
||||
}
|
||||
|
||||
public static void stopMainService(Context context) {
|
||||
LogUtils.d(TAG, "stopMainService");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
public static void startMainService(Context context) {
|
||||
LogUtils.d(TAG, "startMainService");
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
public static void restartMainService(Context context) {
|
||||
LogUtils.d(TAG, "restartMainService");
|
||||
|
||||
MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (bean != null && bean.isEnable()) {
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
// try {
|
||||
// Thread.sleep(1000);
|
||||
// } catch (InterruptedException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// }
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
LogUtils.d(TAG, "已重启 MainService");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助初始化方法区(业务组件初始化,统一归类) ======================
|
||||
/**
|
||||
* 初始化TomCat组件(号码识别核心,加载本地数据)
|
||||
*/
|
||||
private void initTomCatComponent() {
|
||||
sTomCatInstance = TomCat.getInstance(this);
|
||||
if (sTomCatInstance.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "initTomCatComponent: BoBullToon号码库加载成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "initTomCatComponent: BoBullToon号码库未下载,加载失败(不影响服务运行)");
|
||||
}
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑白名单规则配置(加载本地规则,保障通话筛选生效)
|
||||
*/
|
||||
private void initRulesConfig() {
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules != null) {
|
||||
rules.loadRules();
|
||||
LogUtils.d(TAG, "initRulesConfig: 黑白名单通话规则加载完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initRulesConfig: Rules实例获取失败,通话规则加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化广播接收器(监听系统事件,如开机启动、网络变化)
|
||||
*/
|
||||
private void initMainReceiver() {
|
||||
if (mMainReceiver == null) {
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
LogUtils.d(TAG, "initMainReceiver: 全局广播接收器注册完成");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initMainReceiver: 广播接收器已初始化,跳过重复注册");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动通话相关服务(通话监听+通话筛选,核心功能服务)
|
||||
*/
|
||||
private void startCallRelatedServices() {
|
||||
// 启动通话监听服务
|
||||
try {
|
||||
Intent callListenerIntent = new Intent(this, CallListenerService.class);
|
||||
startService(callListenerIntent);
|
||||
LogUtils.d(TAG, "startCallRelatedServices: CallListenerService启动成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCallRelatedServices: CallListenerService启动失败", e);
|
||||
}
|
||||
|
||||
// 启动通话筛选服务(API10+生效,低版本兼容)
|
||||
try {
|
||||
Intent screeningIntent = new Intent(this, MyCallScreeningService.class);
|
||||
startService(screeningIntent);
|
||||
LogUtils.d(TAG, "startCallRelatedServices: MyCallScreeningService启动成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCallRelatedServices: MyCallScreeningService启动失败", e);
|
||||
}
|
||||
public static void startMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.telecom.CallScreeningService;
|
||||
import android.telephony.TelephonyManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Describe 通话筛选服务(无前台服务),负责识别通话类型、拦截指定号码、处理正常通话逻辑
|
||||
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 强化空指针防护 | 无冗余代码
|
||||
*/
|
||||
@RequiresApi(api = 29) // 父类 CallScreeningService 最低要求 API29,明确标注
|
||||
public class MyCallScreeningService extends CallScreeningService {
|
||||
|
||||
// ====================== 常量定义区(全硬编码,无高版本API依赖) ======================
|
||||
public static final String TAG = "MyCallScreeningService";
|
||||
|
||||
// 通话方向常量(硬编码替代 Call.Details 高版本字段,适配API29-30)
|
||||
private static final int CALL_DIRECTION_INCOMING = 1; // 来电
|
||||
private static final int CALL_DIRECTION_OUTGOING = 2; // 外拨
|
||||
|
||||
// ====================== 成员属性区(精简必要属性,命名规范) ======================
|
||||
private Context mContext; // 上下文对象,避免重复调用 getApplicationContext()
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mContext = this;
|
||||
LogUtils.d(TAG, "===== onCreate: 通话筛选服务启动 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId);
|
||||
|
||||
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
|
||||
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
|
||||
return startMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 置空上下文,释放引用,避免内存泄漏
|
||||
mContext = null;
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话筛选服务销毁完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 核心通话筛选方法区(父类抽象方法实现) ======================
|
||||
/**
|
||||
* 核心:通话筛选入口(API29-30标准方法,100%兼容)
|
||||
* 功能:识别通话号码/类型、拦截指定号码、处理正常通话
|
||||
*/
|
||||
@Override
|
||||
@RequiresApi(api = 29)
|
||||
public void onScreenCall(@NonNull android.telecom.Call.Details details) {
|
||||
LogUtils.d(TAG, "===== onScreenCall: 开始筛选通话 =====");
|
||||
|
||||
// 1. 安全获取通话号码(多层空指针防护,Java7规范写法)
|
||||
String phoneNumber = getSafePhoneNumber(details);
|
||||
// 2. 识别通话方向(来电/外拨/未知)
|
||||
int callDirection = details.getCallDirection();
|
||||
String callTypeDesc = getCallDirectionDesc(callDirection);
|
||||
int callState = getCallStateByDirection(callDirection);
|
||||
|
||||
LogUtils.d(TAG, "筛选结果:通话类型=" + callTypeDesc + ",号码=" + phoneNumber);
|
||||
|
||||
// 3. 自定义拦截逻辑(示例:拦截指定号码10086,可按需扩展黑白名单)
|
||||
boolean isNeedBlock = isTargetBlockNumber(phoneNumber);
|
||||
|
||||
// 4. 构建筛选响应(Java7分步调用,不使用链式写法,避免兼容问题)
|
||||
CallResponse callResponse = buildCallScreeningResponse(isNeedBlock);
|
||||
|
||||
// 5. 提交筛选结果(必须调用父类方法,完成拦截/放行逻辑)
|
||||
respondToCall(details, callResponse);
|
||||
|
||||
// 6. 分场景处理后续逻辑(拦截日志/正常通话业务)
|
||||
handleCallAfterScreening(phoneNumber, callTypeDesc, isNeedBlock);
|
||||
|
||||
LogUtils.d(TAG, "===== onScreenCall: 通话筛选完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法区(按功能拆分,低耦合) ======================
|
||||
/**
|
||||
* 安全获取通话号码(多层空指针+空字符串防护,避免崩溃)
|
||||
*/
|
||||
private String getSafePhoneNumber(android.telecom.Call.Details details) {
|
||||
String phoneNumber = "未知号码";
|
||||
if (details == null) {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 通话详情为空,无法获取号码");
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
Uri handle = details.getHandle();
|
||||
if (handle != null) {
|
||||
String schemePart = handle.getSchemeSpecificPart();
|
||||
if (schemePart != null && !schemePart.trim().isEmpty()) {
|
||||
phoneNumber = schemePart.trim();
|
||||
LogUtils.d(TAG, "getSafePhoneNumber: 成功获取号码,原始值=" + schemePart + ",处理后=" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 号码格式异常,schemePart=" + schemePart);
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 通话 handle 为空,无法获取号码");
|
||||
}
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为目标拦截号码(可扩展黑白名单逻辑,当前示例拦截10086)
|
||||
*/
|
||||
private boolean isTargetBlockNumber(String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "isTargetBlockNumber: 号码为空,不拦截");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 示例拦截逻辑:拦截 10086(实际可替换为黑白名单查询)
|
||||
boolean isBlock = "10086".equals(phoneNumber.trim());
|
||||
if (isBlock) {
|
||||
LogUtils.d(TAG, "isTargetBlockNumber: 命中拦截规则,号码=" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.d(TAG, "isTargetBlockNumber: 未命中拦截规则,号码=" + phoneNumber);
|
||||
}
|
||||
return isBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通话筛选响应(按需配置拦截/放行参数,适配API29-30)
|
||||
*/
|
||||
private CallResponse buildCallScreeningResponse(boolean isNeedBlock) {
|
||||
CallResponse.Builder responseBuilder = new CallResponse.Builder();
|
||||
// 拦截配置:是否禁止通话+是否拒绝通话(两者配合实现拦截)
|
||||
responseBuilder.setDisallowCall(isNeedBlock);
|
||||
responseBuilder.setRejectCall(isNeedBlock);
|
||||
// 日志/通知配置:拦截的通话跳过日志和通知,正常通话保留
|
||||
responseBuilder.setSkipCallLog(isNeedBlock);
|
||||
responseBuilder.setSkipNotification(isNeedBlock);
|
||||
|
||||
CallResponse response = responseBuilder.build();
|
||||
LogUtils.d(TAG, "buildCallScreeningResponse: 响应构建完成,拦截状态=" + isNeedBlock);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选后分场景处理(拦截日志/正常通话业务扩展)
|
||||
*/
|
||||
private void handleCallAfterScreening(String phoneNumber, String callTypeDesc, boolean isNeedBlock) {
|
||||
if (isNeedBlock) {
|
||||
// 拦截场景:仅打日志(可扩展:添加拦截记录、本地存储等)
|
||||
LogUtils.d(TAG, "handleCallAfterScreening: 已拦截通话,类型=" + callTypeDesc + ",号码=" + phoneNumber);
|
||||
} else {
|
||||
// 正常通话场景:处理业务逻辑(可扩展:通话记录、广播通知、号码识别等)
|
||||
int callState = getCallStateByDirection(getCallDirectionFromDesc(callTypeDesc));
|
||||
handleNormalCallBusiness(phoneNumber, callState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常通话业务处理(核心业务扩展入口,强化空指针防护)
|
||||
*/
|
||||
private void handleNormalCallBusiness(String phoneNumber, int callState) {
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "handleNormalCallBusiness: 号码为空,跳过业务处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String callStateDesc = getCallStateDesc(callState);
|
||||
LogUtils.d(TAG, "handleNormalCallBusiness: 处理正常通话业务,号码=" + phoneNumber + ",状态=" + callStateDesc);
|
||||
|
||||
// 此处可扩展业务逻辑(示例):
|
||||
// 1. 保存通话记录到本地
|
||||
// 2. 发送广播通知其他组件(如通话监听服务)
|
||||
// 3. 调用号码识别接口,匹配联系人信息
|
||||
}
|
||||
|
||||
// ====================== 工具辅助方法区(统一归类,复用性强) ======================
|
||||
/**
|
||||
* 通话方向转文字描述(便于日志查看,快速定位场景)
|
||||
*/
|
||||
private String getCallDirectionDesc(int callDirection) {
|
||||
switch (callDirection) {
|
||||
case CALL_DIRECTION_INCOMING:
|
||||
return "来电";
|
||||
case CALL_DIRECTION_OUTGOING:
|
||||
return "外拨";
|
||||
default:
|
||||
return "未知通话";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字描述转通话方向(配合业务逻辑反向匹配,避免重复判断)
|
||||
*/
|
||||
private int getCallDirectionFromDesc(String callTypeDesc) {
|
||||
if ("来电".equals(callTypeDesc)) {
|
||||
return CALL_DIRECTION_INCOMING;
|
||||
} else if ("外拨".equals(callTypeDesc)) {
|
||||
return CALL_DIRECTION_OUTGOING;
|
||||
} else {
|
||||
return -1; // 未知方向
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通话方向转 TelephonyManager 状态(统一状态标准,便于业务复用)
|
||||
*/
|
||||
private int getCallStateByDirection(int callDirection) {
|
||||
switch (callDirection) {
|
||||
case CALL_DIRECTION_INCOMING:
|
||||
return TelephonyManager.CALL_STATE_RINGING; // 来电=响铃中
|
||||
case CALL_DIRECTION_OUTGOING:
|
||||
return TelephonyManager.CALL_STATE_OFFHOOK; // 外拨=通话中
|
||||
default:
|
||||
return TelephonyManager.CALL_STATE_IDLE; // 未知=空闲
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TelephonyManager 状态转文字描述(统一日志格式,提升可读性)
|
||||
*/
|
||||
private String getCallStateDesc(int callState) {
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃中";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中";
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,73 @@
|
||||
package cc.winboll.studio.contacts.threads;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
* @Describe 主服务后台工作线程,负责定时轮询与消息调度
|
||||
*/
|
||||
public class MainServiceThread extends Thread {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "MainServiceThread";
|
||||
// 线程休眠周期(1秒)
|
||||
private static final long THREAD_SLEEP_INTERVAL = 1000L;
|
||||
|
||||
volatile static MainServiceThread _MainServiceThread;
|
||||
// 控制线程是否退出的标志
|
||||
volatile boolean isExit = false;
|
||||
volatile boolean isStarted = false;
|
||||
Context mContext;
|
||||
// 服务Handler, 用于线程发送消息使用
|
||||
WeakReference<MainServiceHandler> mwrMainServiceHandler;
|
||||
|
||||
// ====================== 静态成员变量区 ======================
|
||||
private static volatile MainServiceThread sInstance;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 线程运行控制标记
|
||||
private volatile boolean mIsExit;
|
||||
private volatile boolean mIsStarted;
|
||||
// 弱引用持有上下文和Handler,避免内存泄漏
|
||||
private WeakReference<Context> mContextWeakRef;
|
||||
private WeakReference<MainServiceHandler> mHandlerWeakRef;
|
||||
|
||||
// ====================== 私有构造函数 ======================
|
||||
private MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
this.mContextWeakRef = new WeakReference<>(context);
|
||||
this.mHandlerWeakRef = new WeakReference<>(handler);
|
||||
this.mIsExit = false;
|
||||
this.mIsStarted = false;
|
||||
LogUtils.d(TAG, "MainServiceThread: 线程实例初始化完成");
|
||||
MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
mContext = context;
|
||||
mwrMainServiceHandler = new WeakReference<MainServiceHandler>(handler);
|
||||
}
|
||||
|
||||
// ====================== 单例获取方法 ======================
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
// 若已有实例,先标记退出并销毁旧实例
|
||||
if (sInstance != null) {
|
||||
LogUtils.d(TAG, "getInstance: 存在旧线程实例,标记退出");
|
||||
sInstance.setIsExit(true);
|
||||
sInstance = null;
|
||||
}
|
||||
// 创建新线程实例
|
||||
sInstance = new MainServiceThread(context, handler);
|
||||
LogUtils.d(TAG, "getInstance: 新线程实例已创建");
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ====================== 运行状态控制方法 ======================
|
||||
public void setIsExit(boolean isExit) {
|
||||
this.mIsExit = isExit;
|
||||
LogUtils.d(TAG, "setIsExit: 线程退出标记已更新 | " + isExit);
|
||||
this.isExit = isExit;
|
||||
}
|
||||
|
||||
public boolean isExit() {
|
||||
return mIsExit;
|
||||
return isExit;
|
||||
}
|
||||
|
||||
public void setIsStarted(boolean isStarted) {
|
||||
this.mIsStarted = isStarted;
|
||||
this.isStarted = isStarted;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return mIsStarted;
|
||||
return isStarted;
|
||||
}
|
||||
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
if (_MainServiceThread != null) {
|
||||
_MainServiceThread.setIsExit(true);
|
||||
}
|
||||
_MainServiceThread = new MainServiceThread(context, handler);
|
||||
return _MainServiceThread;
|
||||
}
|
||||
|
||||
// ====================== 线程核心执行方法 ======================
|
||||
@Override
|
||||
public void run() {
|
||||
// 防止重复启动
|
||||
if (mIsStarted) {
|
||||
LogUtils.w(TAG, "run: 线程已启动,避免重复执行");
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记线程启动状态
|
||||
mIsStarted = true;
|
||||
LogUtils.i(TAG, "run: 线程开始运行");
|
||||
|
||||
// 线程主循环
|
||||
while (!mIsExit) {
|
||||
try {
|
||||
// 此处可添加业务逻辑(如定时任务、消息分发)
|
||||
Thread.sleep(THREAD_SLEEP_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "run: 线程休眠被中断", e);
|
||||
// 恢复线程中断状态
|
||||
Thread.currentThread().interrupt();
|
||||
if (isStarted == false) {
|
||||
isStarted = true;
|
||||
LogUtils.d(TAG, "run()");
|
||||
|
||||
while (!isExit()) {
|
||||
//ToastUtils.show("run");
|
||||
//LogUtils.d(TAG, "run()");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
_MainServiceThread = null;
|
||||
LogUtils.d(TAG, "run() exit");
|
||||
}
|
||||
|
||||
// 线程退出清理
|
||||
mIsStarted = false;
|
||||
mContextWeakRef.clear();
|
||||
mHandlerWeakRef.clear();
|
||||
sInstance = null;
|
||||
LogUtils.i(TAG, "run: 线程正常退出");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,268 +1,270 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 调用应用属性设置页工具类
|
||||
* 来源:https://blog.csdn.net/zhuhai__yizhi/article/details/78737593
|
||||
* Created by zyy on 2018/3/12.
|
||||
* 直接跳转到权限后返回,可以监控权限授权情况,但是,跳转到应用详情页,无法监测权限情况
|
||||
* 是否要加以区分,若是应用详情页,则跳转回来后,onRestart检测所求权限,如果授权,则收回提示,如果没授权,则继续提示
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 应用权限设置页跳转工具类,适配主流手机厂商的权限页路径,跳转失败时降级到应用详情页
|
||||
*/
|
||||
public class AppGoToSettingsUtil {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "AppGoToSettingsUtil";
|
||||
// 跳转设置页的 Activity 结果码,复用 MainActivity 的请求码
|
||||
|
||||
public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS;
|
||||
|
||||
// 主流手机厂商品牌常量
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";
|
||||
private static final String MANUFACTURER_SONY = "Sony";
|
||||
/**
|
||||
* Build.MANUFACTURER判断各大手机厂商品牌
|
||||
*/
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";//华为
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";//魅族
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";//小米
|
||||
private static final String MANUFACTURER_SONY = "Sony";//索尼
|
||||
private static final String MANUFACTURER_OPPO = "OPPO";
|
||||
private static final String MANUFACTURER_LG = "LG";
|
||||
private static final String MANUFACTURER_VIVO = "vivo";
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";
|
||||
private static final String MANUFACTURER_LETV = "Letv";
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";//三星
|
||||
private static final String MANUFACTURER_LETV = "Letv";//乐视
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";//中兴
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";//酷派
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";//联想
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 标记当前跳转的是应用详情页(true)还是厂商权限页(false)
|
||||
public static boolean isAppSettingOpen = false;
|
||||
|
||||
// ====================== 核心跳转方法区 ======================
|
||||
public static boolean isAppSettingOpen=false;
|
||||
/**
|
||||
* 跳转到对应品牌手机的系统权限设置页,跳转失败则降级到应用详情页
|
||||
* @param activity 上下文 Activity
|
||||
* 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页
|
||||
* 这里需要改造成返回true或者false,应用详情页:true,应用权限页:false
|
||||
* @param activity
|
||||
*/
|
||||
public static void goToSetting(Activity activity) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "goToSetting: Activity 为 null,无法跳转设置页");
|
||||
return;
|
||||
}
|
||||
|
||||
String manufacturer = Build.MANUFACTURER;
|
||||
LogUtils.d(TAG, "goToSetting: 当前设备厂商 | " + manufacturer);
|
||||
|
||||
// 根据厂商跳转对应权限页
|
||||
switch (manufacturer) {
|
||||
case MANUFACTURER_HUAWEI:
|
||||
gotoHuaweiSetting(activity);
|
||||
public static void GoToSetting(Activity activity) {
|
||||
switch (Build.MANUFACTURER) {
|
||||
case MANUFACTURER_HUAWEI://华为
|
||||
Huawei(activity);
|
||||
break;
|
||||
case MANUFACTURER_MEIZU:
|
||||
gotoMeizuSetting(activity);
|
||||
case MANUFACTURER_MEIZU://魅族
|
||||
Meizu(activity);
|
||||
break;
|
||||
case MANUFACTURER_XIAOMI:
|
||||
gotoXiaomiSetting(activity);
|
||||
case MANUFACTURER_XIAOMI://小米
|
||||
Xiaomi(activity);
|
||||
break;
|
||||
case MANUFACTURER_SONY:
|
||||
gotoSonySetting(activity);
|
||||
case MANUFACTURER_SONY://索尼
|
||||
Sony(activity);
|
||||
break;
|
||||
case MANUFACTURER_OPPO:
|
||||
gotoOppoSetting(activity);
|
||||
case MANUFACTURER_OPPO://oppo
|
||||
OPPO(activity);
|
||||
break;
|
||||
case MANUFACTURER_LG:
|
||||
gotoLgSetting(activity);
|
||||
case MANUFACTURER_LG://lg
|
||||
LG(activity);
|
||||
break;
|
||||
case MANUFACTURER_LETV:
|
||||
gotoLetvSetting(activity);
|
||||
case MANUFACTURER_LETV://乐视
|
||||
Letv(activity);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "goToSetting: 未适配当前厂商,跳转应用详情页");
|
||||
openAppDetailSetting(activity);
|
||||
default://其他
|
||||
try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
} catch (Exception e) {
|
||||
SystemConfig(activity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 各厂商权限页跳转方法区 ======================
|
||||
/**
|
||||
* 跳转华为手机权限设置页
|
||||
* 华为跳转权限设置页
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoHuaweiSetting(Activity activity) {
|
||||
public static void Huawei(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
intent.setComponent(new ComponentName("com.huawei.systemmanager",
|
||||
"com.huawei.permissionmanager.ui.MainActivity"));
|
||||
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoHuaweiSetting: 跳转华为权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoHuaweiSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转魅族手机权限设置页
|
||||
* 魅族跳转权限设置页,测试时,点击无反应,具体原因不明
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoMeizuSetting(Activity activity) {
|
||||
public static void Meizu(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoMeizuSetting: 跳转魅族权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoMeizuSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转小米手机权限设置页
|
||||
* 小米,功能正常
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoXiaomiSetting(Activity activity) {
|
||||
try {
|
||||
// 适配 MIUI 8/9 及以上版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
public static void Xiaomi(Activity activity) {
|
||||
try { //MIUI 8 9
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI8+)成功");
|
||||
//activity.startActivity(localIntent);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
// 适配 MIUI 5/6/7 版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
try { //MIUI 5/6/7
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI5-7)成功");
|
||||
} catch (Exception e1) {
|
||||
LogUtils.e(TAG, "gotoXiaomiSetting: 所有版本适配失败,降级到应用详情页", e1);
|
||||
//activity.startActivity(localIntent);
|
||||
} catch (Exception e1) { //否则跳转到应用详情
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
//这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果
|
||||
//activity.startActivity(getAppDetailSettingIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转索尼手机权限设置页
|
||||
* 索尼,6.0以上的手机非常少,基本没看见
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoSonySetting(Activity activity) {
|
||||
public static void Sony(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
intent.setComponent(new ComponentName("com.sonymobile.cta",
|
||||
"com.sonymobile.cta.SomcCTAMainActivity"));
|
||||
ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoSonySetting: 跳转索尼权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoSonySetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转OPPO手机权限设置页
|
||||
* OPPO
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoOppoSetting(Activity activity) {
|
||||
public static void OPPO(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
intent.setComponent(new ComponentName("com.color.safecenter",
|
||||
"com.color.safecenter.permission.PermissionManagerActivity"));
|
||||
ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoOppoSetting: 跳转OPPO权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoOppoSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转LG手机权限设置页
|
||||
* LG经过测试,正常使用
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoLgSetting(Activity activity) {
|
||||
public static void LG(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
intent.setComponent(new ComponentName("com.android.settings",
|
||||
"com.android.settings.Settings$AccessLockSummaryActivity"));
|
||||
ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLgSetting: 跳转LG权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLgSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转乐视手机权限设置页
|
||||
* 乐视6.0以上很少,基本都可以忽略了,现在乐视手机不多
|
||||
* @param activity
|
||||
*/
|
||||
private static void gotoLetvSetting(Activity activity) {
|
||||
public static void Letv(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
intent.setComponent(new ComponentName("com.letv.android.letvsafe",
|
||||
"com.letv.android.letvsafe.PermissionAndApps"));
|
||||
ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLetvSetting: 跳转乐视权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLetvSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 降级跳转方法区 ======================
|
||||
/**
|
||||
* 跳转系统设置主界面
|
||||
* 只能打开到自带安全软件
|
||||
* @param activity
|
||||
*/
|
||||
public static void gotoSystemConfig(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "gotoSystemConfig: Activity 为 null,无法跳转");
|
||||
return;
|
||||
public static void _360(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 系统设置界面
|
||||
* @param activity
|
||||
*/
|
||||
public static void SystemConfig(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivity(intent);
|
||||
LogUtils.d(TAG, "gotoSystemConfig: 跳转系统设置主界面成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用详情页的 Intent
|
||||
* 获取应用详情页面
|
||||
* @return
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
Intent intent = new Intent();
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
return intent;
|
||||
Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//if (Build.VERSION.SDK_INT >= 9) {
|
||||
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
/*} else if (Build.VERSION.SDK_INT <= 8) {
|
||||
localIntent.setAction(Intent.ACTION_VIEW);
|
||||
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
|
||||
localIntent.putExtra("com.android.settings.ApplicationPkgName", activity.getPackageName());
|
||||
}*/
|
||||
return localIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开应用详情设置页
|
||||
*/
|
||||
public static void openAppDetailSetting(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "openAppDetailSetting: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = true;
|
||||
LogUtils.d(TAG, "openAppDetailSetting: 跳转应用详情设置页成功");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
@@ -8,344 +13,203 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集:提供联系人查询、添加、编辑、号码格式化等功能,适配主流机型
|
||||
*/
|
||||
public class ContactUtils {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "ContactUtils";
|
||||
// 手机号正则(11位中国大陆手机号)
|
||||
private static final String REGEX_CHINA_MOBILE = "^1[0-9]{10}$";
|
||||
|
||||
// ====================== 单例与成员变量区 ======================
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile ContactUtils sInstance;
|
||||
// 上下文(弱引用避免内存泄漏,Java7 兼容)
|
||||
private final Context mContext;
|
||||
// 缓存联系人:key=纯数字号码,value=联系人姓名
|
||||
private final Map<String, String> mContactMap = new HashMap<>();
|
||||
Map<String, String> contactMap = new HashMap<>();
|
||||
|
||||
// ====================== 单例构造区 ======================
|
||||
/**
|
||||
* 私有构造器:初始化上下文并加载联系人
|
||||
*/
|
||||
private ContactUtils(Context context) {
|
||||
// 传入应用上下文,避免Activity上下文泄漏
|
||||
this.mContext = context.getApplicationContext();
|
||||
LogUtils.d(TAG, "ContactUtils 初始化,开始加载联系人");
|
||||
reloadContacts();
|
||||
static volatile ContactUtils _ContactUtils;
|
||||
Context mContext;
|
||||
ContactUtils(Context context) {
|
||||
mContext = context;
|
||||
relaodContacts();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,Java7 安全)
|
||||
*/
|
||||
public static ContactUtils getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为null,无法创建实例");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
public synchronized static ContactUtils getInstance(Context context) {
|
||||
if (_ContactUtils == null) {
|
||||
_ContactUtils = new ContactUtils(context);
|
||||
}
|
||||
if (sInstance == null) {
|
||||
synchronized (ContactUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ContactUtils(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
return _ContactUtils;
|
||||
}
|
||||
|
||||
// ====================== 联系人缓存与查询区 ======================
|
||||
/**
|
||||
* 重新加载联系人到缓存
|
||||
*/
|
||||
public void reloadContacts() {
|
||||
LogUtils.d(TAG, "reloadContacts: 开始刷新联系人缓存");
|
||||
mContactMap.clear();
|
||||
readContactsFromSystem();
|
||||
LogUtils.d(TAG, "reloadContacts: 联系人缓存刷新完成,共缓存 " + mContactMap.size() + " 个联系人");
|
||||
public void relaodContacts() {
|
||||
readContacts();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从系统通讯录读取所有联系人(核心方法)
|
||||
*/
|
||||
private void readContactsFromSystem() {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
// 只查询姓名和号码字段,减少IO开销
|
||||
String[] projection = {
|
||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||
};
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor == null) {
|
||||
LogUtils.w(TAG, "readContactsFromSystem: 通讯录查询Cursor为null,可能缺少权限");
|
||||
return;
|
||||
}
|
||||
|
||||
private void readContacts() {
|
||||
contactMap.clear();
|
||||
ContentResolver contentResolver = mContext.getContentResolver();
|
||||
Cursor cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
null, null, null, null);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
|
||||
if (phone != null) {
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
mContactMap.put(simplePhone, name != null ? name : "[UnknownName]");
|
||||
LogUtils.v(TAG, "readContactsFromSystem: 缓存联系人 - 号码:" + simplePhone + ",姓名:" + name);
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录失败,缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 确保游标关闭,避免内存泄漏
|
||||
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
//Map<String, String> contactMap = new HashMap<>();
|
||||
contactMap.put(formatToSimplePhoneNumber(phoneNumber), displayName);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
// 此时 contactList 就是存储联系人信息的 Map 列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取联系人姓名
|
||||
*/
|
||||
public String getContactName(String phone) {
|
||||
if (phone == null) {
|
||||
LogUtils.w(TAG, "getContactName: 输入号码为null");
|
||||
return "[NotInContacts]";
|
||||
}
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
String name = mContactMap.get(simplePhone);
|
||||
LogUtils.d(TAG, "getContactName: 查询号码 " + simplePhone + ",姓名:" + (name == null ? "[NotInContacts]" : name));
|
||||
return name == null ? "[NotInContacts]" : name;
|
||||
public String getContactsName(String phone) {
|
||||
String result = contactMap.get(formatToSimplePhoneNumber(phone));
|
||||
return result == null ? "[NotInContacts]" : result;
|
||||
}
|
||||
|
||||
// ====================== 号码格式化工具区 ======================
|
||||
/**
|
||||
* 格式化号码为纯数字(去除所有非数字字符)
|
||||
*/
|
||||
// static String getSimplePhone(String phone) {
|
||||
// return phone.replaceAll("[+\\s]", "");
|
||||
// }
|
||||
|
||||
public static String formatToSimplePhoneNumber(String number) {
|
||||
if (number == null || number.isEmpty()) {
|
||||
LogUtils.w(TAG, "formatToSimplePhoneNumber: 输入号码为空");
|
||||
return "";
|
||||
}
|
||||
String simpleNumber = number.replaceAll("[^0-9]", "");
|
||||
LogUtils.v(TAG, "formatToSimplePhoneNumber: 原号码 " + number + " → 纯数字号码 " + simpleNumber);
|
||||
return simpleNumber;
|
||||
// 去除所有空格和非数字字符
|
||||
return number.replaceAll("[^0-9]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化11位手机号为带空格格式(如:138 0000 1234)
|
||||
*/
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
if (simpleNumber == null || !simpleNumber.matches(REGEX_CHINA_MOBILE)) {
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 号码不符合11位手机号格式,无需格式化");
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(simpleNumber.substring(0, 3))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(3, 7))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(7, 11));
|
||||
|
||||
String formatted = sb.toString();
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 纯数字号码 " + simpleNumber + " → 带空格号码 " + formatted);
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// ====================== 联系人查询(直接查系统,不走缓存)区 ======================
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按原始号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhone(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhone: 上下文或号码为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
String displayName = null;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = null;
|
||||
String displayName = null;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER + "=?",
|
||||
new String[]{phoneNumber},
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
}
|
||||
LogUtils.d(TAG, "getDisplayNameByPhone: 按原始号码 " + phoneNumber + " 查询,姓名:" + displayName);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{phoneNumber}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按纯数字号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhoneSimple(Context context, String phoneNumber) {
|
||||
if (phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhoneSimple: 输入号码为null");
|
||||
return null;
|
||||
String displayName = null;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{formatToSimplePhoneNumber(phoneNumber)}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
}
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
LogUtils.d(TAG, "getDisplayNameByPhoneSimple: 按纯数字号码 " + simplePhone + " 查询");
|
||||
return getDisplayNameByPhone(context, simplePhone);
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断号码是否在系统通讯录中
|
||||
*/
|
||||
public static boolean isPhoneInContacts(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "isPhoneInContacts: 上下文或号码为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
String displayName = getDisplayNameByPhone(context, simplePhone);
|
||||
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 未找到联系人(纯数字匹配)");
|
||||
String spacePhone = formatToSpacePhoneNumber(simplePhone);
|
||||
displayName = getDisplayNameByPhone(context, spacePhone);
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + spacePhone + " 未找到联系人(带空格匹配)");
|
||||
String szPhoneNumber = formatToSimplePhoneNumber(phoneNumber);
|
||||
String szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
szPhoneNumber = formatToSpacePhoneNumber(szPhoneNumber);
|
||||
szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 已在联系人中,姓名:" + displayName);
|
||||
LogUtils.d(TAG, String.format("Phone %s is found in contacts %s.", szPhoneNumber, szDisplayName));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配定制机型)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "getContactIdByPhone: 上下文或号码为空");
|
||||
return -1L;
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
// 去除所有空格和非数字字符
|
||||
StringBuilder sbSpaceNumber = new StringBuilder();
|
||||
String regex = "^1[0-9]{10}$";
|
||||
if (simpleNumber.matches(regex)) {
|
||||
sbSpaceNumber.append(simpleNumber.substring(0, 3));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(3, 7));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(7, 11));
|
||||
}
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
Long contactId = -1L;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
LogUtils.d(TAG, "getContactIdByPhone: 号码 " + phoneNumber + " 对应的联系人ID:" + contactId);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return contactId;
|
||||
return sbSpaceNumber.toString();
|
||||
}
|
||||
|
||||
// ====================== 联系人跳转工具区 ======================
|
||||
/**
|
||||
* 跳转至系统添加联系人界面
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 预填号码(可为null)
|
||||
*/
|
||||
public static void jumpToAddContact(Context context, String phoneNumber) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToAddContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
if (phoneNumber != null) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,预填号码:" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,无预填号码");
|
||||
}
|
||||
/**
|
||||
* 跳转至系统添加联系人界面的工具函数
|
||||
* @param context 上下文(如 PhoneCallService、Activity、Fragment 均可,需传入有效上下文)
|
||||
* @param phoneNumber 可选参数:预填的联系人电话(传 null 则跳转空表单)
|
||||
*/
|
||||
public static void jumpToAddContact(Context mContext, String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
intent.putExtra(android.provider.ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 支持非Activity上下文调用
|
||||
context.startActivity(intent);
|
||||
}
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文(Activity/Service/Fragment)
|
||||
* @param phoneNumber 待编辑联系人的电话号码(用于匹配已有联系人,必传)
|
||||
* @param contactId 可选:已有联系人的ID(通过 ContactsContract 获取,传null则自动匹配号码)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
// 关键:小米等机型需明确设置数据类型为“单个联系人”,避免参数丢失
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待编辑号码(必传)
|
||||
* @param contactId 联系人ID(可选,优先使用)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
// 场景A:已知联系人ID(精准定位,优先用此方式,参数传递最稳定)
|
||||
if (contactId != null && contactId > 0) {
|
||||
// 构建联系人的Uri(格式:content://contacts/people/[contactId],系统标准格式)
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
//ToastUtils.show("1");
|
||||
} else if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
// 方式1:小米等机型兼容的“通过号码定位联系人”参数(部分系统认此参数)
|
||||
//intent.putExtra(ContactsContract.Intents.Insert.PHONE_NUMBER, phoneNumber);
|
||||
// 方式2:补充系统标准的“数据Uri”,强化匹配(避免参数被定制系统忽略)
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
} else {
|
||||
LogUtils.d(TAG, "编辑联系人失败:电话号码和联系人ID均为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验必要参数
|
||||
if (contactId == null || contactId <= 0) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 联系人ID和号码均为空,无法编辑");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新)
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// 启动活动(加防护,避免无联系人应用崩溃)
|
||||
// 小米机型在Service/非Activity中调用,需加NEW_TASK标志,否则可能无法启动
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
// 优先通过ID定位(精准)
|
||||
if (contactId != null && contactId > 0) {
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过ID " + contactId + " 定位联系人,准备编辑");
|
||||
} else {
|
||||
// 通过号码定位
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过号码 " + phoneNumber + " 定位联系人,准备编辑");
|
||||
}
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配小米机型,解决编辑时匹配不稳定问题)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待查询的电话号码
|
||||
* @return 联系人ID(无匹配时返回-1)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
// 预填最新号码
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
// 1. 构建电话查询Uri(系统标准:通过号码过滤联系人数据)
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
// 2. 只查询“联系人ID”字段(高效,避免冗余数据)
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = cr.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
// 3. 读取联系人ID(返回Long类型,避免int溢出)
|
||||
return cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "查询联系人ID失败。" + e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
}
|
||||
}
|
||||
return -1L; // 无匹配联系人
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,24 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import android.widget.EditText;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 00:59:13
|
||||
* @Describe Int类型数字输入框工具集:安全读取 EditText 中的整数内容
|
||||
* @Describe Int类型数字输入框工具集
|
||||
*/
|
||||
public class EditTextIntUtils {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "EditTextIntUtils";
|
||||
// 默认返回值:读取失败时返回
|
||||
private static final int DEFAULT_INT_VALUE = 0;
|
||||
|
||||
// ====================== 工具方法区 ======================
|
||||
/**
|
||||
* 从 EditText 中安全读取整数
|
||||
* @param editText 目标输入框
|
||||
* @return 输入框中的整数,读取失败返回 0
|
||||
*/
|
||||
public static int getIntFromEditText(EditText editText) {
|
||||
// 空值校验:防止 EditText 为 null 导致空指针
|
||||
if (editText == null) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: EditText 实例为 null,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 获取并去除首尾空格
|
||||
String inputStr = editText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "getIntFromEditText: 输入框原始内容 | " + inputStr);
|
||||
|
||||
// 校验空字符串
|
||||
if (inputStr.isEmpty()) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: 输入框内容为空,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 安全转换整数,捕获格式异常
|
||||
try {
|
||||
int result = Integer.parseInt(inputStr);
|
||||
LogUtils.d(TAG, "getIntFromEditText: 转换成功 | 结果=" + result);
|
||||
return result;
|
||||
String sz = editText.getText().toString().trim();
|
||||
return Integer.parseInt(sz);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "getIntFromEditText: 内容不是有效整数 | 输入内容=" + inputStr, e);
|
||||
return DEFAULT_INT_VALUE;
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,64 +1,37 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 01:16:28
|
||||
* @Describe Int数字操作工具集:提供整数范围限制、数值边界校准功能
|
||||
* @Describe Int数字操作工具集
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class IntUtils {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "IntUtils";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 将整数限制在指定区间内,自动校准超出边界的数值
|
||||
* @param origin 原始整数
|
||||
* @param range_a 区间端点1(无需区分大小)
|
||||
* @param range_b 区间端点2(无需区分大小)
|
||||
* @return 校准后的整数,结果始终在 [min(range_a,range_b), max(range_a,range_b)] 内
|
||||
*/
|
||||
public static int getIntInRange(int origin, int range_a, int range_b) {
|
||||
int min = Math.min(range_a, range_b);
|
||||
int max = Math.max(range_a, range_b);
|
||||
int res = Math.min(origin, max);
|
||||
res = Math.max(res, min);
|
||||
|
||||
// 打印调试日志,记录参数与计算结果
|
||||
LogUtils.d(TAG, String.format("getIntInRange: 原始值=%d, 区间=[%d,%d], 校准后=%d",
|
||||
origin, min, max, res));
|
||||
return res;
|
||||
}
|
||||
|
||||
// ====================== 单元测试方法区 ======================
|
||||
/**
|
||||
* 单元测试:验证 getIntInRange 方法在不同场景下的正确性
|
||||
*/
|
||||
public static void unittest_getIntInRange() {
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 开始执行单元测试");
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, 5, 10); %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(8, 5, 10); %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(200, 5, 10); %d", getIntInRange(200, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, -5, 10); %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(9, -5, 10); %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(100, -5, 10); %d", getIntInRange(100, -5, 10)));
|
||||
|
||||
// 正数区间测试
|
||||
LogUtils.d(TAG, String.format("测试1: getIntInRange(-100, 5, 10) = %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试2: getIntInRange(8, 5, 10) = %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试3: getIntInRange(200, 5, 10) = %d", getIntInRange(200, 5, 10)));
|
||||
|
||||
// 跨正负区间测试
|
||||
LogUtils.d(TAG, String.format("测试4: getIntInRange(-100, -5, 10) = %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试5: getIntInRange(9, -5, 10) = %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试6: getIntInRange(100, -5, 10) = %d", getIntInRange(100, -5, 10)));
|
||||
|
||||
// 端点顺序颠倒测试
|
||||
LogUtils.d(TAG, String.format("测试7: getIntInRange(500, 5, -10) = %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试8: getIntInRange(4, 5, -10) = %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试9: getIntInRange(-20, 5, -10) = %d", getIntInRange(-20, 5, -10)));
|
||||
|
||||
// 大数区间测试
|
||||
LogUtils.d(TAG, String.format("测试10: getIntInRange(500, 50, 10) = %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试11: getIntInRange(30, 50, 10) = %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试12: getIntInRange(6, 50, 10) = %d", getIntInRange(6, 50, 10)));
|
||||
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 单元测试执行完毕");
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 5, -10); %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(4, 5, -10); %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-20, 5, -10); %d", getIntInRange(-20, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 50, 10); %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(30, 50, 10); %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(6, 50, 10); %d", getIntInRange(6, 50, 10)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.telecom.TelecomManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/12 16:28
|
||||
* @Describe 敏感权限申请工具类(完全适配 Android API 30 + Java 7 语法)
|
||||
* 修复 ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP / EXTRA_PACKAGE_NAME 未定义问题
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static final String TAG = "PermissionUtils";
|
||||
|
||||
// API 版本硬编码常量(Java 7 兼容,不依赖 Build.VERSION_CODES 高版本字段)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
private static final int ANDROID_10_API = 29;
|
||||
private static final int ANDROID_13_API = 33;
|
||||
private static final int ANDROID_14_API = 34;
|
||||
|
||||
// 硬编码系统常量字符串,解决 API 30 下未定义问题
|
||||
private static final String ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP =
|
||||
"android.telecom.action.CHANGE_DEFAULT_CALL_SCREENING_APP";
|
||||
private static final String EXTRA_PACKAGE_NAME =
|
||||
"android.telecom.extra.PACKAGE_NAME";
|
||||
|
||||
// 基础权限组(严格适配 API 30,移除废弃/不存在的权限)
|
||||
public static final String[] BASE_PERMISSIONS = {
|
||||
android.Manifest.permission.READ_CONTACTS,
|
||||
android.Manifest.permission.WRITE_CONTACTS,
|
||||
android.Manifest.permission.READ_CALL_LOG,
|
||||
android.Manifest.permission.CALL_PHONE,
|
||||
android.Manifest.permission.RECORD_AUDIO,
|
||||
android.Manifest.permission.MODIFY_AUDIO_SETTINGS
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有需要申请的权限(Java 7 传统 for 循环,无菱形运算符)
|
||||
*/
|
||||
public static String[] getAllNeedPermissions() {
|
||||
List<String> permissions = new ArrayList<String>();
|
||||
// Java 7 传统循环遍历数组
|
||||
for (int i = 0; i < BASE_PERMISSIONS.length; i++) {
|
||||
permissions.add(BASE_PERMISSIONS[i]);
|
||||
}
|
||||
// 显式创建数组并转换,避免 Java 7 泛型转换警告
|
||||
String[] permissionArray = new String[permissions.size()];
|
||||
return permissions.toArray(permissionArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个权限是否授予(使用 PackageManager 标准常量)
|
||||
*/
|
||||
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
|
||||
return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限组是否全部授予(Java 7 传统循环)
|
||||
*/
|
||||
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
// Java 7 遍历数组,避免增强 for 循环的语法糖问题
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
if (!checkPermission(context, permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请权限组(Activity 中调用,Java 7 兼容)
|
||||
*/
|
||||
public static void requestPermissions(@NonNull FragmentActivity activity,
|
||||
@NonNull String[] permissions,
|
||||
int requestCode) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请权限组(Fragment 中调用,Java 7 兼容)
|
||||
*/
|
||||
public static void requestPermissions(@NonNull Fragment fragment,
|
||||
@NonNull String[] permissions,
|
||||
int requestCode) {
|
||||
fragment.requestPermissions(permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查悬浮窗权限(API 30 适配)
|
||||
*/
|
||||
public static boolean isOverlayPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
}
|
||||
// 6.0 以下默认授予
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请悬浮窗权限(Java 7 规范,拆分 Intent 创建步骤)
|
||||
*/
|
||||
public static void requestOverlayPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !isOverlayPermissionGranted(context)) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
if (context instanceof FragmentActivity) {
|
||||
((FragmentActivity) context).startActivityForResult(intent, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查修改系统设置权限(API 30 适配)
|
||||
*/
|
||||
public static boolean isWriteSettingsPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
return Settings.System.canWrite(context);
|
||||
}
|
||||
// 6.0 以下默认授予
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请修改系统设置权限(Java 7 规范)
|
||||
*/
|
||||
public static void requestWriteSettingsPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !isWriteSettingsPermissionGranted(context)) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
if (context instanceof FragmentActivity) {
|
||||
((FragmentActivity) context).startActivityForResult(intent, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查通话筛选权限(适配 API 30,优化反射逻辑 + 异常捕获)
|
||||
*/
|
||||
public static boolean isCallScreeningPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
TelecomManager telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
|
||||
if (telecomManager == null) {
|
||||
return false;
|
||||
}
|
||||
String defaultPackage = null;
|
||||
// 反射调用高版本方法,捕获所有异常避免崩溃(Java 7 必须显式捕获 Exception)
|
||||
try {
|
||||
Method method = TelecomManager.class.getMethod("getDefaultCallScreeningAppPackage");
|
||||
defaultPackage = (String) method.invoke(telecomManager);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// API 30-32 无此方法,返回 false
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
// 其他反射异常,返回 false
|
||||
return false;
|
||||
}
|
||||
return defaultPackage != null && defaultPackage.equals(context.getPackageName());
|
||||
}
|
||||
// 10.0 以下无此权限,默认返回 true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请通话筛选权限(完全适配 API 30,解决 ActivityNotFoundException 崩溃)
|
||||
*/
|
||||
public static void requestCallScreeningPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API && !isCallScreeningPermissionGranted(context)) {
|
||||
FragmentActivity activity = null;
|
||||
if (context instanceof FragmentActivity) {
|
||||
activity = (FragmentActivity) context;
|
||||
}
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = null;
|
||||
// 版本分级处理:避免高版本 ACTION 失效
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_14_API) {
|
||||
// Android 14+:跳转默认应用设置页
|
||||
intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
} else if (Build.VERSION.SDK_INT >= ANDROID_13_API) {
|
||||
// Android 13:使用硬编码 ACTION
|
||||
intent = new Intent(ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP);
|
||||
intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
} else {
|
||||
// API 30-32:直接跳转应用详情页
|
||||
goAppDetailsSettings(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 捕获 Activity 找不到异常,兜底处理(Java 7 必须显式捕获)
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (android.content.ActivityNotFoundException e) {
|
||||
// 兜底:跳转应用详情页
|
||||
goAppDetailsSettings(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转应用详情页(权限兜底引导,Java 7 规范)
|
||||
*/
|
||||
public static void goAppDetailsSettings(@NonNull Context context) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析被拒绝的权限(Java 7 字符串操作,无 Lambda)
|
||||
*/
|
||||
public static String getDeniedPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
StringBuilder deniedPerms = new StringBuilder();
|
||||
// Java 7 传统循环遍历权限数组
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
if (!checkPermission(context, permission)) {
|
||||
// 截取权限名称,优化展示
|
||||
int lastDotIndex = permission.lastIndexOf(".");
|
||||
if (lastDotIndex != -1 && lastDotIndex < permission.length() - 1) {
|
||||
String permName = permission.substring(lastDotIndex + 1);
|
||||
deniedPerms.append(permName).append("、");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 移除最后一个分隔符(Java 7 字符串操作)
|
||||
if (deniedPerms.length() > 0) {
|
||||
deniedPerms.deleteCharAt(deniedPerms.length() - 1);
|
||||
}
|
||||
return deniedPerms.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,27 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe PhoneUtils
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe 拨打电话工具类:封装拨打电话逻辑与权限校验
|
||||
*/
|
||||
public class PhoneUtils {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "PhoneUtils";
|
||||
// 拨打电话 Action 与 Uri 前缀
|
||||
private static final String CALL_ACTION = Intent.ACTION_CALL;
|
||||
private static final String TEL_URI_PREFIX = "tel:";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 直接拨打电话(需申请 CALL_PHONE 权限)
|
||||
* @param context 上下文对象
|
||||
* @param phoneNumber 目标电话号码
|
||||
*/
|
||||
|
||||
public static void call(Context context, String phoneNumber) {
|
||||
// 空值校验:防止上下文或号码为空导致异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "call: Context 为 null,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "call: 电话号码为空,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
String targetPhone = phoneNumber.trim();
|
||||
LogUtils.d(TAG, "call: 准备拨打号码 | " + targetPhone);
|
||||
|
||||
// 权限校验:检查是否持有拨打电话权限
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "call: 缺少 CALL_PHONE 权限,无法直接拨打电话");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建拨打电话 Intent 并启动
|
||||
Intent callIntent = new Intent(CALL_ACTION);
|
||||
callIntent.setData(Uri.parse(TEL_URI_PREFIX + targetPhone));
|
||||
// 添加 FLAG 支持非 Activity 上下文启动
|
||||
callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
context.startActivity(callIntent);
|
||||
LogUtils.i(TAG, "call: 拨打电话 Intent 已发送 | 号码=" + targetPhone);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe 正则前置校验工具类(RegexPPi):检验文本是否满足基础正则匹配要求
|
||||
*/
|
||||
public class RegexPPiUtils {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "RegexPPiUtils";
|
||||
// 基础匹配正则:匹配任意文本(包括空字符串)
|
||||
private static final String BASE_REGEX = ".*";
|
||||
// 预编译正则 Pattern,提升重复调用效率
|
||||
private static final Pattern BASE_PATTERN = Pattern.compile(BASE_REGEX);
|
||||
|
||||
// ====================== 核心校验方法区 ======================
|
||||
/**
|
||||
* 检验文本是否满足基础正则表达式模式(.*)匹配要求
|
||||
* @param text 待校验的文本内容
|
||||
* @return 匹配结果,文本为null时返回false
|
||||
*/
|
||||
//
|
||||
// 检验文本是否满足适合正则表达式模式计算
|
||||
//
|
||||
public static boolean isPPiOK(String text) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (text == null) {
|
||||
LogUtils.w(TAG, "isPPiOK: 待校验文本为 null,返回 false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行正则匹配
|
||||
Matcher matcher = BASE_PATTERN.matcher(text);
|
||||
boolean isMatch = matcher.matches();
|
||||
|
||||
// 打印调试日志,记录校验结果
|
||||
LogUtils.d(TAG, String.format("isPPiOK: 文本=[%s],匹配结果=%b", text, isMatch));
|
||||
return isMatch;
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = ".*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +1,68 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 21:11:03
|
||||
* @Describe 云盾防御信息视图控件:展示云盾防御值统计,并支持消息驱动更新
|
||||
* @Describe 云盾防御信息
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class DuInfoTextView extends TextView {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "DuInfoTextView";
|
||||
|
||||
public static final int MSG_NOTIFY_INFO_UPDATE = 0;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Handler mHandler;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DuInfoTextView(Context context) {
|
||||
|
||||
Context mContext;
|
||||
|
||||
public DuInfoTextView(android.content.Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(Context context, AttributeSet attrs) {
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView: 开始初始化云盾信息控件");
|
||||
this.mContext = context;
|
||||
initHandler();
|
||||
void initView(android.content.Context context) {
|
||||
mContext = context;
|
||||
updateInfo();
|
||||
LogUtils.d(TAG, "initView: 云盾信息控件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Handler,处理信息更新消息
|
||||
*/
|
||||
private void initHandler() {
|
||||
mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到信息更新消息,开始刷新视图");
|
||||
updateInfo();
|
||||
}
|
||||
|
||||
void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo()");
|
||||
SettingsModel settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", settingsModel.getDunCurrentCount(), settingsModel.getDunTotalCount());
|
||||
setText(info);
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if(msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
updateInfo();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 视图更新方法区 ======================
|
||||
/**
|
||||
* 更新云盾防御信息显示
|
||||
*/
|
||||
private void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo: 开始更新云盾防御信息");
|
||||
// 空值校验,避免上下文为空导致异常
|
||||
if (mContext == null) {
|
||||
LogUtils.w(TAG, "updateInfo: 上下文为空,跳过信息更新");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsBean settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
// 校验 SettingsBean 非空,防止空指针
|
||||
if (settingsModel == null) {
|
||||
LogUtils.w(TAG, "updateInfo: SettingsBean 为空,显示默认值");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentCount = settingsModel.getDunCurrentCount();
|
||||
int totalCount = settingsModel.getDunTotalCount();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", currentCount, totalCount);
|
||||
setText(info);
|
||||
LogUtils.d(TAG, "updateInfo: 云盾防御信息更新完成 | " + info);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "updateInfo: 信息更新异常", e);
|
||||
setText("(云盾防御值【--/--】)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供的信息更新通知方法
|
||||
*/
|
||||
|
||||
};
|
||||
|
||||
public void notifyInfoUpdate() {
|
||||
LogUtils.d(TAG, "notifyInfoUpdate: 发送信息更新通知");
|
||||
if (mHandler != null) {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyInfoUpdate: Handler 未初始化,无法发送更新消息");
|
||||
}
|
||||
LogUtils.d(TAG, "notifyInfoUpdate()");
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/19 14:04:20
|
||||
* @Describe 云盾华氏度热力视图,垂直盾值温度视图控件(带颜色渐变+静态Handler更新)
|
||||
* 采用绘图方式展示盾值温度,填充色随盾值比例渐变,支持设置文本在温度条左侧/右侧,底部对齐竖排显示
|
||||
* 温度条宽度=5dp,文本区宽度固定=5dp,整体左右边距=0,无任何多余空白间距
|
||||
*/
|
||||
public class DunTemperatureView extends View {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DunTemperatureView";
|
||||
// 控件默认高度
|
||||
private static final int DEFAULT_HEIGHT = 200;
|
||||
// 温度条宽度(5dp)、文本区宽度(固定5dp)
|
||||
private static final int THERMOMETER_WIDTH_DP = 5;
|
||||
private static final int TEXT_AREA_WIDTH_DP = 5;
|
||||
// 填充区域内边距(左右=0,上下=2避免贴边)
|
||||
private static final int FILL_PADDING_HORIZONTAL = 0;
|
||||
private static final int FILL_PADDING_VERTICAL = 2;
|
||||
// 竖排文本字间距
|
||||
private static final float TEXT_CHAR_SPACING = 8f;
|
||||
// Handler消息标识
|
||||
public static final int MSG_UPDATE_DUN_VALUE = 0x01;
|
||||
// 消息参数Key
|
||||
public static final String KEY_MAX_VALUE = "max_value";
|
||||
public static final String KEY_CURRENT_VALUE = "current_value";
|
||||
|
||||
// ====================== 静态成员区 ======================
|
||||
// 弱引用缓存控件实例,避免内存泄漏
|
||||
private static WeakHashMap<DunTemperatureView, Object> sViewCache = new WeakHashMap<>();
|
||||
// 静态Handler,处理跨线程更新消息
|
||||
private static Handler sStaticHandler;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 画笔相关
|
||||
private Paint mThermometerPaint;
|
||||
private Paint mFillPaint;
|
||||
private Paint mTextPaint;
|
||||
// 尺寸参数(dp转px后的值)
|
||||
private int mThermometerWidth;
|
||||
private int mTextAreaWidth;
|
||||
private int mMaxValue = 100; // 最高盾值
|
||||
private int mCurrentValue = 0; // 当前盾值
|
||||
private RectF mThermometerRect; // 温度条矩形区域
|
||||
// 渐变颜色配置(低→中→高 对应绿→黄→红)
|
||||
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
|
||||
private float[] mGradientPositions = {0.0f, 0.5f, 1.0f};
|
||||
// 布局配置:true=文本在温度条右侧(默认),false=文本在温度条左侧
|
||||
private boolean isTextOnRight = true;
|
||||
// 其他颜色配置
|
||||
private int mBorderColor = Color.parseColor("#FF444444");
|
||||
private int mTextColor = Color.parseColor("#FF000000");
|
||||
|
||||
// ====================== 静态代码块 ======================
|
||||
static {
|
||||
// 初始化静态Handler,绑定主线程Looper
|
||||
sStaticHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_UPDATE_DUN_VALUE) {
|
||||
// 获取消息中的盾值参数
|
||||
int maxValue = msg.getData().getInt(KEY_MAX_VALUE, 100);
|
||||
int currentValue = msg.getData().getInt(KEY_CURRENT_VALUE, 0);
|
||||
LogUtils.d(TAG, "sStaticHandler: 收到更新消息,max=" + maxValue + ", current=" + currentValue);
|
||||
|
||||
// 遍历缓存的控件实例,更新所有实例
|
||||
for (DunTemperatureView view : sViewCache.keySet()) {
|
||||
if (view != null && view.isShown()) {
|
||||
view.setMaxValue(maxValue);
|
||||
view.setCurrentValue(currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DunTemperatureView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public DunTemperatureView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public DunTemperatureView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
// ====================== 初始化方法区 ======================
|
||||
/**
|
||||
* 初始化画笔和参数
|
||||
*/
|
||||
private void init() {
|
||||
LogUtils.d(TAG, "init: 开始初始化云盾温度视图控件");
|
||||
// dp转px(适配不同分辨率)
|
||||
mThermometerWidth = dp2px(getContext(), THERMOMETER_WIDTH_DP);
|
||||
mTextAreaWidth = dp2px(getContext(), TEXT_AREA_WIDTH_DP);
|
||||
LogUtils.d(TAG, "init: 温度条宽度5dp转px=" + mThermometerWidth + ",文本区宽度5dp转px=" + mTextAreaWidth);
|
||||
|
||||
// 温度条边框画笔(宽度1px,适配5dp宽度)
|
||||
mThermometerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mThermometerPaint.setColor(mBorderColor);
|
||||
mThermometerPaint.setStyle(Paint.Style.STROKE);
|
||||
mThermometerPaint.setStrokeWidth(1);
|
||||
|
||||
// 温度条填充画笔(支持渐变)
|
||||
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mFillPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
// 文本画笔(适配5dp窄文本区,文字居中绘制)
|
||||
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mTextPaint.setColor(mTextColor);
|
||||
mTextPaint.setTextSize(18); // 缩小字号适配5dp窄文本区(避免文字超出)
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setFakeBoldText(true); // 文字加粗,提升窄区域可读性
|
||||
|
||||
// 初始化温度条矩形
|
||||
mThermometerRect = new RectF();
|
||||
|
||||
// 将当前实例加入静态缓存
|
||||
sViewCache.put(this, null);
|
||||
LogUtils.d(TAG, "init: 云盾温度视图控件初始化完成,实例已加入缓存");
|
||||
}
|
||||
|
||||
// ====================== 工具方法区 ======================
|
||||
/**
|
||||
* dp 转 px(适配不同屏幕分辨率)
|
||||
*/
|
||||
private int dp2px(Context context, float dpValue) {
|
||||
return (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dpValue,
|
||||
context.getResources().getDisplayMetrics()
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== 对外控制方法区 ======================
|
||||
/**
|
||||
* 设置文本相对于温度条的位置
|
||||
* @param isOnRight true=文本在温度条右侧(默认),false=文本在温度条左侧
|
||||
*/
|
||||
public void setTextPosition(boolean isOnRight) {
|
||||
this.isTextOnRight = isOnRight;
|
||||
invalidate(); // 刷新布局绘制
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文本位置配置
|
||||
* @return true=右侧,false=左侧
|
||||
*/
|
||||
public boolean isTextOnRight() {
|
||||
return isTextOnRight;
|
||||
}
|
||||
|
||||
// ====================== 对外静态方法区 ======================
|
||||
/**
|
||||
* 静态外部方法:发送消息更新所有 DunTemperatureView 实例的盾值
|
||||
* 可在子线程中调用
|
||||
* @param maxValue 最高盾值
|
||||
* @param currentValue 当前盾值
|
||||
*/
|
||||
public static void updateDunValue(int maxValue, int currentValue) {
|
||||
if (sStaticHandler == null) {
|
||||
LogUtils.w(TAG, "updateDunValue: 静态Handler未初始化");
|
||||
return;
|
||||
}
|
||||
// 封装参数到消息
|
||||
Message msg = sStaticHandler.obtainMessage(MSG_UPDATE_DUN_VALUE);
|
||||
msg.getData().putInt(KEY_MAX_VALUE, maxValue);
|
||||
msg.getData().putInt(KEY_CURRENT_VALUE, currentValue);
|
||||
// 发送消息
|
||||
sStaticHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
// ====================== 对外实例方法区 ======================
|
||||
/**
|
||||
* 设置最高盾值
|
||||
* @param maxValue 最高盾值(需大于0)
|
||||
*/
|
||||
public void setMaxValue(int maxValue) {
|
||||
if (maxValue <= 0) {
|
||||
LogUtils.w(TAG, "setMaxValue: 最高盾值必须大于0,当前值=" + maxValue);
|
||||
return;
|
||||
}
|
||||
this.mMaxValue = maxValue;
|
||||
// 限制当前值不超过最大值
|
||||
mCurrentValue = Math.min(mCurrentValue, maxValue);
|
||||
invalidate(); // 重绘控件
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前盾值
|
||||
* @param currentValue 当前盾值(范围 0~maxValue)
|
||||
*/
|
||||
public void setCurrentValue(int currentValue) {
|
||||
int oldValue = this.mCurrentValue;
|
||||
this.mCurrentValue = Math.max(0, Math.min(currentValue, mMaxValue));
|
||||
if (oldValue != this.mCurrentValue) {
|
||||
LogUtils.d(TAG, "setCurrentValue: 当前盾值从" + oldValue + "更新为" + mCurrentValue);
|
||||
invalidate(); // 重绘控件
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前盾值
|
||||
*/
|
||||
public int getCurrentValue() {
|
||||
return mCurrentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最高盾值
|
||||
*/
|
||||
public int getMaxValue() {
|
||||
return mMaxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义渐变颜色
|
||||
* @param colors 渐变颜色数组(至少2种颜色)
|
||||
* @param positions 颜色位置数组(与colors长度一致,0.0~1.0)
|
||||
*/
|
||||
public void setGradientColors(int[] colors, float[] positions) {
|
||||
if (colors == null || colors.length < 2 || positions == null || positions.length != colors.length) {
|
||||
LogUtils.w(TAG, "setGradientColors: 渐变颜色参数不合法,颜色数组长度=" + (colors == null ? "null" : colors.length));
|
||||
return;
|
||||
}
|
||||
this.mGradientColors = colors;
|
||||
this.mGradientPositions = positions;
|
||||
LogUtils.d(TAG, "setGradientColors: 自定义渐变颜色已设置");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置温度条边框颜色
|
||||
*/
|
||||
public void setBorderColor(int color) {
|
||||
this.mBorderColor = color;
|
||||
mThermometerPaint.setColor(color);
|
||||
LogUtils.d(TAG, "setBorderColor: 边框颜色已更新");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文本颜色
|
||||
*/
|
||||
public void setTextColor(int color) {
|
||||
this.mTextColor = color;
|
||||
mTextPaint.setColor(color);
|
||||
LogUtils.d(TAG, "setTextColor: 文本颜色已更新");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
// 控件从窗口移除时,从缓存中清除,避免内存泄漏
|
||||
sViewCache.remove(this);
|
||||
LogUtils.d(TAG, "onDetachedFromWindow: 控件实例已从缓存移除");
|
||||
}
|
||||
|
||||
// ====================== 测量与绘制区 ======================
|
||||
/**
|
||||
* 测量辅助函数
|
||||
*/
|
||||
private int measureSize(int defaultSize, int measureSpec) {
|
||||
int result = defaultSize;
|
||||
int specMode = MeasureSpec.getMode(measureSpec);
|
||||
int specSize = MeasureSpec.getSize(measureSpec);
|
||||
if (specMode == MeasureSpec.EXACTLY) {
|
||||
result = specSize;
|
||||
} else if (specMode == MeasureSpec.AT_MOST) {
|
||||
result = Math.min(defaultSize, specSize);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
// 强制控件整体左右内边距=0,彻底消除外部边距
|
||||
setPadding(0, getPaddingTop(), 0, getPaddingBottom());
|
||||
|
||||
// 控件宽度=温度条宽度 + 文本区宽度(均为5dp转px,无额外空白)
|
||||
int defaultWidth = mThermometerWidth + mTextAreaWidth;
|
||||
int width = measureSize(defaultWidth, widthMeasureSpec);
|
||||
int height = measureSize(DEFAULT_HEIGHT, heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
|
||||
// 根据文本位置配置,计算温度条矩形坐标
|
||||
float thermometerLeft, thermometerRight;
|
||||
if (isTextOnRight) {
|
||||
// 文本在右侧:温度条靠左,右接文本区
|
||||
thermometerLeft = 0;
|
||||
thermometerRight = thermometerLeft + mThermometerWidth;
|
||||
} else {
|
||||
// 文本在左侧:温度条靠右,左接文本区
|
||||
thermometerLeft = width - mThermometerWidth;
|
||||
thermometerRight = width;
|
||||
}
|
||||
float thermometerTop = getPaddingTop();
|
||||
float thermometerBottom = height - getPaddingBottom();
|
||||
mThermometerRect.set(thermometerLeft, thermometerTop, thermometerRight, thermometerBottom);
|
||||
|
||||
LogUtils.v(TAG, "onMeasure: 文本位置=" + (isTextOnRight ? "右侧" : "左侧") + ",控件尺寸=" + width + "x" + height + ",温度条区域=" + mThermometerRect.toShortString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
// 1. 绘制温度条边框(5dp宽度,小圆角适配)
|
||||
canvas.drawRoundRect(mThermometerRect, 3, 3, mThermometerPaint);
|
||||
|
||||
// 2. 计算填充高度(根据当前值占最大值的比例)
|
||||
float fillRatio = (float) mCurrentValue / mMaxValue;
|
||||
float fillHeight = mThermometerRect.height() * fillRatio;
|
||||
float fillTop = mThermometerRect.bottom - fillHeight;
|
||||
|
||||
// 3. 绘制渐变填充部分(左右贴边框,无间距)
|
||||
if (fillHeight > 0) {
|
||||
RectF fillRect = new RectF(
|
||||
mThermometerRect.left + FILL_PADDING_HORIZONTAL,
|
||||
fillTop + FILL_PADDING_VERTICAL,
|
||||
mThermometerRect.right - FILL_PADDING_HORIZONTAL,
|
||||
mThermometerRect.bottom - FILL_PADDING_VERTICAL
|
||||
);
|
||||
LinearGradient gradient = new LinearGradient(
|
||||
fillRect.centerX(), fillRect.bottom,
|
||||
fillRect.centerX(), fillRect.top,
|
||||
mGradientColors,
|
||||
mGradientPositions,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
mFillPaint.setShader(gradient);
|
||||
canvas.drawRoundRect(fillRect, 2, 2, mFillPaint);
|
||||
mFillPaint.setShader(null);
|
||||
}
|
||||
|
||||
// 4. 绘制文本(5dp固定宽度文本区,底部对齐竖排,文字居中)
|
||||
String text = String.format("%d/%d", mCurrentValue, mMaxValue);
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
float textBaseX;
|
||||
if (isTextOnRight) {
|
||||
// 文本在右侧:X=温度条右边缘 + 文本区宽度的一半(紧贴温度条)
|
||||
textBaseX = mThermometerRect.right + (mTextAreaWidth / 2f);
|
||||
} else {
|
||||
// 文本在左侧:X=文本区宽度的一半(紧贴控件左边缘)
|
||||
textBaseX = mTextAreaWidth / 2f;
|
||||
}
|
||||
|
||||
// 文本绘制参数(适配5dp窄区域,缩小字间距提升紧凑度)
|
||||
float singleCharHeight = mTextPaint.getTextSize() + (TEXT_CHAR_SPACING - 2f); // 字间距减为6f
|
||||
float totalTextHeight = (singleCharHeight * text.length()) - (TEXT_CHAR_SPACING - 2f);
|
||||
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
|
||||
float charBottomOffset = fontMetrics.bottom;
|
||||
|
||||
// 文本起始Y(底部对齐控件底部,无间距)
|
||||
float startTextY = getHeight() - getPaddingBottom() - charBottomOffset;
|
||||
|
||||
// 逐字竖排绘制(文字居中于5dp文本区,无超出)
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char singleChar = text.charAt(i);
|
||||
float currentTextY = startTextY - (i * singleCharHeight);
|
||||
canvas.drawText(String.valueOf(singleChar), textBaseX, currentTextY, mTextPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe CustomHorizontalScrollView
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
@@ -11,17 +16,10 @@ import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe 左滑显示操作按钮的自定义滚动视图,支持编辑、删除、上移、下移功能
|
||||
*/
|
||||
public class LeftScrollView extends HorizontalScrollView {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "LeftScrollView";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 布局控件
|
||||
private LinearLayout contentLayout;
|
||||
private LinearLayout toolLayout;
|
||||
private TextView textView;
|
||||
@@ -29,15 +27,11 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
private Button deleteButton;
|
||||
private Button upButton;
|
||||
private Button downButton;
|
||||
// 滑动事件相关
|
||||
private float mStartX;
|
||||
private float mEndX;
|
||||
private boolean isScrolling = false;
|
||||
private int nScrollAcceptSize;
|
||||
// 回调接口
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public LeftScrollView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
@@ -53,254 +47,174 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
init();
|
||||
}
|
||||
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void init() {
|
||||
LogUtils.d(TAG, "init: 开始初始化左滑滚动视图");
|
||||
// 加载布局
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
if (viewMain == null) {
|
||||
LogUtils.e(TAG, "init: 布局加载失败,无法初始化控件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定布局控件
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 校验控件是否绑定成功
|
||||
if (contentLayout == null || toolLayout == null) {
|
||||
LogUtils.e(TAG, "init: 核心布局控件绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加主布局到当前视图
|
||||
addView(viewMain);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListener();
|
||||
|
||||
LogUtils.d(TAG, "init: 左滑滚动视图初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作按钮的点击事件
|
||||
*/
|
||||
private void setButtonClickListener() {
|
||||
// 编辑按钮
|
||||
if (editButton != null) {
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击编辑按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
if (deleteButton != null) {
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击删除按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 上移按钮
|
||||
if (upButton != null) {
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击上移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 下移按钮
|
||||
if (downButton != null) {
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击下移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供的方法区 ======================
|
||||
/**
|
||||
* 添加内容视图到容器
|
||||
* @param viewContent 待添加的内容视图
|
||||
*/
|
||||
public void addContentLayout(View viewContent) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 内容布局未初始化,无法添加视图");
|
||||
return;
|
||||
}
|
||||
if (viewContent == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 待添加视图为null");
|
||||
return;
|
||||
}
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
LogUtils.d(TAG, "addContentLayout: 内容视图添加成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容布局的宽度
|
||||
* @param contentWidth 目标宽度
|
||||
*/
|
||||
public void setContentWidth(int contentWidth) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "setContentWidth: 内容布局未初始化,无法设置宽度");
|
||||
return;
|
||||
}
|
||||
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) contentLayout.getLayoutParams();
|
||||
layoutParams.width = contentWidth;
|
||||
contentLayout.setLayoutParams(layoutParams);
|
||||
LogUtils.d(TAG, "setContentWidth: 内容布局宽度设置为 " + contentWidth);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文本内容(原代码未初始化textView,添加空校验)
|
||||
* @param text 待显示的文本
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
if (textView == null) {
|
||||
LogUtils.w(TAG, "setText: 文本控件未初始化,无法设置文本");
|
||||
return;
|
||||
}
|
||||
textView.setText(text);
|
||||
LogUtils.d(TAG, "setText: 文本设置为 " + text);
|
||||
private void init() {
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
|
||||
// 创建内容布局
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
|
||||
//LogUtils.d(TAG, String.format("getWidth() %d", getWidth()));
|
||||
|
||||
addView(viewMain);
|
||||
|
||||
// 创建编辑按钮
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
// 创建删除按钮
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
// 向上按钮
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
// 向下按钮
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 编辑按钮点击事件
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 编辑按钮点击事件
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件回调监听器
|
||||
* @param listener 回调接口实例
|
||||
*/
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
LogUtils.d(TAG, "setOnActionListener: 事件监听器已设置");
|
||||
}
|
||||
|
||||
// ====================== 滑动事件处理区 ======================
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (event == null) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
LogUtils.d(TAG, "ACTION_DOWN");
|
||||
mStartX = event.getX();
|
||||
LogUtils.d(TAG, "onTouchEvent: ACTION_DOWN,起始X坐标 = " + mStartX);
|
||||
// isScrolling = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// 可根据需求添加滑动中逻辑
|
||||
//LogUtils.d(TAG, "ACTION_MOVE");
|
||||
// float currentX = event.getX();
|
||||
// float deltaX = mStartX - currentX;
|
||||
// //mLastX = currentX;
|
||||
// if (Math.abs(deltaX) > 0) {
|
||||
// isScrolling = true;
|
||||
// }
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mEndX = event.getX();
|
||||
int scrollX = getScrollX();
|
||||
LogUtils.d(TAG, String.format("onTouchEvent: ACTION_UP/CANCEL,起始X=%f 结束X=%f 滚动距离=%d",
|
||||
mStartX, mEndX, scrollX));
|
||||
|
||||
if (scrollX > 0) {
|
||||
handleScrollLogic();
|
||||
if (getScrollX() > 0) {
|
||||
LogUtils.d(TAG, "ACTION_UP");
|
||||
mEndX = event.getX();
|
||||
LogUtils.d(TAG, String.format("mStartX %f, mEndX %f", mStartX, mEndX));
|
||||
if (mEndX < mStartX) {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > editButton.getWidth()) {
|
||||
if (Math.abs(mStartX - mEndX) > editButton.getWidth()) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > deleteButton.getWidth()) {
|
||||
if (Math.abs(mEndX - mStartX) > deleteButton.getWidth()) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滑动结束后的逻辑,判断滑动方向并执行滚动
|
||||
*/
|
||||
private void handleScrollLogic() {
|
||||
float deltaX = Math.abs(mStartX - mEndX);
|
||||
// 校验按钮是否存在,避免空指针
|
||||
float threshold = editButton != null ? editButton.getWidth() : 50;
|
||||
|
||||
if (mEndX < mStartX) {
|
||||
// 向左滑,显示操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
// 向右滑,隐藏操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
void smoothScrollToRight() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
// 计算需要滑动到最右边的距离
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
// 确保滑动距离不小于0
|
||||
final int scrollToX2 = Math.max(0, scrollToX);
|
||||
// 平滑滑动到最右边
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(scrollToX2, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "smoothScrollTo(scrollToX, 0);");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动到右侧(显示操作按钮)
|
||||
*/
|
||||
private void smoothScrollToRight() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
int targetX = Math.max(0, scrollToX);
|
||||
smoothScrollTo(targetX, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToRight: 滚动到右侧,目标X坐标 = " + targetX);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动到左侧(隐藏操作按钮)
|
||||
*/
|
||||
private void smoothScrollToLeft() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToLeft: 滚动到左侧");
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置滑动坐标
|
||||
*/
|
||||
private void resetScrollCoordinate() {
|
||||
mStartX = 0;
|
||||
void smoothScrollToLeft() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
// 在手指抬起时,使用 post 方法调用 smoothScrollTo(0, 0)
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 回调接口定义区 ======================
|
||||
// 设置文本内容
|
||||
public void setText(CharSequence text) {
|
||||
textView.setText(text);
|
||||
}
|
||||
|
||||
// 定义回调接口
|
||||
public interface OnActionListener {
|
||||
void onEdit();
|
||||
void onDelete();
|
||||
void onUp();
|
||||
void onDown();
|
||||
}
|
||||
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/19 14:04:20
|
||||
* @Describe 云盾滑视度热备控件
|
||||
*/
|
||||
public class ScrollDoView {
|
||||
|
||||
public static final String TAG = "ScrollDoView";
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import android.content.Intent;
|
||||
import android.widget.RemoteViews;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
|
||||
public class APPStatusWidget extends AppWidgetProvider {
|
||||
|
||||
|
||||
@@ -12,52 +12,26 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitymainToolbar1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<cc.winboll.studio.contacts.views.DunTemperatureView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/dun_temp_view_left"
|
||||
android:layout_alignParentLeft="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainLinearLayout1"
|
||||
android:layout_toRightOf="@id/dun_temp_view_left"
|
||||
android:layout_toLeftOf="@id/dun_temp_view_right">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewPager"/>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewPager"/>
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:id="@+id/tabLayout"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:id="@+id/tabLayout"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.contacts.views.DunTemperatureView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/dun_temp_view_right"
|
||||
android:layout_alignParentRight="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -195,48 +195,30 @@
|
||||
android:text="拨不通电话记录查询:"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/bobulltoonurl_et"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="重置地址"
|
||||
android:onClick="onResetBoBullToonURL"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="下载数据"
|
||||
android:onClick="onDownloadBoBullToon"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="清空 BoBullToon 数据"
|
||||
android:onClick="onCleanBoBullToonData"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="重置地址"
|
||||
android:onClick="onResetBoBullToonURL"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="下载数据"
|
||||
android:onClick="onDownloadBoBullToon"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -336,11 +318,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="其他:"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsControlView
|
||||
android:id="@+id/ads_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Contacts</string>
|
||||
<string name="default_bobulltoon_url">https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip</string>
|
||||
<string name="default_bobulltoon_url">https://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,48 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 方案1:无 ActionBar 主题(推荐,适合自定义标题栏) -->
|
||||
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
|
||||
</style>
|
||||
|
||||
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorTittle">@color/colorAccent</item>
|
||||
<item name="colorTittleBackgound">@color/colorPrimary</item>
|
||||
<item name="colorText">@color/colorAccent</item>
|
||||
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
|
||||
|
||||
</style>
|
||||
|
||||
<!-- 方案2:带 ActionBar 主题(如需系统默认标题栏,启用此方案) -->
|
||||
<!--
|
||||
<style name="MyAppTheme" parent="Theme.MaterialComponents.Light">
|
||||
<style name="MyAppTheme" parent="AESTheme">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
|
||||
<item name="android:textSizeHeadline">18sp</item>
|
||||
<item name="android:textSizeBody">16sp</item>
|
||||
<item name="android:textSizeSubtitle">14sp</item>
|
||||
<item name="android:textSizeCaption">12sp</item>
|
||||
</style>
|
||||
|
||||
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light">
|
||||
<style name="GlobalCrashActivityTheme" parent="AESTheme">
|
||||
<item name="colorTittle">@color/colorAccent</item>
|
||||
<item name="colorTittleBackgound">@color/colorPrimary</item>
|
||||
<item name="colorText">@color/colorAccent</item>
|
||||
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
|
||||
|
||||
<item name="android:textSizeHeadline">20sp</item>
|
||||
<item name="android:textSizeBody">14sp</item>
|
||||
<item name="android:textSizeButton">16sp</item>
|
||||
</style>
|
||||
-->
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ android {
|
||||
applicationId "cc.winboll.studio.powerbell"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 6
|
||||
versionCode 7
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.11"
|
||||
versionName "15.14"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
@@ -56,7 +56,12 @@ dependencies {
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
|
||||
// uCrop 核心依赖(最新稳定版)
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||
// 兼容AndroidX(若项目用AndroidX,必须添加)
|
||||
//implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// SSH
|
||||
@@ -77,8 +82,13 @@ dependencies {
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
implementation 'cc.winboll.studio:libaes:15.11.6'
|
||||
implementation 'cc.winboll.studio:libappbase:15.11.0'
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Nov 26 16:27:33 HKT 2025
|
||||
stageCount=9
|
||||
#Tue Dec 23 14:29:30 HKT 2025
|
||||
stageCount=25
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.8
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.24
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.9
|
||||
baseBetaVersion=15.14.25
|
||||
|
||||
279
powerbell/build_copyright_pdf.sh
Normal file
279
powerbell/build_copyright_pdf.sh
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
# PowerBell软著版本号快速修改+生成脚本
|
||||
# 无需手动改主脚本,输入版本号直接运行
|
||||
|
||||
# 颜色输出函数
|
||||
red_echo() { echo -e "\033[31m$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m$1\033[0m"; }
|
||||
|
||||
# 1. 提示用户输入新版本号
|
||||
blue_echo "==== 请输入软著版本号(格式示例:V15、V15.0.1) ===="
|
||||
read -p "输入版本号:" NEW_VERSION
|
||||
|
||||
# 校验版本号格式(避免特殊符号)
|
||||
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
|
||||
red_echo "错误:版本号格式无效!请遵循「V+数字」格式(如V15、V15.0.1),不含特殊符号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
|
||||
SOFTWARE_NAME="PowerBell"
|
||||
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
|
||||
LINES_PER_PAGE=55
|
||||
|
||||
# 3. 生成主脚本(自动替换新版本号)
|
||||
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
|
||||
cat > build_copyright_pdf_temp.sh << EOF
|
||||
#!/bin/bash
|
||||
# PowerBell软著PDF生成脚本(版本:$NEW_VERSION)
|
||||
red_echo() { echo -e "\033[31m\$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m\$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
|
||||
|
||||
# 配置项(已自动替换为${NEW_VERSION})
|
||||
SOFTWARE_NAME="$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION="$NEW_VERSION"
|
||||
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE=$LINES_PER_PAGE
|
||||
|
||||
# 步骤1:检查依赖
|
||||
blue_echo "==== 1/7 检查并安装依赖 ===="
|
||||
sudo apt update > /dev/null 2>&1
|
||||
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
|
||||
for pkg in "\${REQUIRED_PKGS[@]}"; do
|
||||
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
|
||||
green_echo "安装依赖:\$pkg"
|
||||
sudo apt install -y "\$pkg" > /dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
# 步骤2:生成纯文本源码
|
||||
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
|
||||
cat > generate_source.py << GEN_EOF
|
||||
import os
|
||||
PROJECT_PATH = "./"
|
||||
OUTPUT_TXT = "PowerBell_Core_Source.txt"
|
||||
INCLUDE_EXT = [".java", ".kt"]
|
||||
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
|
||||
MIN_LINE_COUNT = 3
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
|
||||
def clean_text(text):
|
||||
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
|
||||
|
||||
def generate_source_txt():
|
||||
valid_files = []
|
||||
main_dir = os.path.join(PROJECT_PATH, "src", "main")
|
||||
if not os.path.exists(main_dir):
|
||||
print("Error: src/main directory not found!")
|
||||
return
|
||||
for root, dirs, files in os.walk(main_dir):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] in INCLUDE_EXT:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
|
||||
if len(code_lines) >= MIN_LINE_COUNT:
|
||||
valid_files.append(file_path)
|
||||
except:
|
||||
continue
|
||||
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
|
||||
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
|
||||
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
|
||||
for idx, file_path in enumerate(valid_files, 1):
|
||||
f.write(f"\\n{'='*60}\\n")
|
||||
f.write(f"文件 \{idx}:\{file_path.replace(PROJECT_PATH, '')}\\n")
|
||||
f.write(f"{'='*60}\\n\\n")
|
||||
try:
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
except UnicodeDecodeError:
|
||||
with open(file_path, "r", encoding="gbk") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
f.write(content)
|
||||
f.write("\\n\\n")
|
||||
except Exception as e:
|
||||
f.write(f"文件读取失败:\{str(e)}\\n\\n")
|
||||
continue
|
||||
print(f"有效源码文件数:\{len(valid_files)}")
|
||||
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_source_txt()
|
||||
GEN_EOF
|
||||
|
||||
python3 generate_source.py
|
||||
if [ ! -f "PowerBell_Core_Source.txt" ]; then
|
||||
red_echo "纯文本源码生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤3:生成带版本号页眉的HTML
|
||||
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
|
||||
cat > txt2html.py << TXT_EOF
|
||||
import os
|
||||
TXT_FILE = "PowerBell_Core_Source.txt"
|
||||
HTML_FILE = "PowerBell_Source.html"
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE = $LINES_PER_PAGE
|
||||
|
||||
CSS_STYLE = """
|
||||
<style>
|
||||
@page {{
|
||||
size: A4;
|
||||
margin: 10mm 5mm;
|
||||
@top-center {{
|
||||
content: "{} {} - 源代码(著作权人:{})";
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}}
|
||||
@bottom-center {{
|
||||
content: "页码 " counter(page) " / " counter(pages);
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
}}
|
||||
}}
|
||||
body {{
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
padding: 5mm 0 0 0;
|
||||
counter-reset: code-line;
|
||||
}}
|
||||
.file-header {{
|
||||
background: #f0f0f0;
|
||||
padding: 3px;
|
||||
margin: 6px 0;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}}
|
||||
.code-block {{
|
||||
white-space: pre;
|
||||
margin-left: 8px;
|
||||
line-height: 1.1;
|
||||
counter-increment: code-line;
|
||||
}}
|
||||
.code-block:before {{
|
||||
content: counter(code-line) " ";
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}}
|
||||
.page-break {{ page-break-after: always; counter-reset: code-line; }}
|
||||
</style>
|
||||
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
|
||||
|
||||
def txt_to_html():
|
||||
with open(TXT_FILE, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
|
||||
content_lines = content.split("\\n")[2:]
|
||||
content_clean = "\\n".join(content_lines)
|
||||
blocks = content_clean.split("====")
|
||||
|
||||
line_count = 0
|
||||
for block in blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
if "文件 " in block and ":" in block:
|
||||
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
|
||||
html_content += f"<div class='file-header'>\{file_header}</div>"
|
||||
code_part = block.split("\\n")[1:] if "\\n" in block else []
|
||||
block = "\\n".join(code_part)
|
||||
code_lines = block.split("\\n")
|
||||
for line in code_lines:
|
||||
if line.strip() or line_count > 0:
|
||||
line_count += 1
|
||||
html_content += f"<div class='code-block'>\{line}</div>"
|
||||
if line_count >= LINES_PER_PAGE:
|
||||
html_content += "<div class='page-break'></div>"
|
||||
line_count = 0
|
||||
html_content += "</body></html>"
|
||||
with open(HTML_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
print(f"HTML文件路径:\{os.path.abspath(HTML_FILE)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
txt_to_html()
|
||||
TXT_EOF
|
||||
|
||||
python3 txt2html.py
|
||||
if [ ! -f "PowerBell_Source.html" ]; then
|
||||
red_echo "HTML文件生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤4:生成完整PDF
|
||||
blue_echo -e "\n==== 4/7 生成完整PDF(版本:${NEW_VERSION}) ===="
|
||||
wkhtmltopdf --page-size A4 \
|
||||
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
|
||||
--encoding utf-8 \
|
||||
--no-images --disable-javascript \
|
||||
--enable-local-file-access \
|
||||
--no-stop-slow-scripts \
|
||||
PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
|
||||
if [ ! -f "PowerBell_soft_full.pdf" ]; then
|
||||
red_echo "完整PDF生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤5:截取60页
|
||||
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
|
||||
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "源码完整PDF总页数:\$TOTAL_PAGES 页"
|
||||
|
||||
if [ "\$TOTAL_PAGES" -le 60 ]; then
|
||||
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
green_echo "源码不足60页,直接使用完整PDF"
|
||||
else
|
||||
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
|
||||
START_PAGE=\$((TOTAL_PAGES - 29))
|
||||
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
|
||||
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
|
||||
green_echo "源码超过60页,已截取前30页+后30页合并为60页"
|
||||
fi
|
||||
|
||||
# 步骤6:验证规范
|
||||
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
|
||||
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "最终PDF页数:\$FINAL_PAGES 页"
|
||||
green_echo "每页代码行数:\$LINES_PER_PAGE 行(≥50行)"
|
||||
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER)"
|
||||
|
||||
# 步骤7:清理临时文件
|
||||
blue_echo -e "\n==== 7/7 清理临时文件 ===="
|
||||
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
green_echo "临时文件清理完成!"
|
||||
|
||||
# 输出结果
|
||||
green_echo -e "\n====================================="
|
||||
green_echo "✅ $SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功!🎉"
|
||||
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
|
||||
green_echo "💡 可直接提交软著登记,无需手动修改!"
|
||||
green_echo "====================================="
|
||||
EOF
|
||||
|
||||
# 4. 赋予执行权限并运行
|
||||
chmod +x build_copyright_pdf_temp.sh
|
||||
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
|
||||
./build_copyright_pdf_temp.sh
|
||||
|
||||
# 5. 删除临时主脚本(可选,保留则注释此行)
|
||||
rm -f build_copyright_pdf_temp.sh
|
||||
|
||||
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="
|
||||
@@ -4,55 +4,50 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<!-- 前台服务权限 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 开机启动 -->
|
||||
<!-- 系统事件权限 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 显示通知 -->
|
||||
<!-- 通知权限 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- PACKAGE_USAGE_STATS -->
|
||||
<!-- 应用统计与查询权限 -->
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
|
||||
|
||||
<!-- BATTERY_STATS -->
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<!-- 电池与存储统计权限 -->
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- 外部存储权限 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 相机权限 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<!-- 硬件特性声明 -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false"/>
|
||||
|
||||
<!-- 应用查询 -->
|
||||
<queries>
|
||||
<package android:name="com.miui.securitycenter"/>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -64,18 +59,17 @@
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<!-- 主活动 -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.CrashActivity"/>
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<!-- 活动别名(启动器) -->
|
||||
<activity-alias
|
||||
android:name=".MainActivityEN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
@@ -83,19 +77,13 @@
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmainen1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
@@ -105,19 +93,13 @@
|
||||
android:label="@string/app_name_cn1"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
@@ -127,109 +109,129 @@
|
||||
android:label="@string/app_name_cn2"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn2"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<!-- 功能活动 -->
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
</activity>
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.activities.BackgroundPictureActivity"
|
||||
android:name=".activities.BackgroundSettingsActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="image/jpeg"/>
|
||||
|
||||
<data android:mimeType="image/jpg"/>
|
||||
|
||||
<data android:mimeType="image/png"/>
|
||||
|
||||
<data android:mimeType="image/webp"/>
|
||||
|
||||
<data android:mimeType="image/*"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.BatteryReporterActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.PixelPickerActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.BatteryReportActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".unittest.MainUnitTestActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ShortcutActionActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
|
||||
android:exported="false"/>
|
||||
|
||||
<!-- 第三方活动 -->
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true"/>
|
||||
|
||||
<!-- 广播接收器 -->
|
||||
<receiver
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<!-- 服务 -->
|
||||
<service
|
||||
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".controlcenterservice"/>
|
||||
android:process=".controlcenterservice"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="cc.winboll.studio.powerbell.services.AssistantService"
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".assistantservice"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
|
||||
android:process=".assistantservice">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
</service>
|
||||
|
||||
<!-- 内容提供者 -->
|
||||
<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.powerbell.activities.ShortcutActionActivity"/>
|
||||
<!-- 元数据 -->
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
|
||||
BIN
powerbell/src/main/assets/images/blank100x100.png
Normal file
BIN
powerbell/src/main/assets/images/blank100x100.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
BIN
powerbell/src/main/assets/unittest/unittest-miku.png
Normal file
BIN
powerbell/src/main/assets/unittest/unittest-miku.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1,97 +1,269 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 应用全局入口类(适配Android API 30,基于Java 7编写)
|
||||
* 核心策略:极致强制缓存 - 无论内存紧张程度,永不自动清理任何缓存(Bitmap/视图控件/路径记录)
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
// ===================== 常量定义区(按功能分类排序) =====================
|
||||
public static final String TAG = "App";
|
||||
|
||||
public static final String TAG = "GlobalApplication";
|
||||
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
// 数据配置存储工具
|
||||
static AppConfigUtils _mAppConfigUtils;
|
||||
static AppCacheUtils _mAppCacheUtils;
|
||||
GlobalApplicationReceiver mReceiver;
|
||||
static String szTempDir = "";
|
||||
// 组件跳转常量
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
|
||||
public static String getTempDirPath() {
|
||||
return szTempDir;
|
||||
// 动作跳转常量
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
// 缓存防护常量
|
||||
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
|
||||
|
||||
// ===================== 静态属性区(按工具类优先级排序) =====================
|
||||
// 数据配置工具
|
||||
private static AppConfigUtils sAppConfigUtils;
|
||||
private static AppCacheUtils sAppCacheUtils;
|
||||
|
||||
// 全局Bitmap缓存工具(极致强制保持:一旦初始化,永不销毁)
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
|
||||
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
|
||||
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
|
||||
// ===================== 成员属性区(按生命周期关联度排序) =====================
|
||||
// 全局广播接收器
|
||||
private GlobalApplicationReceiver mGlobalReceiver;
|
||||
|
||||
// 通知管理工具
|
||||
private NotificationManagerUtils mNotificationManager;
|
||||
|
||||
// ===================== 公共静态方法区(工具类实例获取) =====================
|
||||
/**
|
||||
* 获取应用配置工具实例
|
||||
*/
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppConfigUtils() 调用,传入Context类型:" + (context != null ? context.getClass().getSimpleName() : "null"));
|
||||
if (sAppConfigUtils == null) {
|
||||
sAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppConfigUtils():AppConfigUtils实例已初始化");
|
||||
}
|
||||
return sAppConfigUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用缓存工具实例
|
||||
*/
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppCacheUtils() 调用,传入Context类型:" + (context != null ? context.getClass().getSimpleName() : "null"));
|
||||
if (sAppCacheUtils == null) {
|
||||
sAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppCacheUtils():AppCacheUtils实例已初始化");
|
||||
}
|
||||
return sAppCacheUtils;
|
||||
}
|
||||
|
||||
// ===================== 公共成员方法区(业务功能) =====================
|
||||
/**
|
||||
* 清除电池历史数据
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
LogUtils.d(TAG, "clearBatteryHistory() 调用");
|
||||
if (sAppCacheUtils != null) {
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
LogUtils.d(TAG, "clearBatteryHistory():电池历史数据已清除");
|
||||
} else {
|
||||
LogUtils.w(TAG, "clearBatteryHistory():AppCacheUtils未初始化,清除失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
|
||||
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
|
||||
*/
|
||||
public static void manualClearAllCache() {
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存调用(极致强制缓存策略下,需谨慎使用)");
|
||||
// 清理Bitmap缓存
|
||||
if (sBitmapCacheUtils != null) {
|
||||
sBitmapCacheUtils.clearAllCache();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存已手动清理");
|
||||
}
|
||||
// 清理视图控件缓存(仅清除静态引用,不销毁实例)
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存实例保持,仅清除静态引用");
|
||||
sMemoryCachedBackgroundView = null;
|
||||
}
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存完成(部分缓存实例仍可能保留在内存中)");
|
||||
}
|
||||
|
||||
// ===================== 生命周期方法区(按执行顺序排序) =====================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
LogUtils.d(TAG, "onCreate() 应用启动,开始初始化");
|
||||
|
||||
// 临时文件夹方案1
|
||||
// 获取Pictures文件夹路径(Android 10及以上推荐使用MediaStore,此处为传统方式)
|
||||
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
// 定义目标文件路径(在Pictures目录下创建"PowerBell"子文件夹及文件)
|
||||
File powerBellDir = new File(picturesDir, "PowerBell");
|
||||
|
||||
// 临时文件夹方案2 <图片保存失败>
|
||||
// 获取Pictures文件夹路径(Android 10及以上推荐使用MediaStore,此处为传统方式)
|
||||
//File powerBellDir = getExternalFilesDir("TempDir");
|
||||
// 初始化调试模式
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
LogUtils.d(TAG, "onCreate() 调试模式:" + BuildConfig.DEBUG);
|
||||
|
||||
// 先创建文件夹(如果不存在)
|
||||
if (!powerBellDir.exists()) {
|
||||
powerBellDir.mkdirs();
|
||||
}
|
||||
szTempDir = powerBellDir.getAbsolutePath();
|
||||
// 初始化基础工具
|
||||
initBaseTools();
|
||||
// 初始化工具类实例(核心:极致强制缓存,永不销毁)
|
||||
initUtils();
|
||||
// 初始化广播接收器
|
||||
initReceiver();
|
||||
|
||||
LogUtils.d(TAG, "onCreate() 应用初始化完成,极致强制缓存策略已启用");
|
||||
}
|
||||
|
||||
// 初始化 Toast 框架
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放非缓存资源");
|
||||
|
||||
// 释放Toast工具
|
||||
ToastUtils.release();
|
||||
LogUtils.d(TAG, "onTerminate():Toast工具已释放");
|
||||
// 释放通知工具
|
||||
releaseNotificationManager();
|
||||
// 释放广播接收器
|
||||
releaseReceiver();
|
||||
|
||||
// 核心修改:应用终止时也不清理缓存,保持静态实例
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 应用终止,极致强制缓存策略生效,不清理任何缓存");
|
||||
|
||||
LogUtils.d(TAG, "onTerminate() 非缓存资源释放完成,缓存实例保持");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
// 极致强制缓存:禁止任何缓存清理操作,仅记录日志
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onTrimMemory() 调用,内存等级level:" + level + ",极致强制保持所有缓存");
|
||||
// 记录详细缓存状态,不执行任何清理
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
// 极致强制缓存:低内存时也不清理任何缓存
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onLowMemory() 调用,极致强制保持所有缓存");
|
||||
// 记录详细缓存状态,不执行任何清理
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
// ===================== 私有初始化方法区(按初始化顺序排序) =====================
|
||||
/**
|
||||
* 初始化基础工具(Activity管理、Toast)
|
||||
*/
|
||||
private void initBaseTools() {
|
||||
LogUtils.d(TAG, "initBaseTools() 开始初始化基础工具");
|
||||
WinBoLLActivityManager.init(this);
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.toast_custom_view);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
// 设置数据配置存储工具
|
||||
_mAppConfigUtils = getAppConfigUtils(this);
|
||||
_mAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
mReceiver = new GlobalApplicationReceiver(this);
|
||||
mReceiver.registerAction();
|
||||
LogUtils.d(TAG, "initBaseTools() 基础工具初始化完成");
|
||||
}
|
||||
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
if (_mAppConfigUtils == null) {
|
||||
_mAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
/**
|
||||
* 初始化工具类实例(核心:极致强制缓存,一旦初始化永不销毁)
|
||||
*/
|
||||
private void initUtils() {
|
||||
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用极致强制缓存策略");
|
||||
sAppConfigUtils = getAppConfigUtils(this);
|
||||
sAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
// 极致强制初始化Bitmap缓存工具(必初始化,永不销毁)
|
||||
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化(极致强制保持,永不销毁)");
|
||||
|
||||
// 极致强制初始化视图控件缓存工具(必初始化,永不销毁)
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
|
||||
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(极致强制保持,永不销毁)");
|
||||
|
||||
mNotificationManager = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "initUtils() 工具类初始化完成,极致强制缓存策略已生效");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化广播接收器
|
||||
*/
|
||||
private void initReceiver() {
|
||||
LogUtils.d(TAG, "initReceiver() 开始初始化广播接收器");
|
||||
mGlobalReceiver = new GlobalApplicationReceiver(this);
|
||||
mGlobalReceiver.registerAction();
|
||||
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
|
||||
}
|
||||
|
||||
// ===================== 私有释放方法区(按资源重要性排序) =====================
|
||||
/**
|
||||
* 释放广播接收器资源
|
||||
*/
|
||||
private void releaseReceiver() {
|
||||
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
|
||||
if (mGlobalReceiver != null) {
|
||||
mGlobalReceiver.unregisterAction();
|
||||
mGlobalReceiver = null;
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
|
||||
} else {
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
|
||||
}
|
||||
return _mAppConfigUtils;
|
||||
}
|
||||
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
if (_mAppCacheUtils == null) {
|
||||
_mAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
/**
|
||||
* 释放通知管理工具资源
|
||||
*/
|
||||
private void releaseNotificationManager() {
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 开始释放通知工具");
|
||||
if (mNotificationManager != null) {
|
||||
mNotificationManager.release();
|
||||
mNotificationManager = null;
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 通知工具资源已释放");
|
||||
} else {
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 通知工具未初始化,无需释放");
|
||||
}
|
||||
return _mAppCacheUtils;
|
||||
}
|
||||
|
||||
public void clearBatteryHistory() {
|
||||
_mAppCacheUtils.clearBatteryHistory();
|
||||
// ===================== 私有工具方法区(辅助功能) =====================
|
||||
/**
|
||||
* 记录详细缓存状态(用于调试,监控极致强制缓存效果)
|
||||
*/
|
||||
private void logDetailedCacheStatus() {
|
||||
LogUtils.d(TAG, "logDetailedCacheStatus() 开始记录详细缓存状态");
|
||||
// Bitmap缓存状态
|
||||
if (sBitmapCacheUtils != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存工具实例有效(极致强制保持)");
|
||||
// 假设BitmapCacheUtils有获取缓存数量的方法
|
||||
try {
|
||||
int cacheCount = sBitmapCacheUtils.getCacheCount();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量:" + cacheCount);
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量获取失败(不影响缓存),异常信息:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
// 视图控件缓存状态
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存工具实例有效(极致强制保持)");
|
||||
// 记录视图实例总数
|
||||
int viewInstanceCount = MemoryCachedBackgroundView.getInstanceCount();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件实例总数:" + viewInstanceCount);
|
||||
}
|
||||
LogUtils.d(TAG, "logDetailedCacheStatus() 详细缓存状态记录完成,所有缓存均极致强制保持");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/25 01:16:32
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libaes.views.AboutView;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
public class AboutActivity extends Activity {
|
||||
|
||||
Context mContext;
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
mContext = this;
|
||||
|
||||
// 初始化工具栏
|
||||
AToolbar mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(getString(R.string.text_about));
|
||||
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
AboutView aboutView = CreateAboutView();
|
||||
// 在 Activity 的 onCreate 或其他生命周期方法中调用
|
||||
LinearLayout llRoot = findViewById(R.id.root_ll);
|
||||
//layout.setOrientation(LinearLayout.VERTICAL);
|
||||
// 创建布局参数(宽度和高度)
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
llRoot.addView(aboutView, params);
|
||||
|
||||
}
|
||||
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "powerbell";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_launcher);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
|
||||
appInfo.setAppAPKName("PowerBell");
|
||||
appInfo.setAppAPKFolderName("PowerBell");
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,659 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class BackgroundPictureActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
|
||||
|
||||
public static final String TAG = "BackgroundPictureActivity";
|
||||
public BackgroundPictureUtils mBackgroundPictureUtils;
|
||||
|
||||
// 图片选择请求码
|
||||
public static final int REQUEST_SELECT_PICTURE = 0;
|
||||
public static final int REQUEST_TAKE_PHOTO = 1;
|
||||
public static final int REQUEST_CROP_IMAGE = 2;
|
||||
private static final int STORAGE_PERMISSION_REQUEST = 100;
|
||||
|
||||
private AToolbar mAToolbar;
|
||||
private File mfBackgroundDir; // 背景图片存储文件夹
|
||||
private File mfPictureDir; // 拍照与剪裁临时文件夹
|
||||
private File mfTakePhoto; // 拍照文件
|
||||
private File mfRecivedPicture; // 接收的图片文件
|
||||
private File mfTempCropPicture; // 剪裁临时文件
|
||||
private File mfRecivedCropPicture; // 剪裁后的目标文件
|
||||
|
||||
private String preViewFileBackgroundView = "";
|
||||
BackgroundView bvPreviewBackground;
|
||||
boolean isCommitSettings = false;
|
||||
|
||||
// 静态变量
|
||||
public static String _mszRecivedCropPicture = "RecivedCrop.jpg";
|
||||
private static String _mszCommonFileType = "jpeg";
|
||||
private int mnPictureCompress = 100;
|
||||
private static String _RecivedPictureFileName;
|
||||
|
||||
@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_backgroundpicture);
|
||||
initEnv();
|
||||
|
||||
// 初始化工具类和文件夹
|
||||
mBackgroundPictureUtils = BackgroundPictureUtils.getInstance(this);
|
||||
mfBackgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
|
||||
if (!mfBackgroundDir.exists()) {
|
||||
mfBackgroundDir.mkdirs();
|
||||
}
|
||||
|
||||
mfPictureDir = new File(App.getTempDirPath());
|
||||
if (!mfPictureDir.exists()) {
|
||||
mfPictureDir.mkdirs();
|
||||
}
|
||||
|
||||
// 初始化文件对象
|
||||
mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg");
|
||||
mfTempCropPicture = new File(mfPictureDir, "TempCrop.jpg");
|
||||
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
mfRecivedCropPicture = new File(mfBackgroundDir, _mszRecivedCropPicture);
|
||||
|
||||
// 初始化工具栏
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish(); // 点击导航栏返回按钮,触发 finish()
|
||||
}
|
||||
});
|
||||
|
||||
// 设置按钮点击事件
|
||||
findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
|
||||
|
||||
updatePreviewBackground();
|
||||
|
||||
// 处理分享的图片
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this);
|
||||
dlg.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void initEnv() {
|
||||
LogUtils.d(TAG, "initEnv()");
|
||||
_RecivedPictureFileName = "Recived.data";
|
||||
}
|
||||
|
||||
public static String getBackgroundFileName() {
|
||||
return _mszRecivedCropPicture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(String szPreRecivedPictureName) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
|
||||
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
utils.saveData();
|
||||
|
||||
File sourceFile = new File(utils.getBackgroundDir(), szPreRecivedPictureName);
|
||||
if (FileUtils.copyFile(sourceFile, mfRecivedPicture)) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("图片复制失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新背景图片预览
|
||||
*/
|
||||
public void updatePreviewBackground() {
|
||||
LogUtils.d(TAG, "updatePreviewBackground");
|
||||
//ImageView ivPreviewBackground = (ImageView) findViewById(R.id.activitybackgroundpictureImageView1);
|
||||
bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1);
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
|
||||
utils.loadBackgroundPictureBean();
|
||||
|
||||
boolean isUseBackgroundFile = utils.getBackgroundPictureBean().isUseBackgroundFile();
|
||||
if (isUseBackgroundFile && mfRecivedCropPicture.exists()) {
|
||||
//try {
|
||||
String filePath = utils.getBackgroundDir() + getBackgroundFileName();
|
||||
preViewFileBackgroundView = filePath;
|
||||
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
|
||||
/*Drawable drawable = FileUtils.getImageDrawable(filePath);
|
||||
if (drawable != null) {
|
||||
//drawable.setAlpha(120);
|
||||
//bvPreviewBackground.setImageDrawable(drawable);
|
||||
}*/
|
||||
//ToastUtils.show("背景图片已更新");
|
||||
// } catch (IOException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// ToastUtils.show("背景图片加载失败");
|
||||
// }
|
||||
} else {
|
||||
ToastUtils.show("未使用背景图片");
|
||||
preViewFileBackgroundView = "";
|
||||
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
|
||||
// Drawable drawable = getResources().getDrawable(R.drawable.blank10x10);
|
||||
// if (drawable != null) {
|
||||
// drawable.setAlpha(120);
|
||||
// bvPreviewBackground.setImageDrawable(drawable);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件监听器
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setIsUseBackgroundFile(false);
|
||||
utils.saveData();
|
||||
updatePreviewBackground();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (checkAndRequestStoragePermission()) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
startActivityForResult(intent, REQUEST_SELECT_PICTURE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
|
||||
if (fCheck.exists()) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("没有可剪裁的图片");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
|
||||
if (fCheck.exists()) {
|
||||
startCropImageActivity(true);
|
||||
} else {
|
||||
ToastUtils.show("没有可剪裁的图片");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onTakePhotoClickListener");
|
||||
LogUtils.d(TAG, "mfTakePhoto : " + mfTakePhoto.getPath());
|
||||
|
||||
if (mfTakePhoto.exists()) {
|
||||
mfTakePhoto.delete();
|
||||
}
|
||||
try {
|
||||
mfTakePhoto.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkAndRequestStoragePermission()) {
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
utils.saveData();
|
||||
updatePreviewBackground();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 从文件路径启动像素拾取活动
|
||||
//String imagePath = "/storage/emulated/0/DCIM/Camera/sample.jpg";
|
||||
String imagePath = mfRecivedCropPicture.toString();
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", imagePath);
|
||||
startActivity(intent);
|
||||
//App.getWinBoLLActivityManager().startWinBoLLActivity(getActivity(), intent, PixelPickerActivity.class);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setPixelColor(0);
|
||||
utils.saveData();
|
||||
setBackgroundColor();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 压缩图片并保存到接收文件
|
||||
*/
|
||||
void compressQualityToRecivedPicture(Bitmap bitmap) {
|
||||
OutputStream outStream = null;
|
||||
try {
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
if (!mfRecivedPicture.exists()) {
|
||||
mfRecivedPicture.createNewFile();
|
||||
}
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(mfRecivedPicture);
|
||||
outStream = new BufferedOutputStream(fos);
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
|
||||
outStream.flush();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("图片压缩失败");
|
||||
} finally {
|
||||
if (outStream != null) {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片裁剪活动
|
||||
* @param isCropFree 是否自由裁剪
|
||||
*/
|
||||
public void startCropImageActivity(boolean isCropFree) {
|
||||
LogUtils.d(TAG, "startCropImageActivity");
|
||||
BackgroundPictureBean bean = mBackgroundPictureUtils.loadBackgroundPictureBean();
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
|
||||
LogUtils.d(TAG, "uri : " + uri.toString());
|
||||
|
||||
if (mfTempCropPicture.exists()) {
|
||||
mfTempCropPicture.delete();
|
||||
}
|
||||
try {
|
||||
mfTempCropPicture.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("剪裁临时文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Uri cropOutPutUri = Uri.fromFile(mfTempCropPicture);
|
||||
LogUtils.d(TAG, "mfTempCropPicture : " + mfTempCropPicture.getPath());
|
||||
|
||||
Intent intent = new Intent("com.android.camera.action.CROP");
|
||||
intent.setDataAndType(uri, "image/" + _mszCommonFileType);
|
||||
intent.putExtra("crop", "true");
|
||||
intent.putExtra("noFaceDetection", true);
|
||||
|
||||
if (!isCropFree) {
|
||||
intent.putExtra("aspectX", bean.getBackgroundWidth());
|
||||
intent.putExtra("aspectY", bean.getBackgroundHeight());
|
||||
}
|
||||
|
||||
intent.putExtra("return-data", true);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri);
|
||||
intent.putExtra("scale", true);
|
||||
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
startActivityForResult(intent, REQUEST_CROP_IMAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存剪裁后的Bitmap(优化版)
|
||||
*/
|
||||
private void saveCropBitmap(Bitmap bitmap) {
|
||||
if (bitmap == null) {
|
||||
ToastUtils.show("剪裁图片为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 内存优化:大图片自动缩放
|
||||
Bitmap scaledBitmap = bitmap;
|
||||
if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB
|
||||
float scale = 1.0f;
|
||||
while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) {
|
||||
scale -= 0.2f; // 每次缩小20%
|
||||
if (scale < 0.2f) break; // 最小缩放到20%
|
||||
scaledBitmap = scaleBitmap(scaledBitmap, scale);
|
||||
}
|
||||
if (scaledBitmap != bitmap) {
|
||||
bitmap.recycle(); // 回收原Bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// 优化:创建保存目录
|
||||
File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
|
||||
if (!backgroundDir.exists()) {
|
||||
if (!backgroundDir.mkdirs()) {
|
||||
ToastUtils.show("无法创建保存目录");
|
||||
if (scaledBitmap != bitmap) scaledBitmap.recycle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File saveFile = new File(backgroundDir, getBackgroundFileName());
|
||||
|
||||
// 优化:检查文件是否可写
|
||||
if (saveFile.exists() && !saveFile.canWrite()) {
|
||||
if (!saveFile.delete()) {
|
||||
ToastUtils.show("无法删除旧文件");
|
||||
if (scaledBitmap != bitmap) scaledBitmap.recycle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(saveFile);
|
||||
boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
|
||||
fos.flush();
|
||||
if (success) {
|
||||
ToastUtils.show("保存成功");
|
||||
// 更新数据
|
||||
mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
updatePreviewBackground();
|
||||
} else {
|
||||
ToastUtils.show("图片压缩保存失败");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
LogUtils.e(TAG, "文件未找到" + e);
|
||||
ToastUtils.show("保存失败:文件路径错误");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "写入异常" + e);
|
||||
ToastUtils.show("保存失败:磁盘可能已满或路径错误");
|
||||
} finally {
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "流关闭异常" + e);
|
||||
}
|
||||
}
|
||||
if (scaledBitmap != null && !scaledBitmap.isRecycled()) {
|
||||
scaledBitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放Bitmap
|
||||
*/
|
||||
private Bitmap scaleBitmap(Bitmap original, float scale) {
|
||||
if (original == null) {
|
||||
return null;
|
||||
}
|
||||
int width = (int) (original.getWidth() * scale);
|
||||
int height = (int) (original.getHeight() * scale);
|
||||
return Bitmap.createScaledBitmap(original, width, height, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享图片
|
||||
*/
|
||||
void sharePicture() {
|
||||
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
shareIntent.setType("image/" + _mszCommonFileType);
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivity(Intent.createChooser(shareIntent, "Share Image"));
|
||||
}
|
||||
|
||||
public static File getRecivedPictureFile(Context context) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(context);
|
||||
utils.loadBackgroundPictureBean();
|
||||
return new File(utils.getBackgroundDir(), _RecivedPictureFileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) {
|
||||
try {
|
||||
Uri selectedImage = data.getData();
|
||||
LogUtils.d(TAG, "Uri is : " + selectedImage.toString());
|
||||
File fSrcImage = new File(UriUtil.getFilePathFromUri(this, selectedImage));
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
if (FileUtils.copyFile(fSrcImage, mfRecivedPicture)) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("图片复制失败,请重试");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "选择图片异常" + e);
|
||||
ToastUtils.show("选择图片失败:" + e.getMessage());
|
||||
}
|
||||
} else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) {
|
||||
LogUtils.d(TAG, "REQUEST_TAKE_PHOTO");
|
||||
Bundle extras = data.getExtras();
|
||||
if (extras != null) {
|
||||
Bitmap imageBitmap = (Bitmap) extras.get("data");
|
||||
if (imageBitmap != null) {
|
||||
compressQualityToRecivedPicture(imageBitmap);
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("拍照图片为空");
|
||||
}
|
||||
} else {
|
||||
ToastUtils.show("拍照数据获取失败");
|
||||
}
|
||||
} else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) {
|
||||
LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE");
|
||||
try {
|
||||
Bitmap cropBitmap = null;
|
||||
// 方案1:通过Intent获取剪裁后的Bitmap
|
||||
if (data != null && data.hasExtra("data")) {
|
||||
cropBitmap = data.getParcelableExtra("data");
|
||||
} else if (mfTempCropPicture.exists()) {
|
||||
cropBitmap = BitmapFactory.decodeFile(mfTempCropPicture.getPath());
|
||||
} else {
|
||||
ToastUtils.show("剪裁文件不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cropBitmap != null) {
|
||||
saveCropBitmap(cropBitmap);
|
||||
} else {
|
||||
ToastUtils.show("获取剪裁图片失败");
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "内存溢出" + e);
|
||||
ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "剪裁保存异常" + e);
|
||||
ToastUtils.show("保存失败:" + e.getMessage());
|
||||
}/* finally {
|
||||
// 安全删除临时文件
|
||||
if (mfTempCropPicture.exists()) {
|
||||
mfTempCropPicture.delete();
|
||||
}
|
||||
}*/
|
||||
} else if (resultCode != RESULT_OK) {
|
||||
LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode);
|
||||
ToastUtils.show("操作已取消");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类型是否为图片
|
||||
*/
|
||||
private boolean isImageType(String type) {
|
||||
return type.startsWith("image/") || "image/jpeg".equals(type) ||
|
||||
"image/jpg".equals(type) || "image/png".equals(type) ||
|
||||
"image/webp".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并申请存储权限
|
||||
*/
|
||||
private boolean checkAndRequestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
STORAGE_PERMISSION_REQUEST);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == STORAGE_PERMISSION_REQUEST) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
ToastUtils.show("存储权限已获取");
|
||||
} else {
|
||||
ToastUtils.show("需要存储权限才能保存图片");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setBackgroundColor() {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
int nPixelColor = bean.getPixelColor();
|
||||
RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
public void onNetworkBackgroundDialog(View view) {
|
||||
// 在需要显示对话框的地方(如网络状态监听回调中)
|
||||
NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() {
|
||||
@Override
|
||||
public void onConfirm() {
|
||||
ToastUtils.show("onConfirm");
|
||||
// 处理确认逻辑(如允许后台网络使用)
|
||||
LogUtils.d("MainActivity", "用户允许后台网络使用");
|
||||
// 执行具体业务:如开启后台网络请求服务
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
ToastUtils.show("onCancel");
|
||||
// 处理取消逻辑(如禁止后台网络使用)
|
||||
LogUtils.d("MainActivity", "用户禁止后台网络使用");
|
||||
// 执行具体业务:如关闭后台网络请求
|
||||
}
|
||||
});
|
||||
|
||||
// 可选:修改对话框标题和内容(适配自定义场景)
|
||||
dialog.setTitle("网络图片下载对话框");
|
||||
dialog.setContent("是否下载地址中的图片资源,作为应用背景图片?");
|
||||
|
||||
// 显示对话框
|
||||
dialog.show();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写finish方法,确保所有退出场景都触发Toast
|
||||
*/
|
||||
@Override
|
||||
public void finish() {
|
||||
if (!isCommitSettings) {
|
||||
YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
bvPreviewBackground.saveToBackgroundSources(preViewFileBackgroundView);
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
super.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
public class BackgroundSettingsActivity extends WinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义(按功能分类置顶)======================
|
||||
public static final String TAG = "BackgroundSettingsActivity";
|
||||
// 系统版本常量
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
// 请求码(按功能分组)
|
||||
public static final int REQUEST_SELECT_PICTURE = 0;
|
||||
public static final int REQUEST_TAKE_PHOTO = 1;
|
||||
public static final int REQUEST_CROP_IMAGE = 2;
|
||||
private static final int REQUEST_PIXELPICKER = 1001;
|
||||
private static final int REQUEST_CAMERA_PERMISSION = 1004;
|
||||
// Bitmap解析常量
|
||||
private static final int BITMAP_MAX_SIZE = 2048;
|
||||
private static final int BITMAP_MAX_SAMPLE_SIZE = 16;
|
||||
|
||||
// ====================== 成员变量(按依赖优先级+功能分类)======================
|
||||
// 工具类实例
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
private BitmapCacheUtils mBitmapCache;
|
||||
// 视图组件
|
||||
private Toolbar mToolbar;
|
||||
private BackgroundView mBackgroundView;
|
||||
// 状态标记(volatile保证多线程可见性)
|
||||
private volatile boolean isCommitSettings = false;
|
||||
private volatile boolean isPreviewBackgroundChanged = false;
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列)======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "【生命周期】onCreate 开始初始化");
|
||||
setContentView(R.layout.activity_background_settings);
|
||||
|
||||
// 初始化核心组件
|
||||
initCoreComponents();
|
||||
// 初始化界面与事件
|
||||
initToolbar();
|
||||
initClickListeners();
|
||||
LogUtils.d(TAG, "【初始化】界面与事件绑定完成");
|
||||
|
||||
// 处理分享意图或初始化预览
|
||||
handleIntentOrPreview();
|
||||
|
||||
// 初始化预览环境并刷新
|
||||
initPreviewEnvironment();
|
||||
LogUtils.d(TAG, "【生命周期】onCreate 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "【生命周期】onPostCreate 执行双重刷新预览");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode);
|
||||
|
||||
try {
|
||||
if (resultCode != RESULT_OK) {
|
||||
LogUtils.d(TAG, "【回调处理】结果非RESULT_OK,执行取消逻辑");
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
handleActivityResult(requestCode, data);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【回调异常】requestCode:" + requestCode + ",异常信息:" + e.getMessage());
|
||||
ToastUtils.show("操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
LogUtils.d(TAG, "【生命周期】finish 触发,isCommitSettings:" + isCommitSettings + ",isPreviewBackgroundChanged:" + isPreviewBackgroundChanged);
|
||||
if (isCommitSettings) {
|
||||
super.finish();
|
||||
} else {
|
||||
handleFinishConfirmation();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 权限回调方法(单独分类)======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "【权限回调】requestCode:" + requestCode + ",权限数量:" + permissions.length);
|
||||
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
handleCameraPermissionResult(grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 界面初始化方法(Toolbar + 点击事件)======================
|
||||
private void initToolbar() {
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "【初始化异常】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, "【界面初始化】Toolbar 配置完成");
|
||||
}
|
||||
|
||||
private void initClickListeners() {
|
||||
LogUtils.d(TAG, "【界面初始化】开始绑定按钮点击事件");
|
||||
// 绑定所有按钮点击事件
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton3, onTakePhotoClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton4, onSelectPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton5, onNetworkBackgroundDialog);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton6, onCropPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton7, onCropFreePictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
|
||||
LogUtils.d(TAG, "【界面初始化】按钮点击事件绑定完成");
|
||||
}
|
||||
|
||||
// 通用按钮绑定工具方法
|
||||
private void bindClickListener(int resId, View.OnClickListener listener) {
|
||||
View view = findViewById(resId);
|
||||
if (view != null) {
|
||||
view.setOnClickListener(listener);
|
||||
} else {
|
||||
LogUtils.e(TAG, "【绑定异常】未找到视图:" + resId);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 按钮点击事件(按功能分类)======================
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】取消背景图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【操作异常】预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】选择图片");
|
||||
launchImageSelector();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener(){
|
||||
@Override
|
||||
public void onConfirm(String szConfirmFilePath) {
|
||||
// 拷贝文件到预览数据并启动裁剪
|
||||
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
}
|
||||
});
|
||||
networkBackgroundDialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】固定比例裁剪");
|
||||
startImageCrop(false);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】自由裁剪");
|
||||
startImageCrop(true);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】拍照");
|
||||
// 动态申请相机权限
|
||||
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "【拍照准备】相机权限未授予,发起申请");
|
||||
ActivityCompat.requestPermissions(
|
||||
BackgroundSettingsActivity.this,
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
REQUEST_CAMERA_PERMISSION);
|
||||
return;
|
||||
}
|
||||
handleTakePhoto();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】恢复收到的图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【操作异常】预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】像素拾取");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【操作异常】预览Bean为空");
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
return;
|
||||
}
|
||||
String targetImagePath = previewBean.getBackgroundFilePath();
|
||||
File targetFile = new File(targetImagePath);
|
||||
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
LogUtils.e(TAG, "【像素拾取失败】文件无效:" + targetImagePath);
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", targetImagePath);
|
||||
startActivityForResult(intent, REQUEST_PIXELPICKER);
|
||||
LogUtils.d(TAG, "【像素拾取启动】路径:" + targetImagePath);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【操作异常】预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int oldColor = previewBean.getPixelColor();
|
||||
previewBean.setPixelColor(0xFF000000);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
ToastUtils.show("像素颜色已清空");
|
||||
LogUtils.d(TAG, "【像素清空】旧颜色:" + String.format("#%08X", oldColor));
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】调色板按钮");
|
||||
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【操作异常】预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int initialColor = previewBean.getPixelColor();
|
||||
LogUtils.d(TAG, "【调色板】初始颜色:" + String.format("#%08X", initialColor));
|
||||
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
previewBean.setPixelColor(color);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
LogUtils.d(TAG, "【颜色选择】选中颜色:" + String.format("#%08X", color));
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "【调色板】对话框已显示");
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 工具方法(通用工具 + 视图工具)======================
|
||||
/**
|
||||
* 生成 FileProvider Uri,适配 Android 7.0+
|
||||
* @param file 目标文件
|
||||
* @return 适配后的Uri,失败返回null
|
||||
*/
|
||||
public Uri getFileProviderUri(File file) {
|
||||
LogUtils.d(TAG, "【工具方法】生成FileProvider Uri,文件路径:" + (file != null ? file.getAbsolutePath() : "null"));
|
||||
if (file == null) {
|
||||
LogUtils.e(TAG, "【工具异常】文件为空");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
|
||||
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【工具异常】生成Uri失败:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Bitmap 是否有效(未被回收且不为空)
|
||||
* @param bitmap 目标Bitmap
|
||||
* @return 有效返回true,否则false
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
boolean isValid = bitmap != null && !bitmap.isRecycled();
|
||||
LogUtils.d(TAG, "【工具方法】Bitmap有效性校验:" + isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
LogUtils.d(TAG, "【工具方法】开始双重刷新预览");
|
||||
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
|
||||
LogUtils.w(TAG, "【双重刷新】跳过:对象为空或Activity已结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
mBackgroundView.setBackgroundColor(previewBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二重刷新(延迟执行)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
mBackgroundView.setBackgroundColor(previewBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(按功能分类)======================
|
||||
/**
|
||||
* 初始化核心组件(工具类+视图)
|
||||
*/
|
||||
private void initCoreComponents() {
|
||||
// 初始化视图
|
||||
mBackgroundView = findViewById(R.id.background_view);
|
||||
if (mBackgroundView == null) {
|
||||
LogUtils.e(TAG, "【初始化异常】BackgroundView未找到");
|
||||
}
|
||||
// 初始化工具类
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBitmapCache = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "【初始化】视图与工具类加载完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理意图或初始化预览
|
||||
*/
|
||||
private void handleIntentOrPreview() {
|
||||
if (handleShareIntent()) {
|
||||
ToastUtils.show("已接收分享图片");
|
||||
} else {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
LogUtils.d(TAG, "【预览初始化】加载当前背景配置");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化预览环境
|
||||
*/
|
||||
private void initPreviewEnvironment() {
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分享意图
|
||||
* @return 处理成功返回true,否则false
|
||||
*/
|
||||
private boolean handleShareIntent() {
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
LogUtils.d(TAG, "【分享处理】action:" + action + ",type:" + type);
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
showSharePreviewDialog();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示分享图片预览对话框
|
||||
*/
|
||||
private void showSharePreviewDialog() {
|
||||
LogUtils.d(TAG, "showSharePreviewDialog()");
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() {
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
|
||||
if (putUriFileToPreviewSource(uriRecivedPicture)) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
dlg.show();
|
||||
LogUtils.d(TAG, "【分享处理】显示图片预览对话框");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片类型
|
||||
* @param mimeType MIME类型
|
||||
* @return 是图片返回true,否则false
|
||||
*/
|
||||
private boolean isImageType(String mimeType) {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerMimeType = mimeType.toLowerCase();
|
||||
LogUtils.d("isImageType", "mimeType: " + mimeType + ", lowerMimeType: " + lowerMimeType);
|
||||
return lowerMimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
*/
|
||||
private void launchImageSelector() {
|
||||
LogUtils.d(TAG, "【业务逻辑】启动图片选择器");
|
||||
Intent[] intents = createImageSelectorIntents();
|
||||
Intent validIntent = findValidIntent(intents);
|
||||
|
||||
if (validIntent != null) {
|
||||
launchImageChooser(validIntent);
|
||||
} else {
|
||||
showNoGalleryDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片选择器意图数组
|
||||
* @return 意图数组
|
||||
*/
|
||||
private Intent[] createImageSelectorIntents() {
|
||||
Intent[] intents = new Intent[3];
|
||||
// ACTION_GET_CONTENT
|
||||
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
getContentIntent.setType("image/*");
|
||||
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[0] = getContentIntent;
|
||||
|
||||
// ACTION_PICK
|
||||
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
pickIntent.setType("image/*");
|
||||
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[1] = pickIntent;
|
||||
|
||||
// ACTION_OPEN_DOCUMENT(API19+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
openDocIntent.setType("image/*");
|
||||
openDocIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
intents[2] = openDocIntent;
|
||||
}
|
||||
return intents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找有效的意图
|
||||
* @param intents 意图数组
|
||||
* @return 有效意图,无则返回null
|
||||
*/
|
||||
private Intent findValidIntent(Intent[] intents) {
|
||||
for (Intent intent : intents) {
|
||||
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
* @param validIntent 有效意图
|
||||
*/
|
||||
private void launchImageChooser(Intent validIntent) {
|
||||
Intent chooser = Intent.createChooser(validIntent, "选择图片");
|
||||
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
|
||||
LogUtils.d(TAG, "【选图意图】启动图片选择");
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示无相册应用提示对话框
|
||||
*/
|
||||
private void showNoGalleryDialog() {
|
||||
LogUtils.d(TAG, "【选图意图】无相册应用");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("未找到相册应用,请安装后重试");
|
||||
new AlertDialog.Builder(BackgroundSettingsActivity.this)
|
||||
.setTitle("无图片选择应用")
|
||||
.setMessage("需要安装相册应用才能选择图片")
|
||||
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
launchGalleryMarket();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用商店下载相册
|
||||
*/
|
||||
private void launchGalleryMarket() {
|
||||
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
|
||||
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
|
||||
if (marketIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(marketIntent);
|
||||
} else {
|
||||
ToastUtils.show("无法打开应用商店");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理操作取消或失败
|
||||
*/
|
||||
private void handleOperationCancelOrFail() {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
LogUtils.d(TAG, "【业务逻辑】操作取消或失败,恢复预览");
|
||||
ToastUtils.show("操作取消或失败");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照逻辑(权限通过后执行)
|
||||
*/
|
||||
void handleTakePhoto() {
|
||||
LogUtils.d(TAG, "【业务逻辑】开始处理拍照");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【拍照失败】预览Bean为空");
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
File takePhotoFile = new File(previewBean.getBackgroundFilePath());
|
||||
if (!takePhotoFile.exists()) {
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
LogUtils.e(TAG, "【拍照失败】文件不存在:" + takePhotoFile.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
try {
|
||||
Uri photoUri = getFileProviderUri(takePhotoFile);
|
||||
if (photoUri == null) {
|
||||
throw new Exception("生成FileProvider Uri失败");
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
LogUtils.d(TAG, "【拍照启动】Uri:" + photoUri.toString());
|
||||
} catch (Exception e) {
|
||||
String errMsg = "拍照启动异常:" + e.getMessage();
|
||||
ToastUtils.show(errMsg.substring(0, 20));
|
||||
LogUtils.e(TAG, "【拍照失败】" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理ActivityResult分发
|
||||
* @param requestCode 请求码
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleActivityResult(int requestCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_SELECT_PICTURE:
|
||||
handleSelectPictureResult(data);
|
||||
break;
|
||||
case REQUEST_TAKE_PHOTO:
|
||||
handleTakePhotoResult(data);
|
||||
break;
|
||||
case REQUEST_CROP_IMAGE:
|
||||
handleCropImageResult(data);
|
||||
break;
|
||||
case REQUEST_PIXELPICKER:
|
||||
handlePixelPickerResult();
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, "【回调忽略】未知requestCode:" + requestCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleTakePhotoResult(Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】处理拍照结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【拍照结果处理】预览Bean为空");
|
||||
return;
|
||||
}
|
||||
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
|
||||
startImageCrop(false);
|
||||
LogUtils.d(TAG, "【拍照完成】已启动裁剪");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选图结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleSelectPictureResult(Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】处理选图结果");
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
LogUtils.e(TAG, "【选图结果】Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString());
|
||||
|
||||
// 申请持久化权限(API33+)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
|
||||
}
|
||||
|
||||
// 同步文件并启动裁剪
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "【选图同步】路径绑定完成");
|
||||
startImageCrop(false);
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "【选图同步】文件复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Uri 文件同步到预览 Bean
|
||||
* @param srcUriFile 源Uri
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(Uri srcUriFile) {
|
||||
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
LogUtils.e(TAG, "【选图同步】Uri解析路径为空");
|
||||
return false;
|
||||
}
|
||||
File srcFile = new File(filePath);
|
||||
return putUriFileToPreviewSource(srcFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 File 同步到预览 Bean
|
||||
* @param srcFile 源文件
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(File srcFile) {
|
||||
LogUtils.d(TAG, "【选图同步】源文件:" + srcFile.getAbsolutePath());
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
File dstFile = new File(previewBean.getBackgroundFilePath());
|
||||
LogUtils.d(TAG, "【选图同步】目标文件:" + dstFile.getAbsolutePath());
|
||||
if (FileUtils.copyFile(srcFile, dstFile)) {
|
||||
LogUtils.d(TAG, "【选图同步】文件拷贝成功");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "【选图同步】文件无法拷贝");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleCropImageResult(Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】处理裁剪结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【裁剪结果处理】预览Bean为空");
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
File cropTempFile = new File(previewBean.getBackgroundScaledCompressFilePath());
|
||||
boolean isFileExist = cropTempFile.exists();
|
||||
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
|
||||
long fileSize = isFileExist ? cropTempFile.length() : 0;
|
||||
boolean isCropSuccess = isFileExist && isFileReadable && fileSize > 100;
|
||||
|
||||
if (isCropSuccess) {
|
||||
handleCropSuccess(previewBean, fileSize);
|
||||
} else {
|
||||
handleCropFailure(isFileExist, isFileReadable, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪成功
|
||||
* @param previewBean 预览Bean
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
|
||||
isPreviewBackgroundChanged = true;
|
||||
LogUtils.d(TAG, "【裁剪结果】裁剪成功,文件大小:" + fileSize);
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪失败
|
||||
* @param isFileExist 文件是否存在
|
||||
* @param isFileReadable 文件是否可读
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
|
||||
handleOperationCancelOrFail();
|
||||
LogUtils.e(TAG, "【裁剪结果】裁剪失败,文件状态:存在=" + isFileExist + ",可读=" + isFileReadable + ",大小=" + fileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理像素拾取结果
|
||||
*/
|
||||
private void handlePixelPickerResult() {
|
||||
LogUtils.d(TAG, "【业务逻辑】处理像素拾取结果");
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相机权限申请结果
|
||||
* @param grantResults 权限结果数组
|
||||
*/
|
||||
private void handleCameraPermissionResult(int[] grantResults) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "【权限申请】相机权限授予成功");
|
||||
handleTakePhoto();
|
||||
} else {
|
||||
LogUtils.d(TAG, "【权限申请】相机权限授予失败");
|
||||
ToastUtils.show("相机权限被拒绝,无法拍照");
|
||||
// 引导用户到设置页面开启权限(用户选择不再询问时)
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
|
||||
launchAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用设置页面
|
||||
*/
|
||||
private void launchAppSettings() {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
ToastUtils.show("请在设置中开启相机权限");
|
||||
LogUtils.d(TAG, "【权限引导】启动应用设置页面");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Finish确认对话框
|
||||
*/
|
||||
private void handleFinishConfirmation() {
|
||||
if (isPreviewBackgroundChanged) {
|
||||
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
mBgSourceUtils.commitPreviewSourceToCurrent();
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片裁剪
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
*/
|
||||
private void startImageCrop(boolean isFreeCrop) {
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【裁剪启动】预览Bean为空");
|
||||
ToastUtils.show("裁剪失败:无有效图片");
|
||||
return;
|
||||
}
|
||||
int width = isFreeCrop ? 0 : mBackgroundView.getWidth();
|
||||
int height = isFreeCrop ? 0 : mBackgroundView.getHeight();
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
previewBean,
|
||||
width,
|
||||
height,
|
||||
isFreeCrop,
|
||||
REQUEST_CROP_IMAGE);
|
||||
LogUtils.d(TAG, "【裁剪启动】是否自由裁剪:" + isFreeCrop + ",目标尺寸:" + width + "x" + height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/22 13:21
|
||||
* @Describe BatteryReportActivity
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -23,8 +18,11 @@ import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -32,88 +30,88 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class BatteryReportActivity extends Activity {
|
||||
/**
|
||||
* 电池报告页面,统计应用24小时运行时长与电池消耗情况
|
||||
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/22 13:21
|
||||
*/
|
||||
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BatteryReportActivity";
|
||||
private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数
|
||||
private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private RecyclerView rvBatteryReport;
|
||||
private EditText etSearch;
|
||||
// 数据与适配器
|
||||
private BatteryReportAdapter adapter;
|
||||
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
|
||||
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
|
||||
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
|
||||
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
|
||||
// 电池相关
|
||||
private BroadcastReceiver batteryReceiver;
|
||||
private int batteryCapacity = 5400; // 电池容量(mAh)
|
||||
private float lastBatteryPercent = 100.0f;
|
||||
private long lastCheckTime = System.currentTimeMillis();
|
||||
private EditText etSearch;
|
||||
// 缓存相关
|
||||
private Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
|
||||
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_battery_report);
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化PackageManager
|
||||
mPackageManager = getPackageManager();
|
||||
|
||||
// 权限检查(Java7 传统条件判断)
|
||||
if (!hasUsageStatsPermission(this)) {
|
||||
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
|
||||
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
|
||||
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启");
|
||||
return;
|
||||
}
|
||||
|
||||
etSearch = (EditText) findViewById(R.id.et_search);
|
||||
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
|
||||
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
// 初始化流程:新增“加载24小时累计耗电”步骤
|
||||
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
|
||||
loadAllAppPackage();
|
||||
preCacheAllAppNames();
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电
|
||||
calculateInitial24hTotalConsumption();
|
||||
filteredList.addAll(dataList);
|
||||
|
||||
// 初始化适配器
|
||||
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
|
||||
rvBatteryReport.setAdapter(adapter);
|
||||
LogUtils.d(TAG, "【onCreate】适配器初始化完成,数据量:" + filteredList.size());
|
||||
|
||||
// 搜索监听(不变)
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
// 绑定搜索监听
|
||||
bindSearchListener();
|
||||
// 注册电池广播
|
||||
registerBatteryReceiver();
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterAppsByPackageAndName(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
|
||||
// 电池广播:调用修改后的“单次耗电计算+累计累加”方法
|
||||
batteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int level = intent.getIntExtra("level", 100);
|
||||
int scale = intent.getIntExtra("scale", 100);
|
||||
float currentPercent = (float) level / scale * 100;
|
||||
LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
|
||||
|
||||
if (currentPercent < lastBatteryPercent) {
|
||||
float dropPercent = lastBatteryPercent - currentPercent;
|
||||
long duration = System.currentTimeMillis() - lastCheckTime;
|
||||
LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms");
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); // 单次+累计逻辑
|
||||
}
|
||||
|
||||
lastBatteryPercent = currentPercent;
|
||||
lastCheckTime = System.currentTimeMillis();
|
||||
}
|
||||
};
|
||||
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,33 +120,133 @@ public class BatteryReportActivity extends Activity {
|
||||
// Java7 显式非空判断
|
||||
if (batteryReceiver != null) {
|
||||
unregisterReceiver(batteryReceiver);
|
||||
LogUtils.d(TAG, "【onDestroy】电池广播已注销");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化RecyclerView与搜索框
|
||||
etSearch = (EditText) findViewById(R.id.et_search);
|
||||
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
|
||||
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 搜索监听绑定方法 =========================
|
||||
private void bindSearchListener() {
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + s.toString());
|
||||
filterAppsByPackageAndName(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 电池广播注册方法 =========================
|
||||
private void registerBatteryReceiver() {
|
||||
batteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int level = intent.getIntExtra("level", 100);
|
||||
int scale = intent.getIntExtra("scale", 100);
|
||||
float currentPercent = (float) level / scale * 100;
|
||||
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
|
||||
|
||||
if (currentPercent < lastBatteryPercent) {
|
||||
float dropPercent = lastBatteryPercent - currentPercent;
|
||||
long duration = System.currentTimeMillis() - lastCheckTime;
|
||||
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
|
||||
}
|
||||
|
||||
lastBatteryPercent = currentPercent;
|
||||
lastCheckTime = System.currentTimeMillis();
|
||||
}
|
||||
};
|
||||
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
|
||||
}
|
||||
|
||||
// ======================== 权限检查方法 =========================
|
||||
/**
|
||||
* 加载所有应用(仅获取包名,初始化模型时单次耗电、累计耗电均设为0)
|
||||
* 检查是否拥有使用情况访问权限
|
||||
* @param context 上下文
|
||||
* @return 拥有权限返回true,否则返回false
|
||||
*/
|
||||
private boolean hasUsageStatsPermission(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP,不支持使用情况访问权限");
|
||||
return false;
|
||||
}
|
||||
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
if (manager == null) {
|
||||
LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_MINUTE_MS;
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
boolean hasPermission = statsList != null && !statsList.isEmpty();
|
||||
LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果:" + hasPermission);
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
// ======================== 数据加载与缓存方法 =========================
|
||||
/**
|
||||
* 加载所有应用包名,初始化数据模型
|
||||
*/
|
||||
private void loadAllAppPackage() {
|
||||
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
dataList.clear();
|
||||
|
||||
LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
|
||||
for (ApplicationInfo appInfo : appList) {
|
||||
String packageName = appInfo.packageName;
|
||||
// 初始化:单次耗电(consumption)=0,累计耗电(totalConsumption)=0,运行时长=0
|
||||
// 初始化:单次耗电=0,累计耗电=0,运行时长=0
|
||||
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。");
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
|
||||
}
|
||||
|
||||
/**
|
||||
* 预缓存应用名称(逻辑不变)
|
||||
* 预缓存所有应用名称,减少PackageManager重复调用
|
||||
*/
|
||||
private void preCacheAllAppNames() {
|
||||
packageToAppNameCache.clear();
|
||||
LogUtils.d(TAG, "开始预缓存包名-应用名称映射");
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
@@ -156,48 +254,78 @@ public class BatteryReportActivity extends Activity {
|
||||
packageToAppNameCache.put(packageName, appName);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过包名获取应用名称(逻辑不变)
|
||||
* 通过包名获取应用名称,带异常处理
|
||||
* @param packageName 应用包名
|
||||
* @return 应用名称,获取失败返回包名
|
||||
*/
|
||||
private String getAppNameByPackage(String packageName) {
|
||||
try {
|
||||
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
|
||||
return mPackageManager.getApplicationLabel(appInfo).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
return packageName;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运行时长到模型(逻辑不变)
|
||||
* 更新运行时长到数据模型
|
||||
*/
|
||||
private void updateAppRunTimeToModel() {
|
||||
int nCount = 0;
|
||||
int updateCount = 0;
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long runTime;
|
||||
if (appRunTimeCache.containsKey(packageName)) {
|
||||
runTime = appRunTimeCache.get(packageName);
|
||||
LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName));
|
||||
nCount++;
|
||||
} else {
|
||||
runTime = 0L;
|
||||
}
|
||||
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
|
||||
model.setRunTime(runTime);
|
||||
if (runTime > 0) {
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, String.format("dataList.size() %d, appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size()));
|
||||
LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount));
|
||||
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增】初始化时计算24小时累计耗电(赋值给totalConsumption)
|
||||
* 获取应用24小时运行时长
|
||||
* @return 应用包名-运行时长(ms)映射
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
Map<String, Long> runTimeMap = new HashMap<String, Long>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_DAY_MS; // 近24小时
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
for (android.app.usage.UsageStats stats : statsList) {
|
||||
long runTimeMs = stats.getTotalTimeInForeground();
|
||||
String packageName = stats.getPackageName();
|
||||
runTimeMap.put(packageName, runTimeMs);
|
||||
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
|
||||
if (packageName.equals("aidepro.top")) {
|
||||
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量:" + runTimeMap.size());
|
||||
return runTimeMap;
|
||||
}
|
||||
|
||||
// ======================== 核心计算方法 =========================
|
||||
/**
|
||||
* 初始化时计算24小时累计耗电(赋值给totalConsumption)
|
||||
* 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗
|
||||
*/
|
||||
private void calculateInitial24hTotalConsumption() {
|
||||
@@ -206,23 +334,26 @@ public class BatteryReportActivity extends Activity {
|
||||
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
|
||||
total24hRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
|
||||
|
||||
// 2. 按运行时长占比分配24小时累计耗电(假设电池满电循环,用总容量近似24小时总消耗)
|
||||
// 2. 按运行时长占比分配24小时累计耗电
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
|
||||
|
||||
// 计算占比与累计耗电
|
||||
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
|
||||
float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗
|
||||
model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电
|
||||
LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化:%.1f mAh", packageName, initialTotalConsumption));
|
||||
float initialTotalConsumption = batteryCapacity * ratio;
|
||||
model.setTotalConsumption(initialTotalConsumption);
|
||||
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh");
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心修改】计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption)
|
||||
* 计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption)
|
||||
* @param dropPercent 电池下降百分比
|
||||
* @param runTimeMap 应用运行时长映射
|
||||
*/
|
||||
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
|
||||
long totalSingleRunTime = 0;
|
||||
@@ -230,25 +361,26 @@ public class BatteryReportActivity extends Activity {
|
||||
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
|
||||
totalSingleRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长:" + formatRunTime(totalSingleRunTime));
|
||||
|
||||
// 2. 遍历计算每个应用的“单次耗电”并“累加至累计”
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
|
||||
|
||||
// 步骤1:计算本次单次耗电(赋值给consumption)
|
||||
// 步骤1:计算本次单次耗电
|
||||
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗
|
||||
model.setConsumption(singleConsumption); // 存储单次耗电
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
|
||||
model.setConsumption(singleConsumption);
|
||||
|
||||
// 步骤2:累加单次耗电到累计耗电(totalConsumption = 原有累计 + 本次单次)
|
||||
// 步骤2:累加单次耗电到累计耗电
|
||||
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
|
||||
model.setTotalConsumption(newTotalConsumption); // 更新累计耗电
|
||||
model.setTotalConsumption(newTotalConsumption);
|
||||
|
||||
// 同步运行时长
|
||||
model.setRunTime(appSingleRunTime);
|
||||
|
||||
LogUtils.d(TAG, String.format("应用包 %s:单次耗电%.1f mAh,累计耗电%.1f mAh",
|
||||
LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s:单次耗电%.1f mAh,累计耗电%.1f mAh",
|
||||
packageName, singleConsumption, newTotalConsumption));
|
||||
}
|
||||
|
||||
@@ -262,69 +394,43 @@ public class BatteryReportActivity extends Activity {
|
||||
|
||||
// 4. 重新应用过滤并刷新列表
|
||||
filterAppsByPackageAndName(etSearch.getText().toString());
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成,列表已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 双维度过滤(逻辑不变)
|
||||
* 双维度过滤(包名+应用名)
|
||||
* @param keyword 搜索关键词
|
||||
*/
|
||||
private void filterAppsByPackageAndName(String keyword) {
|
||||
filteredList.clear();
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空,显示全部应用,数量:" + filteredList.size());
|
||||
} else {
|
||||
String lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String packageNameLower = packageName.toLowerCase();
|
||||
String appName = packageToAppNameCache.get(packageName);
|
||||
String appNameLower = appName.toLowerCase();
|
||||
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword)
|
||||
|| appNameLower.contains(lowerKeyword);
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword)
|
||||
|| appNameLower.contains(lowerKeyword);
|
||||
|
||||
if (isMatched) {
|
||||
filteredList.add(model);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size());
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 获取应用运行时长(逻辑不变,返回24小时运行时长)
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
Map<String, Long> runTimeMap = new HashMap<String, Long>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - 24 * 3600 * 1000; // 近24小时
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
for (android.app.usage.UsageStats stats : statsList) {
|
||||
long runTimeMs = stats.getTotalTimeInForeground();
|
||||
String packageName = stats.getPackageName();
|
||||
LogUtils.d(TAG, "包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
|
||||
runTimeMap.put(packageName, runTimeMs);
|
||||
if (packageName.equals("aidepro.top")) {
|
||||
LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size()));
|
||||
return runTimeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化运行时长(逻辑不变)
|
||||
* 格式化运行时长
|
||||
* @param runTimeMs 运行时长(ms)
|
||||
* @return 格式化后的运行时长字符串
|
||||
*/
|
||||
private String formatRunTime(long runTimeMs) {
|
||||
if (runTimeMs <= 0) {
|
||||
@@ -344,66 +450,47 @@ public class BatteryReportActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:数据模型 =========================
|
||||
/**
|
||||
* 权限检查(逻辑不变)
|
||||
*/
|
||||
private boolean hasUsageStatsPermission(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
if (manager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - 1000 * 60;
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
return statsList != null && !statsList.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心修改】数据模型:明确字段含义
|
||||
* - consumption:单次耗电(两次电池广播间的消耗,float类型便于计算)
|
||||
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加,显示用)
|
||||
* 应用电池数据模型
|
||||
* - consumption:单次耗电(两次电池广播间的消耗)
|
||||
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加)
|
||||
* - runTime:运行时长(ms)
|
||||
* - packageName:应用包名
|
||||
*/
|
||||
public static class AppBatteryModel {
|
||||
private String packageName; // 应用包名(核心标识)
|
||||
private float consumption; // 单次耗电(mAh,float类型)
|
||||
private float totalConsumption;// 累计耗电(mAh,显示+排序用)
|
||||
private float consumption; // 单次耗电(mAh)
|
||||
private float totalConsumption;// 累计耗电(mAh)
|
||||
private long runTime; // 运行时长(ms)
|
||||
|
||||
// Java7 显式构造:初始化单次耗电、累计耗电为0
|
||||
// Java7 显式构造
|
||||
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
|
||||
this.packageName = packageName;
|
||||
this.consumption = consumption; // 单次耗电初始为0
|
||||
this.totalConsumption = totalConsumption; // 累计耗电初始为0(后续初始化时赋值)
|
||||
this.consumption = consumption;
|
||||
this.totalConsumption = totalConsumption;
|
||||
this.runTime = runTime;
|
||||
}
|
||||
|
||||
// Getter/Setter:覆盖所有字段,确保数据操作正常
|
||||
// Getter/Setter
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public float getConsumption() {
|
||||
return consumption; // 获取单次耗电
|
||||
return consumption;
|
||||
}
|
||||
|
||||
public void setConsumption(float consumption) {
|
||||
this.consumption = consumption; // 设置单次耗电
|
||||
this.consumption = consumption;
|
||||
}
|
||||
|
||||
public float getTotalConsumption() {
|
||||
return totalConsumption; // 获取累计耗电(显示用)
|
||||
return totalConsumption;
|
||||
}
|
||||
|
||||
public void setTotalConsumption(float totalConsumption) {
|
||||
this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用)
|
||||
this.totalConsumption = totalConsumption;
|
||||
}
|
||||
|
||||
public long getRunTime() {
|
||||
@@ -415,8 +502,9 @@ public class BatteryReportActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:RecyclerView适配器 =========================
|
||||
/**
|
||||
* RecyclerView 适配器:仅显示累计耗电(totalConsumption),逻辑适配模型修改
|
||||
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
|
||||
*/
|
||||
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
|
||||
private Context mContext;
|
||||
@@ -424,8 +512,8 @@ public class BatteryReportActivity extends Activity {
|
||||
private PackageManager mPm;
|
||||
private Map<String, String> mPackageToNameCache;
|
||||
|
||||
// Java7 显式构造:接收名称缓存,确保显示时高效获取应用名
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
// Java7 显式构造
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
PackageManager pm, Map<String, String> packageToNameCache) {
|
||||
this.mContext = context;
|
||||
this.mDataList = dataList;
|
||||
@@ -435,18 +523,18 @@ public class BatteryReportActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
// 加载系统列表项布局(text1显示应用名,text2显示累计耗电+时长)
|
||||
View itemView = LayoutInflater.from(mContext)
|
||||
.inflate(android.R.layout.simple_list_item_2, parent, false);
|
||||
.inflate(android.R.layout.simple_list_item_2, parent, false);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
// Java7 显式非空判断:避免空指针异常
|
||||
// Java7 显式非空判断
|
||||
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
|
||||
holder.tvAppName.setText("未知应用");
|
||||
holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒");
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -454,11 +542,11 @@ public class BatteryReportActivity extends Activity {
|
||||
String packageName = model.getPackageName();
|
||||
String appName = "";
|
||||
|
||||
// 优先从缓存获取应用名:减少PackageManager调用,提升性能
|
||||
// 优先从缓存获取应用名
|
||||
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
|
||||
appName = mPackageToNameCache.get(packageName);
|
||||
} else {
|
||||
// 缓存无数据时兜底查询,并同步更新缓存
|
||||
// 缓存无数据时兜底查询
|
||||
try {
|
||||
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
|
||||
appName = mPm.getApplicationLabel(appInfo).toString();
|
||||
@@ -466,45 +554,40 @@ public class BatteryReportActivity extends Activity {
|
||||
mPackageToNameCache.put(packageName, appName);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
appName = packageName; // 包名不存在时用包名兜底
|
||||
LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
appName = packageName; // 其他异常时用包名兜底
|
||||
LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 显示逻辑:仅展示累计耗电(totalConsumption),隐藏单次耗电
|
||||
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
|
||||
holder.tvAppName.setText(appName);
|
||||
// 格式化运行时长 + 累计耗电(保留1位小数,提升可读性)
|
||||
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
|
||||
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
|
||||
model.getTotalConsumption(), runTimeStr);
|
||||
holder.tvConsumption.setText(totalConsumptionText);
|
||||
|
||||
// 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改)
|
||||
// 显示优化
|
||||
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
|
||||
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
|
||||
|
||||
// 调整文字大小:适配手机屏幕,提升可读性
|
||||
holder.tvAppName.setTextSize(16);
|
||||
holder.tvConsumption.setTextSize(14);
|
||||
}
|
||||
|
||||
// 获取列表长度:Java7 三元运算符判断空值,避免空指针
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mDataList == null ? 0 : mDataList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder:绑定系统布局控件,与显示逻辑对应
|
||||
* ViewHolder:绑定系统布局控件
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAppName; // 显示应用名称
|
||||
TextView tvConsumption; // 显示累计耗电 + 运行时长
|
||||
TextView tvAppName; // 应用名称
|
||||
TextView tvConsumption; // 累计耗电 + 运行时长
|
||||
|
||||
// Java7 显式构造:绑定控件ID(系统布局固定ID:text1、text2)
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
|
||||
|
||||
@@ -6,93 +6,158 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ClearRecordActivity extends Activity {
|
||||
|
||||
/**
|
||||
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
*/
|
||||
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "ClearRecordActivity";
|
||||
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
|
||||
|
||||
AToolbar mAToolbar;
|
||||
TextView mtvRecordText;
|
||||
App mApplication;
|
||||
boolean mIsShowRecordWithEnter = false;
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private TextView mtvRecordText;
|
||||
private TextView tvAOHPCTCSeekBarMSG;
|
||||
private AOHPCTCSeekBar aOHPCTCSeekBar;
|
||||
// 应用与配置
|
||||
private App mApplication;
|
||||
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_clearrecord);
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
|
||||
|
||||
// 初始化应用实例
|
||||
mApplication = (App) getApplication();
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化滑动清理控件
|
||||
initSeekBar();
|
||||
// 初始化记录显示文本
|
||||
initRecordText();
|
||||
|
||||
// 初始化工具栏
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
//mAToolbar.setTitle(getTitle() + " - " + getString(R.string.subtitle_activity_clearrecord));
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_clearrecord);
|
||||
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
//mAToolbar.setSubtitleTextAppearance(this, R.style.Toolbar_SubTitleText);
|
||||
//mAToolbar.setBackgroundColor(getColor(R.color.colorPrimary));
|
||||
setActionBar(mAToolbar);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
|
||||
}
|
||||
|
||||
// 设置滑动清理控件
|
||||
//
|
||||
// 初始化发送拉动控件
|
||||
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化Toolbar与显示文本组件
|
||||
*/
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化显示文本组件
|
||||
tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
|
||||
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动清理控件
|
||||
*/
|
||||
private void initSeekBar() {
|
||||
aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
|
||||
aOHPCTCSeekBar.setThumbOffset(0);
|
||||
aOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
mApplication.clearBatteryHistory();
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_SERVICENOTIFICATION));
|
||||
initRecordText();
|
||||
String szMSG = "The APP battery record is cleaned.";
|
||||
LogUtils.d(TAG, szMSG);
|
||||
ToastUtils.show(szMSG);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// 初始化提示框
|
||||
TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
|
||||
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
|
||||
initRecordText();
|
||||
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发");
|
||||
// 清理电池历史记录
|
||||
mApplication.clearBatteryHistory();
|
||||
// 发送广播更新前台通知
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
// 提示清理成功
|
||||
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
|
||||
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成,已发送更新广播");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
|
||||
*/
|
||||
void initRecordText() {
|
||||
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
|
||||
if (mIsShowRecordWithEnter) {
|
||||
String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
} else {
|
||||
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
}
|
||||
String szRecordText;
|
||||
|
||||
// 判空处理:避免空列表导致异常
|
||||
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
|
||||
szRecordText = getString(R.string.msg_no_battery_record);
|
||||
LogUtils.d(TAG, "【initRecordText】无电池记录数据");
|
||||
} else {
|
||||
// 根据配置切换显示格式
|
||||
if (mIsShowRecordWithEnter) {
|
||||
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
LogUtils.d(TAG, "【initRecordText】使用带换行格式显示记录,数量:" + listBatteryInfo.size());
|
||||
} else {
|
||||
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
LogUtils.d(TAG, "【initRecordText】使用无换行格式显示记录,数量:" + listBatteryInfo.size());
|
||||
}
|
||||
}
|
||||
|
||||
mtvRecordText.setText(szRecordText);
|
||||
LogUtils.d(TAG, "【initRecordText】记录显示文本初始化完成");
|
||||
}
|
||||
|
||||
public void onShowRecordWithEnter(View view) {
|
||||
Switch swShowRecordWithEnter = (Switch)view;
|
||||
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
|
||||
initRecordText();
|
||||
}
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 切换记录显示格式(带换行/不带换行)
|
||||
* @param view 触发事件的Switch控件
|
||||
*/
|
||||
public void onShowRecordWithEnter(View view) {
|
||||
Switch swShowRecordWithEnter = (Switch) view;
|
||||
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
|
||||
LogUtils.d(TAG, "【onShowRecordWithEnter】记录显示格式切换,带换行:" + mIsShowRecordWithEnter);
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/22 14:15
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
@@ -22,160 +18,191 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
|
||||
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 像素拾取页面,支持加载图片并拾取指定位置像素颜色,同步至背景配置
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/22 14:15
|
||||
*/
|
||||
public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "PixelPickerActivity";
|
||||
public static final String EXTRA_IMAGE_PATH = "imagePath"; // 图片路径传递键
|
||||
// 提示文本常量
|
||||
private static final String MSG_IMAGE_LOADED = "图片已加载,点击获取像素值";
|
||||
private static final String MSG_NO_IMAGE_PATH = "未找到图片路径";
|
||||
private static final String MSG_IMAGE_LOAD_FAILED = "图片加载失败";
|
||||
private static final String MSG_FILE_NOT_EXIST = "图片文件不存在";
|
||||
private static final String MSG_FILE_NOT_FOUND = "图片文件未找到";
|
||||
private static final String MSG_PIXEL_OUT_OF_RANGE = "像素坐标超出范围";
|
||||
private static final String MSG_TOUCH_OUT_OF_IMAGE = "点击位置超出图片显示范围";
|
||||
private static final String MSG_PIXEL_CALC_FAILED = "计算像素位置失败";
|
||||
private static final String MSG_PIXEL_RECORDED = "已记录像素值";
|
||||
|
||||
public static final String TAG = "PixelPickerActivity";
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private AToolbar mAToolbar;
|
||||
private ImageView imageView;
|
||||
private TextView infoText;
|
||||
private ViewGroup imageContainer;
|
||||
private RelativeLayout mainLayout;
|
||||
// 图片与像素数据
|
||||
private Bitmap originalBitmap; // 原始图片Bitmap(用于像素拾取)
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private AToolbar mAToolbar;
|
||||
private ImageView imageView;
|
||||
private Bitmap originalBitmap;
|
||||
private TextView infoText;
|
||||
private ViewGroup imageContainer;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pixelpicker);
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pixelpicker);
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 加载传递的图片
|
||||
loadImageFromIntent();
|
||||
// 绑定图片触摸事件
|
||||
bindImageTouchListener();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "【onResume】PixelPickerActivity 恢复显示");
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 回收Bitmap资源,避免内存泄漏
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
LogUtils.d(TAG, "【onDestroy】原始图片Bitmap资源已回收");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】PixelPickerActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化所有UI组件
|
||||
*/
|
||||
private void initView() {
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
imageView = findViewById(R.id.imageView);
|
||||
infoText = findViewById(R.id.infoText);
|
||||
imageContainer = findViewById(R.id.imageContainer);
|
||||
mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化工具栏,设置导航与标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_pixelpicker);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
imageView = findViewById(R.id.imageView);
|
||||
infoText = findViewById(R.id.infoText);
|
||||
imageContainer = findViewById(R.id.imageContainer);
|
||||
|
||||
// 从Intent获取图片路径并加载
|
||||
String imagePath = getIntent().getStringExtra("imagePath");
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText("未找到图片路径");
|
||||
}
|
||||
|
||||
// 设置图片点击事件
|
||||
imageContainer.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) {
|
||||
// 计算点击位置在图片上的实际坐标
|
||||
float touchX = event.getX();
|
||||
float touchY = event.getY();
|
||||
|
||||
int pixelX = -1, pixelY = -1;
|
||||
try {
|
||||
// 获取图片在容器中的实际位置和尺寸
|
||||
int[] imageLocation = new int[2];
|
||||
imageView.getLocationInWindow(imageLocation);
|
||||
int imageWidth = imageView.getWidth();
|
||||
int imageHeight = imageView.getHeight();
|
||||
|
||||
// 计算缩放比例
|
||||
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
|
||||
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
|
||||
|
||||
// 调整触摸坐标到图片坐标系
|
||||
float adjustedX = touchX - imageLocation[0];
|
||||
float adjustedY = touchY - imageLocation[1];
|
||||
|
||||
// 检查是否在图片范围内
|
||||
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
|
||||
// 计算实际像素坐标
|
||||
pixelX = (int) (adjustedX * scaleX);
|
||||
pixelY = (int) (adjustedY * scaleY);
|
||||
|
||||
// 再次检查像素坐标是否在有效范围内
|
||||
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() &&
|
||||
pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
|
||||
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
|
||||
showPixelDialog(pixelColor, pixelX, pixelY);
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, "像素坐标超出范围", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, "点击位置超出图片显示范围", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(PixelPickerActivity.this, "计算像素位置失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图片
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
File file = new File(imagePath);
|
||||
if (file.exists()) {
|
||||
// 解码图片
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = 1; // 加载原图
|
||||
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 从Intent中获取图片路径并加载图片
|
||||
*/
|
||||
private void loadImageFromIntent() {
|
||||
String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径:" + imagePath);
|
||||
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
infoText.setText("图片已加载,点击获取像素值");
|
||||
} else {
|
||||
infoText.setText("图片加载失败");
|
||||
}
|
||||
} else {
|
||||
infoText.setText("图片文件不存在");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
infoText.setText("图片文件未找到");
|
||||
}
|
||||
}
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText(MSG_NO_IMAGE_PATH);
|
||||
LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示像素对话框
|
||||
*/
|
||||
private void showPixelDialog(final int pixelColor, int x, int y) {
|
||||
final Dialog dialog = new Dialog(this);
|
||||
dialog.setContentView(R.layout.dialog_pixel);
|
||||
dialog.setCancelable(true);
|
||||
/**
|
||||
* 加载指定路径的图片
|
||||
* @param imagePath 图片文件路径
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
File file = new File(imagePath);
|
||||
if (file.exists()) {
|
||||
// 解码图片(加载原图)
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = 1;
|
||||
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
|
||||
|
||||
// 设置像素颜色视图背景
|
||||
TextView colorView = dialog.findViewById(R.id.pixelColorView);
|
||||
colorView.setBackgroundColor(pixelColor);
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
infoText.setText(MSG_IMAGE_LOADED);
|
||||
LogUtils.d(TAG, "【loadImage】图片加载成功,尺寸:" + originalBitmap.getWidth() + "x" + originalBitmap.getHeight());
|
||||
} else {
|
||||
infoText.setText(MSG_IMAGE_LOAD_FAILED);
|
||||
LogUtils.e(TAG, "【loadImage】图片解码失败");
|
||||
}
|
||||
} else {
|
||||
infoText.setText(MSG_FILE_NOT_EXIST);
|
||||
LogUtils.w(TAG, "【loadImage】图片文件不存在:" + imagePath);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
infoText.setText(MSG_FILE_NOT_FOUND);
|
||||
LogUtils.e(TAG, "【loadImage】图片文件未找到:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 显示颜色信息
|
||||
TextView infoText = dialog.findViewById(R.id.colorInfoText);
|
||||
String colorInfo = String.format(
|
||||
/**
|
||||
* 显示像素颜色信息对话框
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
* @param x 像素X坐标
|
||||
* @param y 像素Y坐标
|
||||
*/
|
||||
private void showPixelDialog(final int pixelColor, int x, int y) {
|
||||
final Dialog dialog = new Dialog(this);
|
||||
dialog.setContentView(R.layout.dialog_pixel);
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// 设置颜色预览与信息展示
|
||||
TextView colorView = dialog.findViewById(R.id.pixelColorView);
|
||||
TextView infoTextView = dialog.findViewById(R.id.colorInfoText);
|
||||
colorView.setBackgroundColor(pixelColor);
|
||||
|
||||
String colorInfo = String.format(
|
||||
"RGB: (%d, %d, %d)\n" +
|
||||
"ARGB: #%08X\n" +
|
||||
"实际像素位置: (%d, %d)",
|
||||
@@ -184,75 +211,129 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
Color.blue(pixelColor),
|
||||
pixelColor,
|
||||
x, y);
|
||||
infoText.setText(colorInfo);
|
||||
infoTextView.setText(colorInfo);
|
||||
LogUtils.d(TAG, "【showPixelDialog】显示像素信息:" + colorInfo);
|
||||
|
||||
// 设置确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
// 确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
// 可以在这里添加确定后的回调逻辑
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveData();
|
||||
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
|
||||
// 保存像素颜色到背景配置
|
||||
savePixelColor(pixelColor);
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 回收Bitmap资源
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 保存拾取的像素颜色到背景配置
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
*/
|
||||
private void savePixelColor(int pixelColor) {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
LogUtils.d(TAG, "【savePixelColor】像素颜色已保存:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步背景颜色为拾取的像素颜色
|
||||
*/
|
||||
void setBackgroundColor() {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
int pixelColor = bean.getPixelColor();
|
||||
mainLayout.setBackgroundColor(pixelColor);
|
||||
LogUtils.d(TAG, "【setBackgroundColor】背景颜色已同步:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
void setBackgroundColor() {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
int nPixelColor = bean.getPixelColor();
|
||||
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
}
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 绑定图片容器的触摸事件,处理像素拾取逻辑
|
||||
*/
|
||||
private void bindImageTouchListener() {
|
||||
imageContainer.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) {
|
||||
float touchX = event.getX();
|
||||
float touchY = event.getY();
|
||||
LogUtils.v(TAG, "【onTouch】触摸坐标:(" + touchX + ", " + touchY + ")");
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取图片在窗口中的位置与尺寸
|
||||
int[] imageLocation = new int[2];
|
||||
imageView.getLocationInWindow(imageLocation);
|
||||
int imageWidth = imageView.getWidth();
|
||||
int imageHeight = imageView.getHeight();
|
||||
LogUtils.v(TAG, "【onTouch】图片显示尺寸:" + imageWidth + "x" + imageHeight + ",位置:(" + imageLocation[0] + ", " + imageLocation[1] + ")");
|
||||
|
||||
// 计算缩放比例
|
||||
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
|
||||
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
|
||||
LogUtils.v(TAG, "【onTouch】图片缩放比例:X=" + scaleX + ",Y=" + scaleY);
|
||||
|
||||
// 调整触摸坐标到图片显示区域坐标系
|
||||
float adjustedX = touchX - imageLocation[0];
|
||||
float adjustedY = touchY - imageLocation[1];
|
||||
LogUtils.v(TAG, "【onTouch】调整后触摸坐标:(" + adjustedX + ", " + adjustedY + ")");
|
||||
|
||||
// 检查是否在图片显示范围内
|
||||
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
|
||||
// 计算原始图片的像素坐标
|
||||
int pixelX = (int) (adjustedX * scaleX);
|
||||
int pixelY = (int) (adjustedY * scaleY);
|
||||
LogUtils.v(TAG, "【onTouch】计算后像素坐标:(" + pixelX + ", " + pixelY + ")");
|
||||
|
||||
// 检查像素坐标是否在原始图片范围内
|
||||
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() && pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
|
||||
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
|
||||
showPixelDialog(pixelColor, pixelX, pixelY);
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_OUT_OF_RANGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】像素坐标超出原始图片范围");
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_TOUCH_OUT_OF_IMAGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】触摸位置超出图片显示范围");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_CALC_FAILED, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.e(TAG, "【onTouch】计算像素位置失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindImageTouchListener】图片触摸事件已绑定");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundPictureActivity.class);
|
||||
LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
|
||||
Intent intent = new Intent(this, BackgroundSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
|
||||
return true;
|
||||
}
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundPictureActivity.class);
|
||||
startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
|
||||
}
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
LogUtils.d(TAG, "【onBackPressed】返回键触发,页面关闭");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package cc.winboll.studio.powerbell.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.powerbell.R;
|
||||
|
||||
/**
|
||||
* 应用设置窗口,提供应用配置项的统一入口
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/27 14:26
|
||||
* @Describe 应用设置窗口
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
// 权限请求常量(为后续读取媒体图片权限预留)
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
private Toolbar mToolbar; // 顶部工具栏
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化开始");
|
||||
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化顶部工具栏,设置导航返回与样式
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
// 设置工具栏副标题与标题样式
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
// 显示返回按钮
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// 绑定导航点击事件
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,47 +3,73 @@ package cc.winboll.studio.powerbell.activities;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.APPPlusUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
|
||||
/**
|
||||
* 应用快捷方式活动类,处理应用图标快捷菜单的切换请求
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/15 13:45
|
||||
* @Describe 应用快捷方式活动类
|
||||
*/
|
||||
public class ShortcutActionActivity extends Activity {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "ShortcutActionActivity";
|
||||
// 快捷指令常量
|
||||
private static final String ACTION_SWITCH_TO_EN1 = "switchto_en1";
|
||||
private static final String ACTION_SWITCH_TO_CN1 = "switchto_cn1";
|
||||
private static final String ACTION_SWITCH_TO_CN2 = "switchto_cn2";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 处理应用级别的切换请求
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "【onCreate】ShortcutActionActivity 启动,开始处理快捷方式请求");
|
||||
|
||||
// 处理应用图标快捷菜单的切换请求
|
||||
handleSwitchRequest();
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理应用图标快捷菜单的请求
|
||||
LogUtils.d(TAG, "【onCreate】快捷方式请求处理完成,关闭活动");
|
||||
finish();
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 处理应用图标快捷菜单的请求,根据意图数据切换应用启动组件
|
||||
*/
|
||||
private void handleSwitchRequest() {
|
||||
Intent intent = getIntent();
|
||||
if (intent != null && "switchto_en1".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "【handleSwitchRequest】意图为空,无法处理快捷方式请求");
|
||||
return;
|
||||
}
|
||||
if (intent != null && "switchto_cn1".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
}
|
||||
if (intent != null && "switchto_cn2".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
|
||||
String dataString = intent.getDataString();
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】获取到快捷指令:" + dataString);
|
||||
|
||||
// 匹配快捷指令并切换组件
|
||||
if (ACTION_SWITCH_TO_EN1.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至EN1组件:" + App.COMPONENT_EN1);
|
||||
} else if (ACTION_SWITCH_TO_CN1.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name_cn1) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN1组件:" + App.COMPONENT_CN1);
|
||||
} else if (ACTION_SWITCH_TO_CN2.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name_cn2) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN2组件:" + App.COMPONENT_CN2);
|
||||
} else {
|
||||
LogUtils.w(TAG, "【handleSwitchRequest】未匹配到有效快捷指令:" + dataString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/19 20:35
|
||||
* @Describe 应用窗口基类
|
||||
*/
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
@@ -21,98 +16,190 @@ import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
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;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/19 20:35
|
||||
* @Describe 应用窗口基类,提供主题设置、Activity 管理、工具栏配置、全屏切换、版本标签显示等通用功能
|
||||
* 适配 API30,基于 Java7 开发,所有子类需继承此类实现统一窗口行为
|
||||
*/
|
||||
public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
protected TextView mTagView;
|
||||
|
||||
private static final String VERSION_TAG_TEXT = "MIMO SDK V%s"; // 版本标签文本格式
|
||||
private static final float VERSION_TAG_TEXT_SIZE = 10f; // 版本标签字体大小(sp)
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
protected volatile AESThemeBean.ThemeType mThemeType; // 当前主题类型
|
||||
protected TextView mTagView; // 版本标签显示控件
|
||||
|
||||
// ======================== 接口实现 & 抽象方法 =========================
|
||||
@Override
|
||||
public abstract Activity getActivity();
|
||||
|
||||
@Override
|
||||
public abstract String getTag();
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化开始", getTag()));
|
||||
// 初始化主题
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
changeFullScreen(this);
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化完成,当前主题:%s", getTag(), mThemeType));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
LogUtils.d(TAG, String.format("【%s-onStart】添加版本标签到页面", getTag()));
|
||||
// 添加版本标签
|
||||
addVersionNameToContentView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
// 注册到Activity管理器
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
LogUtils.d(TAG, String.format("【%s-onPostCreate】已注册到Activity管理器", getTag()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 从Activity管理器移除
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
LogUtils.d(TAG, String.format("【%s-onDestroy】已从Activity管理器移除", getTag()));
|
||||
}
|
||||
|
||||
// ======================== 主题相关方法 =========================
|
||||
/**
|
||||
* 获取当前主题类型
|
||||
* @return 主题类型枚举
|
||||
*/
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
|
||||
AESThemeBean.ThemeType themeType = AESThemeBean.getThemeStyleType(themeId);
|
||||
LogUtils.d(TAG, String.format("【%s-getThemeType】获取主题类型,ID:%d,类型:%s", getTag(), themeId, themeType));
|
||||
return themeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题样式
|
||||
*/
|
||||
void setThemeStyle() {
|
||||
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
|
||||
setTheme(themeId);
|
||||
LogUtils.d(TAG, String.format("【%s-setThemeStyle】应用主题样式,ID:%d", getTag(), themeId));
|
||||
}
|
||||
|
||||
// ======================== UI 配置方法 =========================
|
||||
/**
|
||||
* 添加版本标签到页面底部
|
||||
*/
|
||||
protected void addVersionNameToContentView() {
|
||||
if (!isTagViewVisible()) {
|
||||
LogUtils.d(TAG, String.format("【%s-addVersionNameToContentView】版本标签不可见,跳过添加", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mTagView == null) {
|
||||
mTagView = new TextView(this);
|
||||
// 配置版本标签样式
|
||||
mTagView.setTextColor(Color.GRAY);
|
||||
mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||
mTagView.setText("MIMO SDK V" + BuildConfig.VERSION_NAME);
|
||||
mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, VERSION_TAG_TEXT_SIZE);
|
||||
mTagView.setText(String.format(VERSION_TAG_TEXT, BuildConfig.VERSION_NAME));
|
||||
// 配置布局参数
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||
// 添加到根布局
|
||||
FrameLayout frameLayout = findViewById(android.R.id.content);
|
||||
frameLayout.addView(mTagView, params);
|
||||
if (frameLayout != null) {
|
||||
frameLayout.addView(mTagView, params);
|
||||
LogUtils.d(TAG, String.format("【%s-addVersionNameToContentView】版本标签添加完成,版本:%s", getTag(), BuildConfig.VERSION_NAME));
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("【%s-addVersionNameToContentView】根布局为空,无法添加版本标签", getTag()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isTagViewVisible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置工具栏,显示返回按钮
|
||||
*/
|
||||
public void setupToolbar() {
|
||||
Toolbar mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar != null) {
|
||||
setSupportActionBar(mToolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
LogUtils.d(TAG, String.format("【%s-setupToolbar】工具栏配置完成,已显示返回按钮", getTag()));
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("【%s-setupToolbar】ActionBar为空,无法显示返回按钮", getTag()));
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("【%s-setupToolbar】未找到工具栏控件(ID:toolbar)", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
//GlobalApplication.getWinBoLLActivityManager().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
/**
|
||||
* 版本标签是否可见
|
||||
* @return 默认为true,子类可重写修改
|
||||
*/
|
||||
protected boolean isTagViewVisible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 菜单 & 返回键处理 =========================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
|
||||
LogUtils.d(TAG, String.format("【%s-onOptionsItemSelected】点击返回菜单", getTag()));
|
||||
return true;
|
||||
}
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
|
||||
}
|
||||
|
||||
public void changeFullScreen(Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
if (window == null){
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
LogUtils.d(TAG, String.format("【%s-onBackPressed】触发返回键", getTag()));
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 切换至全屏模式,隐藏状态栏与导航栏
|
||||
* @param activity 目标Activity
|
||||
*/
|
||||
public void changeFullScreen(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】目标Activity为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
Window window = activity.getWindow();
|
||||
if (window == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】窗口为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
View decorView = window.getDecorView();
|
||||
if (decorView == null){
|
||||
if (decorView == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】DecorView为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置全屏标志位
|
||||
int flag = decorView.getSystemUiVisibility();
|
||||
flag |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
flag |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
@@ -120,6 +207,9 @@ public abstract class WinBoLLActivity extends AppCompatActivity implements IWinB
|
||||
flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
decorView.setSystemUiVisibility(flag);
|
||||
// 配置窗口标志位
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
LogUtils.d(TAG, String.format("【%s-changeFullScreen】已切换至全屏模式", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,106 @@
|
||||
package cc.winboll.studio.powerbell.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:38:55
|
||||
* @Describe 电池报告数据适配器
|
||||
*/
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
|
||||
import cc.winboll.studio.powerbell.beans.BatteryData;
|
||||
import cc.winboll.studio.powerbell.models.BatteryData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 电池报告数据适配器,用于RecyclerView展示电池电量、充放电时间数据
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:38:55
|
||||
* @Describe 电池报告数据适配器
|
||||
*/
|
||||
public class BatteryAdapter extends RecyclerView.Adapter<BatteryAdapter.ViewHolder> {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BatteryAdapter";
|
||||
private List<BatteryData> dataList = new ArrayList<>();
|
||||
private static final String FORMAT_BATTERY_LEVEL = "%d%%"; // 电量显示格式
|
||||
private static final String PREFIX_DISCHARGE_TIME = "使用时间: "; // 放电时间前缀
|
||||
private static final String PREFIX_CHARGE_TIME = "充电时间: "; // 充电时间前缀
|
||||
|
||||
public void updateData(List<BatteryData> newData) {
|
||||
dataList = newData;
|
||||
notifyDataSetChanged();
|
||||
// ======================== 成员变量 =========================
|
||||
private List<BatteryData> dataList = new ArrayList<>(); // 电池数据列表
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
public BatteryAdapter() {
|
||||
LogUtils.d(TAG, "【BatteryAdapter】适配器初始化,初始数据列表为空");
|
||||
}
|
||||
|
||||
// ======================== 数据操作方法 =========================
|
||||
/**
|
||||
* 更新适配器数据并刷新列表
|
||||
* @param newData 新的电池数据列表
|
||||
*/
|
||||
public void updateData(List<BatteryData> newData) {
|
||||
LogUtils.d(TAG, "【updateData】开始更新数据,新数据列表是否为空:" + (newData == null));
|
||||
// 判空处理,避免空指针
|
||||
if (newData != null) {
|
||||
dataList = newData;
|
||||
LogUtils.d(TAG, "【updateData】数据更新完成,当前数据量:" + dataList.size());
|
||||
} else {
|
||||
dataList.clear();
|
||||
LogUtils.w(TAG, "【updateData】新数据列表为空,已清空本地数据");
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "【updateData】已通知列表刷新");
|
||||
}
|
||||
|
||||
// ======================== RecyclerView 重写方法 =========================
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】创建ViewHolder,父容器:" + parent.getContext().getClass().getSimpleName());
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_battery_report, parent, false);
|
||||
return new ViewHolder(view);
|
||||
.inflate(R.layout.item_battery_report, parent, false);
|
||||
ViewHolder viewHolder = new ViewHolder(view);
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】ViewHolder创建完成");
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "【onBindViewHolder】绑定ViewHolder,位置:" + position);
|
||||
// 判空与越界校验
|
||||
if (dataList == null || dataList.isEmpty() || position >= dataList.size()) {
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,无法绑定视图,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
BatteryData item = dataList.get(position);
|
||||
holder.tvLevel.setText(String.format("%d%%", item.getCurrentLevel()));
|
||||
holder.tvDischargeTime.setText("使用时间: " + item.getDischargeTime());
|
||||
holder.tvChargeTime.setText("充电时间: " + item.getChargeTime());
|
||||
// 绑定数据到视图
|
||||
holder.tvLevel.setText(String.format(FORMAT_BATTERY_LEVEL, item.getCurrentLevel()));
|
||||
holder.tvDischargeTime.setText(PREFIX_DISCHARGE_TIME + item.getDischargeTime());
|
||||
holder.tvChargeTime.setText(PREFIX_CHARGE_TIME + item.getChargeTime());
|
||||
|
||||
LogUtils.d(TAG, "【onBindViewHolder】视图绑定完成,位置:" + position + ",电量:" + item.getCurrentLevel() + "%");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dataList.size();
|
||||
int count = dataList.size();
|
||||
LogUtils.d(TAG, "【getItemCount】获取条目数量:" + count);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ======================== ViewHolder 内部类 =========================
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvLevel;
|
||||
TextView tvDischargeTime;
|
||||
TextView tvChargeTime;
|
||||
TextView tvLevel; // 电量显示
|
||||
TextView tvDischargeTime; // 放电时间显示
|
||||
TextView tvChargeTime; // 充电时间显示
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// 初始化视图控件
|
||||
tvLevel = itemView.findViewById(R.id.tvLevel);
|
||||
tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime);
|
||||
tvChargeTime = itemView.findViewById(R.id.tvChargeTime);
|
||||
LogUtils.d(TAG, "【ViewHolder】控件初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/29 17:24:53
|
||||
* @Describe 应用运行参数类
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class AppConfigBean extends BaseBean implements Serializable {
|
||||
|
||||
transient public static final String TAG = "AppConfigBean";
|
||||
|
||||
boolean isEnableUsegeReminder = false;
|
||||
int usegeReminderValue = 45;
|
||||
boolean isEnableChargeReminder = false;
|
||||
int chargeReminderValue = 100;
|
||||
// 铃声提醒间隔时间。.
|
||||
int reminderIntervalTime = 5000;
|
||||
// 电池是否正在充电。
|
||||
boolean isCharging = false;
|
||||
// 电池当前电量。.
|
||||
int currentValue = -1;
|
||||
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setIsEnableChargeReminder(false);
|
||||
setUsegeReminderValue(10);
|
||||
setIsEnableUsegeReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
}
|
||||
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
this.reminderIntervalTime = reminderIntervalTime;
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public void setCurrentValue(int currentValue) {
|
||||
this.currentValue = currentValue;
|
||||
}
|
||||
|
||||
public int getCurrentValue() {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
|
||||
this.isEnableUsegeReminder = isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableUsegeReminder() {
|
||||
return isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public void setUsegeReminderValue(int usegeReminderValue) {
|
||||
this.usegeReminderValue = usegeReminderValue;
|
||||
}
|
||||
|
||||
public int getUsegeReminderValue() {
|
||||
return usegeReminderValue;
|
||||
}
|
||||
|
||||
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
this.chargeReminderValue = chargeReminderValue;
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AppConfigBean bean = this;
|
||||
jsonWriter.name("isEnableUsegeReminder").value(bean.isEnableUsegeReminder());
|
||||
jsonWriter.name("usegeReminderValue").value(bean.getUsegeReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("isEnableUsegeReminder")) {
|
||||
bean.setIsEnableUsegeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("usegeReminderValue")) {
|
||||
bean.setUsegeReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("isEnableChargeReminder")) {
|
||||
bean.setIsEnableChargeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("chargeReminderValue")) {
|
||||
bean.setChargeReminderValue(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class BackgroundPictureBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "BackgroundPictureBean";
|
||||
|
||||
int backgroundWidth = 100;
|
||||
int backgroundHeight = 100;
|
||||
boolean isUseBackgroundFile = false;
|
||||
// 图片拾取像素颜色
|
||||
int pixelColor = 0;
|
||||
|
||||
public BackgroundPictureBean() {
|
||||
}
|
||||
|
||||
public BackgroundPictureBean(String recivedFileName, boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
}
|
||||
|
||||
public int getPixelColor() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setBackgroundWidth(int backgroundWidth) {
|
||||
this.backgroundWidth = backgroundWidth;
|
||||
}
|
||||
|
||||
public int getBackgroundWidth() {
|
||||
return backgroundWidth;
|
||||
}
|
||||
|
||||
public void setBackgroundHeight(int backgroundHeight) {
|
||||
this.backgroundHeight = backgroundHeight;
|
||||
}
|
||||
|
||||
public int getBackgroundHeight() {
|
||||
return backgroundHeight;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public boolean isUseBackgroundFile() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return BackgroundPictureBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BackgroundPictureBean bean = this;
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
|
||||
jsonWriter.name("pixelColor").value(bean.getPixelColor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BackgroundPictureBean bean = new BackgroundPictureBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("backgroundWidth")) {
|
||||
bean.setBackgroundWidth(jsonReader.nextInt());
|
||||
} else if (name.equals("backgroundHeight")) {
|
||||
bean.setBackgroundHeight(jsonReader.nextInt());
|
||||
} else if (name.equals("isUseBackgroundFile")) {
|
||||
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
|
||||
} else if (name.equals("pixelColor")) {
|
||||
bean.setPixelColor(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:30:51
|
||||
* @Describe 电池报告数据模型
|
||||
*/
|
||||
public class BatteryData {
|
||||
|
||||
public static final String TAG = "BatteryData";
|
||||
|
||||
private int currentLevel;
|
||||
private String dischargeTime;
|
||||
private String chargeTime;
|
||||
|
||||
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
|
||||
this.currentLevel = currentLevel;
|
||||
this.dischargeTime = dischargeTime;
|
||||
this.chargeTime = chargeTime;
|
||||
}
|
||||
|
||||
public int getCurrentLevel() { return currentLevel; }
|
||||
public String getDischargeTime() { return dischargeTime; }
|
||||
public String getChargeTime() { return chargeTime; }
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
|
||||
public static final String TAG = "BatteryInfoBean";
|
||||
|
||||
// 记录电量的时间戳
|
||||
long timeStamp;
|
||||
// 电量值
|
||||
int battetyValue;
|
||||
|
||||
public BatteryInfoBean() {
|
||||
this.timeStamp = 0;
|
||||
this.battetyValue = 0;
|
||||
}
|
||||
|
||||
public BatteryInfoBean(long timeStamp, int battetyValue) {
|
||||
this.timeStamp = timeStamp;
|
||||
this.battetyValue = battetyValue;
|
||||
}
|
||||
|
||||
public void setTimeStamp(long timeStamp) {
|
||||
this.timeStamp = timeStamp;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
public void setBattetyValue(int battetyValue) {
|
||||
this.battetyValue = battetyValue;
|
||||
}
|
||||
|
||||
public int getBattetyValue() {
|
||||
return battetyValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return BatteryInfoBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BatteryInfoBean bean = this;
|
||||
jsonWriter.name("timeStamp").value(bean.getTimeStamp());
|
||||
jsonWriter.name("battetyValue").value(bean.getBattetyValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BatteryInfoBean bean = new BatteryInfoBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("timeStamp")) {
|
||||
bean.setTimeStamp(jsonReader.nextLong());
|
||||
} else if (name.equals("battetyValue")) {
|
||||
bean.setBattetyValue(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 07:06:07
|
||||
* @Describe 服务控制参数
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ControlCenterServiceBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "ControlCenterServiceBean";
|
||||
|
||||
boolean isEnableService = false;
|
||||
|
||||
public ControlCenterServiceBean() {
|
||||
this.isEnableService = false;
|
||||
}
|
||||
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
public void setIsEnableService(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
public boolean isEnableService() {
|
||||
return isEnableService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return ControlCenterServiceBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
ControlCenterServiceBean bean = this;
|
||||
jsonWriter.name("isEnableService").value(bean.isEnableService());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("isEnableService")) {
|
||||
bean.setIsEnableService(jsonReader.nextBoolean());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
// 应用消息结构
|
||||
//
|
||||
public class NotificationMessage {
|
||||
|
||||
String Title;
|
||||
String Content;
|
||||
String RemindMSG;
|
||||
|
||||
public NotificationMessage(String title, String content) {
|
||||
Title = title;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
RemindMSG = remindMSG;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return RemindMSG;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
Title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return Title;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return Content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,140 +1,149 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
|
||||
/**
|
||||
* 背景图片的接收分享文件后的预览对话框
|
||||
* 适配 API30,基于 Java7 开发,支持分享图片的Uri解析、预览与确认选择
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/25 16:27:53
|
||||
* @Describe 背景图片的接收分享文件后的预览对话框
|
||||
*/
|
||||
public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BackgroundPicturePreviewDialog";
|
||||
private static final String TOAST_MSG_EMPTY_FILE = "接收到的文件为空。"; // 空文件提示文本
|
||||
|
||||
Context mContext;
|
||||
BackgroundPictureUtils mBackgroundPictureUtils;
|
||||
Button dialogbackgroundpicturepreviewButton1;
|
||||
Button dialogbackgroundpicturepreviewButton2;
|
||||
String mszPreReceivedFileName;
|
||||
// ======================== 成员变量 =========================
|
||||
private Context mContext; // 上下文对象
|
||||
private IOnRecivedPictureListener mIOnRecivedPictureListener; // 图片接收监听
|
||||
private Uri mUriRecivedPicture; // 接收的图片Uri
|
||||
// 控件对象
|
||||
private BackgroundView mBackgroundView; // 背景预览视图
|
||||
private Button dialogbackgroundpicturepreviewButton1; // 取消按钮
|
||||
private Button dialogbackgroundpicturepreviewButton2; // 确认按钮
|
||||
|
||||
public BackgroundPicturePreviewDialog(Context context) {
|
||||
// ======================== 接口定义 =========================
|
||||
/**
|
||||
* 图片接收监听接口,用于通知确认选择的图片Uri
|
||||
*/
|
||||
public interface IOnRecivedPictureListener {
|
||||
void onAcceptRecivedPicture(Uri uriRecivedPicture);
|
||||
}
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
|
||||
super(context);
|
||||
setContentView(R.layout.dialog_backgroundpicturepreview);
|
||||
initEnv();
|
||||
|
||||
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化开始");
|
||||
// 初始化成员变量
|
||||
mContext = context;
|
||||
mBackgroundPictureUtils = ((BackgroundPictureActivity)context).mBackgroundPictureUtils;
|
||||
|
||||
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
|
||||
copyAndViewRecivePicture(imageView);
|
||||
mIOnRecivedPictureListener = iOnRecivedPictureListener;
|
||||
|
||||
// 设置布局与控件
|
||||
setContentView(R.layout.dialog_backgroundpicturepreview);
|
||||
initViews();
|
||||
bindButtonClickEvents();
|
||||
|
||||
// 预览接收的图片
|
||||
previewRecivedPicture();
|
||||
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 视图初始化方法 =========================
|
||||
/**
|
||||
* 初始化对话框内所有控件
|
||||
*/
|
||||
private void initViews() {
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
|
||||
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// 不使用分享到的图片
|
||||
// 跳转到主窗口
|
||||
Intent i = new Intent(mContext, MainActivity.class);
|
||||
mContext.startActivity(i);
|
||||
}
|
||||
});
|
||||
|
||||
dialogbackgroundpicturepreviewButton2 = findViewById(R.id.dialogbackgroundpicturepreviewButton2);
|
||||
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 使用分享到的图片
|
||||
//
|
||||
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
|
||||
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
|
||||
// 关闭对话框
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void initEnv() {
|
||||
LogUtils.d(TAG, "initEnv()");
|
||||
mszPreReceivedFileName = "PreReceived.data";
|
||||
LogUtils.d(TAG, "【initViews】对话框控件初始化完成");
|
||||
}
|
||||
|
||||
void copyAndViewRecivePicture(ImageView imageView) {
|
||||
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
|
||||
BackgroundPictureActivity activity = ((BackgroundPictureActivity)mContext);
|
||||
// ======================== 事件绑定方法 =========================
|
||||
/**
|
||||
* 绑定按钮点击事件
|
||||
*/
|
||||
private void bindButtonClickEvents() {
|
||||
// 取消按钮:跳转到主页面并关闭对话框
|
||||
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
LogUtils.d(TAG, "【onClick】点击取消按钮,跳转到主页面");
|
||||
Intent intent = new Intent(mContext, MainActivity.class);
|
||||
mContext.startActivity(intent);
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "【onClick】对话框已关闭");
|
||||
}
|
||||
});
|
||||
|
||||
//取出文件uri
|
||||
Uri uri = activity.getIntent().getData();
|
||||
if (uri == null) {
|
||||
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
//获取文件真实地址
|
||||
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
|
||||
// 确认按钮:通知监听并关闭对话框
|
||||
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【onClick】点击确认按钮,通知接收图片");
|
||||
if (mIOnRecivedPictureListener != null && mUriRecivedPicture != null) {
|
||||
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
|
||||
LogUtils.d(TAG, "【onClick】已通知监听,图片Uri:" + mUriRecivedPicture);
|
||||
} else {
|
||||
LogUtils.w(TAG, "【onClick】监听为空或图片Uri无效,无法通知");
|
||||
}
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "【onClick】对话框已关闭");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindButtonClickEvents】按钮点击事件绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 预览接收的分享图片
|
||||
*/
|
||||
private void previewRecivedPicture() {
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】开始预览接收的图片");
|
||||
// 校验上下文类型
|
||||
if (!(mContext instanceof BackgroundSettingsActivity)) {
|
||||
LogUtils.e(TAG, "【previewRecivedPicture】上下文不是BackgroundSettingsActivity,无法获取图片Uri");
|
||||
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
File fSrcImage = new File(szSrcImage);
|
||||
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
|
||||
File mfPreReceivedPhoto = new File(activity.mBackgroundPictureUtils.getBackgroundDir(), mszPreReceivedFileName);
|
||||
// 复制源图片到剪裁文件
|
||||
try {
|
||||
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
|
||||
LogUtils.d(TAG, "copyFileUsingFileChannels");
|
||||
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
|
||||
imageView.setBackground(drawable);
|
||||
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 创建图片背景图片目录
|
||||
//
|
||||
boolean createBackgroundFolder2(String szBackgroundFolder) {
|
||||
// 文件路径参数为空值或无效值时返回false.
|
||||
if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
|
||||
File f = new File(szBackgroundFolder);
|
||||
if (f.exists()) {
|
||||
if (f.isDirectory()) {
|
||||
return true;
|
||||
} else {
|
||||
// 工作路径不是一个目录
|
||||
LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
|
||||
return false;
|
||||
}
|
||||
BackgroundSettingsActivity activity = (BackgroundSettingsActivity) mContext;
|
||||
// 从Intent中获取图片Uri(优先getData,其次EXTRA_STREAM)
|
||||
mUriRecivedPicture = activity.getIntent().getData();
|
||||
if (mUriRecivedPicture == null) {
|
||||
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】从EXTRA_STREAM获取Uri:" + mUriRecivedPicture);
|
||||
} else {
|
||||
return f.mkdirs();
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】从getData获取Uri:" + mUriRecivedPicture);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IOnRecivedPictureListener {
|
||||
void onAcceptRecivedPicture(String szBackgroundFileName);
|
||||
}
|
||||
// 解析Uri为文件路径
|
||||
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
LogUtils.w(TAG, "【previewRecivedPicture】解析的文件路径为空");
|
||||
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载图片到预览视图
|
||||
mBackgroundView.loadImage(szSrcImage);
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成,文件路径:" + szSrcImage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
||||
import com.a4455jkjh.colorpicker.view.OnColorChangedListener;
|
||||
|
||||
/**
|
||||
* 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
* 适配 API30,基于 Java7 开发,返回 0xAARRGGBB 格式颜色(含透明度)
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/16 11:47
|
||||
* @Describe 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
*/
|
||||
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "ColorPaletteDialog";
|
||||
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值(0-255)
|
||||
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比(100%,无调节)
|
||||
private static final int BRIGHTNESS_STEP = 5; // 亮度调节步长(每次±5%,精准流畅)
|
||||
private static final int MIN_BRIGHTNESS = 10; // 亮度最小值(10%,避免全黑看不见)
|
||||
private static final int MAX_BRIGHTNESS = 200; // 亮度最大值(200%,避免过曝失真)
|
||||
private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值(100%=不透明)
|
||||
private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值(0%=完全透明)
|
||||
private static final String FORMAT_COLOR_HEX = "#%08X"; // 颜色值格式化(AARRGGBB)
|
||||
private static final String FORMAT_PERCENT = "%d%%"; // 百分比格式化(X%)
|
||||
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
public interface OnColorSelectedListener {
|
||||
void onColorSelected(int color); // 返回0xAARRGGBB格式颜色(含透明度)
|
||||
}
|
||||
|
||||
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
|
||||
// 核心数据:原始基准值(用户输入/选择颜色时更新)+ 实时调节值(亮度/透明度变化时更新)
|
||||
private OnColorSelectedListener mListener; // 颜色选择回调(非空校验)
|
||||
private int mInitialColor; // 初始颜色(传入的默认颜色)
|
||||
private int mCurrentColor; // 当前最终颜色(含亮度+透明度调节)
|
||||
private int mCurrentBrightnessPercent; // 当前亮度百分比(10%-200%)
|
||||
// 透明度:百分比(0-100%,用户直观操作)+ 原始/实时值(0-255,颜色计算用)
|
||||
private int mOriginalAlphaPercent; // 原始透明度百分比(基准值,用户输入/选色时更新)
|
||||
private int mCurrentAlphaPercent; // 实时透明度百分比(调节进度条时更新)
|
||||
private int mOriginalAlpha; // 原始透明度(0-255,基准值)
|
||||
private int mCurrentAlpha; // 实时透明度(0-255,计算用)
|
||||
// RGB:原始基准值+实时调节值
|
||||
private int mOriginalR; // 原始R分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalG; // 原始G分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalB; // 原始B分量(基准值,用户输入/选色时更新)
|
||||
private int mCurrentR; // 实时R分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentG; // 实时G分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentB; // 实时B分量(亮度调节后,同步输入框显示)
|
||||
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
|
||||
private static volatile boolean isAppSelfUpdatingColor = false;
|
||||
|
||||
// 控件引用
|
||||
private ImageView ivColorPicker; // 颜色预览拾取框
|
||||
private ImageView ivColorScaler; // 颜色渐变拾取框
|
||||
private EditText etR; // R分量输入框(显示实时调节值)
|
||||
private EditText etG; // G分量输入框(显示实时调节值)
|
||||
private EditText etB; // B分量输入框(显示实时调节值)
|
||||
private EditText etColorValue; // 颜色值输入框(#AARRGGBB,显示最终值)
|
||||
private SeekBar sbAlpha; // 透明度调节进度条(0-100%)
|
||||
private TextView tvAlphaValue; // 透明度数值显示(X%)
|
||||
private TextView tvBrightnessMinus;// 亮度减少按钮(-)
|
||||
private TextView tvBrightnessValue;// 亮度数值显示(X%,直观易懂)
|
||||
private TextView tvBrightnessPlus; // 亮度增加按钮(+)
|
||||
private TextView tvConfirm; // 确认按钮
|
||||
private TextView tvCancel; // 取消按钮
|
||||
|
||||
// ====================== 构造方法(初始化核心数据,严格校验) ======================
|
||||
public ColorPaletteDialog(Context context, int initialColor, OnColorSelectedListener listener) {
|
||||
super(context, R.style.CustomDialogStyle);
|
||||
this.mInitialColor = initialColor;
|
||||
this.mListener = listener;
|
||||
|
||||
// 1. 强制回调非空,避免后续空指针(容错)
|
||||
if (mListener == null) {
|
||||
throw new IllegalArgumentException("OnColorSelectedListener can not be null!");
|
||||
}
|
||||
|
||||
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
|
||||
this.mOriginalAlpha = Color.alpha(initialColor);
|
||||
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
this.mCurrentAlpha = mOriginalAlpha;
|
||||
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
|
||||
this.mOriginalR = Color.red(initialColor);
|
||||
this.mOriginalG = Color.green(initialColor);
|
||||
this.mOriginalB = Color.blue(initialColor);
|
||||
this.mCurrentR = mOriginalR;
|
||||
this.mCurrentG = mOriginalG;
|
||||
this.mCurrentB = mOriginalB;
|
||||
|
||||
// 3. 初始化当前状态(默认亮度100%,当前颜色=初始颜色)
|
||||
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
this.mCurrentColor = initialColor;
|
||||
|
||||
LogUtils.d(TAG, String.format("init dialog success | 初始颜色:%s | 原始RGB:%d,%d,%d | 原始透明度:%s | 初始亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, initialColor),
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
String.format(FORMAT_PERCENT, mOriginalAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_color_palette, null);
|
||||
setContentView(view);
|
||||
|
||||
// 初始化流程:控件绑定→数据赋值→监听设置→尺寸适配(小米机型优先适配)
|
||||
initViewBind(view);
|
||||
initData();
|
||||
initListener();
|
||||
adjustDialogSize();
|
||||
LogUtils.d(TAG, "dialog create complete | 适配小米API29-30机型");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
// 释放资源,避免内存泄漏(回调引用置空)
|
||||
mListener = null;
|
||||
LogUtils.d(TAG, "dialog dismiss | 释放资源完成");
|
||||
}
|
||||
|
||||
// ====================== 初始化核心方法(职责单一,便于维护) ======================
|
||||
/**
|
||||
* 控件绑定
|
||||
*/
|
||||
private void initViewBind(View view) {
|
||||
ivColorPicker = view.findViewById(R.id.iv_color_picker);
|
||||
ivColorScaler = view.findViewById(R.id.iv_color_scaler);
|
||||
etR = view.findViewById(R.id.et_r);
|
||||
etG = view.findViewById(R.id.et_g);
|
||||
etB = view.findViewById(R.id.et_b);
|
||||
etColorValue = view.findViewById(R.id.et_color_value);
|
||||
sbAlpha = view.findViewById(R.id.sb_alpha);
|
||||
tvAlphaValue = view.findViewById(R.id.tv_alpha_value);
|
||||
tvBrightnessMinus = view.findViewById(R.id.tv_brightness_minus);
|
||||
tvBrightnessValue = view.findViewById(R.id.tv_brightness_value);
|
||||
tvBrightnessPlus = view.findViewById(R.id.tv_brightness_plus);
|
||||
tvConfirm = view.findViewById(R.id.tv_confirm);
|
||||
tvCancel = view.findViewById(R.id.tv_cancel);
|
||||
|
||||
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
|
||||
if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null
|
||||
|| sbAlpha == null || tvAlphaValue == null
|
||||
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|
||||
|| tvConfirm == null || tvCancel == null) {
|
||||
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确!");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "view bind complete | 所有控件绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据初始化(无监听状态下赋值,避免循环回调)
|
||||
*/
|
||||
private void initData() {
|
||||
// 1. 颜色预览(显示当前最终颜色,初始=原始颜色)
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. RGB输入框(显示「实时分量」,初始=原始值)
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB)
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 透明度控件(进度条+文本,初始=原始透明度)
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 亮度控件(显示默认100%,初始化按钮状态)
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
updateBrightnessBtnStatus(); // 禁用边界值按钮
|
||||
|
||||
LogUtils.d(TAG, String.format("init data complete | 原始透明度:%s",
|
||||
String.format(FORMAT_PERCENT, mOriginalAlphaPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听初始化
|
||||
*/
|
||||
private void initListener() {
|
||||
// 点击监听(按钮+颜色拾取框)
|
||||
ivColorPicker.setOnClickListener(this);
|
||||
ivColorScaler.setOnClickListener(this);
|
||||
tvConfirm.setOnClickListener(this);
|
||||
tvCancel.setOnClickListener(this);
|
||||
tvBrightnessMinus.setOnClickListener(this);
|
||||
tvBrightnessPlus.setOnClickListener(this);
|
||||
// 透明度进度条监听
|
||||
sbAlpha.setOnSeekBarChangeListener(this);
|
||||
// 输入框监听(RGB+颜色值,避免循环同步)
|
||||
initTextWatcherListener();
|
||||
LogUtils.d(TAG, "all listener init complete | 监听绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框尺寸适配(小米全面屏+软键盘优化,避免输入框被遮挡)
|
||||
*/
|
||||
private void adjustDialogSize() {
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams lp = window.getAttributes();
|
||||
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
|
||||
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
|
||||
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 软键盘适配:小米虚拟导航栏兼容
|
||||
window.setAttributes(lp);
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
|
||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
|
||||
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 监听子方法(细分类型,逻辑清晰) ======================
|
||||
/**
|
||||
* 输入框文本监听(RGB+颜色值,传入触发ID避免循环同步)
|
||||
*/
|
||||
private void initTextWatcherListener() {
|
||||
// RGB输入框监听(复用方法,减少冗余)
|
||||
setEditTextWatcher(etR, R.id.et_r);
|
||||
setEditTextWatcher(etG, R.id.et_g);
|
||||
setEditTextWatcher(etB, R.id.et_b);
|
||||
|
||||
// 颜色值输入框监听(支持#RRGGBB/#AARRGGBB格式)
|
||||
etColorValue.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
parseColorFromStr(s.toString().trim(), R.id.et_color_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 透明度进度条监听实现 ======================
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
// 仅处理用户手动拖动进度条(避免应用自身更新时触发)
|
||||
if (fromUser && !isAppSelfUpdatingColor) {
|
||||
updateAlphaBySeekBar(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
/**
|
||||
* 拖动透明度进度条更新颜色
|
||||
*/
|
||||
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
// 更新实时透明度(百分比+0-255值)
|
||||
mCurrentAlphaPercent = alphaPercent;
|
||||
mCurrentAlpha = percent2Alpha(alphaPercent);
|
||||
// 重新计算最终颜色(基于当前亮度+新透明度)
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("update alpha by seekbar | 透明度:%s",
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false; // 释放标记
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 颜色核心逻辑 ======================
|
||||
/**
|
||||
* 核心计算:基于原始RGB+当前亮度+当前透明度,计算实时RGB+最终颜色
|
||||
*/
|
||||
private void calculateBrightnessAndUpdate() {
|
||||
// 亮度百分比转调节系数(10%→0.1,100%→1.0,200%→2.0)
|
||||
float brightnessFactor = mCurrentBrightnessPercent / 100.0f;
|
||||
|
||||
// RGB三个分量同时调节(基于原始基准值,避免叠加失真),限制0-255
|
||||
mCurrentR = Math.min(Math.max(Math.round(mOriginalR * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentG = Math.min(Math.max(Math.round(mOriginalG * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentB = Math.min(Math.max(Math.round(mOriginalB * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
|
||||
// 拼接「实时透明度」+「实时RGB」,得到最终颜色(0xAARRGGBB)
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度减少(每次减5%,最低10%)
|
||||
*/
|
||||
private void decreaseBrightness() {
|
||||
changeBrightness(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度增加(每次加5%,最高200%)
|
||||
*/
|
||||
private void increaseBrightness() {
|
||||
changeBrightness(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度调节核心方法(统一逻辑,加并发控制)
|
||||
*/
|
||||
private synchronized void changeBrightness(boolean isIncrease) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (isIncrease) {
|
||||
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent += BRIGHTNESS_STEP;
|
||||
} else {
|
||||
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent -= BRIGHTNESS_STEP;
|
||||
}
|
||||
// 计算亮度调节后的实时RGB+最终颜色
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("%s brightness | 亮度:%s | 实时RGB:%d,%d,%d",
|
||||
isIncrease ? "increase" : "decrease",
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent),
|
||||
mCurrentR, mCurrentG, mCurrentB));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB,更新原始基准值+实时值)
|
||||
*/
|
||||
private void parseColorFromStr(String colorStr, int triggerViewId) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (TextUtils.isEmpty(colorStr)) return;
|
||||
|
||||
// 补全#前缀(兼容用户输入习惯)
|
||||
if (!colorStr.startsWith("#")) {
|
||||
colorStr = "#" + colorStr;
|
||||
}
|
||||
|
||||
// 格式校验(仅支持6位RRGGBB/8位AARRGGBB)
|
||||
if (colorStr.length() != 7 && colorStr.length() != 9) {
|
||||
LogUtils.e(TAG, String.format("parse color failed | 格式错误(需#RRGGBB/#AARRGGBB),输入:%s", colorStr));
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析颜色
|
||||
int parsedColor = Color.parseColor(colorStr);
|
||||
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalAlpha = Color.alpha(parsedColor);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(parsedColor);
|
||||
mOriginalG = Color.green(parsedColor);
|
||||
mOriginalB = Color.blue(parsedColor);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = parsedColor;
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("parse color success | 解析颜色:%s | 透明度:%s | 重置亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, parsedColor),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, String.format("parse color failed | 非法颜色格式,输入:%s", colorStr), e);
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过RGB输入框更新颜色(用户输入后,更新原始基准值+实时值,重置亮度为100%)
|
||||
*/
|
||||
private synchronized void updateColorByRGB(int triggerViewId) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
// 解析用户输入的RGB值(限制0-255,非法输入设为0)
|
||||
int inputR = parseInputValue(etR.getText().toString());
|
||||
int inputG = parseInputValue(etG.getText().toString());
|
||||
int inputB = parseInputValue(etB.getText().toString());
|
||||
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalR = inputR;
|
||||
mOriginalG = inputG;
|
||||
mOriginalB = inputB;
|
||||
mCurrentR = inputR;
|
||||
mCurrentG = inputG;
|
||||
mCurrentB = inputB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("update color by RGB | 新原始RGB:%d,%d,%d | 透明度:%s | 重置亮度:%s",
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "update color by RGB failed", e);
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心同步:更新所有控件显示
|
||||
*/
|
||||
private void updateAllViews() {
|
||||
// 1. 同步颜色预览
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. 同步RGB输入框
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 同步颜色值输入框
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 同步透明度控件
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 同步亮度控件
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
updateBrightnessBtnStatus();
|
||||
|
||||
LogUtils.d(TAG, String.format("sync all views complete | 最终颜色:%s | 实时RGB:%d,%d,%d | 透明度:%s | 亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor),
|
||||
mCurrentR, mCurrentG, mCurrentB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新亮度按钮状态(边界值禁用,提升交互体验)
|
||||
*/
|
||||
private void updateBrightnessBtnStatus() {
|
||||
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
|
||||
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
|
||||
|
||||
tvBrightnessMinus.setEnabled(canMinus);
|
||||
tvBrightnessPlus.setEnabled(canPlus);
|
||||
tvBrightnessMinus.setTextColor(canMinus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
}
|
||||
|
||||
// ====================== 工具方法 ======================
|
||||
/**
|
||||
* 透明度:0-255 → 0-100%
|
||||
*/
|
||||
private int alpha2Percent(int alpha) {
|
||||
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 透明度:0-100% → 0-255
|
||||
*/
|
||||
private int percent2Alpha(int percent) {
|
||||
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输入值(限制0-255,非法输入返回0)
|
||||
*/
|
||||
private int parseInputValue(String input) {
|
||||
if (TextUtils.isEmpty(input)) return 0;
|
||||
try {
|
||||
int value = Integer.parseInt(input);
|
||||
return Math.min(Math.max(value, 0), MAX_RGB_VALUE);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, String.format("parse input failed | 非法数字,输入:%s", input), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB输入框监听复用
|
||||
*/
|
||||
private void setEditTextWatcher(EditText editText, final int viewId) {
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
updateColorByRGB(viewId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px(适配小米不同分辨率)
|
||||
*/
|
||||
private int dp2px(float dp) {
|
||||
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示系统颜色选择器(兼容API29-30,无高版本依赖,小米机型适配)
|
||||
*/
|
||||
private void showSystemColorPicker() {
|
||||
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30,支持横向滚动");
|
||||
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
|
||||
builder.setTitle("选择基础颜色");
|
||||
|
||||
// 50种常用颜色:按彩虹光谱顺序排列
|
||||
final int[] systemColors = {
|
||||
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
|
||||
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
|
||||
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
|
||||
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
|
||||
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
|
||||
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
|
||||
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
|
||||
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
|
||||
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
|
||||
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
|
||||
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
|
||||
};
|
||||
|
||||
// 1. 第一级:水平滚动容器
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
|
||||
horizontalScrollView.setHorizontalScrollBarEnabled(true);
|
||||
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
|
||||
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||
|
||||
// 2. 第二级:颜色排列容器(横向)
|
||||
LinearLayout colorLayout = new LinearLayout(getContext());
|
||||
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
|
||||
|
||||
// 3. 循环添加颜色按钮(内置圆形效果)
|
||||
for (int i = 0; i < systemColors.length; i++) {
|
||||
final int color = systemColors[i];
|
||||
ImageView colorBtn = new ImageView(getContext());
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
|
||||
if (i != systemColors.length - 1) {
|
||||
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
|
||||
}
|
||||
colorBtn.setLayoutParams(lp);
|
||||
|
||||
// 内置圆形背景(白色边框+圆形形状)
|
||||
GradientDrawable circleBg = new GradientDrawable();
|
||||
circleBg.setShape(GradientDrawable.OVAL);
|
||||
circleBg.setColor(color);
|
||||
circleBg.setStroke(dp2px(2), Color.WHITE);
|
||||
colorBtn.setBackground(circleBg);
|
||||
|
||||
colorBtn.setClickable(true);
|
||||
colorBtn.setFocusable(true);
|
||||
|
||||
// 点击事件
|
||||
colorBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
builder.create().dismiss();
|
||||
LogUtils.d(TAG, String.format("select system color | 选择颜色:%s | 透明度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, color),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
colorLayout.addView(colorBtn);
|
||||
}
|
||||
|
||||
// 层级嵌套
|
||||
horizontalScrollView.addView(colorLayout);
|
||||
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
|
||||
}
|
||||
|
||||
// ====================== 点击事件实现 ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
// 所有点击事件均加并发判断
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
if (id == R.id.iv_color_picker) {
|
||||
showSystemColorPicker();
|
||||
} else if (id == R.id.iv_color_scaler) {
|
||||
openColorScalerDialog(mCurrentColor);
|
||||
} else if (id == R.id.tv_confirm) {
|
||||
mListener.onColorSelected(mCurrentColor);
|
||||
LogUtils.d(TAG, String.format("confirm color | 回调颜色:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor)));
|
||||
dismiss();
|
||||
} else if (id == R.id.tv_cancel) {
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
|
||||
} else if (id == R.id.tv_brightness_minus) {
|
||||
decreaseBrightness();
|
||||
} else if (id == R.id.tv_brightness_plus) {
|
||||
increaseBrightness();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开颜色渐变选择器
|
||||
*/
|
||||
void openColorScalerDialog(int nColor) {
|
||||
LogUtils.d(TAG, String.format("openColorScalerDialog | 初始颜色:%s",
|
||||
String.format(FORMAT_COLOR_HEX, nColor)));
|
||||
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
|
||||
dlg.setOnColorChangedListener(new OnColorChangedListener() {
|
||||
@Override
|
||||
public void beforeColorChanged() {}
|
||||
|
||||
@Override
|
||||
public void onColorChanged(int color) {
|
||||
dlg.currentColorScalerDialogColor = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterColorChanged() {}
|
||||
});
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
// ====================== 内部类 ======================
|
||||
class ColorScalerDialog extends ColorPickerDialog {
|
||||
public int currentColorScalerDialogColor = 0;
|
||||
|
||||
public ColorScalerDialog(Context context, int p) {
|
||||
super(context, p);
|
||||
this.currentColorScalerDialogColor = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
int color = currentColorScalerDialogColor;
|
||||
ToastUtils.show(String.format("选择颜色:%s", String.format(FORMAT_COLOR_HEX, color)));
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("select scaler color | 选择颜色:%s | 透明度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, color),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -15,7 +15,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.PictureUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageDownloader;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -24,56 +25,72 @@ import java.io.IOException;
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 20:11
|
||||
* @Describe 网络后台使用提示对话框
|
||||
* @Describe 网络背景使用提示对话框
|
||||
* 继承 AndroidX AlertDialog,绑定自定义布局 dialog_networkbackground.xml
|
||||
* 适配 API30,基于 Java7 开发,支持网络图片下载、预览与回调
|
||||
*/
|
||||
public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "NetworkBackgroundDialog";
|
||||
// 消息标识:图片加载成功
|
||||
private static final int MSG_IMAGE_LOAD_SUCCESS = 1001;
|
||||
// 消息标识:图片加载失败
|
||||
private static final int MSG_IMAGE_LOAD_FAILED = 1002;
|
||||
private static final int MSG_IMAGE_LOAD_SUCCESS = 1001; // 图片加载成功消息标识
|
||||
private static final int MSG_IMAGE_LOAD_FAILED = 1002; // 图片加载失败消息标识
|
||||
|
||||
// 控件引用
|
||||
private TextView tvTitle;
|
||||
private TextView tvContent;
|
||||
private Button btnCancel;
|
||||
private Button btnConfirm;
|
||||
private Button btnPreview;
|
||||
private EditText etURL;
|
||||
BackgroundView bvBackgroundPreview;
|
||||
Context mContext;
|
||||
// 主线程 Handler,用于接收子线程消息并更新 UI
|
||||
private Handler mUiHandler;
|
||||
String previewFilePath;
|
||||
|
||||
// 按钮点击回调接口(Java7 接口实现)
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
/**
|
||||
* 按钮点击回调接口(Java7 接口实现)
|
||||
*/
|
||||
public interface OnDialogClickListener {
|
||||
void onConfirm(); // 确认按钮点击
|
||||
void onCancel(); // 取消按钮点击
|
||||
void onConfirm(String szConfirmFilePath); // 确认按钮点击,返回图片路径
|
||||
void onCancel(); // 取消按钮点击
|
||||
}
|
||||
|
||||
private OnDialogClickListener listener;
|
||||
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
|
||||
// 核心数据
|
||||
private OnDialogClickListener listener; // 按钮点击回调
|
||||
private Context mContext; // 上下文对象
|
||||
private Handler mUiHandler; // 主线程 Handler,用于接收子线程消息更新 UI
|
||||
private String mPreviewFilePath; // 预览图片文件路径
|
||||
private String mPreviewFileUrl; // 预览图片网络 URL
|
||||
private String mDownloadSavedPath; // 下载图片保存路径
|
||||
// 控件引用
|
||||
private TextView tvTitle; // 对话框标题
|
||||
private TextView tvContent; // 对话框内容
|
||||
private Button btnCancel; // 取消按钮
|
||||
private Button btnConfirm; // 确认按钮
|
||||
private Button btnPreview; // 预览按钮
|
||||
private EditText etURL; // URL 输入框
|
||||
private BackgroundView mBackgroundView; // 背景预览视图
|
||||
|
||||
// Java7 显式构造(必须传入 Context)
|
||||
// ====================== 构造方法(Java7 显式构造,按参数重载排序) ======================
|
||||
/**
|
||||
* 基础构造(仅传入 Context)
|
||||
* @param context 上下文
|
||||
*/
|
||||
public NetworkBackgroundDialog(@NonNull Context context) {
|
||||
super(context);
|
||||
initHandler(); // 初始化 Handler
|
||||
initView(); // 初始化布局和控件
|
||||
setDismissListener(); // 设置对话框消失监听
|
||||
}
|
||||
|
||||
// 带回调的构造(便于外部处理点击事件)
|
||||
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
initHandler(); // 初始化 Handler
|
||||
LogUtils.d(TAG, "NetworkBackgroundDialog: 基础构造初始化");
|
||||
initHandler();
|
||||
initView();
|
||||
setDismissListener(); // 设置对话框消失监听
|
||||
setDismissListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主线程 Handler,用于更新 UI
|
||||
* 带回调的构造(便于外部处理点击事件)
|
||||
* @param context 上下文
|
||||
* @param listener 按钮点击回调
|
||||
*/
|
||||
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
LogUtils.d(TAG, "NetworkBackgroundDialog: 带回调构造初始化");
|
||||
initHandler();
|
||||
initView();
|
||||
setDismissListener();
|
||||
}
|
||||
|
||||
// ====================== 生命周期相关方法(对话框消失监听、Handler 初始化) ======================
|
||||
/**
|
||||
* 初始化主线程 Handler,用于接收子线程消息并更新 UI
|
||||
*/
|
||||
private void initHandler() {
|
||||
mUiHandler = new Handler() {
|
||||
@@ -82,22 +99,28 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
super.handleMessage(msg);
|
||||
// 对话框已消失时,不再处理 UI 消息
|
||||
if (!isShowing()) {
|
||||
LogUtils.d(TAG, "handleMessage: 对话框已消失,忽略消息");
|
||||
return;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MSG_IMAGE_LOAD_SUCCESS:
|
||||
// 图片加载成功,获取文件路径并设置背景
|
||||
String filePath = (String) msg.obj;
|
||||
setBackgroundFromPath(filePath);
|
||||
mDownloadSavedPath = (String) msg.obj;
|
||||
LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
|
||||
mBackgroundView.loadImage(mDownloadSavedPath);
|
||||
break;
|
||||
case MSG_IMAGE_LOAD_FAILED:
|
||||
// 图片加载失败,设置默认背景
|
||||
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
LogUtils.e(TAG, "handleMessage: 图片加载失败");
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
ToastUtils.show("图片预览失败,请检查链接");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initHandler: 主线程 Handler 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,20 +133,22 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// 对话框消失时,移除所有未处理的消息和回调
|
||||
if (mUiHandler != null) {
|
||||
mUiHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDismiss: Handler 消息已清理");
|
||||
}
|
||||
LogUtils.d(TAG, "对话框已消失,Handler 消息已清理");
|
||||
LogUtils.d(TAG, "onDismiss: 对话框已消失");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "setDismissListener: 对话框消失监听已设置");
|
||||
}
|
||||
|
||||
// ====================== 初始化方法(布局、控件、点击事件) ======================
|
||||
/**
|
||||
* 初始化布局和控件
|
||||
*/
|
||||
private void initView() {
|
||||
mContext = this.getContext();
|
||||
// 加载自定义布局
|
||||
View dialogView = LayoutInflater.from(getContext())
|
||||
.inflate(R.layout.dialog_networkbackground, null);
|
||||
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_networkbackground, null);
|
||||
// 设置对话框内容视图
|
||||
setView(dialogView);
|
||||
|
||||
@@ -134,10 +159,22 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
|
||||
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
|
||||
etURL = (EditText) dialogView.findViewById(R.id.et_url);
|
||||
bvBackgroundPreview = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
|
||||
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
|
||||
|
||||
// 控件非空校验
|
||||
if (tvTitle == null || tvContent == null || btnCancel == null || btnConfirm == null || btnPreview == null
|
||||
|| etURL == null || mBackgroundView == null) {
|
||||
LogUtils.e(TAG, "initView: 控件绑定失败,请检查布局ID是否正确");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载初始图片
|
||||
mBackgroundView.setBackgroundResource(R.drawable.blank100x100);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListeners();
|
||||
|
||||
LogUtils.d(TAG, "initView: 布局和控件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,10 +185,14 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
|
||||
LogUtils.d(TAG, "onClick: 取消按钮点击");
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.setCurrentSourceToPreview();
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if (listener != null) {
|
||||
listener.onCancel();
|
||||
LogUtils.d(TAG, "onClick: 取消回调已执行");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -160,13 +201,16 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
|
||||
// 确定预览背景资源
|
||||
bvBackgroundPreview.saveToBackgroundSources(previewFilePath);
|
||||
|
||||
LogUtils.d(TAG, "onClick: 确认按钮点击");
|
||||
dismiss(); // 关闭对话框
|
||||
if (TextUtils.isEmpty(mDownloadSavedPath)) {
|
||||
ToastUtils.show("未下载图片。");
|
||||
LogUtils.w(TAG, "onClick: 确认失败,未下载图片");
|
||||
return;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onConfirm();
|
||||
listener.onConfirm(mDownloadSavedPath);
|
||||
LogUtils.d(TAG, String.format("onClick: 确认回调已执行,图片路径:%s", mDownloadSavedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -175,117 +219,120 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认预览点击");
|
||||
LogUtils.d(TAG, "onClick: 预览按钮点击");
|
||||
downloadImageToAlbumAndPreview();
|
||||
/*String url = etURL.getText().toString().trim();
|
||||
if (url.isEmpty()) {
|
||||
ToastUtils.show("请输入图片链接");
|
||||
return;
|
||||
}
|
||||
ImageDownloader.getInstance(mContext).downloadImage(url, mDownloadCallback);*/
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "setButtonClickListeners: 按钮点击监听已设置");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(图片下载、预览) ======================
|
||||
/**
|
||||
* 根据文件路径设置 BackgroundView 背景(主线程调用)
|
||||
* @param filePath 图片文件路径
|
||||
* 下载网络图片并预览
|
||||
*/
|
||||
private void setBackgroundFromPath(String filePath) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
File imageFile = new File(filePath);
|
||||
if (!imageFile.exists()) {
|
||||
LogUtils.e(TAG, "图片文件不存在:" + filePath);
|
||||
ToastUtils.show("Test");
|
||||
//bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览背景
|
||||
previewFilePath = filePath;
|
||||
bvBackgroundPreview.previewBackgroundImage(previewFilePath);
|
||||
|
||||
LogUtils.d(TAG, "图片预览成功:" + filePath);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
|
||||
} finally {
|
||||
// Java7 手动关闭流,避免资源泄漏
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
void downloadImageToAlbumAndPreview() {
|
||||
mPreviewFileUrl = etURL.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mPreviewFileUrl)) {
|
||||
ToastUtils.show("请输入图片URL");
|
||||
LogUtils.w(TAG, "downloadImageToAlbumAndPreview: 图片URL为空");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:修改对话框标题(灵活适配不同场景)
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
if (tvTitle != null) {
|
||||
tvTitle.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:修改对话框内容(灵活适配不同场景)
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
if (tvContent != null) {
|
||||
tvContent.setText(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:设置按钮点击回调(替代带参构造)
|
||||
*/
|
||||
public void setOnDialogClickListener(OnDialogClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/*ImageDownloader.DownloadCallback mDownloadCallback = new ImageDownloader.DownloadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String filePath) {
|
||||
ToastUtils.show("图片下载成功:" + filePath);
|
||||
LogUtils.d(TAG, filePath);
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, filePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
ToastUtils.show("下载失败:" + errorMsg);
|
||||
LogUtils.e(TAG, errorMsg);
|
||||
// 发送图片加载失败消息
|
||||
mUiHandler.sendEmptyMessage(MSG_IMAGE_LOAD_FAILED);
|
||||
}
|
||||
};*/
|
||||
|
||||
void downloadImageToAlbumAndPreview() {
|
||||
//String imgUrl = "https://example.com/test.jpg";
|
||||
String imgUrl = etURL.getText().toString();
|
||||
PictureUtils.downloadImageToAlbum(mContext, imgUrl, new PictureUtils.DownloadCallback(){
|
||||
LogUtils.d(TAG, String.format("downloadImageToAlbumAndPreview: 开始下载图片,URL:%s", mPreviewFileUrl));
|
||||
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String savePath) {
|
||||
ToastUtils.show("下载成功:" + savePath);
|
||||
LogUtils.d(TAG, String.format("onSuccess: 图片下载成功,保存路径:%s", savePath));
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
ToastUtils.show("下载失败:" + e.getMessage());
|
||||
public void onFailure(String errorMsg) {
|
||||
LogUtils.e(TAG, String.format("onFailure: 图片下载失败,错误信息:%s", errorMsg));
|
||||
ToastUtils.show("下载失败:" + errorMsg);
|
||||
// 发送图片加载失败消息
|
||||
Message failMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_FAILED);
|
||||
mUiHandler.sendMessage(failMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 根据文件路径设置 BackgroundView 背景(主线程调用)
|
||||
* @param previewFilePath 图片文件路径
|
||||
*/
|
||||
private void previewBackground(String previewFilePath) {
|
||||
if (TextUtils.isEmpty(previewFilePath)) {
|
||||
LogUtils.w(TAG, "previewBackground: 预览文件路径为空");
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
File imageFile = new File(previewFilePath);
|
||||
if (!imageFile.exists()) {
|
||||
ToastUtils.show("图片文件不存在:" + previewFilePath);
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片文件不存在,路径:%s", previewFilePath));
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览背景
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
|
||||
LogUtils.d(TAG, String.format("previewBackground: 图片预览成功,路径:%s", previewFilePath));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片预览失败,错误信息:%s", e.getMessage()), e);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
} finally {
|
||||
// Java7 手动关闭流,避免资源泄漏
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
LogUtils.d(TAG, "previewBackground: 文件输入流已关闭");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, String.format("previewBackground: 关闭文件输入流失败,错误信息:%s", e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供方法(灵活适配不同场景) ======================
|
||||
/**
|
||||
* 对外提供方法:修改对话框标题
|
||||
* @param title 标题文本
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
if (tvTitle != null && !TextUtils.isEmpty(title)) {
|
||||
tvTitle.setText(title);
|
||||
LogUtils.d(TAG, String.format("setTitle: 对话框标题已修改为:%s", title));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:修改对话框内容
|
||||
* @param content 内容文本
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
if (tvContent != null && !TextUtils.isEmpty(content)) {
|
||||
tvContent.setText(content);
|
||||
LogUtils.d(TAG, String.format("setContent: 对话框内容已修改为:%s", content));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:设置按钮点击回调(替代带参构造)
|
||||
* @param listener 按钮点击回调
|
||||
*/
|
||||
public void setOnDialogClickListener(OnDialogClickListener listener) {
|
||||
this.listener = listener;
|
||||
LogUtils.d(TAG, "setOnDialogClickListener: 按钮点击回调已设置");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/10 19:32:55
|
||||
* @Describe 用户确定与否选择框
|
||||
*/
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
public class YesNoAlertDialog {
|
||||
|
||||
public static final String TAG = "YesNoAlertDialog";
|
||||
|
||||
public static void show(Context context, String szTitle, String szMessage, final OnDialogResultListener listener) {
|
||||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
|
||||
context);
|
||||
|
||||
// set title
|
||||
alertDialogBuilder.setTitle(szTitle);
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setMessage(szMessage)
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener(){
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
listener.onNo();
|
||||
}
|
||||
})
|
||||
.setPositiveButton("YES", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
// if this button is clicked, close
|
||||
// current activity
|
||||
listener.onYes();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("NO", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
// if this button is clicked, just close
|
||||
// the dialog box and do nothing
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
// create alert dialog
|
||||
AlertDialog alertDialog = alertDialogBuilder.create();
|
||||
|
||||
// show it
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
public interface OnDialogResultListener {
|
||||
abstract void onYes();
|
||||
abstract void onNo();
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.views.BatteryDrawable;
|
||||
import cc.winboll.studio.powerbell.views.VerticalSeekBar;
|
||||
|
||||
public class MainViewFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "MainViewFragment";
|
||||
|
||||
public static final int MSG_RELOAD_APPCONFIG = 0;
|
||||
public static final int MSG_CURRENTVALUEBATTERY = 1;
|
||||
|
||||
static MainViewFragment _mMainViewFragment;
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
View mView;
|
||||
Drawable mDrawableFrame;
|
||||
LinearLayout mllLeftSeekBar;
|
||||
LinearLayout mllRightSeekBar;
|
||||
CheckBox mcbIsEnableChargeReminder;
|
||||
CheckBox mcbIsEnableUsegeReminder;
|
||||
Switch mswIsEnableService;
|
||||
TextView mtvTips;
|
||||
|
||||
// 背景布局
|
||||
//LinearLayout mLinearLayoutloadBackground;
|
||||
|
||||
// 现在电量图示
|
||||
BatteryDrawable mCurrentValueBatteryDrawable;
|
||||
// 现在充电提醒电量图示
|
||||
BatteryDrawable mChargeReminderValueBatteryDrawable;
|
||||
// 现在耗电提醒电量图示
|
||||
BatteryDrawable mUsegeReminderValueBatteryDrawable;
|
||||
|
||||
ImageView mCurrentValueBatteryImageView;
|
||||
ImageView mChargeReminderValueBatteryImageView;
|
||||
ImageView mUsegeReminderValueBatteryImageView;
|
||||
|
||||
VerticalSeekBar mChargeReminderSeekBar;
|
||||
ChargeReminderSeekBarChangeListener mChargeReminderSeekBarChangeListener;
|
||||
TextView mtvChargeReminderValue;
|
||||
|
||||
|
||||
VerticalSeekBar mUsegeReminderSeekBar;
|
||||
UsegeReminderSeekBarChangeListener mUsegeReminderSeekBarChangeListener;
|
||||
TextView mtvUsegeReminderValue;
|
||||
CheckBox mcbUsegeReminderValue;
|
||||
TextView mtvCurrentValue;
|
||||
BackgroundView bvPreviewBackground;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
|
||||
_mMainViewFragment = MainViewFragment.this;
|
||||
mAppConfigUtils = App.getAppConfigUtils(getActivity());
|
||||
|
||||
// 获取指定ID的View实例
|
||||
bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
|
||||
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
|
||||
|
||||
// 注册OnGlobalLayoutListener
|
||||
mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
// 获取宽度和高度
|
||||
int width = mainImageView.getMeasuredWidth();
|
||||
int height = mainImageView.getMeasuredHeight();
|
||||
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity());
|
||||
BackgroundPictureBean bean = utils.loadBackgroundPictureBean();
|
||||
bean.setBackgroundWidth(width);
|
||||
bean.setBackgroundHeight(height);
|
||||
utils.saveData();
|
||||
// 移除监听器以避免内存泄漏
|
||||
mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});*/
|
||||
|
||||
mDrawableFrame = getActivity().getDrawable(R.drawable.bg_frame);
|
||||
mllLeftSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout1);
|
||||
mllRightSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout2);
|
||||
|
||||
// 初始化充电电量提醒设置控件
|
||||
mtvChargeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView2);
|
||||
mChargeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
|
||||
mcbIsEnableChargeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox1);
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mtvUsegeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView3);
|
||||
mUsegeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
|
||||
mcbIsEnableUsegeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox2);
|
||||
|
||||
// 初始化现在电量显示控件
|
||||
mtvCurrentValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView4);
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService = (Switch) mView.findViewById(R.id.fragmentandroidviewSwitch1);
|
||||
mtvTips = mView.findViewById(R.id.fragmentandroidviewTextView1);
|
||||
|
||||
// 设置视图显示数据
|
||||
setViewData();
|
||||
// 设置视图控件响应
|
||||
setViewListener();
|
||||
|
||||
// 注册一个广播接收
|
||||
//mMainActivityReceiver = new MainActivityReceiver(this);
|
||||
//mMainActivityReceiver.registerAction();
|
||||
|
||||
// 启动的时候检查一下服务
|
||||
if (mAppConfigUtils.getIsEnableService()
|
||||
&& ServiceUtils.isServiceAlive(getActivity(), ControlCenterService.class.getName()) == false) {
|
||||
// 如果配置了服务启动,服务没有启动
|
||||
// 就启动服务
|
||||
Intent intent = new Intent(getActivity(), ControlCenterService.class);
|
||||
getActivity().startForegroundService(intent);
|
||||
}
|
||||
|
||||
return mView;
|
||||
}
|
||||
|
||||
void setViewData() {
|
||||
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
|
||||
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
|
||||
int nCurrentValue = mAppConfigUtils.getCurrentValue();
|
||||
|
||||
mllLeftSeekBar.setBackground(mDrawableFrame);
|
||||
mllRightSeekBar.setBackground(mDrawableFrame);
|
||||
|
||||
// 初始化电量图
|
||||
mCurrentValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCurrent));
|
||||
mCurrentValueBatteryDrawable.setValue(mAppConfigUtils.getCurrentValue());
|
||||
mCurrentValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView1);
|
||||
mCurrentValueBatteryImageView.setImageDrawable(mCurrentValueBatteryDrawable);
|
||||
|
||||
// 初始化充电电量提醒图
|
||||
mChargeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCharge));
|
||||
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
|
||||
mChargeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView3);
|
||||
mChargeReminderValueBatteryImageView.setImageDrawable(mChargeReminderValueBatteryDrawable);
|
||||
|
||||
// 初始化耗电电量提醒图
|
||||
mUsegeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorUsege));
|
||||
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
|
||||
mUsegeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView2);
|
||||
mUsegeReminderValueBatteryImageView.setImageDrawable(mUsegeReminderValueBatteryDrawable);
|
||||
|
||||
// 初始化充电电量提醒设置控件
|
||||
mtvChargeReminderValue.setTextColor(getActivity().getColor(R.color.colorCharge));
|
||||
//LogUtils.d(TAG, "Color.YELLOW is " + Integer.toString(mApplication.getColor(R.color.colorCharge)));
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
mChargeReminderSeekBar.setProgress(nChargeReminderValue);
|
||||
mcbIsEnableChargeReminder.setChecked(mAppConfigUtils.getIsEnableChargeReminder());
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mtvUsegeReminderValue.setTextColor(getActivity().getColor(R.color.colorUsege));
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
mUsegeReminderSeekBar.setProgress(nUsegeReminderValue);
|
||||
mcbIsEnableUsegeReminder.setChecked(mAppConfigUtils.getIsEnableUsegeReminder());
|
||||
|
||||
// 初始化现在电量显示控件
|
||||
mtvCurrentValue.setTextColor(getActivity().getColor(R.color.colorCurrent));
|
||||
mtvCurrentValue.setText(Integer.toString(nCurrentValue) + "%");
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService.setChecked(mAppConfigUtils.getIsEnableService());
|
||||
if (mAppConfigUtils.getIsEnableService()) {
|
||||
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
|
||||
ControlCenterService.startControlCenterService(getActivity());
|
||||
} else {
|
||||
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
|
||||
ControlCenterService.stopControlCenterService(getActivity());
|
||||
}
|
||||
mswIsEnableService.setText(getString(R.string.txt_aboveswitch));
|
||||
mtvTips.setText(getString(R.string.txt_aboveswitchtips));
|
||||
|
||||
}
|
||||
|
||||
void setViewListener() {
|
||||
// 初始化充电电量提醒设置控件
|
||||
mChargeReminderSeekBarChangeListener = new ChargeReminderSeekBarChangeListener();
|
||||
mChargeReminderSeekBar.setOnSeekBarChangeListener(mChargeReminderSeekBarChangeListener);
|
||||
mcbIsEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "setIsEnableChargeReminder");
|
||||
mAppConfigUtils.setIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
|
||||
//ControlCenterService.updateIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mUsegeReminderSeekBarChangeListener = new UsegeReminderSeekBarChangeListener();
|
||||
mUsegeReminderSeekBar.setOnSeekBarChangeListener(mUsegeReminderSeekBarChangeListener);
|
||||
mcbIsEnableUsegeReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "setIsEnableUsegeReminder");
|
||||
mAppConfigUtils.setIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
|
||||
//ControlCenterService.updateIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService.setOnClickListener(new CompoundButton.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
mAppConfigUtils.setIsEnableService(getActivity(), mswIsEnableService.isChecked());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setCurrentValueBattery(int value) {
|
||||
//LogUtils.d(TAG, "setCurrentValueBattery");
|
||||
mtvCurrentValue.setText(Integer.toString(value) + "%");
|
||||
mCurrentValueBatteryDrawable.setValue(value);
|
||||
mCurrentValueBatteryDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
class ChargeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
//LogUtils.d(TAG, "call onProgressChanged");
|
||||
int nChargeReminderValue = progress;
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
|
||||
mChargeReminderValueBatteryDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStartTrackingTouch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStopTrackingTouch");
|
||||
//取得当前进度条的刻度
|
||||
int nChargeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
|
||||
|
||||
mAppConfigUtils.setChargeReminderValue(nChargeReminderValue);
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
//ControlCenterService.updateChargeReminderValue(nChargeReminderValue);
|
||||
}
|
||||
}
|
||||
|
||||
class UsegeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
//LogUtils.d(TAG, "call onProgressChanged");
|
||||
int nUsegeReminderValue = progress;
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
|
||||
mUsegeReminderValueBatteryDrawable.invalidateSelf();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStartTrackingTouch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStopTrackingTouch");
|
||||
//取得当前进度条的刻度
|
||||
int nUsegeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
|
||||
LogUtils.d(TAG, "nUsegeReminderValue is " + Integer.toString(nUsegeReminderValue));
|
||||
//LogUtils.d(TAG, "mPowerReminder is " + mApplication);
|
||||
mAppConfigUtils.setUsegeReminderValue(nUsegeReminderValue);
|
||||
//LogUtils.d(TAG, "opopopopopopopop");
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
//ControlCenterService.updateUsegeReminderValue(nUsegeReminderValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadBackground() {
|
||||
bvPreviewBackground.reloadBackgroundImage();
|
||||
// BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean();
|
||||
// ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1);
|
||||
// String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName();
|
||||
// File fBackgroundFilePath = new File(szBackgroundFilePath);
|
||||
// LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath);
|
||||
// LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists()));
|
||||
// if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) {
|
||||
// Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath);
|
||||
// //drawableBackground.setAlpha(120);
|
||||
// imageView.setImageDrawable(drawableBackground);
|
||||
// } else {
|
||||
// Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10);
|
||||
// //drawableBackground.setAlpha(120);
|
||||
// imageView.setImageDrawable(drawableBackground);
|
||||
// }
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_RELOAD_APPCONFIG : {
|
||||
setViewData();
|
||||
break;
|
||||
}
|
||||
case MSG_CURRENTVALUEBATTERY : {
|
||||
setCurrentValueBattery(msg.arg1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static void relaodAppConfigs() {
|
||||
if (_mMainViewFragment != null) {
|
||||
Handler handler = _mMainViewFragment.mHandler;
|
||||
handler.sendMessage(handler.obtainMessage(MSG_RELOAD_APPCONFIG));
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMsgCurrentValueBattery(int value) {
|
||||
if (_mMainViewFragment != null) {
|
||||
Handler handler = _mMainViewFragment.mHandler;
|
||||
Message msg = handler.obtainMessage(MSG_CURRENTVALUEBATTERY);
|
||||
msg.arg1 = value;
|
||||
handler.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,35 +2,119 @@ package cc.winboll.studio.powerbell.handlers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* 服务通信Handler
|
||||
* 功能:处理电量提醒消息,构建并发送标准化通知
|
||||
* 特性:弱引用防泄漏、参数严格校验、通知格式统一
|
||||
* 适配:Java7 | API30 | 小米手机
|
||||
*/
|
||||
public class ControlCenterServiceHandler extends Handler {
|
||||
public static final String TAG = ControlCenterServiceHandler.class.getSimpleName();
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ControlCenterServiceHandler";
|
||||
public static final int MSG_REMIND_TEXT = 1001; // 电量提醒消息标识
|
||||
|
||||
public static final int MSG_REMIND_TEXT = 0;
|
||||
// 提醒类型常量
|
||||
private static final String REMIND_TYPE_CHARGE = "+";
|
||||
private static final String REMIND_TYPE_USAGE = "-";
|
||||
|
||||
WeakReference<ControlCenterService> serviceWeakReference;
|
||||
// 电量范围常量
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// 通知文案常量(抽离魔法值,便于统一修改)
|
||||
private static final String CHARGE_REMIND_TITLE = "充电提醒";
|
||||
private static final String USAGE_REMIND_TITLE = "耗电提醒";
|
||||
private static final String CHARGE_REMIND_CONTENT_FORMAT = "(+)电量已达额定值。当前电量%d%%,%s。";
|
||||
private static final String USAGE_REMIND_CONTENT_FORMAT = "(-)电量低于指定值。当前电量%d%%,%s。";
|
||||
private static final String CHARGE_STATE_CHARGING = "充电中";
|
||||
private static final String CHARGE_STATE_NOT_CHARGING = "未充电";
|
||||
|
||||
// ================================== 成员变量区(弱引用防泄漏,final保证不可变)=================================
|
||||
private final WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
|
||||
// ================================== 构造方法(强制传入服务,初始化弱引用)=================================
|
||||
public ControlCenterServiceHandler(ControlCenterService service) {
|
||||
serviceWeakReference = new WeakReference<ControlCenterService>(service);
|
||||
LogUtils.d(TAG, "构造方法执行 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ================================== 核心消息处理(重写handleMessage,解析多参数消息)=================================
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
// 解析消息参数:obj=提醒类型(+/-),arg1=当前电量,arg2=充电状态(1=充电/0=未充电)
|
||||
String remindType = (msg.obj != null) ? (String) msg.obj : "";
|
||||
int currentBattery = msg.arg1;
|
||||
boolean isCharging = msg.arg2 == 1;
|
||||
|
||||
LogUtils.d(TAG, "handleMessage: 接收消息 | what=" + msg.what + " | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 弱引用获取服务,避免内存泄漏
|
||||
ControlCenterService service = mwrControlCenterService.get();
|
||||
if (service == null) {
|
||||
LogUtils.e(TAG, "handleMessage: 服务实例已被GC回收,终止消息处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按消息类型分发处理
|
||||
switch (msg.what) {
|
||||
case MSG_REMIND_TEXT: // 处理下载完成消息,更新UI
|
||||
{
|
||||
// 显示提醒消息
|
||||
//
|
||||
//LogUtils.d(TAG, "显示提醒消息");
|
||||
ControlCenterService controlCenterService = serviceWeakReference.get();
|
||||
if (controlCenterService != null) {
|
||||
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getTitle());
|
||||
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getContent());
|
||||
controlCenterService.appenRemindMSG((String)msg.obj);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_REMIND_TEXT:
|
||||
handleRemindMessage(service, remindType, currentBattery, isCharging);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "handleMessage: 未知消息类型,忽略处理 | what=" + msg.what);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务辅助方法(构建通知并发送,全链路参数校验)=================================
|
||||
/**
|
||||
* 处理电量提醒消息,构建带电量+充电状态的通知并发送
|
||||
* @param service 控制中心服务实例(已校验非空)
|
||||
* @param remindType 提醒类型(+充电/-耗电)
|
||||
* @param currentBattery 当前电量(0-100)
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
private void handleRemindMessage(ControlCenterService service, String remindType, int currentBattery, boolean isCharging) {
|
||||
LogUtils.d(TAG, "handleRemindMessage: 开始处理提醒消息 | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 1. 前置校验:通知工具类+参数有效性
|
||||
if (service.getNotificationManager() == null) {
|
||||
LogUtils.e(TAG, "handleRemindMessage: 通知管理工具类未初始化,无法发送提醒");
|
||||
return;
|
||||
}
|
||||
if (!REMIND_TYPE_CHARGE.equals(remindType) && !REMIND_TYPE_USAGE.equals(remindType)) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 提醒类型无效,忽略 | type=" + remindType + " | 允许值:" + REMIND_TYPE_CHARGE + "/" + REMIND_TYPE_USAGE);
|
||||
return;
|
||||
}
|
||||
if (currentBattery < BATTERY_LEVEL_MIN || currentBattery > BATTERY_LEVEL_MAX) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 电量值超出范围,忽略 | battery=" + currentBattery + " | 允许范围:" + BATTERY_LEVEL_MIN + "-" + BATTERY_LEVEL_MAX);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 构建通知模型,使用统一格式
|
||||
NotificationMessage remindMsg = new NotificationMessage();
|
||||
String chargeStateDesc = isCharging ? CHARGE_STATE_CHARGING : CHARGE_STATE_NOT_CHARGING;
|
||||
if (REMIND_TYPE_CHARGE.equals(remindType)) {
|
||||
remindMsg.setTitle(CHARGE_REMIND_TITLE);
|
||||
remindMsg.setContent(String.format(CHARGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
|
||||
remindMsg.setRemindMSG("charge_remind");
|
||||
} else {
|
||||
remindMsg.setTitle(USAGE_REMIND_TITLE);
|
||||
remindMsg.setContent(String.format(USAGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
|
||||
remindMsg.setRemindMSG("usage_remind");
|
||||
}
|
||||
LogUtils.d(TAG, "handleRemindMessage: 通知模型构建完成 | title=" + remindMsg.getTitle() + " | content=" + remindMsg.getContent());
|
||||
|
||||
// 3. 调用通知工具类发送提醒
|
||||
LogUtils.d(TAG, "handleRemindMessage: 调用通知工具类发送提醒 | remindMSG=" + remindMsg.getRemindMSG());
|
||||
service.getNotificationManager().showRemindNotification(service, remindMsg);
|
||||
LogUtils.d(TAG, "handleRemindMessage: 提醒通知发送流程执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/29 17:24:53
|
||||
* @Describe 应用运行参数类
|
||||
* 适配 API30,支持 Serializable 持久化、Parcelable Intent 传递、JSON 序列化/反序列化
|
||||
* 包含耗电提醒、充电提醒、电量检测、铃声提醒等核心配置
|
||||
*/
|
||||
public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
// 序列化版本号(Serializable 必备,避免反序列化失败)
|
||||
private static final long serialVersionUID = 1L;
|
||||
// 日志标签(全局统一,替换 Log 为 LogUtils)
|
||||
transient public static final String TAG = "AppConfigBean";
|
||||
// 字段校验常量(统一阈值,避免硬编码)
|
||||
private static final int MIN_INTERVAL = 500; // 最小检测间隔(ms)
|
||||
private static final int MIN_REMIND_INTERVAL = 1000; // 最小提醒间隔(ms)
|
||||
private static final int BATTERY_MIN = 0; // 电量最小值
|
||||
private static final int BATTERY_MAX = 100; // 电量最大值
|
||||
private static final int INVALID_BATTERY = -1; // 无效电量标识
|
||||
|
||||
// ====================== 成员变量(按功能分类:提醒配置→电量状态→检测配置) ======================
|
||||
// 耗电提醒配置
|
||||
boolean isEnableUsageReminder = false; // 耗电提醒开关
|
||||
int usageReminderValue = 45; // 耗电提醒阈值(0-100)
|
||||
// 充电提醒配置
|
||||
boolean isEnableChargeReminder = false;// 充电提醒开关
|
||||
int chargeReminderValue = 100; // 充电提醒阈值(0-100)
|
||||
// 铃声提醒配置
|
||||
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms)
|
||||
// 电量状态
|
||||
boolean isCharging = false; // 是否充电
|
||||
int currentBatteryValue = INVALID_BATTERY; // 当前电池电量(统一命名,替代原 currentValue)
|
||||
// 电量检测配置
|
||||
int batteryDetectInterval = 2000; // 电量检测间隔(ms,适配 RemindThread)
|
||||
|
||||
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setEnableChargeReminder(false);
|
||||
setUsageReminderValue(10);
|
||||
setEnableUsageReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
setBatteryDetectInterval(1000); // 默认检测间隔1秒
|
||||
setCurrentBatteryValue(INVALID_BATTERY); // 初始化无效电量标识
|
||||
LogUtils.d(TAG, "AppConfigBean: 初始化默认配置完成");
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法(Setter/Getter,按字段功能分类,补充调试日志) ======================
|
||||
// --------------- 电量状态相关 ---------------
|
||||
/**
|
||||
* 设置当前电池电量(Receiver 监听电池变化时调用)
|
||||
* @param currentBatteryValue 当前电量(0-100)
|
||||
*/
|
||||
public void setCurrentBatteryValue(int currentBatteryValue) {
|
||||
this.currentBatteryValue = (currentBatteryValue >= BATTERY_MIN && currentBatteryValue <= BATTERY_MAX)
|
||||
? currentBatteryValue : INVALID_BATTERY;
|
||||
LogUtils.d(TAG, String.format("setCurrentBatteryValue: 当前电量设置为 %d(输入值:%d)",
|
||||
this.currentBatteryValue, currentBatteryValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前电池电量(RemindThread 同步配置时调用)
|
||||
* @return 当前电量(0-100 或 INVALID_BATTERY)
|
||||
*/
|
||||
public int getCurrentBatteryValue() {
|
||||
return currentBatteryValue;
|
||||
}
|
||||
|
||||
// --------------- 铃声提醒配置相关 ---------------
|
||||
/**
|
||||
* 设置铃声提醒间隔
|
||||
* @param reminderIntervalTime 提醒间隔(ms,不小于 MIN_REMIND_INTERVAL)
|
||||
*/
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setReminderIntervalTime: 提醒间隔设置为 %dms(输入值:%d)",
|
||||
this.reminderIntervalTime, reminderIntervalTime));
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
// --------------- 充电状态相关 ---------------
|
||||
/**
|
||||
* 设置是否充电
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
LogUtils.d(TAG, String.format("setIsCharging: 充电状态设置为 %b", isCharging));
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
// --------------- 耗电提醒配置相关 ---------------
|
||||
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
|
||||
this.isEnableUsageReminder = isEnableUsageReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableUsageReminder: 耗电提醒开关设置为 %b", isEnableUsageReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableUsageReminder() {
|
||||
return isEnableUsageReminder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置耗电提醒阈值
|
||||
* @param usageReminderValue 阈值(0-100)
|
||||
*/
|
||||
public void setUsageReminderValue(int usageReminderValue) {
|
||||
this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setUsageReminderValue: 耗电提醒阈值设置为 %d(输入值:%d)",
|
||||
this.usageReminderValue, usageReminderValue));
|
||||
}
|
||||
|
||||
public int getUsageReminderValue() {
|
||||
return usageReminderValue;
|
||||
}
|
||||
|
||||
// --------------- 充电提醒配置相关 ---------------
|
||||
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableChargeReminder: 充电提醒开关设置为 %b", isEnableChargeReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置充电提醒阈值
|
||||
* @param chargeReminderValue 阈值(0-100)
|
||||
*/
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setChargeReminderValue: 充电提醒阈值设置为 %d(输入值:%d)",
|
||||
this.chargeReminderValue, chargeReminderValue));
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
// --------------- 电量检测配置相关 ---------------
|
||||
/**
|
||||
* 设置电量检测间隔
|
||||
* @param batteryDetectInterval 检测间隔(ms,不小于 MIN_INTERVAL)
|
||||
*/
|
||||
public void setBatteryDetectInterval(int batteryDetectInterval) {
|
||||
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setBatteryDetectInterval: 检测间隔设置为 %dms(输入值:%d)",
|
||||
this.batteryDetectInterval, batteryDetectInterval));
|
||||
}
|
||||
|
||||
public int getBatteryDetectInterval() {
|
||||
return batteryDetectInterval;
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化(兼容旧配置,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AppConfigBean bean = this;
|
||||
// 原有字段序列化
|
||||
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder());
|
||||
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
|
||||
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime());
|
||||
jsonWriter.name("isCharging").value(bean.isCharging());
|
||||
// 兼容旧字段 currentValue,同步新字段 currentBatteryValue
|
||||
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue());
|
||||
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue());
|
||||
// 新增字段序列化
|
||||
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
// 兼容拼写错误字段(isEnableUsegeReminder → isEnableUsageReminder)
|
||||
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
|
||||
bean.setEnableUsageReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
|
||||
bean.setUsageReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("isEnableChargeReminder")) {
|
||||
bean.setEnableChargeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("chargeReminderValue")) {
|
||||
bean.setChargeReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("reminderIntervalTime")) {
|
||||
bean.setReminderIntervalTime(jsonReader.nextInt());
|
||||
} else if (name.equals("isCharging")) {
|
||||
bean.setIsCharging(jsonReader.nextBoolean());
|
||||
} else if (name.equals("currentValue")) {
|
||||
// 优先读取旧字段,兼容历史配置
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 currentValue 完成");
|
||||
} else if (name.equals("currentBatteryValue")) {
|
||||
// 新字段覆盖旧字段,保证数据最新
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 currentBatteryValue 完成");
|
||||
} else if (name.equals("batteryDetectInterval")) {
|
||||
bean.setBatteryDetectInterval(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口实现(API30 Intent 传递必备,补充调试日志) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0; // 无特殊内容描述,固定返回0
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
// 按成员变量顺序写入,boolean 转 byte 存储
|
||||
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
|
||||
dest.writeInt(usageReminderValue);
|
||||
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0));
|
||||
dest.writeInt(chargeReminderValue);
|
||||
dest.writeInt(reminderIntervalTime);
|
||||
dest.writeByte((byte) (isCharging ? 1 : 0));
|
||||
dest.writeInt(currentBatteryValue);
|
||||
dest.writeInt(batteryDetectInterval);
|
||||
LogUtils.d(TAG, "writeToParcel: Parcel 序列化完成");
|
||||
}
|
||||
|
||||
// 反序列化 Creator(必须 public static final 修饰,Java7 适配)
|
||||
public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() {
|
||||
@Override
|
||||
public AppConfigBean createFromParcel(Parcel source) {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
// 按 writeToParcel 顺序读取
|
||||
bean.isEnableUsageReminder = source.readByte() != 0;
|
||||
bean.usageReminderValue = source.readInt();
|
||||
bean.isEnableChargeReminder = source.readByte() != 0;
|
||||
bean.chargeReminderValue = source.readInt();
|
||||
bean.reminderIntervalTime = source.readInt();
|
||||
bean.isCharging = source.readByte() != 0;
|
||||
bean.currentBatteryValue = source.readInt();
|
||||
bean.batteryDetectInterval = source.readInt();
|
||||
LogUtils.d(TAG, "createFromParcel: Parcel 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigBean[] newArray(int size) {
|
||||
return new AppConfigBean[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类
|
||||
* 适配 API30,支持 Serializable 持久化、JSON 序列化/反序列化
|
||||
* 存储正式/预览背景配置,包含原图、压缩图、裁剪比例、像素颜色等核心字段
|
||||
*/
|
||||
public class BackgroundBean extends BaseBean implements Serializable {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
// 日志标签(全局统一,替换 Log 为 LogUtils)
|
||||
public static final String TAG = "BackgroundBean";
|
||||
// 兼容旧字段常量(统一管理,避免硬编码)
|
||||
private static final String OLD_FIELD_USE_SCALED_COMPRESS = "isUseScaledCompress";
|
||||
// 字段默认值常量(统一管理,避免魔法值)
|
||||
private static final int DEFAULT_DIMENSION = 100; // 默认宽高
|
||||
private static final int MIN_DIMENSION = 1; // 最小宽高
|
||||
|
||||
// ====================== 成员变量(按功能分类:原图配置→压缩图配置→控制字段→裁剪配置→像素颜色) ======================
|
||||
// 原图配置
|
||||
private String backgroundFileName = ""; // 背景图片文件名
|
||||
private String backgroundFilePath = ""; // 背景图片完整路径
|
||||
private String backgroundFileInfo = ""; // 图片信息(Uri、网络地址等)
|
||||
// 压缩图配置
|
||||
private String backgroundScaledCompressFileName = ""; // 压缩后背景图片文件名
|
||||
private String backgroundScaledCompressFilePath = ""; // 压缩后背景图片完整路径
|
||||
// 控制字段
|
||||
private boolean isUseBackgroundFile = false; // 是否启用背景图片
|
||||
private boolean isUseBackgroundScaledCompressFile = false; // 是否启用压缩背景图(重命名:原isUseScaledCompress)
|
||||
// 裁剪配置
|
||||
private int backgroundWidth = DEFAULT_DIMENSION; // 背景图宽度
|
||||
private int backgroundHeight = DEFAULT_DIMENSION; // 背景图高度
|
||||
// 像素颜色
|
||||
private int pixelColor = 0; // 拾取的像素颜色(纯色背景用)
|
||||
|
||||
// ====================== 构造方法(无参构造,JSON反序列化必备) ======================
|
||||
/**
|
||||
* 无参构造器(必须,JSON反序列化时需默认构造器)
|
||||
*/
|
||||
public BackgroundBean() {
|
||||
LogUtils.d(TAG, "BackgroundBean: 无参构造初始化完成");
|
||||
}
|
||||
|
||||
// ====================== Getter/Setter 方法(按功能分类,补充调试日志,强化校验) ======================
|
||||
// --------------- 原图配置相关 ---------------
|
||||
public String getBackgroundFileName() {
|
||||
return backgroundFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundFileName(String backgroundFileName) {
|
||||
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFileName: 背景文件名设置为 %s", this.backgroundFileName));
|
||||
}
|
||||
|
||||
public String getBackgroundFilePath() {
|
||||
return backgroundFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundFilePath(String backgroundFilePath) {
|
||||
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFilePath: 背景文件路径设置为 %s", this.backgroundFilePath));
|
||||
}
|
||||
|
||||
public String getBackgroundFileInfo() {
|
||||
return backgroundFileInfo;
|
||||
}
|
||||
|
||||
public void setBackgroundFileInfo(String backgroundFileInfo) {
|
||||
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFileInfo: 背景文件信息设置为 %s", this.backgroundFileInfo));
|
||||
}
|
||||
|
||||
// --------------- 控制字段相关 ---------------
|
||||
public boolean isUseBackgroundFile() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundFile: 是否启用背景图设置为 %b", isUseBackgroundFile));
|
||||
}
|
||||
|
||||
// --------------- 压缩图配置相关 ---------------
|
||||
public String getBackgroundScaledCompressFileName() {
|
||||
return backgroundScaledCompressFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
|
||||
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName;
|
||||
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFileName: 压缩背景文件名设置为 %s", this.backgroundScaledCompressFileName));
|
||||
}
|
||||
|
||||
public String getBackgroundScaledCompressFilePath() {
|
||||
return backgroundScaledCompressFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
|
||||
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath;
|
||||
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFilePath: 压缩背景文件路径设置为 %s", this.backgroundScaledCompressFilePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFile(Getter/Setter同步修改)
|
||||
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
|
||||
*/
|
||||
public boolean isUseBackgroundScaledCompressFile() {
|
||||
return isUseBackgroundScaledCompressFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
|
||||
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundScaledCompressFile: 是否启用压缩背景图设置为 %b", isUseBackgroundScaledCompressFile));
|
||||
}
|
||||
|
||||
// --------------- 裁剪配置相关 ---------------
|
||||
public int getBackgroundWidth() {
|
||||
return backgroundWidth;
|
||||
}
|
||||
|
||||
public void setBackgroundWidth(int backgroundWidth) {
|
||||
this.backgroundWidth = backgroundWidth < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundWidth;
|
||||
LogUtils.d(TAG, String.format("setBackgroundWidth: 背景宽度设置为 %d(输入值:%d)", this.backgroundWidth, backgroundWidth));
|
||||
}
|
||||
|
||||
public int getBackgroundHeight() {
|
||||
return backgroundHeight;
|
||||
}
|
||||
|
||||
public void setBackgroundHeight(int backgroundHeight) {
|
||||
this.backgroundHeight = backgroundHeight < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundHeight;
|
||||
LogUtils.d(TAG, String.format("setBackgroundHeight: 背景高度设置为 %d(输入值:%d)", this.backgroundHeight, backgroundHeight));
|
||||
}
|
||||
|
||||
// --------------- 像素颜色相关 ---------------
|
||||
public int getPixelColor() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
LogUtils.d(TAG, String.format("setPixelColor: 像素颜色设置为 0x%08X", pixelColor));
|
||||
}
|
||||
|
||||
// ====================== 序列化/反序列化方法(适配重命名字段,兼容旧版本,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = BackgroundBean.class.getName();
|
||||
LogUtils.d(TAG, String.format("getName: 类名标识为 %s", className));
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化:同步重命名字段(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile)
|
||||
* 确保新字段能正常持久化,同时兼容旧版本JSON(保留旧字段写入,避免旧版本读取异常)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BackgroundBean bean = this;
|
||||
// 原图配置序列化
|
||||
jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName());
|
||||
jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath());
|
||||
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
|
||||
// 控制字段序列化
|
||||
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
|
||||
// 压缩图配置序列化
|
||||
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
|
||||
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
|
||||
// 关键:新字段序列化(核心)
|
||||
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 兼容旧版本:保留旧字段名写入(避免旧版本Bean读取时缺失字段)
|
||||
jsonWriter.name(OLD_FIELD_USE_SCALED_COMPRESS).value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 裁剪配置与像素颜色序列化
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
jsonWriter.name("pixelColor").value(bean.getPixelColor());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成,已兼容旧字段");
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化:同步处理重命名字段(兼容旧版本JSON,新旧字段都能读取)
|
||||
* 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效)
|
||||
*/
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BackgroundBean bean = new BackgroundBean();
|
||||
jsonReader.beginObject();
|
||||
// 临时变量:存储旧字段值(用于兼容)
|
||||
boolean tempUseScaledCompress = false;
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
switch (name) {
|
||||
case "backgroundFileName":
|
||||
bean.setBackgroundFileName(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundFilePath":
|
||||
bean.setBackgroundFilePath(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundFileInfo":
|
||||
bean.setBackgroundFileInfo(jsonReader.nextString());
|
||||
break;
|
||||
case "isUseBackgroundFile":
|
||||
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
|
||||
break;
|
||||
case "backgroundScaledCompressFileName":
|
||||
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundScaledCompressFilePath":
|
||||
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
|
||||
break;
|
||||
case "isUseBackgroundScaledCompressFile":
|
||||
// 关键:读取新字段(优先)
|
||||
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 isUseBackgroundScaledCompressFile 完成");
|
||||
break;
|
||||
case OLD_FIELD_USE_SCALED_COMPRESS:
|
||||
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
|
||||
tempUseScaledCompress = jsonReader.nextBoolean();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 isUseScaledCompress 完成");
|
||||
break;
|
||||
case "backgroundWidth":
|
||||
bean.setBackgroundWidth(jsonReader.nextInt());
|
||||
break;
|
||||
case "backgroundHeight":
|
||||
bean.setBackgroundHeight(jsonReader.nextInt());
|
||||
break;
|
||||
case "pixelColor":
|
||||
bean.setPixelColor(jsonReader.nextInt());
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
// 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充
|
||||
if (!bean.isUseBackgroundScaledCompressFile()) {
|
||||
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 旧字段值已填充到新字段");
|
||||
}
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== 辅助方法(重置配置、配置校验,补充调试日志) ======================
|
||||
/**
|
||||
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
|
||||
*/
|
||||
public void resetBackgroundConfig() {
|
||||
this.backgroundFileName = "";
|
||||
this.backgroundFilePath = "";
|
||||
this.backgroundScaledCompressFileName = "";
|
||||
this.backgroundScaledCompressFilePath = "";
|
||||
this.backgroundFileInfo = "";
|
||||
this.isUseBackgroundFile = false;
|
||||
this.isUseBackgroundScaledCompressFile = false;
|
||||
this.backgroundWidth = DEFAULT_DIMENSION;
|
||||
this.backgroundHeight = DEFAULT_DIMENSION;
|
||||
LogUtils.d(TAG, "resetBackgroundConfig: 背景配置已重置为默认值");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验)
|
||||
* 同步使用重命名字段判断压缩图是否启用
|
||||
* @return true-配置有效(可显示背景图),false-配置无效
|
||||
*/
|
||||
public boolean isBackgroundConfigValid() {
|
||||
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
|
||||
if (!isUseBackgroundFile) {
|
||||
LogUtils.d(TAG, "isBackgroundConfigValid: 未启用背景图,配置无效");
|
||||
return false;
|
||||
}
|
||||
// 原图校验:路径非空 或 文件名非空
|
||||
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
|
||||
// 压缩图校验:启用压缩图时,路径/文件名需非空
|
||||
boolean isCompressValid = true;
|
||||
if (isUseBackgroundScaledCompressFile()) {
|
||||
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
|
||||
}
|
||||
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
|
||||
boolean isValid = isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
|
||||
LogUtils.d(TAG, String.format("isBackgroundConfigValid: 背景配置有效性为 %b(启用压缩图:%b,原图有效:%b,压缩图有效:%b)",
|
||||
isValid, isUseBackgroundScaledCompressFile(), isOriginalValid, isCompressValid));
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:30:51
|
||||
* @Describe 电池报告数据模型
|
||||
* 适配 API30,存储当前电量、放电时间、充电时间核心数据
|
||||
* 支持参数校验与调试日志输出
|
||||
*/
|
||||
public class BatteryData {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "BatteryData";
|
||||
// 字段校验常量(避免硬编码,统一管理)
|
||||
private static final int BATTERY_MIN = 0;
|
||||
private static final int BATTERY_MAX = 100;
|
||||
private static final String EMPTY_TIME = "00:00:00";
|
||||
|
||||
// ====================== 成员变量(按功能分类:电量→时间) ======================
|
||||
private int currentLevel; // 当前电池电量(0-100)
|
||||
private String dischargeTime; // 放电时间
|
||||
private String chargeTime; // 充电时间
|
||||
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(适配 JSON 反序列化、反射实例化场景)
|
||||
*/
|
||||
public BatteryData() {
|
||||
this.currentLevel = BATTERY_MIN;
|
||||
this.dischargeTime = EMPTY_TIME;
|
||||
this.chargeTime = EMPTY_TIME;
|
||||
LogUtils.d(TAG, "BatteryData: 无参构造初始化完成,默认值已设置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造器(核心构造,初始化所有字段)
|
||||
* @param currentLevel 当前电量(0-100)
|
||||
* @param dischargeTime 放电时间
|
||||
* @param chargeTime 充电时间
|
||||
*/
|
||||
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
|
||||
// 电量范围校验(0-100,异常值置为0)
|
||||
this.currentLevel = currentLevel >= BATTERY_MIN && currentLevel <= BATTERY_MAX
|
||||
? currentLevel : BATTERY_MIN;
|
||||
// 时间字段防 null(空值置为默认空时间)
|
||||
this.dischargeTime = dischargeTime == null ? EMPTY_TIME : dischargeTime;
|
||||
this.chargeTime = chargeTime == null ? EMPTY_TIME : chargeTime;
|
||||
|
||||
// 调试日志:输出入参与最终赋值结果
|
||||
LogUtils.d(TAG, String.format("BatteryData: 带参构造初始化完成 | 当前电量:%d(输入:%d)| 放电时间:%s(输入:%s)| 充电时间:%s(输入:%s)",
|
||||
this.currentLevel, currentLevel,
|
||||
this.dischargeTime, dischargeTime,
|
||||
this.chargeTime, chargeTime));
|
||||
}
|
||||
|
||||
// ====================== Getter 方法(按成员变量顺序排列,补充日志可选) ======================
|
||||
/**
|
||||
* 获取当前电池电量
|
||||
* @return 当前电量(0-100)
|
||||
*/
|
||||
public int getCurrentLevel() {
|
||||
return currentLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取放电时间
|
||||
* @return 放电时间
|
||||
*/
|
||||
public String getDischargeTime() {
|
||||
return dischargeTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充电时间
|
||||
* @return 充电时间
|
||||
*/
|
||||
public String getChargeTime() {
|
||||
return chargeTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 电池信息数据模型
|
||||
* 适配 API30,存储电量时间戳与电量值,支持 JSON 序列化/反序列化
|
||||
* 修复字段拼写错误,补充数据校验与调试日志
|
||||
*/
|
||||
public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "BatteryInfoBean";
|
||||
// 字段校验常量(避免硬编码,统一管理)
|
||||
private static final int BATTERY_MIN = 0;
|
||||
private static final int BATTERY_MAX = 100;
|
||||
private static final long DEFAULT_TIMESTAMP = 0L;
|
||||
private static final int DEFAULT_BATTERY_VALUE = 0;
|
||||
|
||||
// ====================== 成员变量(修复拼写错误:battetyValue → batteryValue) ======================
|
||||
private long timeStamp; // 记录电量的时间戳
|
||||
private int batteryValue; // 电量值(0-100)
|
||||
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(JSON 反序列化、反射实例化必备)
|
||||
*/
|
||||
public BatteryInfoBean() {
|
||||
this.timeStamp = DEFAULT_TIMESTAMP;
|
||||
this.batteryValue = DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, "BatteryInfoBean: 无参构造初始化完成,默认时间戳:" + timeStamp + ",默认电量:" + batteryValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造器(核心构造,初始化所有字段)
|
||||
* @param timeStamp 电量记录时间戳
|
||||
* @param batteryValue 电量值(0-100)
|
||||
*/
|
||||
public BatteryInfoBean(long timeStamp, int batteryValue) {
|
||||
this.timeStamp = timeStamp;
|
||||
// 电量范围校验(0-100,异常值置为默认值)
|
||||
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
|
||||
? batteryValue : DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, String.format("BatteryInfoBean: 带参构造初始化完成 | 时间戳:%d | 电量:%d(输入:%d)",
|
||||
this.timeStamp, this.batteryValue, batteryValue));
|
||||
}
|
||||
|
||||
// ====================== Setter/Getter 方法(按成员变量顺序排列,修复拼写错误,补充日志) ======================
|
||||
/**
|
||||
* 设置电量记录时间戳
|
||||
* @param timeStamp 时间戳
|
||||
*/
|
||||
public void setTimeStamp(long timeStamp) {
|
||||
this.timeStamp = timeStamp;
|
||||
LogUtils.d(TAG, "setTimeStamp: 时间戳设置为 " + timeStamp);
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置电量值(修复拼写错误:battetyValue → batteryValue)
|
||||
* @param batteryValue 电量值(0-100)
|
||||
*/
|
||||
public void setBatteryValue(int batteryValue) {
|
||||
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
|
||||
? batteryValue : DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, String.format("setBatteryValue: 电量设置为 %d(输入:%d)",
|
||||
this.batteryValue, batteryValue));
|
||||
}
|
||||
|
||||
public int getBatteryValue() {
|
||||
return batteryValue;
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化方法(修复字段拼写错误,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = BatteryInfoBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 类名标识为 " + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BatteryInfoBean bean = this;
|
||||
jsonWriter.name("timeStamp").value(bean.getTimeStamp());
|
||||
// 修复 JSON 字段名拼写错误:battetyValue → batteryValue
|
||||
jsonWriter.name("batteryValue").value(bean.getBatteryValue());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BatteryInfoBean bean = new BatteryInfoBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
switch (name) {
|
||||
case "timeStamp":
|
||||
bean.setTimeStamp(jsonReader.nextLong());
|
||||
break;
|
||||
case "batteryValue":
|
||||
bean.setBatteryValue(jsonReader.nextInt());
|
||||
break;
|
||||
// 兼容旧字段名(battetyValue),避免旧配置解析失败
|
||||
case "battetyValue":
|
||||
int oldBatteryValue = jsonReader.nextInt();
|
||||
bean.setBatteryValue(oldBatteryValue);
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 读取旧字段 battetyValue,已兼容为 batteryValue,值:" + oldBatteryValue);
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知字段 " + name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 15:55
|
||||
* @Describe 服务控制参数模型
|
||||
* 适配 API30,管理服务启用状态,支持 Serializable 持久化、Parcelable 组件传递、JSON 序列化解析
|
||||
*/
|
||||
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
|
||||
// ====================== 静态常量(置顶统一管理,避免魔法值) ======================
|
||||
private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
|
||||
private static final String TAG = "ControlCenterServiceBean";
|
||||
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService"; // JSON 字段常量,避免硬编码
|
||||
|
||||
// ====================== 核心成员变量(私有封装,规范命名) ======================
|
||||
private boolean isEnableService = false; // 服务启用状态:true=启用,false=禁用
|
||||
|
||||
// ====================== Parcelable 静态创建器(必须 public static final,适配 API30 组件传递) ======================
|
||||
public static final Parcelable.Creator<ControlCenterServiceBean> CREATOR = new Parcelable.Creator<ControlCenterServiceBean>() {
|
||||
@Override
|
||||
public ControlCenterServiceBean createFromParcel(Parcel source) {
|
||||
boolean isEnable = source.readByte() != 0;
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable);
|
||||
LogUtils.d(TAG, String.format("createFromParcel: 反序列化完成,isEnableService=%b", isEnable));
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlCenterServiceBean[] newArray(int size) {
|
||||
LogUtils.d(TAG, String.format("newArray: 创建数组,长度=%d", size));
|
||||
return new ControlCenterServiceBean[size];
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 构造方法(无参+有参,满足不同初始化场景) ======================
|
||||
/**
|
||||
* 无参构造(JSON解析、反射创建必备)
|
||||
*/
|
||||
public ControlCenterServiceBean() {
|
||||
this.isEnableService = false;
|
||||
LogUtils.d(TAG, "无参构造:初始化服务状态为禁用(false)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 有参构造(指定服务启用状态)
|
||||
* @param isEnableService 服务启用状态
|
||||
*/
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
LogUtils.d(TAG, String.format("有参构造:初始化服务状态,isEnableService=%b", isEnableService));
|
||||
}
|
||||
|
||||
// ====================== Getter/Setter 方法(封装成员变量,控制访问) ======================
|
||||
public boolean isEnableService() {
|
||||
LogUtils.d(TAG, String.format("isEnableService: 当前状态=%b", isEnableService));
|
||||
return isEnableService;
|
||||
}
|
||||
|
||||
public void setIsEnableService(boolean isEnableService) {
|
||||
LogUtils.d(TAG, String.format("setIsEnableService: 旧状态=%b,新状态=%b", this.isEnableService, isEnableService));
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
// ====================== 父类 BaseBean 方法重写(核心业务逻辑:JSON 序列化/反序列化) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = ControlCenterServiceBean.class.getName();
|
||||
LogUtils.d(TAG, String.format("getName: 返回类名=%s", className));
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 JSON(适配数据持久化/网络传输)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_SERVICE).value(this.isEnableService);
|
||||
LogUtils.d(TAG, String.format("writeThisToJsonWriter: 序列化完成,%s=%b", JSON_FIELD_IS_ENABLE_SERVICE, this.isEnableService));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 反序列化创建对象(适配数据恢复)
|
||||
*/
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) {
|
||||
boolean isEnable = jsonReader.nextBoolean();
|
||||
bean.setIsEnableService(isEnable);
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader: 读取字段,%s=%b", fieldName, isEnable));
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段=%s", fieldName));
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口方法实现(适配 Intent 组件间传递,Java7 适配) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
LogUtils.d(TAG, "describeContents: 返回内容描述符=0");
|
||||
return 0; // 无特殊内容(如文件描述符),返回0即可(API30 标准实现)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 Parcel(Intent 传递必备,Java7 适配:用 byte 存储 boolean)
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
byte flag = (byte) (this.isEnableService ? 1 : 0);
|
||||
dest.writeByte(flag);
|
||||
LogUtils.d(TAG, String.format("writeToParcel: 序列化完成,isEnableService=%b(存储为byte=%d)", this.isEnableService, flag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 通知数据模型
|
||||
* 适配 API30,统一存储通知标题、内容、标识信息,支持各组件数据传递
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
|
||||
*/
|
||||
public class NotificationMessage {
|
||||
// ====================== 静态常量(统一管理) ======================
|
||||
private static final String TAG = "NotificationMessage";
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
// ====================== 核心成员变量(按业务逻辑排序) ======================
|
||||
private String title; // 通知标题
|
||||
private String content; // 通知内容
|
||||
private String remindMSG; // 通知标识(区分服务运行/充电/耗电)
|
||||
|
||||
// ====================== 构造方法(无参+全参,满足不同初始化场景) ======================
|
||||
/**
|
||||
* 无参构造器(反射实例化、JSON反序列化必备)
|
||||
*/
|
||||
public NotificationMessage() {
|
||||
this.title = EMPTY_STRING;
|
||||
this.content = EMPTY_STRING;
|
||||
this.remindMSG = EMPTY_STRING;
|
||||
LogUtils.d(TAG, "无参构造:初始化通知数据模型,默认值为空字符串");
|
||||
}
|
||||
|
||||
/**
|
||||
* 全参构造器(直接传参创建实例,简化调用)
|
||||
* @param title 通知标题
|
||||
* @param content 通知内容
|
||||
* @param remindMSG 通知标识
|
||||
*/
|
||||
public NotificationMessage(String title, String content, String remindMSG) {
|
||||
this.title = title == null ? EMPTY_STRING : title;
|
||||
this.content = content == null ? EMPTY_STRING : content;
|
||||
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
|
||||
LogUtils.d(TAG, String.format("全参构造:初始化完成 | 标题:%s | 内容:%s | 标识:%s",
|
||||
this.title, this.content, this.remindMSG));
|
||||
}
|
||||
|
||||
// ====================== Setter 方法(补充空值防护与调试日志) ======================
|
||||
public void setTitle(String title) {
|
||||
this.title = title == null ? EMPTY_STRING : title;
|
||||
LogUtils.d(TAG, String.format("setTitle:通知标题设置为「%s」", this.title));
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content == null ? EMPTY_STRING : content;
|
||||
LogUtils.d(TAG, String.format("setContent:通知内容设置为「%s」", this.content));
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
|
||||
LogUtils.d(TAG, String.format("setRemindMSG:通知标识设置为「%s」", this.remindMSG));
|
||||
}
|
||||
|
||||
// ====================== Getter 方法(按成员变量顺序排列) ======================
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return remindMSG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,83 +5,254 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.beans.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:23
|
||||
* @Describe 控制中心广播接收器
|
||||
* 功能:监听电池状态变化、前台通知更新、配置变更指令
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
*/
|
||||
public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
public static final String TAG = ControlCenterServiceReceiver.class.getSimpleName();
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "ControlCenterServiceReceiver";
|
||||
|
||||
public static final String ACTION_UPDATE_SERVICENOTIFICATION = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_NOTIFICATION";
|
||||
public static final String ACTION_START_REMINDTHREAD = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_REMINDTHREAD";
|
||||
// 广播Action常量(带包名前缀防冲突)
|
||||
public static final String ACTION_UPDATE_FOREGROUND_NOTIFICATION = "cc.winboll.studio.powerbell.action.ACTION_UPDATE_FOREGROUND_NOTIFICATION";
|
||||
public static final String ACTION_APPCONFIG_CHANGED = "cc.winboll.studio.powerbell.action.ACTION_APPCONFIG_CHANGED";
|
||||
public static final String EXTRA_APP_CONFIG_BEAN = "extra_app_config_bean";
|
||||
|
||||
WeakReference<ControlCenterService> mwrService;
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
static volatile boolean _mIsCharging = false;
|
||||
// 广播优先级与电量范围常量
|
||||
private static final int BROADCAST_PRIORITY = IntentFilter.SYSTEM_HIGH_PRIORITY - 10;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
|
||||
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
|
||||
|
||||
// ====================== 成员变量区(弱引用防泄漏,按功能分层) ======================
|
||||
private WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
private boolean isRegistered = false; // 标记广播注册状态,避免冗余操作
|
||||
|
||||
// ====================== 构造方法(初始化弱引用,避免服务强引用泄漏) ======================
|
||||
public ControlCenterServiceReceiver(ControlCenterService service) {
|
||||
mwrService = new WeakReference<ControlCenterService>(service);
|
||||
LogUtils.d(TAG, String.format("构造接收器 | 服务实例:%s", service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,分Action分发处理) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(ACTION_UPDATE_SERVICENOTIFICATION)) {
|
||||
mwrService.get().updateServiceNotification();
|
||||
} else if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
|
||||
boolean isCharging = BatteryUtils.isCharging(intent);
|
||||
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
|
||||
if (mwrService.get().getRemindThread() != null) {
|
||||
// 先设置提醒进程电池状态标志
|
||||
if (_mIsCharging != isCharging) {
|
||||
mwrService.get().getRemindThread().setIsCharging(isCharging);
|
||||
}
|
||||
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mwrService.get().getRemindThread().setQuantityOfElectricity(nTheQuantityOfElectricity);
|
||||
}
|
||||
String action = intent != null ? intent.getAction() : "null";
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action:%s", action));
|
||||
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null || action == null) {
|
||||
LogUtils.e(TAG, "onReceive: 参数无效(context=" + context + " | intent=" + intent + "),终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 弱引用获取服务,双重校验服务有效性
|
||||
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
|
||||
if (service == null || service.isDestroyed()) {
|
||||
LogUtils.e(TAG, "onReceive: 服务已销毁或为空,注销广播");
|
||||
unregisterAction(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 分Action处理业务逻辑
|
||||
switch (action) {
|
||||
case Intent.ACTION_BATTERY_CHANGED:
|
||||
handleBatteryStateChanged(service, intent);
|
||||
break;
|
||||
case ACTION_UPDATE_FOREGROUND_NOTIFICATION:
|
||||
handleUpdateForegroundNotification(service);
|
||||
break;
|
||||
case ACTION_APPCONFIG_CHANGED:
|
||||
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播");
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, String.format("onReceive: 未知Action=%s", action));
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ====================== 业务处理方法(按功能拆分,强化容错与日志) ======================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param service 控制中心服务实例
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
|
||||
try {
|
||||
// 1. 解析并校验当前电池状态
|
||||
boolean currentCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
// 新电池状态标志某一个有变化就更新显示信息
|
||||
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mwrService.get().updateServiceNotification();
|
||||
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
|
||||
appConfigUtils.loadAppConfigBean();
|
||||
AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean;
|
||||
appConfigBean.setCurrentValue(nTheQuantityOfElectricity);
|
||||
appConfigBean.setIsCharging(isCharging);
|
||||
mwrService.get().startRemindThread(appConfigBean);
|
||||
|
||||
// 保存电池报告
|
||||
// 示例数据更新逻辑
|
||||
// List<BatteryData> newData = new ArrayList<>(adapter.getDataList());
|
||||
// newData.add(0, new BatteryData(percentage, "00:00:00", "00:00:00"));
|
||||
// adapter.updateData(newData);
|
||||
|
||||
// 保存好新的电池状态标志
|
||||
_mIsCharging = isCharging;
|
||||
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
|
||||
}
|
||||
} else if (intent.getAction().equals(ACTION_START_REMINDTHREAD)) {
|
||||
LogUtils.d(TAG, "ACTION_START_REMINDTHREAD");
|
||||
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
|
||||
//appConfigUtils.loadAppConfigBean();
|
||||
AppConfigBean appConfigBean = (AppConfigBean)intent.getSerializableExtra("appConfigBean");
|
||||
mwrService.get().startRemindThread(appConfigBean);
|
||||
|
||||
// 3. 更新静态缓存状态,保证多线程可见
|
||||
sIsCharging = currentCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
|
||||
// 4. 同步缓存状态到配置
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 处理成功 | 缓存电量=%d%% | 缓存充电状态=%b", sLastBatteryLevel, sIsCharging));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
/**
|
||||
* 处理配置变更通知,同步缓存状态到配置
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置");
|
||||
try {
|
||||
// 加载最新配置
|
||||
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
|
||||
if (latestConfig == null) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=%d | 耗电阈值=%d",
|
||||
latestConfig.getChargeReminderValue(), latestConfig.getUsageReminderValue()));
|
||||
|
||||
// 同步缓存的电池状态到配置
|
||||
latestConfig.setCurrentBatteryValue(sLastBatteryLevel);
|
||||
latestConfig.setIsCharging(sIsCharging);
|
||||
service.notifyAppConfigUpdate(latestConfig);
|
||||
|
||||
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=%d%% | 充电状态=%b", sLastBatteryLevel, sIsCharging));
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前台服务通知更新
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleUpdateForegroundNotification(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知");
|
||||
try {
|
||||
NotificationManagerUtils notifyUtils = service.getNotificationManager();
|
||||
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
|
||||
|
||||
// 非空校验,避免空指针
|
||||
if (notifyUtils == null || notifyMsg == null) {
|
||||
LogUtils.e(TAG, String.format("handleUpdateForegroundNotification: 通知工具类或消息为空(notifyUtils=%s | notifyMsg=%s)", notifyUtils, notifyMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
notifyUtils.updateForegroundServiceNotify(notifyMsg);
|
||||
LogUtils.d(TAG, String.format("handleUpdateForegroundNotification: 前台通知更新成功 | 标题=%s", notifyMsg.getTitle()));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(ACTION_UPDATE_SERVICENOTIFICATION);
|
||||
filter.addAction(ACTION_START_REMINDTHREAD);
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
context.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 注册广播接收器");
|
||||
if (context == null || isRegistered) {
|
||||
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(ACTION_UPDATE_FOREGROUND_NOTIFICATION);
|
||||
filter.addAction(ACTION_APPCONFIG_CHANGED);
|
||||
filter.setPriority(BROADCAST_PRIORITY);
|
||||
|
||||
context.registerReceiver(this, filter);
|
||||
isRegistered = true;
|
||||
LogUtils.d(TAG, String.format("registerAction: 广播注册成功 | 优先级=%d", BROADCAST_PRIORITY));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播接收器");
|
||||
if (context == null || !isRegistered) {
|
||||
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
isRegistered = false;
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 资源释放与Getter方法(按需开放,防泄漏) ======================
|
||||
/**
|
||||
* 主动释放资源,避免内存泄漏
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 释放广播接收器资源");
|
||||
// 清空弱引用,帮助GC回收
|
||||
if (mwrControlCenterService != null) {
|
||||
mwrControlCenterService.clear();
|
||||
mwrControlCenterService = null;
|
||||
LogUtils.d(TAG, "release: 弱引用已清空");
|
||||
}
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sIsCharging = false;
|
||||
LogUtils.d(TAG, "release: 静态状态缓存已重置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上次记录的电池电量
|
||||
* @return 电量值(0-100),未初始化返回-1
|
||||
*/
|
||||
public static int getLastBatteryLevel() {
|
||||
return sLastBatteryLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上次记录的充电状态
|
||||
* @return true=充电中,false=未充电
|
||||
*/
|
||||
public static boolean isLastCharging() {
|
||||
return sIsCharging;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,63 +4,177 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationHelper;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:13
|
||||
* @Describe 全局应用广播接收器
|
||||
* 功能:监听系统电池状态变化,同步状态到配置工具类,通知页面更新
|
||||
* 适配:Java7 | API30 | 内存泄漏防护
|
||||
*/
|
||||
public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "GlobalApplicationReceiver";
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
App mGlobalApplication;
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
static volatile boolean _mIsCharging = false;
|
||||
// 保存当前实例,
|
||||
// 便利封装 registerAction() 函数
|
||||
GlobalApplicationReceiver mReceiver;
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
private static volatile int sLastBatteryLevel = -1; // 历史电量(0-100)
|
||||
private static volatile boolean sLastIsCharging = false; // 历史充电状态
|
||||
|
||||
// ====================== 成员变量区(按功能分层,移除冗余的mCurrentReceiver) ======================
|
||||
private App mGlobalApplication;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
|
||||
// ====================== 构造方法(强化参数校验,初始化核心依赖) ======================
|
||||
public GlobalApplicationReceiver(App globalApplication) {
|
||||
mReceiver = this;
|
||||
mGlobalApplication = globalApplication;
|
||||
mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, String.format("构造接收器 | App实例:%s", globalApplication));
|
||||
if (globalApplication == null) {
|
||||
LogUtils.e(TAG, "构造失败:App实例为空");
|
||||
throw new IllegalArgumentException("App cannot be null");
|
||||
}
|
||||
this.mGlobalApplication = globalApplication;
|
||||
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, String.format("构造完成 | AppConfigUtils:%s", mAppConfigUtils));
|
||||
}
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,过滤电池状态广播) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
|
||||
// 先设置好新电池状态标志
|
||||
boolean isCharging = BatteryUtils.isCharging(intent);
|
||||
if (_mIsCharging != isCharging) {
|
||||
mAppConfigUtils.setIsCharging(isCharging);
|
||||
String action = intent != null ? intent.getAction() : "null";
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | 上下文:%s | Action:%s", context, action));
|
||||
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null || action == null) {
|
||||
LogUtils.e(TAG, "onReceive: 参数无效,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理电池状态变化广播
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
|
||||
handleBatteryStateChanged(context, intent);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面) ======================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param context 上下文
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(Context context, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
|
||||
try {
|
||||
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
|
||||
boolean currentIsCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentIsCharging, currentBatteryLevel));
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
|
||||
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mAppConfigUtils.setCurrentValue(nTheQuantityOfElectricity);
|
||||
|
||||
// 3. 同步最新状态到配置工具类
|
||||
if (mAppConfigUtils != null) {
|
||||
if (currentIsCharging != sLastIsCharging) {
|
||||
mAppConfigUtils.setCharging(currentIsCharging);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步充电状态 | %b", currentIsCharging));
|
||||
}
|
||||
if (currentBatteryLevel != sLastBatteryLevel) {
|
||||
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步电量 | %d%%", currentBatteryLevel));
|
||||
}
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空,同步失败");
|
||||
}
|
||||
// 新电池状态标志某一个有变化就更新显示信息
|
||||
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
// 电池状态改变先取消旧的提醒消息
|
||||
//NotificationHelper.cancelRemindNotification(context);
|
||||
|
||||
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
|
||||
MainViewFragment.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
|
||||
// 保存好新的电池状态标志
|
||||
_mIsCharging = isCharging;
|
||||
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
|
||||
|
||||
// 4. 执行状态变化后的业务逻辑
|
||||
// 记录电量变化时间
|
||||
if (App.getAppCacheUtils(context) != null) {
|
||||
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
|
||||
}
|
||||
// 通知MainActivity更新电量
|
||||
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 发送电量更新消息到MainActivity | %d%%", currentBatteryLevel));
|
||||
|
||||
// 5. 更新历史状态缓存
|
||||
sLastIsCharging = currentIsCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
*/
|
||||
public void registerAction() {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(mReceiver, filter);
|
||||
LogUtils.d(TAG, "registerAction: 注册广播");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注册失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先注销再注册,避免重复注册异常
|
||||
unregisterAction();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器
|
||||
*/
|
||||
public void unregisterAction() {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注销失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mGlobalApplication.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 资源释放方法(主动释放,彻底避免内存泄漏) ======================
|
||||
/**
|
||||
* 释放接收器资源,供App销毁时调用
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 释放接收器资源");
|
||||
// 注销广播
|
||||
unregisterAction();
|
||||
// 置空引用,帮助GC回收
|
||||
mGlobalApplication = null;
|
||||
mAppConfigUtils = null;
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sLastIsCharging = false;
|
||||
LogUtils.d(TAG, "release: 资源释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.receivers;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/06 15:01:39
|
||||
* @Describe 应用广播消息接收类
|
||||
*/
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -14,30 +9,84 @@ import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/06 15:01:39
|
||||
* @Describe 应用核心广播接收器
|
||||
* 功能:监听开机完成广播,实现服务开机自启
|
||||
* 适配:Java7 | API30 | 服务启动兼容性处理
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "MainReceiver";
|
||||
// 系统广播Action常量
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
// API版本常量(适配前台服务启动要求)
|
||||
private static final int API_LEVEL_26 = 26;
|
||||
|
||||
static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
// 历史电量值,用于校验电量变化(暂未使用,保留扩展能力)
|
||||
private static volatile int sLastBatteryLevel = -1;
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,分Action处理) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
boolean isEnableService = App.getAppConfigUtils(context).getIsEnableService();
|
||||
if (isEnableService) {
|
||||
if (ServiceUtils.isServiceAlive(context.getApplicationContext(), ControlCenterService.class.getName()) == false) {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService");
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(new Intent(context, ControlCenterService.class));
|
||||
} else {
|
||||
context.startService(new Intent(context, ControlCenterService.class));
|
||||
}
|
||||
}
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null) {
|
||||
LogUtils.e(TAG, "onReceive: 上下文或意图为空,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action:%s", action));
|
||||
|
||||
// 仅处理开机完成广播
|
||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
handleBootCompleted(context);
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("onReceive: 忽略未知Action:%s", action));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 业务处理方法(处理开机完成广播,实现服务自启) ======================
|
||||
/**
|
||||
* 处理开机完成广播,自动启动控制中心服务
|
||||
* @param context 上下文
|
||||
*/
|
||||
private void handleBootCompleted(Context context) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 开始处理开机完成广播");
|
||||
try {
|
||||
// 1. 校验服务启用状态
|
||||
boolean isServiceEnabled = App.getAppConfigUtils(context).isServiceEnabled();
|
||||
LogUtils.d(TAG, String.format("handleBootCompleted: 服务启用状态:%b", isServiceEnabled));
|
||||
if (!isServiceEnabled) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务未启用,跳过自启");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 校验服务是否已运行
|
||||
String serviceClassName = ControlCenterService.class.getName();
|
||||
boolean isServiceAlive = ServiceUtils.isServiceAlive(context.getApplicationContext(), serviceClassName);
|
||||
LogUtils.d(TAG, String.format("handleBootCompleted: 服务运行状态:%b", isServiceAlive));
|
||||
if (isServiceAlive) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务已运行,无需重复启动");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 按API版本启动服务(适配前台服务要求)
|
||||
Intent serviceIntent = new Intent(context, ControlCenterService.class);
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
LogUtils.d(TAG, "handleBootCompleted: 启动前台服务(API >= 26)");
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
LogUtils.d(TAG, "handleBootCompleted: 启动普通服务(API < 26)");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务自启处理完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBootCompleted: 服务自启失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user