Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84c6271310 | |||
| a4ab864381 | |||
| 217a27cbcd | |||
| 561abd2398 | |||
| c832cbd1ac | |||
| 98b815f55a | |||
| 97643c3bcd | |||
| 43ed19b364 | |||
| 9a873bf162 | |||
| 2b7108940b | |||
| 9cc211ec51 | |||
| c26f267774 | |||
| a1a337558e | |||
| 8fe7444065 | |||
| 61a20f6811 | |||
| b6a820b281 | |||
| f591db6611 | |||
| 268688b8d8 | |||
| 416079c356 | |||
| c1d2158578 | |||
| 01e4e8031b | |||
| 181e3e8a34 | |||
| 5614848a65 | |||
| 63d365b175 | |||
| 2dafa7bf9f | |||
| be52292203 | |||
| fc9f15c70c | |||
| b872da5dcc | |||
| de94b23acb | |||
| 6f80e86031 | |||
| 09854f3333 | |||
| 498b2e0eae | |||
| 0800a0e935 |
@@ -25,30 +25,33 @@ android {
|
||||
applicationId "cc.winboll.studio.contacts"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionCode 2
|
||||
// versionName 更新后需要手动设置
|
||||
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.3"
|
||||
versionName "15.14"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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'
|
||||
|
||||
|
||||
// 米盟
|
||||
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'
|
||||
|
||||
// 权限请求框架:https://github.com/getActivity/XXPermissions
|
||||
api 'com.github.getActivity:XXPermissions:18.63'
|
||||
// 下拉控件
|
||||
@@ -65,8 +68,6 @@ 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'
|
||||
|
||||
@@ -84,4 +85,15 @@ 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 Nov 03 12:01:02 HKT 2025
|
||||
stageCount=22
|
||||
#Mon Dec 15 20:54:20 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=
|
||||
baseVersion=15.3
|
||||
publishVersion=15.3.21
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.3.22
|
||||
baseBetaVersion=15.14.2
|
||||
|
||||
138
contacts/proguard-rules.pro
vendored
138
contacts/proguard-rules.pro
vendored
@@ -9,9 +9,135 @@
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep 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
|
||||
|
||||
|
||||
@@ -9,33 +9,41 @@
|
||||
<!-- 拨打电话 -->
|
||||
<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"/>
|
||||
|
||||
<!-- 重新设置外拨电话的路径 -->
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
|
||||
|
||||
<!-- 读取联系人 -->
|
||||
<!-- 联系人权限(适配 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.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"
|
||||
@@ -51,11 +59,8 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
@@ -65,7 +70,6 @@
|
||||
android:label="CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -74,89 +78,92 @@
|
||||
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="cc.winboll.studio.contacts.services.MainService"
|
||||
android:exported="true"/>
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<service android:name="cc.winboll.studio.contacts.services.AssistantService"/>
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:stopWithTask="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:exported="false"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name=".service.CallShowService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver">
|
||||
|
||||
<!-- 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="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<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:exported="true"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<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
|
||||
@@ -165,14 +172,11 @@
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener">
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
@@ -183,7 +187,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/studio_provider"/>
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
@@ -194,3 +198,4 @@
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -1,59 +1,313 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public void addActivity(Activity activity) {
|
||||
activities.add(activity);
|
||||
// 私有构造,禁止外部实例化
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除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() {
|
||||
if (activities.isEmpty()) {
|
||||
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());
|
||||
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() {
|
||||
if (!activities.isEmpty()) {
|
||||
activities.remove(activities.size() - 1).finish();
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void finishActivity(Activity activity) {
|
||||
if (activity != null) {
|
||||
activities.remove(activity);
|
||||
activity.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(Class activityClass) {
|
||||
for (Activity activity : activities) {
|
||||
if (activity.getClass().equals(activityClass)) {
|
||||
finishActivity(activity);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 销毁指定类的所有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); // 清理无效残留
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁栈中所有Activity(退出应用/清空栈场景用)
|
||||
*/
|
||||
public void finishAllActivity() {
|
||||
if (!activities.isEmpty()) {
|
||||
for (Activity activity : activities) {
|
||||
activity.finish();
|
||||
activities.remove(activity);
|
||||
}
|
||||
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 (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,10 +5,9 @@ package cc.winboll.studio.contacts;
|
||||
* @Date 2024/12/08 15:10:51
|
||||
* @Describe 全局应用类
|
||||
*/
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
@@ -16,22 +15,19 @@ public class App extends GlobalApplication {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// 必须在调用基类前设置应用调试标志,
|
||||
// 这样可以预先设置日志与数据的存储根目录。
|
||||
//setIsDebuging(BuildConfig.DEBUG);
|
||||
super.onCreate();
|
||||
// 设置 WinBoLL 应用 UI 类型
|
||||
getWinBoLLActivityManager().setWinBoLLUI_TYPE(WinBoLLActivityManager.WinBoLLUI_TYPE.Aplication);
|
||||
|
||||
//LogUtils.d(TAG, "onCreate");
|
||||
|
||||
// 设置应用调试标志
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
// 初始化窗口管理类
|
||||
WinBoLLActivityManager.init(this);
|
||||
// 初始化 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,23 +1,19 @@
|
||||
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.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
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;
|
||||
@@ -25,64 +21,79 @@ 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.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.activities.WinBollActivity;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
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.AppGoToSettingsUtil;
|
||||
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.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;
|
||||
|
||||
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||
/**
|
||||
* @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 {
|
||||
|
||||
// ====================== 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";
|
||||
|
||||
static MainActivity _MainActivity;
|
||||
LogView mLogView;
|
||||
Toolbar mToolbar;
|
||||
CheckBox cbMainService;
|
||||
MainServiceBean mMainServiceBean;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
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 // 通话记录读取(新增,核心修复)
|
||||
};
|
||||
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;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private List<View> views;
|
||||
private ImageView[] imageViews;
|
||||
private LinearLayout linearLayout;
|
||||
|
||||
// ====================== 5. 业务逻辑成员区 ======================
|
||||
private int currentPoint = 0;
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
// 记录已初始化的Fragment位置(避免重复初始化)
|
||||
private boolean[] isFragmentInit;
|
||||
|
||||
// ====================== 6. 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -93,102 +104,235 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 7. 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "===== onCreate: 主Activity开始创建 =====");
|
||||
_MainActivity = this;
|
||||
|
||||
// 优先检查所有必需权限(含新增的 READ_CALL_LOG)
|
||||
if (!checkAllRequiredPermissions()) {
|
||||
requestAllRequiredPermissions();
|
||||
} else {
|
||||
initUIAndLogic(savedInstanceState);
|
||||
// 直接初始化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: 广告栏资源已恢复");
|
||||
}
|
||||
|
||||
//ToastUtils.show("onCreate");
|
||||
}
|
||||
|
||||
// 权限检查方法(无需修改,自动包含新增的 READ_CALL_LOG)
|
||||
private boolean checkAllRequiredPermissions() {
|
||||
for (String permission : REQUIRED_PERMISSIONS) {
|
||||
if (ActivityCompat.checkSelfPermission(this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 主Activity开始销毁 =====");
|
||||
// 释放广告资源
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
LogUtils.d(TAG, "onDestroy: 广告栏资源已释放");
|
||||
}
|
||||
return true;
|
||||
// 清空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);
|
||||
}
|
||||
|
||||
// 权限结果回调(无需修改,确保所有权限(含 READ_CALL_LOG)都通过才加载UI)
|
||||
// ====================== 8. 权限相关回调函数区 ======================
|
||||
@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) {
|
||||
boolean allPermissionsGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allPermissionsGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPermissionsGranted) {
|
||||
initUIAndLogic(null);
|
||||
String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions);
|
||||
if (deniedPerms.length() == 0) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功");
|
||||
checkAndRequestRemainingPermissions();
|
||||
} else {
|
||||
// 关键修改2:更新提示文案,告知用户新增的“通话记录权限”
|
||||
showPermissionDeniedDialogAndExit();
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 被拒权限:" + deniedPerms);
|
||||
showPermissionDeniedDialogAndExit("应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 核心修改:新增“设置权限”按钮,点击调用 AppGoToSettingsUtil 跳转设置页
|
||||
private void showPermissionDeniedDialogAndExit() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("权限不足,无法使用")
|
||||
// 文案修改:明确新增“通话记录读取”权限
|
||||
.setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。")
|
||||
.setCancelable(false)
|
||||
// 新增:左侧“设置权限”按钮(先添加的按钮在左侧)
|
||||
.setNegativeButton("设置权限", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
// 调用工具类跳转应用设置页(按需求实现)
|
||||
AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil();
|
||||
appGoToSettingsUtil.GoToSetting(MainActivity.this);
|
||||
}
|
||||
})
|
||||
// 原有:右侧“确定退出”按钮(后添加的按钮在右侧)
|
||||
.setPositiveButton("确定退出", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化UI和逻辑(无需修改,权限通过后才加载 CallLogFragment)
|
||||
/**
|
||||
* 处理悬浮窗权限申请结果
|
||||
*/
|
||||
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() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择去设置权限");
|
||||
PermissionUtils.goAppDetailsSettings(MainActivity.this);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择退出应用");
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
// ====================== 9. UI与业务逻辑初始化区 ======================
|
||||
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>();
|
||||
// CallLogFragment 仅在权限通过后才实例化(避免提前触发读取)
|
||||
|
||||
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
|
||||
fragmentList.add(CallLogFragment.newInstance(0));
|
||||
fragmentList.add(ContactsFragment.newInstance(1));
|
||||
fragmentList.add(LogFragment.newInstance(2));
|
||||
@@ -196,41 +340,172 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
tabTitleList.add("联系人");
|
||||
tabTitleList.add("应用日志");
|
||||
|
||||
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||
// 初始化懒加载标记数组(默认均未初始化)
|
||||
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);
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(0); // 关闭预加载,避免提前初始化 CallLogFragment
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
// 关闭预加载(设为0仅加载当前页,关键)
|
||||
viewPager.setOffscreenPageLimit(0);
|
||||
viewPager.addOnPageChangeListener(this);
|
||||
|
||||
// 原有服务启动、电话监听等逻辑...
|
||||
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);
|
||||
}
|
||||
// 关键优化:延迟50ms初始化首屏(确保Fragment已完成onCreateView,控件绑定就绪)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initFragmentByPosition(0);
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: 延迟初始化首屏Fragment,位置=0");
|
||||
}
|
||||
}, 50);
|
||||
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: ViewPager初始化完成,等待延迟初始化首屏");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据位置初始化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);
|
||||
}
|
||||
}
|
||||
|
||||
// 以下为原有代码(无需修改)
|
||||
private class MyPagerAdapter extends FragmentPagerAdapter {
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
// ====================== 10. 菜单相关函数区 ======================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu: 菜单加载完成");
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
public MyPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, 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) {
|
||||
super(fm);
|
||||
this.fragmentList = fragmentList;
|
||||
this.tabTitleList = tabTitleList;
|
||||
LogUtils.d(MainActivity.TAG, "LazyLoadPagerAdapter: 初始化完成,Fragment数量=" + fragmentList.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -247,108 +522,8 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return tabTitleList.get(position);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
// 【已删除】移除setPrimaryItem方法,避免与手动初始化+onPageSelected回调冲突
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
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.winboll.APPInfo;
|
||||
import cc.winboll.studio.libaes.winboll.AboutView;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
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;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:15:54
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AboutActivity";
|
||||
private static final String BRANCH_NAME = "contacts";
|
||||
|
||||
Context mContext;
|
||||
Toolbar mToolbar;
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Toolbar mToolbar;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -35,58 +42,75 @@ public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 关于页面开始创建");
|
||||
|
||||
mContext = this;
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 初始化关于页面视图
|
||||
initAboutView();
|
||||
// 注册Activity管理
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
|
||||
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);
|
||||
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面开始销毁");
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面销毁完成");
|
||||
}
|
||||
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "contacts";
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
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");
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("Contacts");
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=4&extra=page%3D1");
|
||||
appInfo.setAppGitAPPBranch(BRANCH_NAME);
|
||||
appInfo.setAppGitAPPSubProjectFolder(BRANCH_NAME);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
|
||||
appInfo.setAppAPKName("Contacts");
|
||||
appInfo.setAppAPKFolderName("Contacts");
|
||||
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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;
|
||||
@@ -20,99 +15,144 @@ 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";
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallActivity";
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private EditText phoneNumberEditText;
|
||||
private TextView callStatusTextView;
|
||||
private Button dialButton;
|
||||
|
||||
// ====================== 业务成员区 ======================
|
||||
private TelephonyManager telephonyManager;
|
||||
private MyPhoneStateListener phoneStateListener;
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
//setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面开始创建");
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
phoneNumberEditText = findViewById(R.id.phone_number);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
callStatusTextView = findViewById(R.id.call_status);
|
||||
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化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) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
callStatusTextView.setText("电话已挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
callStatusTextView.setText("正在通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
callStatusTextView.setText("来电: " + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 初始化控件
|
||||
initViews();
|
||||
// 初始化电话状态监听
|
||||
initPhoneStateListener();
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 取消监听
|
||||
if (telephonyManager != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
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 = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
// ====================== 核心业务函数区 ======================
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,80 @@
|
||||
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);
|
||||
|
||||
phoneNumberEditText = findViewById(R.id.phone_number_edit_text);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
// 初始化UI控件与点击事件
|
||||
initViews();
|
||||
LogUtils.d(TAG, "onCreate: 拨号盘页面初始化完成");
|
||||
}
|
||||
|
||||
@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();
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
@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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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;
|
||||
@@ -24,49 +20,59 @@ 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.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
/**
|
||||
* @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 static final String TAG = "SettingsActivity";
|
||||
// API版本硬编码(替代Build.VERSION_CODES,适配Java7)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
|
||||
Toolbar mToolbar;
|
||||
Switch swSilent;
|
||||
SeekBar msbVolume;
|
||||
TextView mtvVolume;
|
||||
int mnStreamMaxVolume;
|
||||
int mnStreamVolume;
|
||||
Switch mswMainService;
|
||||
static DuInfoTextView _DuInfoTextView;
|
||||
// ====================== 静态成员属性区 ======================
|
||||
private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀
|
||||
|
||||
// 云盾防御层数量
|
||||
EditText etDunTotalCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
EditText etDunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
EditText etDunResumeCount;
|
||||
// 是否启用云盾
|
||||
Switch swIsEnableDun;
|
||||
// ====================== 数据业务属性区 ======================
|
||||
private int mStreamMaxVolume; // 铃音最大音量
|
||||
private int mStreamVolume; // 当前铃音音量
|
||||
private List<PhoneConnectRuleBean> mRuleList; // 通话规则列表
|
||||
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private PhoneConnectRuleAdapter adapter;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
// ====================== 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; // 号码查询输入框
|
||||
|
||||
// ====================== 接口实现区(IWinBoLLActivity规范实现) ======================
|
||||
@Override
|
||||
public AppCompatActivity getActivity() {
|
||||
return this;
|
||||
@@ -77,268 +83,531 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区(按执行顺序排列) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 设置页面启动");
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
// 初始化工具栏
|
||||
mToolbar = findViewById(R.id.activitymainToolbar1);
|
||||
// 初始化核心流程(按优先级执行)
|
||||
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);
|
||||
setSupportActionBar(mToolbar);
|
||||
// 显示后退按钮
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getTag());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
msbVolume = findViewById(R.id.bellvolume);
|
||||
mtvVolume = findViewById(R.id.tv_volume);
|
||||
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// 设置SeekBar的最大值为系统铃声音量的最大刻度
|
||||
mnStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setMax(mnStreamMaxVolume);
|
||||
// 获取当前铃声音量并设置为SeekBar的初始进度
|
||||
mnStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setProgress(mnStreamVolume);
|
||||
|
||||
updateStreamVolumeTextView();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// 当开始拖动SeekBar时的操作
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// 当停止拖动SeekBar时的操作
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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) {
|
||||
Rules.getInstance(this).getPhoneBlacRuleBeanList().add(new PhoneConnectRuleModel());
|
||||
Rules.getInstance(this).saveRules();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void onDefaultPhone(View view) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onCanDrawOverlays(View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !Settings.canDrawOverlays(this)) {
|
||||
// 请求 悬浮框 权限
|
||||
askForDrawOverlay();
|
||||
} else {
|
||||
ToastUtils.show("悬浮窗已开启");
|
||||
}
|
||||
}
|
||||
|
||||
public void onResetBoBullToonURL(View view) {
|
||||
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();
|
||||
}
|
||||
|
||||
public void onDownloadBoBullToon(View view) {
|
||||
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());
|
||||
// 显示后退按钮(空指针防护)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
}
|
||||
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (tomCat.downloadBoBullToon()) {
|
||||
LogUtils.d(TAG, "BoBullToon downlaod OK!");
|
||||
MainService.restartMainService(SettingsActivity.this);
|
||||
Rules.getInstance(SettingsActivity.this).reload();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void onSearchBoBullToonPhone(View view) {
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
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 {
|
||||
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();
|
||||
// 后退按钮点击事件(Java7匿名内部类)
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗管理设置界面
|
||||
* 初始化主服务开关(联动MainService启停)
|
||||
*/
|
||||
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();
|
||||
}
|
||||
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);
|
||||
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// 空指针防护:AudioManager获取失败直接返回
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化音量参数
|
||||
mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
mSbVolume.setMax(mStreamMaxVolume);
|
||||
mSbVolume.setProgress(mStreamVolume);
|
||||
updateVolumeDisplay(); // 更新音量文本显示
|
||||
|
||||
// 音量调节监听
|
||||
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 onAbout(View view) {
|
||||
App.getWinBoLLActivityManager().startWinBoLLActivity(this, AboutActivity.class);
|
||||
// ====================== 点击事件回调区(按功能模块归类) ======================
|
||||
/**
|
||||
* 云盾开关点击事件(联动参数编辑权限+配置保存)
|
||||
*/
|
||||
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("参数不能为空");
|
||||
}
|
||||
|
||||
// 转换参数并保存
|
||||
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);
|
||||
|
||||
// 提示信息
|
||||
String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话";
|
||||
ToastUtils.show(toastMsg);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e);
|
||||
ToastUtils.show("参数格式错误,请输入整数");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存开关状态并刷新配置
|
||||
dunSettings.setIsEnableDun(isChecked);
|
||||
rules.saveDun();
|
||||
rules.reload();
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成");
|
||||
}
|
||||
|
||||
public void onLogView(View view) {
|
||||
App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
|
||||
/**
|
||||
* 添加新通话规则(黑白名单)
|
||||
*/
|
||||
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() + "条规则");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转默认电话应用设置
|
||||
*/
|
||||
public void onDefaultPhone(View view) {
|
||||
LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置");
|
||||
startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮窗权限检查与请求
|
||||
*/
|
||||
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();
|
||||
} else {
|
||||
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: 地址重置完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载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;
|
||||
}
|
||||
|
||||
// 校验并更新地址
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询号码是否为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();
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +1,145 @@
|
||||
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 Activity {
|
||||
public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "UnitTestActivity";
|
||||
|
||||
LogView logView;
|
||||
// ====================== UI控件区 ======================
|
||||
private LogView logView;
|
||||
private EditText etPhone;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@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);
|
||||
logView = findViewById(R.id.logview);
|
||||
|
||||
// 初始化控件
|
||||
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.start();
|
||||
LogUtils.d(TAG, "initViews: LogView已启动");
|
||||
}
|
||||
|
||||
// ====================== 点击事件测试函数区 ======================
|
||||
/**
|
||||
* 测试单个号码匹配规则
|
||||
*/
|
||||
public void onTestPhone(View view) {
|
||||
// 开始测试数据
|
||||
EditText etPhone = findViewById(R.id.phone_et);
|
||||
Rules rules = Rules.getInstance(this);
|
||||
LogUtils.d(TAG, "onTestPhone: 开始测试单个号码规则匹配");
|
||||
String phone = etPhone.getText().toString().trim();
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
}
|
||||
|
||||
public void onTestMain(View view) {
|
||||
LogUtils.d(TAG, "IntUtils.unittest_getIntInRange();");
|
||||
IntUtils.unittest_getIntInRange();
|
||||
|
||||
Rules rules = Rules.getInstance(this);
|
||||
|
||||
// 如果没有规则就添加测试规则
|
||||
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
|
||||
// 手机号码允许
|
||||
// 中国手机号码正则表达式,以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();
|
||||
if (phone.isEmpty()) {
|
||||
LogUtils.w(TAG, "onTestPhone: 测试号码为空,跳过匹配");
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始测试数据
|
||||
String phone = "16769764848";
|
||||
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));
|
||||
}
|
||||
|
||||
phone = "16856582777";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
/**
|
||||
* 批量测试预设号码规则匹配
|
||||
*/
|
||||
public void onTestMain(View view) {
|
||||
LogUtils.d(TAG, "onTestMain: 开始批量测试号码规则匹配");
|
||||
// 测试IntUtils工具类方法
|
||||
LogUtils.d(TAG, "onTestMain: 执行 IntUtils.unittest_getIntInRange() 测试");
|
||||
IntUtils.unittest_getIntInRange();
|
||||
|
||||
phone = "17519703124";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 初始化规则实例
|
||||
Rules rules = Rules.getInstance(this);
|
||||
// 无规则时添加测试规则集
|
||||
initTestRulesIfEmpty(rules);
|
||||
|
||||
phone = "0205658955";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 预设测试号码列表
|
||||
String[] testPhones = {
|
||||
"16769764848", "16856582777", "17519703124",
|
||||
"0205658955", "0108965253", "+8616769764848",
|
||||
"4005816769764848", "95566"
|
||||
};
|
||||
|
||||
phone = "0108965253";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 遍历测试号码并输出结果
|
||||
for (String phone : testPhones) {
|
||||
boolean isAllowed = rules.isAllowed(phone);
|
||||
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
|
||||
}
|
||||
|
||||
phone = "+8616769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// ====================== 私有工具函数区 ======================
|
||||
/**
|
||||
* 规则集为空时初始化测试规则
|
||||
*/
|
||||
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);
|
||||
|
||||
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)));
|
||||
// 保存规则到本地
|
||||
rules.saveRules();
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
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.beans.AESThemeBean;
|
||||
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.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
@@ -29,14 +33,24 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
//LogUtils.d(TAG, "onCreate: 基类页面开始创建");
|
||||
// 优先设置主题,再执行父类初始化
|
||||
// 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())))];
|
||||
@@ -44,17 +58,27 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用当前主题样式
|
||||
*/
|
||||
void setThemeStyle() {
|
||||
//setTheme(AESThemeBean.getThemeStyle(getThemeType()));
|
||||
LogUtils.d(TAG, "setThemeStyle: 开始设置应用主题");
|
||||
// 替换原注释逻辑,使用AESThemeUtil获取的主题ID
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
LogUtils.d(TAG, "setThemeStyle: 主题设置完成");
|
||||
}
|
||||
|
||||
// ====================== 菜单与导航函数区 ======================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if(item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "onOptionsItemSelected: 菜单选项点击,itemId=" + item.getItemId());
|
||||
// 处理导航栏返回按钮点击事件
|
||||
// if (item.getItemId() == android.R.id.home) {
|
||||
// LogUtils.d(TAG, "onOptionsItemSelected: 点击导航返回按钮,关闭当前页面");
|
||||
// finish();
|
||||
// return true;
|
||||
// }
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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;
|
||||
@@ -13,125 +8,167 @@ 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.beans.CallLogModel;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.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;
|
||||
ContactUtils mContactUtils;
|
||||
Context mContext;
|
||||
private ContactUtils mContactUtils;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public CallLogAdapter(Context context, List<CallLogModel> callLogList) {
|
||||
mContext = context;
|
||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
||||
LogUtils.d(TAG, "CallLogAdapter: 初始化适配器,数据量=" + callLogList.size());
|
||||
this.mContext = context;
|
||||
this.callLogList = callLogList;
|
||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
||||
}
|
||||
|
||||
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);
|
||||
holder.phoneNumber.setText(callLog.getPhoneNumber() + "☎" + mContactUtils.getContactsName(callLog.getPhoneNumber()));
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
|
||||
// 绑定通话号码与联系人名称
|
||||
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
|
||||
String phoneText = callLog.getPhoneNumber() + "☎" + (contactName == null ? "" : contactName);
|
||||
holder.phoneNumber.setText(phoneText);
|
||||
|
||||
// 号码长按弹出菜单事件
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View p1) {
|
||||
// 弹出复制菜单
|
||||
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();
|
||||
|
||||
showPhonePopupMenu(holder.phoneNumber, callLog);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
holder.callStatus.setText(callLog.getCallStatus());
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
holder.callDate.setText(dateFormat.format(callLog.getCallDate()));
|
||||
|
||||
// 初始化拉动后拨号控件
|
||||
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);
|
||||
}
|
||||
});
|
||||
// 绑定通话状态与时间
|
||||
holder.callStatus.setText(callLog.getCallStatus());
|
||||
holder.callDate.setText(DATE_FORMAT.format(callLog.getCallDate()));
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, callLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return callLogList.size();
|
||||
return callLogList == null ? 0 : 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, callStatus, callDate;
|
||||
Button dialButton;
|
||||
TextView phoneNumber;
|
||||
TextView callStatus;
|
||||
TextView callDate;
|
||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
||||
|
||||
public CallLogViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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;
|
||||
@@ -20,111 +15,142 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.beans.ContactModel;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.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 static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<ContactModel> contactList;
|
||||
Context mContext;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public ContactAdapter(Context context, List<ContactModel> contactList) {
|
||||
mContext = context;
|
||||
LogUtils.d(TAG, "ContactAdapter: 初始化适配器,联系人数量=" + contactList.size());
|
||||
this.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.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
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());
|
||||
|
||||
// 初始化拉动后拨号控件
|
||||
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);
|
||||
}
|
||||
});
|
||||
// 长按联系人条目弹出操作菜单
|
||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
showContactPopupMenu(holder.llPhoneNumberMain, contact);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return contactList.size();
|
||||
// 增加空指针判断,避免空列表崩溃
|
||||
return contactList == null ? 0 : 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);
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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,10 +1,5 @@
|
||||
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;
|
||||
@@ -17,226 +12,230 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.views.LeftScrollView;
|
||||
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.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 context;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<PhoneConnectRuleBean> mRuleList;
|
||||
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleModel> ruleList) {
|
||||
this.context = context;
|
||||
this.ruleList = ruleList;
|
||||
// ====================== 构造函数区 ======================
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleBean> ruleList) {
|
||||
LogUtils.d(TAG, "PhoneConnectRuleAdapter: 初始化适配器,规则数量=" + ruleList.size());
|
||||
this.mContext = context;
|
||||
this.mRuleList = ruleList;
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
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(parent, view);
|
||||
return new EditViewHolder(view);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
|
||||
final PhoneConnectRuleModel model = ruleList.get(position);
|
||||
final PhoneConnectRuleBean model = mRuleList.get(position);
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定规则数据,position=" + position + ",视图类型=" + getItemViewType(position));
|
||||
|
||||
if (holder instanceof SimpleViewHolder) {
|
||||
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;
|
||||
// }
|
||||
// });
|
||||
bindSimpleViewHolder((SimpleViewHolder) holder, model, position);
|
||||
} else if (holder instanceof EditViewHolder) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
bindEditViewHolder((EditViewHolder) holder, model, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return ruleList.size();
|
||||
return mRuleList == null ? 0 : mRuleList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
PhoneConnectRuleModel model = ruleList.get(position);
|
||||
// 这里可以根据模型的状态来决定视图类型,简单起见,假设点击按钮后进入编辑视图
|
||||
return model.isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
return mRuleList.get(position).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 {
|
||||
|
||||
private final LeftScrollView scrollView;
|
||||
private final TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
LeftScrollView scrollView;
|
||||
TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
CheckBox checkBoxEnable;
|
||||
|
||||
|
||||
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
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 = (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.setContentWidth(parent.getWidth());
|
||||
//scrollView.setContentWidth(600);
|
||||
scrollView.addContentLayout(viewContent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||
@@ -245,17 +244,14 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
|
||||
CheckBox checkBoxEnable;
|
||||
Button buttonConfirm;
|
||||
|
||||
public EditViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
public EditViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCheckBoxTouchListener(CheckBox checkBox) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
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.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
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.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
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.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";
|
||||
|
||||
ArrayList<PhoneConnectRuleModel> _PhoneConnectRuleModelList;
|
||||
static volatile Rules _Rules;
|
||||
// 单例核心:volatile 保证多线程可见性,禁止指令重排
|
||||
private static volatile Rules sInstance;
|
||||
// 上下文需使用 ApplicationContext 避免内存泄漏
|
||||
private static Context sApplicationContext;
|
||||
|
||||
ArrayList<PhoneConnectRuleBean> _PhoneConnectRuleModelList;
|
||||
Context mContext;
|
||||
SettingsModel mSettingsModel;
|
||||
SettingsBean mSettingsModel;
|
||||
Timer mDunResumeTimer;
|
||||
|
||||
Rules(Context context) {
|
||||
mContext = context;
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleModel>();
|
||||
/**
|
||||
* 私有化构造方法,禁止外部 new 实例
|
||||
*/
|
||||
private Rules(Context context) {
|
||||
mContext = context.getApplicationContext();
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleBean>();
|
||||
reload();
|
||||
}
|
||||
|
||||
public static synchronized Rules getInstance(Context context) {
|
||||
if (_Rules == null) {
|
||||
_Rules = new Rules(context);
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,线程安全)
|
||||
* @param context 上下文,建议传入 ApplicationContext
|
||||
* @return Rules 唯一实例
|
||||
*/
|
||||
public static Rules getInstance(Context context) {
|
||||
// 第一次校验:无锁,提高性能
|
||||
if (sInstance == null) {
|
||||
// 加锁:保证多线程下仅初始化一次
|
||||
synchronized (Rules.class) {
|
||||
// 第二次校验:防止多线程并发时重复创建
|
||||
if (sInstance == null) {
|
||||
sInstance = new Rules(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _Rules;
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
@@ -57,32 +77,35 @@ public class Rules {
|
||||
|
||||
// 盾牌恢复定时器
|
||||
mDunResumeTimer = new Timer();
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsModel.MIN_INTRANGE, SettingsModel.MAX_INTRANGE);
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsBean.MIN_INTRANGE, SettingsBean.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();
|
||||
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();
|
||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
||||
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
}
|
||||
}
|
||||
}, 1000, ss);
|
||||
}
|
||||
|
||||
public void loadRules() {
|
||||
_PhoneConnectRuleModelList.clear();
|
||||
PhoneConnectRuleModel.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
PhoneConnectRuleBean.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
}
|
||||
|
||||
public void saveRules() {
|
||||
LogUtils.d(TAG, String.format("saveRules()"));
|
||||
PhoneConnectRuleModel.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
PhoneConnectRuleBean.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
}
|
||||
|
||||
public void resetDefaultBoBullToonURL() {
|
||||
@@ -100,16 +123,16 @@ public class Rules {
|
||||
}
|
||||
|
||||
public void loadDun() {
|
||||
mSettingsModel = SettingsModel.loadBean(mContext, SettingsModel.class);
|
||||
mSettingsModel = SettingsBean.loadBean(mContext, SettingsBean.class);
|
||||
if (mSettingsModel == null) {
|
||||
mSettingsModel = new SettingsModel();
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
mSettingsModel = new SettingsBean();
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveDun() {
|
||||
LogUtils.d(TAG, String.format("saveDun()"));
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
|
||||
public boolean isAllowed(String phoneNumber) {
|
||||
@@ -119,8 +142,7 @@ public class Rules {
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// 以下是云盾防御体系
|
||||
// 云盾防御体系
|
||||
boolean isDefend = false; // 盾牌是否生效
|
||||
boolean isConnect = true; // 防御结果是否连接
|
||||
|
||||
@@ -189,10 +211,7 @@ public class Rules {
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
} else if (isDefend) {
|
||||
// 如果触发了以上某个防御模块,
|
||||
// 就减少防御盾牌层数。
|
||||
// 每校验一次规则,云盾防御层数减1
|
||||
// 当云盾防御层数为0时,再次进行以下程序段则恢复满值防御。
|
||||
// 如果触发了以上某个防御模块,减少防御盾牌层数
|
||||
int newDunCount = nDunCurrentCount;
|
||||
LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount));
|
||||
|
||||
@@ -203,7 +222,7 @@ public class Rules {
|
||||
} else {
|
||||
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
|
||||
LogUtils.d(TAG, String.format("盾值不在[0,%d]区间,恢复防御最大值%d", mSettingsModel.getDunTotalCount(), mSettingsModel.getDunTotalCount()));
|
||||
}
|
||||
}
|
||||
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
@@ -211,18 +230,35 @@ 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 PhoneConnectRuleModel(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleBean(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
}
|
||||
|
||||
public ArrayList<PhoneConnectRuleModel> getPhoneBlacRuleBeanList() {
|
||||
public ArrayList<PhoneConnectRuleBean> getPhoneBlacRuleBeanList() {
|
||||
return _PhoneConnectRuleModelList;
|
||||
}
|
||||
|
||||
public SettingsModel getSettingsModel() {
|
||||
public SettingsBean 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,10 +1,5 @@
|
||||
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;
|
||||
@@ -24,42 +19,48 @@ 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.beans.CallLogModel;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
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 = "CallFragment";
|
||||
// ====================== 常量定义区 ======================
|
||||
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;
|
||||
|
||||
// ====================== 静态成员区 ======================
|
||||
static volatile CallLogFragment _CallLogFragment;
|
||||
|
||||
public static final int MSG_UPDATE = 1; // 添加消息常量
|
||||
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
|
||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
||||
// ====================== UI控件与适配器区 ======================
|
||||
private RecyclerView recyclerView;
|
||||
private CallLogAdapter callLogAdapter;
|
||||
private List<CallLogModel> callLogList = new ArrayList<>();
|
||||
private List<CallLogModel> callLogList = new ArrayList<CallLogModel>();
|
||||
|
||||
// 添加Handler
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
readCallLog(); // 接收到消息时更新通话记录
|
||||
}
|
||||
}
|
||||
};
|
||||
// ====================== 业务逻辑成员区 ======================
|
||||
private Handler mHandler;
|
||||
// 懒加载标记:记录当前Fragment是否已初始化数据(避免重复加载)
|
||||
private boolean isDataInited = false;
|
||||
|
||||
// ====================== 单例与实例化函数区 ======================
|
||||
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();
|
||||
@@ -68,67 +69,159 @@ public class CallLogFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_call_log, container, false);
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@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 onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
recyclerView = view.findViewById(R.id.recyclerView);
|
||||
LogUtils.d(TAG, "onViewCreated: 视图创建完成,仅初始化控件(不加载数据)");
|
||||
// 初始化RecyclerView(仅绑定控件、设置布局管理器,不设置数据/发起请求)
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
// 初始化适配器(传入空列表,后续懒加载时更新数据)
|
||||
callLogAdapter = new CallLogAdapter(getContext(), callLogList);
|
||||
recyclerView.setAdapter(callLogAdapter);
|
||||
|
||||
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触发更新
|
||||
}
|
||||
LogUtils.d(TAG, "onViewCreated: RecyclerView控件初始化完成(未加载数据)");
|
||||
}
|
||||
|
||||
@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) {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE); // 通过Handler触发更新
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 通话记录权限授予成功,开始加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 通话记录权限被拒绝,无法加载数据");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供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() {
|
||||
callLogList.clear(); // 清空原有数据
|
||||
Cursor cursor = requireContext().getContentResolver().query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC");
|
||||
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);
|
||||
|
||||
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));
|
||||
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: 游标已关闭");
|
||||
}
|
||||
cursor.close();
|
||||
callLogAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,27 +238,21 @@ public class CallLogFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mHandler.removeCallbacksAndMessages(null); // 清理Handler防止内存泄漏
|
||||
}
|
||||
|
||||
// ====================== 外部调用函数区 ======================
|
||||
public void triggerUpdate() {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
LogUtils.d(TAG, "triggerUpdate: 外部触发通话记录更新");
|
||||
if (isDataInited) { // 已初始化才触发更新(避免未加载时调用)
|
||||
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,11 +1,5 @@
|
||||
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;
|
||||
@@ -29,42 +23,55 @@ 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.beans.ContactModel;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.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();
|
||||
@@ -72,61 +79,154 @@ 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) {
|
||||
// 加载布局(已移除进度条相关代码)
|
||||
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
return view;
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
return inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// 初始化RecyclerView
|
||||
LogUtils.d(TAG, "onViewCreated: 开始初始化UI控件(仅绑定,不加载数据/功能)");
|
||||
// 初始化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();
|
||||
if (!isViewInitialized) {
|
||||
initSearchAndDial(); // 初始化搜索和拨号功能
|
||||
checkContactPermission(); // 检查权限并加载数据
|
||||
isViewInitialized = true;
|
||||
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: 恢复缓存数据,列表已显示");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化搜索框和拨号按钮
|
||||
// ====================== 权限相关函数区 ======================
|
||||
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(300) {
|
||||
// 搜索防抖监听
|
||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(DEBOUNCE_DELAY) {
|
||||
@Override
|
||||
public void onDebounceTextChanged(String query) {
|
||||
filterContacts(query);
|
||||
@@ -142,68 +242,58 @@ 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) {
|
||||
recyclerView.setVisibility(View.GONE); // 加载中隐藏列表
|
||||
|
||||
LogUtils.d(TAG, "loadContacts: 无缓存,异步读取联系人数据");
|
||||
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()));
|
||||
|
||||
// 数据加载后显示列表
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
isDataLoaded = true;
|
||||
|
||||
LogUtils.d(TAG, "loadContacts: 联系人数据加载完成,共" + contactList.size() + "条");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,12 +301,11 @@ 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[]{
|
||||
@@ -231,66 +320,52 @@ 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.d(TAG, "读取联系人失败:" + e);
|
||||
LogUtils.e(TAG, "readContactsInBackground: 读取联系人异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, "readContactsInBackground: 游标已关闭");
|
||||
}
|
||||
}
|
||||
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());
|
||||
@@ -301,17 +376,13 @@ 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() {
|
||||
@@ -322,33 +393,9 @@ 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,10 +1,5 @@
|
||||
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;
|
||||
@@ -13,18 +8,34 @@ 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;
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
|
||||
// ====================== 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();
|
||||
@@ -32,30 +43,76 @@ 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);
|
||||
mLogView = view.findViewById(R.id.logview);
|
||||
mLogView.start();
|
||||
// Java7 适配:添加强制类型转换,仅初始化LogView控件(不启动)
|
||||
mLogView = (LogView) view.findViewById(R.id.logview);
|
||||
LogUtils.d(TAG, "onCreateView: LogView控件初始化完成(未启动)");
|
||||
// 标记视图控件绑定完成
|
||||
isViewInitialized = true;
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//ToastUtils.show("onResume");
|
||||
mLogView.start();
|
||||
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逻辑迁移至此)
|
||||
mLogView.start();
|
||||
isLogViewStarted = true;
|
||||
// 标记懒加载总流程完成(仅执行一次)
|
||||
isLazyInitCompleted = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化完成,LogView正常启动");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ 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;
|
||||
@@ -14,198 +16,377 @@ 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 {
|
||||
|
||||
private View phoneCallView;
|
||||
private TextView tvCallNumber;
|
||||
private Button btnOpenApp;
|
||||
// ====================== 常量定义区(精准适配API29-30,无冗余) ======================
|
||||
public static final String TAG = "CallListenerService";
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams params;
|
||||
// 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 PhoneStateListener phoneStateListener;
|
||||
private TelephonyManager telephonyManager;
|
||||
// 延迟初始化参数(让出主线程,避免启动阻塞)
|
||||
private static final long DELAY_INIT_MS = 100L;
|
||||
|
||||
private String callNumber;
|
||||
private boolean hasShown;
|
||||
private boolean isCallingIn;
|
||||
// ====================== 成员属性区(按功能归类,命名规范) ======================
|
||||
// 延迟初始化核心
|
||||
private Handler mDelayHandler; // 延迟处理器(避免onCreate阻塞)
|
||||
|
||||
// 通话监听核心
|
||||
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: 通话监听服务启动 =====");
|
||||
|
||||
initPhoneStateListener();
|
||||
// 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度)
|
||||
initDelayHandlerAndLogic();
|
||||
|
||||
initPhoneCallView();
|
||||
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;
|
||||
}
|
||||
|
||||
@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: 通话监听服务开始销毁 =====");
|
||||
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
// 全量清理资源,彻底避免内存泄漏
|
||||
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 "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
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,161 +1,362 @@
|
||||
package cc.winboll.studio.contacts.phonecallui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
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 androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.widget.Toast;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
||||
*
|
||||
* @author aJIEw
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/14 21:01
|
||||
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
|
||||
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
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; // 小米机型关闭延迟时间
|
||||
|
||||
private TextView tvCallNumberLabel;
|
||||
private TextView tvCallNumber;
|
||||
private TextView tvPickUp;
|
||||
private TextView tvCallingTime;
|
||||
private TextView tvHangUp;
|
||||
// 静态属性区(单例核心+全局工具对象)
|
||||
private static volatile boolean sIsActivityAlive = false;
|
||||
private static Handler sCloseHandler;
|
||||
|
||||
private PhoneCallManager phoneCallManager;
|
||||
private PhoneCallService.CallType callType;
|
||||
private String phoneNumber;
|
||||
// 控件属性区(按界面布局顺序排列)
|
||||
private TextView mTvCallNumberLabel;
|
||||
private TextView mTvCallNumber;
|
||||
private TextView mTvPickUp;
|
||||
private TextView mTvCallingTime;
|
||||
private TextView mTvHangUp;
|
||||
|
||||
private Timer onGoingCallTimer;
|
||||
private int callingTime;
|
||||
// 业务属性区(按依赖优先级排列)
|
||||
private PhoneCallManager mPhoneCallManager;
|
||||
private PhoneCallService.CallType mCallType;
|
||||
private String mPhoneNumber;
|
||||
private Timer mOnGoingCallTimer;
|
||||
private int mCallingTime;
|
||||
private boolean isClosing = false; // 新增:避免重复关闭页面
|
||||
|
||||
public static void actionStart(Context context, String phoneNumber,
|
||||
PhoneCallService.CallType callType) {
|
||||
// 对外静态接口(单例启动+外部关闭)
|
||||
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());
|
||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||
intent.putExtra("call_type", 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);
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//MainActivity.updateCallLogFragment();
|
||||
phoneCallManager.destroy();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,57 +6,199 @@ import android.os.Build;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.VideoProfile;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/15 20:11
|
||||
* @Describe 通话核心管理类
|
||||
* 功能:接听/挂断通话、免提控制、资源释放,适配API29-30及小米机型
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
|
||||
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; // 音频模式备份默认值
|
||||
|
||||
public static Call call;
|
||||
// 成员属性区(按依赖优先级排序,移除静态call避免跨组件冲突)
|
||||
private Context mContext;
|
||||
private AudioManager mAudioManager;
|
||||
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
|
||||
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
|
||||
|
||||
private Context context;
|
||||
private AudioManager audioManager;
|
||||
|
||||
public PhoneCallManager(Context context) {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
// 构造方法(单例化改造,避免多实例冲突)
|
||||
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 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() {
|
||||
if (call != null) {
|
||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
||||
openSpeaker();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开电话,包括来电时的拒接以及接听后的挂断
|
||||
* 断开通话(支持来电拒接、通话中挂断)
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (call != null) {
|
||||
call.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开免提
|
||||
* 打开免提,适配小米机型音频通道切换(解决MIUI音频混乱)
|
||||
*/
|
||||
public void openSpeaker() {
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源
|
||||
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
|
||||
*/
|
||||
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() {
|
||||
call = null;
|
||||
context = null;
|
||||
audioManager = null;
|
||||
LogUtils.d(TAG, "开始销毁通话管理资源");
|
||||
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
|
||||
// 释放资源(应用上下文无需主动置空,避免空指针)
|
||||
mAudioManager = null;
|
||||
sInstance = null; // 单例置空,下次重新初始化
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:获取当前免提状态(供UI层同步显示)
|
||||
*/
|
||||
public boolean isSpeakerOpened() {
|
||||
return mIsSpeakerOpened;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,215 +1,284 @@
|
||||
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;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
||||
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
* 适配:Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
|
||||
*/
|
||||
@RequiresApi(api = 29)
|
||||
public class PhoneCallService extends InCallService {
|
||||
|
||||
// 常量定义区
|
||||
public static final String TAG = "PhoneCallService";
|
||||
// 小米设备适配标识,便于日志区分
|
||||
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
|
||||
|
||||
MediaRecorder mediaRecorder;
|
||||
// 成员属性区(按依赖顺序排列)
|
||||
private Call.Callback mCallCallback;
|
||||
private AudioManager mAudioManager;
|
||||
|
||||
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);
|
||||
// 内部枚举类(通话类型定义)
|
||||
public enum CallType {
|
||||
CALL_IN, // 来电
|
||||
CALL_OUT // 去电
|
||||
}
|
||||
|
||||
// 电话接通,开始录音
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
// Service生命周期方法区(按执行流程排序)
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
|
||||
initAudioManager();
|
||||
initCallCallback();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAdded(Call call) {
|
||||
super.onCallAdded(call);
|
||||
|
||||
call.registerCallback(callback);
|
||||
PhoneCallManager.call = call;
|
||||
CallType callType = null;
|
||||
|
||||
if (call.getState() == Call.STATE_RINGING) {
|
||||
callType = CallType.CALL_IN;
|
||||
} else if (call.getState() == Call.STATE_CONNECTING) {
|
||||
callType = CallType.CALL_OUT;
|
||||
LogUtils.d(TAG, "检测到新通话");
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "通话对象为空,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重校验回调,避免重复注册
|
||||
if (mCallCallback != null) {
|
||||
call.registerCallback(mCallCallback);
|
||||
}
|
||||
// 绑定通话对象到管理器,供UI层调用
|
||||
PhoneCallManager.call = call;
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
|
||||
|
||||
CallType callType = judgeCallType(call);
|
||||
if (callType != null) {
|
||||
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);
|
||||
handleValidCall(call, callType);
|
||||
} else {
|
||||
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallRemoved(Call call) {
|
||||
super.onCallRemoved(call);
|
||||
call.unregisterCallback(callback);
|
||||
PhoneCallManager.call = null;
|
||||
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 + " 通话资源清理完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "服务开始销毁");
|
||||
CallLogFragment.updateCallLogFragment();
|
||||
// 释放资源,适配小米设备内存管理,避免内存泄漏
|
||||
mCallCallback = null;
|
||||
mAudioManager = null;
|
||||
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 initAudioManager() {
|
||||
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, MI_DEVICE_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 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 + ")");
|
||||
|
||||
private void stopRecording() {
|
||||
LogUtils.d(TAG, "stopRecording()");
|
||||
if (mediaRecorder != null) {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.release();
|
||||
mediaRecorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex("_id"));
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
};
|
||||
LogUtils.d(TAG, "通话状态回调初始化完成");
|
||||
}
|
||||
|
||||
// 核心业务处理方法区
|
||||
private CallType judgeCallType(Call call) {
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
|
||||
return 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;
|
||||
}
|
||||
|
||||
return -1;
|
||||
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();
|
||||
|
||||
try {
|
||||
// 小米机型适配:调整音量时添加权限校验
|
||||
if (currentVolume != configVolume) {
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "音量调整失败,权限不足", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验拦截规则
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,98 @@
|
||||
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.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.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";
|
||||
public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
WeakReference<MainService> mwrService;
|
||||
// 监听的系统广播 Action
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 使用弱引用关联 MainService,避免内存泄漏
|
||||
private WeakReference<MainService> mMainServiceWeakRef;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainReceiver(MainService service) {
|
||||
mwrService = new WeakReference<MainService>(service);
|
||||
this.mMainServiceWeakRef = new WeakReference<>(service);
|
||||
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
|
||||
}
|
||||
|
||||
// ====================== 重写 BroadcastReceiver 核心方法 ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
ToastUtils.show("ACTION_BOOT_COMPLETED");
|
||||
// 空值校验,避免空指针异常
|
||||
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("设备开机,启动拨号主服务");
|
||||
MainService.startMainService(context);
|
||||
} else {
|
||||
ToastUtils.show(szAction);
|
||||
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
|
||||
ToastUtils.show("收到广播:" + action);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
// ====================== 广播注册/注销方法区 ======================
|
||||
/**
|
||||
* 注册广播接收器,监听指定系统广播
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(ACTION_BOOT_COMPLETED);
|
||||
//filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
context.registerReceiver(this, filter);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +1,271 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
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.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
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;
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
MainService mMainService;
|
||||
boolean isBound = false;
|
||||
volatile boolean isThreadAlive = false;
|
||||
// ====================== 成员变量区 ======================
|
||||
private MainServiceBean mMainServiceBean;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private MainService mMainService;
|
||||
private boolean mIsBound = false;
|
||||
private volatile boolean mIsThreadAlive = false;
|
||||
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
LogUtils.d(TAG, "setIsThreadAlive(...)");
|
||||
LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive));
|
||||
this.isThreadAlive = isThreadAlive;
|
||||
// ====================== 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程存活状态
|
||||
*/
|
||||
public boolean isThreadAlive() {
|
||||
return 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
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(...)");
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
||||
// 每次启动都执行守护逻辑,确保主服务存活
|
||||
assistantService();
|
||||
// START_STICKY:服务被杀死后系统尝试重启
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
setIsThreadAlive(false);
|
||||
// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
super.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;
|
||||
}
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
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();
|
||||
}
|
||||
// ====================== 核心守护逻辑方法区 ======================
|
||||
/**
|
||||
* 守护服务核心逻辑:检查配置并保活主服务
|
||||
*/
|
||||
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: 主服务已禁用,停止保活");
|
||||
}
|
||||
}
|
||||
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain()");
|
||||
// 绑定服务的Intent
|
||||
/**
|
||||
* 唤醒并绑定主服务 MainService(适配后台启动限制)
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
if (mMyServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
startService(new Intent(this, MainService.class));
|
||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||
startForegroundService(intent);
|
||||
|
||||
// BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// startService(new Intent(this, MainService.class));
|
||||
// bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
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;
|
||||
}
|
||||
// ====================== 辅助方法区 ======================
|
||||
/**
|
||||
* 重新加载主服务配置
|
||||
*/
|
||||
private void reloadMainServiceConfig() {
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @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.ActivityManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -17,309 +11,586 @@ 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 cc.winboll.studio.contacts.App;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
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;
|
||||
|
||||
static MainService _mControlCenterService;
|
||||
// 铃声音量监控参数(定时检查+恢复)
|
||||
private static final long VOLUME_CHECK_DELAY = 1000L; // 首次检查延迟1s
|
||||
private static final long VOLUME_CHECK_PERIOD = 60000L; // 后续每60s检查一次
|
||||
|
||||
volatile boolean isServiceRunning;
|
||||
// 前台服务配置(固定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类型硬编码
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MainServiceThread mMainServiceThread;
|
||||
MainServiceHandler mMainServiceHandler;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
AssistantService mAssistantService;
|
||||
boolean isBound = false;
|
||||
MainReceiver mMainReceiver;
|
||||
Timer mStreamVolumeCheckTimer;
|
||||
static volatile TomCat _TomCat;
|
||||
// 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
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new MyBinder();
|
||||
}
|
||||
// 守护服务重绑定延迟(仅保留核心重试逻辑)
|
||||
private static final long RETRY_DELAY_MS = 3000L;
|
||||
|
||||
public MainServiceThread getRemindThread() {
|
||||
return mMainServiceThread;
|
||||
}
|
||||
// ====================== 静态成员属性区(全局共享实例,统一前缀s) ======================
|
||||
private static MainService sMainServiceInstance; // 主服务全局实例
|
||||
private static volatile TomCat sTomCatInstance; // 号码识别核心实例(volatile保证可见性)
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate()");
|
||||
_mControlCenterService = MainService.this;
|
||||
isServiceRunning = false;
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
// ====================== 成员属性区(业务+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; // 铃声音量检查定时器(定时恢复配置)
|
||||
|
||||
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(...)");
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
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() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
//LogUtils.d(TAG, "onDestroy done");
|
||||
if (mMainServiceBean.isEnable() == false) {
|
||||
// 设置运行状态
|
||||
isServiceRunning = false;// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
// 停止守护进程
|
||||
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
|
||||
// ====================== 内部类:Binder(服务绑定通信,优先定义) ======================
|
||||
public class MyBinder extends Binder {
|
||||
MainService getService() {
|
||||
LogUtils.d(TAG, "MainService MyBinder getService()");
|
||||
/**
|
||||
* 外部组件绑定服务时,获取主服务实例
|
||||
* @return MainService 主服务实例
|
||||
*/
|
||||
public MainService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 外部获取主服务实例");
|
||||
return MainService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// //
|
||||
// // 启动服务
|
||||
// //
|
||||
// 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);
|
||||
// }
|
||||
// ====================== 内部类: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;
|
||||
}
|
||||
|
||||
public void appenMessage(String message) {
|
||||
LogUtils.d(TAG, String.format("Message : %s", message));
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainService(Context context) {
|
||||
LogUtils.d(TAG, "stopMainService");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开");
|
||||
mAssistantService = null;
|
||||
mIsAssistantBound = false;
|
||||
|
||||
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");
|
||||
// 服务启用状态下,重试绑定守护服务(主服务存活核心保障)
|
||||
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: 主服务已禁用,跳过重试绑定");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
// ====================== 对外静态方法区(服务启停/重启/状态查询,全局调用) ======================
|
||||
/**
|
||||
* 检查号码是否在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) {
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
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: 主服务创建完成 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 主服务被外部组件绑定,Intent=" + (intent != null ? intent.getAction() : "null"));
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 主服务被启动,startId=" + startId);
|
||||
// 重复启动时再次执行核心业务(避免服务被杀死后重启失败)
|
||||
startCoreBusiness();
|
||||
|
||||
// 服务启用则返回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;
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
// 重新加载最新配置(避免配置修改后未生效)
|
||||
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: 铃声音量与配置一致,无需调整");
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足,恢复失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消音量检查定时器(释放Timer资源,避免内存泄漏)
|
||||
*/
|
||||
private void cancelVolumeCheckTimer() {
|
||||
if (mVolumeCheckTimer != null) {
|
||||
mVolumeCheckTimer.cancel();
|
||||
mVolumeCheckTimer = null;
|
||||
LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量监控定时器已取消");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助初始化方法区(业务组件初始化,统一归类) ======================
|
||||
/**
|
||||
* 初始化TomCat组件(号码识别核心,加载本地数据)
|
||||
*/
|
||||
private void initTomCatComponent() {
|
||||
sTomCatInstance = TomCat.getInstance(this);
|
||||
if (sTomCatInstance.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "initTomCatComponent: BoBullToon号码库加载成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "initTomCatComponent: BoBullToon号码库未下载,加载失败(不影响服务运行)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑白名单规则配置(加载本地规则,保障通话筛选生效)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
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,73 +1,104 @@
|
||||
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";
|
||||
|
||||
volatile static MainServiceThread _MainServiceThread;
|
||||
// 控制线程是否退出的标志
|
||||
volatile boolean isExit = false;
|
||||
volatile boolean isStarted = false;
|
||||
Context mContext;
|
||||
// 服务Handler, 用于线程发送消息使用
|
||||
WeakReference<MainServiceHandler> mwrMainServiceHandler;
|
||||
// 线程休眠周期(1秒)
|
||||
private static final long THREAD_SLEEP_INTERVAL = 1000L;
|
||||
|
||||
MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
mContext = context;
|
||||
mwrMainServiceHandler = new WeakReference<MainServiceHandler>(handler);
|
||||
// ====================== 静态成员变量区 ======================
|
||||
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: 线程实例初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 单例获取方法 ======================
|
||||
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.isExit = isExit;
|
||||
this.mIsExit = isExit;
|
||||
LogUtils.d(TAG, "setIsExit: 线程退出标记已更新 | " + isExit);
|
||||
}
|
||||
|
||||
public boolean isExit() {
|
||||
return isExit;
|
||||
return mIsExit;
|
||||
}
|
||||
|
||||
public void setIsStarted(boolean isStarted) {
|
||||
this.isStarted = isStarted;
|
||||
this.mIsStarted = isStarted;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
if (_MainServiceThread != null) {
|
||||
_MainServiceThread.setIsExit(true);
|
||||
}
|
||||
_MainServiceThread = new MainServiceThread(context, handler);
|
||||
return _MainServiceThread;
|
||||
return mIsStarted;
|
||||
}
|
||||
|
||||
// ====================== 线程核心执行方法 ======================
|
||||
@Override
|
||||
public void run() {
|
||||
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");
|
||||
// 防止重复启动
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// 线程退出清理
|
||||
mIsStarted = false;
|
||||
mContextWeakRef.clear();
|
||||
mHandlerWeakRef.clear();
|
||||
sInstance = null;
|
||||
LogUtils.i(TAG, "run: 线程正常退出");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,270 +1,268 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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_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";
|
||||
|
||||
public static boolean isAppSettingOpen=false;
|
||||
// ====================== 成员变量区 ======================
|
||||
// 标记当前跳转的是应用详情页(true)还是厂商权限页(false)
|
||||
public static boolean isAppSettingOpen = false;
|
||||
|
||||
// ====================== 核心跳转方法区 ======================
|
||||
/**
|
||||
* 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页
|
||||
* 这里需要改造成返回true或者false,应用详情页:true,应用权限页:false
|
||||
* @param activity
|
||||
* 跳转到对应品牌手机的系统权限设置页,跳转失败则降级到应用详情页
|
||||
* @param activity 上下文 Activity
|
||||
*/
|
||||
public static void GoToSetting(Activity activity) {
|
||||
switch (Build.MANUFACTURER) {
|
||||
case MANUFACTURER_HUAWEI://华为
|
||||
Huawei(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);
|
||||
break;
|
||||
case MANUFACTURER_MEIZU://魅族
|
||||
Meizu(activity);
|
||||
case MANUFACTURER_MEIZU:
|
||||
gotoMeizuSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_XIAOMI://小米
|
||||
Xiaomi(activity);
|
||||
case MANUFACTURER_XIAOMI:
|
||||
gotoXiaomiSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_SONY://索尼
|
||||
Sony(activity);
|
||||
case MANUFACTURER_SONY:
|
||||
gotoSonySetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_OPPO://oppo
|
||||
OPPO(activity);
|
||||
case MANUFACTURER_OPPO:
|
||||
gotoOppoSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LG://lg
|
||||
LG(activity);
|
||||
case MANUFACTURER_LG:
|
||||
gotoLgSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LETV://乐视
|
||||
Letv(activity);
|
||||
case MANUFACTURER_LETV:
|
||||
gotoLetvSetting(activity);
|
||||
break;
|
||||
default://其他
|
||||
try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
} catch (Exception e) {
|
||||
SystemConfig(activity);
|
||||
}
|
||||
default:
|
||||
LogUtils.w(TAG, "goToSetting: 未适配当前厂商,跳转应用详情页");
|
||||
openAppDetailSetting(activity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 各厂商权限页跳转方法区 ======================
|
||||
/**
|
||||
* 华为跳转权限设置页
|
||||
* @param activity
|
||||
* 跳转华为手机权限设置页
|
||||
*/
|
||||
public static void Huawei(Activity activity) {
|
||||
private static void gotoHuaweiSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.huawei.systemmanager",
|
||||
"com.huawei.permissionmanager.ui.MainActivity"));
|
||||
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
|
||||
* 跳转魅族手机权限设置页
|
||||
*/
|
||||
public static void Meizu(Activity activity) {
|
||||
private static void gotoMeizuSetting(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
|
||||
* 跳转小米手机权限设置页
|
||||
*/
|
||||
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);
|
||||
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);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI8+)成功");
|
||||
} catch (Exception e) {
|
||||
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);
|
||||
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);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
} catch (Exception e1) { //否则跳转到应用详情
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI5-7)成功");
|
||||
} catch (Exception e1) {
|
||||
LogUtils.e(TAG, "gotoXiaomiSetting: 所有版本适配失败,降级到应用详情页", e1);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
//这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果
|
||||
//activity.startActivity(getAppDetailSettingIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索尼,6.0以上的手机非常少,基本没看见
|
||||
* @param activity
|
||||
* 跳转索尼手机权限设置页
|
||||
*/
|
||||
public static void Sony(Activity activity) {
|
||||
private static void gotoSonySetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.sonymobile.cta",
|
||||
"com.sonymobile.cta.SomcCTAMainActivity"));
|
||||
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
|
||||
* @param activity
|
||||
* 跳转OPPO手机权限设置页
|
||||
*/
|
||||
public static void OPPO(Activity activity) {
|
||||
private static void gotoOppoSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.color.safecenter",
|
||||
"com.color.safecenter.permission.PermissionManagerActivity"));
|
||||
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经过测试,正常使用
|
||||
* @param activity
|
||||
* 跳转LG手机权限设置页
|
||||
*/
|
||||
public static void LG(Activity activity) {
|
||||
private static void gotoLgSetting(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.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.android.settings",
|
||||
"com.android.settings.Settings$AccessLockSummaryActivity"));
|
||||
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
|
||||
* 跳转乐视手机权限设置页
|
||||
*/
|
||||
public static void Letv(Activity activity) {
|
||||
private static void gotoLetvSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.letv.android.letvsafe",
|
||||
"com.letv.android.letvsafe.PermissionAndApps"));
|
||||
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 _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);
|
||||
public static void gotoSystemConfig(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "gotoSystemConfig: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 系统设置界面
|
||||
* @param activity
|
||||
*/
|
||||
public static void SystemConfig(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
/**
|
||||
* 获取应用详情页面
|
||||
* @return
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
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;
|
||||
LogUtils.d(TAG, "gotoSystemConfig: 跳转系统设置主界面成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用详情页的 Intent
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开应用详情设置页
|
||||
*/
|
||||
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,10 +1,5 @@
|
||||
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;
|
||||
@@ -13,203 +8,344 @@ 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}$";
|
||||
|
||||
Map<String, String> contactMap = new HashMap<>();
|
||||
// ====================== 单例与成员变量区 ======================
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile ContactUtils sInstance;
|
||||
// 上下文(弱引用避免内存泄漏,Java7 兼容)
|
||||
private final Context mContext;
|
||||
// 缓存联系人:key=纯数字号码,value=联系人姓名
|
||||
private final Map<String, String> mContactMap = new HashMap<>();
|
||||
|
||||
static volatile ContactUtils _ContactUtils;
|
||||
Context mContext;
|
||||
ContactUtils(Context context) {
|
||||
mContext = context;
|
||||
relaodContacts();
|
||||
// ====================== 单例构造区 ======================
|
||||
/**
|
||||
* 私有构造器:初始化上下文并加载联系人
|
||||
*/
|
||||
private ContactUtils(Context context) {
|
||||
// 传入应用上下文,避免Activity上下文泄漏
|
||||
this.mContext = context.getApplicationContext();
|
||||
LogUtils.d(TAG, "ContactUtils 初始化,开始加载联系人");
|
||||
reloadContacts();
|
||||
}
|
||||
public synchronized static ContactUtils getInstance(Context context) {
|
||||
if (_ContactUtils == null) {
|
||||
_ContactUtils = new ContactUtils(context);
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,Java7 安全)
|
||||
*/
|
||||
public static ContactUtils getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为null,无法创建实例");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
}
|
||||
return _ContactUtils;
|
||||
}
|
||||
|
||||
public void relaodContacts() {
|
||||
readContacts();
|
||||
}
|
||||
|
||||
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 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);
|
||||
if (sInstance == null) {
|
||||
synchronized (ContactUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ContactUtils(context);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
// 此时 contactList 就是存储联系人信息的 Map 列表
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public String getContactsName(String phone) {
|
||||
String result = contactMap.get(formatToSimplePhoneNumber(phone));
|
||||
return result == null ? "[NotInContacts]" : result;
|
||||
// ====================== 联系人缓存与查询区 ======================
|
||||
/**
|
||||
* 重新加载联系人到缓存
|
||||
*/
|
||||
public void reloadContacts() {
|
||||
LogUtils.d(TAG, "reloadContacts: 开始刷新联系人缓存");
|
||||
mContactMap.clear();
|
||||
readContactsFromSystem();
|
||||
LogUtils.d(TAG, "reloadContacts: 联系人缓存刷新完成,共缓存 " + mContactMap.size() + " 个联系人");
|
||||
}
|
||||
|
||||
// static String getSimplePhone(String phone) {
|
||||
// return phone.replaceAll("[+\\s]", "");
|
||||
// }
|
||||
/**
|
||||
* 从系统通讯录读取所有联系人(核心方法)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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(); // 确保游标关闭,避免内存泄漏
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取联系人姓名
|
||||
*/
|
||||
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 static String formatToSimplePhoneNumber(String number) {
|
||||
// 去除所有空格和非数字字符
|
||||
return number.replaceAll("[^0-9]", "");
|
||||
if (number == null || number.isEmpty()) {
|
||||
LogUtils.w(TAG, "formatToSimplePhoneNumber: 输入号码为空");
|
||||
return "";
|
||||
}
|
||||
String simpleNumber = number.replaceAll("[^0-9]", "");
|
||||
LogUtils.v(TAG, "formatToSimplePhoneNumber: 原号码 " + number + " → 纯数字号码 " + simpleNumber);
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化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) {
|
||||
String displayName = null;
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhone: 上下文或号码为空");
|
||||
return 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[]{phoneNumber}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
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();
|
||||
}
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按纯数字号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhoneSimple(Context context, String phoneNumber) {
|
||||
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();
|
||||
if (phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhoneSimple: 输入号码为null");
|
||||
return null;
|
||||
}
|
||||
return displayName;
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
LogUtils.d(TAG, "getDisplayNameByPhoneSimple: 按纯数字号码 " + simplePhone + " 查询");
|
||||
return getDisplayNameByPhone(context, simplePhone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断号码是否在系统通讯录中
|
||||
*/
|
||||
public static boolean isPhoneInContacts(Context context, String phoneNumber) {
|
||||
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));
|
||||
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 + " 未找到联系人(带空格匹配)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, String.format("Phone %s is found in contacts %s.", szPhoneNumber, szDisplayName));
|
||||
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 已在联系人中,姓名:" + displayName);
|
||||
return true;
|
||||
}
|
||||
|
||||
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));
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配定制机型)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "getContactIdByPhone: 上下文或号码为空");
|
||||
return -1L;
|
||||
}
|
||||
return sbSpaceNumber.toString();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ====================== 联系人跳转工具区 ======================
|
||||
/**
|
||||
* 跳转至系统添加联系人界面
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 预填号码(可为null)
|
||||
*/
|
||||
public static void jumpToAddContact(Context context, String phoneNumber) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToAddContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至系统添加联系人界面的工具函数
|
||||
* @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 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 上下文(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);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 支持非Activity上下文调用
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
// 场景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;
|
||||
}
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @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;
|
||||
}
|
||||
|
||||
// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新)
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
// 校验必要参数
|
||||
if (contactId == null || contactId <= 0) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 联系人ID和号码均为空,无法编辑");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动活动(加防护,避免无联系人应用崩溃)
|
||||
// 小米机型在Service/非Activity中调用,需加NEW_TASK标志,否则可能无法启动
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配小米机型,解决编辑时匹配不稳定问题)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待查询的电话号码
|
||||
* @return 联系人ID(无匹配时返回-1)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
return -1L;
|
||||
}
|
||||
// 优先通过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 + " 定位联系人,准备编辑");
|
||||
}
|
||||
|
||||
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; // 无匹配联系人
|
||||
}
|
||||
// 预填最新号码
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
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类型数字输入框工具集
|
||||
* @Describe Int类型数字输入框工具集:安全读取 EditText 中的整数内容
|
||||
*/
|
||||
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 {
|
||||
String sz = editText.getText().toString().trim();
|
||||
return Integer.parseInt(sz);
|
||||
int result = Integer.parseInt(inputStr);
|
||||
LogUtils.d(TAG, "getIntFromEditText: 转换成功 | 结果=" + result);
|
||||
return result;
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
return 0;
|
||||
LogUtils.e(TAG, "getIntFromEditText: 内容不是有效整数 | 输入内容=" + inputStr, e);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,64 @@
|
||||
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.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.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)));
|
||||
// 正数区间测试
|
||||
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: 单元测试执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
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,27 +1,58 @@
|
||||
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) {
|
||||
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) {
|
||||
// 空值校验:防止上下文或号码为空导致异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "call: Context 为 null,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
context.startActivity(intent);
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "call: 电话号码为空,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
String targetPhone = phoneNumber.trim();
|
||||
LogUtils.d(TAG, "call: 准备拨打号码 | " + targetPhone);
|
||||
|
||||
// 权限校验:检查是否持有拨打电话权限
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
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) {
|
||||
//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();
|
||||
// 空值校验,避免空指针异常
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,117 @@
|
||||
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;
|
||||
|
||||
Context mContext;
|
||||
|
||||
public DuInfoTextView(android.content.Context context) {
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Handler mHandler;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DuInfoTextView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
void initView(android.content.Context context) {
|
||||
mContext = context;
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView: 开始初始化云盾信息控件");
|
||||
this.mContext = context;
|
||||
initHandler();
|
||||
updateInfo();
|
||||
LogUtils.d(TAG, "initView: 云盾信息控件初始化完成");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* 初始化 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 视图更新方法区 ======================
|
||||
/**
|
||||
* 更新云盾防御信息显示
|
||||
*/
|
||||
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()");
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
LogUtils.d(TAG, "notifyInfoUpdate: 发送信息更新通知");
|
||||
if (mHandler != null) {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyInfoUpdate: Handler 未初始化,无法发送更新消息");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
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,10 +1,5 @@
|
||||
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;
|
||||
@@ -16,10 +11,17 @@ 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;
|
||||
@@ -27,11 +29,15 @@ 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();
|
||||
@@ -47,174 +53,254 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
init();
|
||||
}
|
||||
|
||||
public void addContentLayout(View viewContent) {
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
// ====================== 初始化方法区 ======================
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 设置文本内容(原代码未初始化textView,添加空校验)
|
||||
* @param text 待显示的文本
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
if (textView == null) {
|
||||
LogUtils.w(TAG, "setText: 文本控件未初始化,无法设置文本");
|
||||
return;
|
||||
}
|
||||
textView.setText(text);
|
||||
LogUtils.d(TAG, "setText: 文本设置为 " + text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件回调监听器
|
||||
* @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();
|
||||
// isScrolling = false;
|
||||
LogUtils.d(TAG, "onTouchEvent: ACTION_DOWN,起始X坐标 = " + mStartX);
|
||||
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:
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
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 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 smoothScrollToLeft() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
// 在手指抬起时,使用 post 方法调用 smoothScrollTo(0, 0)
|
||||
/**
|
||||
* 平滑滚动到右侧(显示操作按钮)
|
||||
*/
|
||||
private void smoothScrollToRight() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
@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();
|
||||
}
|
||||
|
||||
// 设置文本内容
|
||||
public void setText(CharSequence text) {
|
||||
textView.setText(text);
|
||||
/**
|
||||
* 平滑滚动到左侧(隐藏操作按钮)
|
||||
*/
|
||||
private void smoothScrollToLeft() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToLeft: 滚动到左侧");
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
// 定义回调接口
|
||||
/**
|
||||
* 重置滑动坐标
|
||||
*/
|
||||
private void resetScrollCoordinate() {
|
||||
mStartX = 0;
|
||||
mEndX = 0;
|
||||
}
|
||||
|
||||
// ====================== 回调接口定义区 ======================
|
||||
public interface OnActionListener {
|
||||
void onEdit();
|
||||
void onDelete();
|
||||
void onUp();
|
||||
void onDown();
|
||||
}
|
||||
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
public class APPStatusWidget extends AppWidgetProvider {
|
||||
|
||||
|
||||
@@ -12,26 +12,52 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitymainToolbar1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0">
|
||||
android:layout_weight="1.0"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewPager"/>
|
||||
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">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:id="@+id/tabLayout"/>
|
||||
|
||||
</LinearLayout>
|
||||
<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>
|
||||
|
||||
<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,30 +195,48 @@
|
||||
android:text="拨不通电话记录查询:"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<EditText
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/bobulltoonurl_et"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="重置地址"
|
||||
android:onClick="onResetBoBullToonURL"/>
|
||||
<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="onDownloadBoBullToon"/>
|
||||
<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>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -318,6 +336,11 @@
|
||||
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://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip</string>
|
||||
<string name="default_bobulltoon_url">https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="MyAppTheme" parent="AESTheme">
|
||||
<!-- 方案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">
|
||||
<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="AESTheme">
|
||||
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light">
|
||||
<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>
|
||||
|
||||
<item name="android:textSizeHeadline">20sp</item>
|
||||
<item name="android:textSizeBody">14sp</item>
|
||||
<item name="android:textSizeButton">16sp</item>
|
||||
</style>
|
||||
-->
|
||||
</resources>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user