Compare commits

...

33 Commits

Author SHA1 Message Date
84c6271310 <contacts>APK 15.14.1 release Publish. 2025-12-15 20:54:20 +08:00
a4ab864381 通话测试通过。 2025-12-15 20:51:56 +08:00
217a27cbcd 更新基础类库 2025-12-15 18:41:04 +08:00
561abd2398 20251214_204905_236 2025-12-14 20:49:11 +08:00
c832cbd1ac <contacts>APK 15.14.0 release Publish. 2025-12-13 15:30:16 +08:00
98b815f55a 数据模型命名有调整,版本不能回退到之前的版本。 2025-12-13 15:27:46 +08:00
97643c3bcd 源码整理,移除通话录音功能。 2025-12-13 15:22:25 +08:00
43ed19b364 恢复2a74fd2c304b571ab5ae349ffc3b7f06c5b4daf7提交点历史 2025-12-13 15:09:04 +08:00
9a873bf162 20251213_150313_142 2025-12-13 15:03:18 +08:00
2b7108940b 20251213_134317_469 2025-12-13 13:43:22 +08:00
9cc211ec51 增强守护进程护航能力。 2025-12-13 11:39:24 +08:00
c26f267774 优化启动页启动速度。 2025-12-13 11:35:51 +08:00
a1a337558e 移除启动页的主服务管理 2025-12-13 11:13:45 +08:00
8fe7444065 服务启动与关闭模块部署基本完成,待调试。。。 2025-12-13 10:53:46 +08:00
61a20f6811 20251213_104727_568服务启动模块修改。 2025-12-13 10:48:00 +08:00
b6a820b281 更新盾力热度条视图。修复一些盾值更新问题。 2025-12-13 01:25:26 +08:00
f591db6611 20251212_211940_525 2025-12-12 21:19:52 +08:00
268688b8d8 20251212_211505_264 2025-12-12 21:15:11 +08:00
416079c356 服务启动问题Bugfix 2025-12-12 21:08:00 +08:00
c1d2158578 调整应用主题 2025-12-12 20:08:21 +08:00
01e4e8031b 服务启动应用配置问题修复。 2025-12-12 19:05:26 +08:00
181e3e8a34 20251212_171425_295应用权限申请部分改造,版本可能会回退。。。 2025-12-12 17:15:52 +08:00
5614848a65 添加盾力温度计 2025-12-12 15:58:49 +08:00
63d365b175 源码整理 2025-12-12 14:47:13 +08:00
2dafa7bf9f 源码整理 2025-12-12 13:40:13 +08:00
be52292203 重构数据模型命名 2025-12-12 13:22:34 +08:00
fc9f15c70c 源码整理 2025-12-12 13:01:16 +08:00
b872da5dcc 添加设置窗口回退按钮响应 2025-12-12 12:29:30 +08:00
de94b23acb <contacts>APK 15.12.0 release Publish. 2025-12-08 19:48:26 +08:00
6f80e86031 添加米盟广告支持 2025-12-08 19:38:09 +08:00
09854f3333 设置默认 BoBullToon URL,添加 BoBullToon 数据清空按钮,移除重置按钮的数据清空功能。添加操作吐司提示。 2025-12-08 19:28:46 +08:00
498b2e0eae 恢复上一个提交点删除的应用介绍窗口。 2025-12-08 19:12:34 +08:00
0800a0e935 升级基础类库版本,设定应用基础版本号,移除应用介绍窗口。调整类引用路径。 2025-12-08 19:01:36 +08:00
60 changed files with 7334 additions and 3452 deletions

View File

@@ -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'])
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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 API26isDestroyed适配用
// 单例与核心成员变量(按优先级排序)
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");
}
}
});
}
// ====================== 栈优化与工具方法 ======================
/**
* 清理栈中所有无效Activitynull/已销毁/已结束),优化小米机型内存占用
*/
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;
}
}

View File

@@ -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();
}
}

View File

@@ -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回调冲突
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}
}
});
}
}

View File

@@ -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: 云盾信息控件未初始化,刷新失败");
}
}
}

View File

@@ -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);
// 规则20660区号号码允许
rules.add("^0660\\d+$", true, true);
// 规则3020区号号码允许
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: 当前已有规则,跳过初始化");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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";
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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();
// 设置盾值在[0DunTotalCount]之内其他值一律重置为 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();
// 设置盾值在[0DunTotalCount]之内其他值一律重置为 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;
}
}
}

View File

@@ -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(); // 窗口回显时更新通话记录
}
}

View File

@@ -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); // 显示空列表
}
}
}
// 防抖TextWatcherJava 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);
}
}
}

View File

@@ -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正常启动");
}
}

View File

@@ -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 "未知状态";
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 "未知状态";
}
}
}

View File

@@ -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: 线程正常退出");
}
}

View File

@@ -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: 跳转应用详情设置页成功");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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: 单元测试执行完毕");
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 未初始化,无法发送更新消息");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>