Compare commits
37 Commits
winboll-v1
...
contacts-v
| Author | SHA1 | Date | |
|---|---|---|---|
| 84c6271310 | |||
| a4ab864381 | |||
| 217a27cbcd | |||
| 561abd2398 | |||
| c832cbd1ac | |||
| 98b815f55a | |||
| 97643c3bcd | |||
| 43ed19b364 | |||
| 9a873bf162 | |||
| 2b7108940b | |||
| 9cc211ec51 | |||
| c26f267774 | |||
| a1a337558e | |||
| 8fe7444065 | |||
| 61a20f6811 | |||
| b6a820b281 | |||
| f591db6611 | |||
| 268688b8d8 | |||
| 416079c356 | |||
| c1d2158578 | |||
| 01e4e8031b | |||
| 181e3e8a34 | |||
| 5614848a65 | |||
| 63d365b175 | |||
| 2dafa7bf9f | |||
| be52292203 | |||
| fc9f15c70c | |||
| b872da5dcc | |||
| de94b23acb | |||
| 6f80e86031 | |||
| 09854f3333 | |||
| 498b2e0eae | |||
| 0800a0e935 | |||
| bb94f87597 | |||
| 669a6eab0c | |||
| a0d65d9f78 | |||
| f5f9d7c46e |
@@ -5,10 +5,11 @@
|
||||
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
|
||||
## WinBoLL 提问
|
||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
||||
能否实现手机编译与电脑编译的源码同步。
|
||||
|
||||
@@ -25,30 +25,33 @@ android {
|
||||
applicationId "cc.winboll.studio.contacts"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionCode 2
|
||||
// versionName 更新后需要手动设置
|
||||
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.3"
|
||||
versionName "15.14"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'cc.winboll.studio:libaes:15.9.3'
|
||||
api 'cc.winboll.studio:libapputils:15.8.5'
|
||||
api 'cc.winboll.studio:libappbase:15.9.5'
|
||||
|
||||
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//api 'androidx.appcompat:appcompat:1.4.1'
|
||||
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
api 'com.google.code.gson:gson:2.8.5'
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
// 权限请求框架:https://github.com/getActivity/XXPermissions
|
||||
api 'com.github.getActivity:XXPermissions:18.63'
|
||||
// 下拉控件
|
||||
@@ -65,8 +68,6 @@ dependencies {
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 吐司类库
|
||||
//api 'com.github.getActivity:ToastUtils:10.5'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
|
||||
@@ -84,4 +85,15 @@ dependencies {
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Nov 03 12:01:02 HKT 2025
|
||||
stageCount=22
|
||||
#Mon Dec 15 20:54:20 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=
|
||||
baseVersion=15.3
|
||||
publishVersion=15.3.21
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.3.22
|
||||
baseBetaVersion=15.14.2
|
||||
|
||||
138
contacts/proguard-rules.pro
vendored
@@ -9,9 +9,135 @@
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep class cc.winboll.studio.** { *; }
|
||||
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||
|
||||
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||
-keepclassmembers class * {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# 保留 R 文件(避免资源ID混淆)
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# 保留 native 方法(避免JNI调用失败)
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# 保留注解和泛型(避免反射/序列化异常)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
|
||||
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||
-dontwarn java.lang.invoke.*
|
||||
-dontwarn android.support.v8.renderscript.*
|
||||
-dontwarn java.util.function.**
|
||||
|
||||
# ============================== 第三方框架专项规则 ==============================
|
||||
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-keep class okhttp3.internal.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
# ============================== 必要补充规则 ==============================
|
||||
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||
-keep class okhttp3.internal.concurrent.** { *; }
|
||||
-keep class okhttp3.internal.connection.** { *; }
|
||||
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||
-dontwarn okhttp3.internal.connection.RealCall
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||
<init>();
|
||||
}
|
||||
-dontwarn com.bumptech.glide.**
|
||||
|
||||
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep interface com.google.gson.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||
-keep class com.miui.zeus.** { *; }
|
||||
-keep interface com.miui.zeus.** { *; }
|
||||
# 保留米盟日志字段(便于广告加载失败排查)
|
||||
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||
-keep class androidx.recyclerview.** { *; }
|
||||
-keep interface androidx.recyclerview.** { *; }
|
||||
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||
public *;
|
||||
}
|
||||
|
||||
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||
# XXPermissions 18.63
|
||||
-keep class com.hjq.permissions.** { *; }
|
||||
-keep interface com.hjq.permissions.** { *; }
|
||||
|
||||
# ZXing 二维码(核心解析组件)
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-keep class com.journeyapps.zxing.** { *; }
|
||||
|
||||
# Jsoup HTML解析
|
||||
-keep class org.jsoup.** { *; }
|
||||
|
||||
# Pinyin4j 拼音搜索
|
||||
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||
|
||||
# JSch SSH组件
|
||||
-keep class com.jcraft.jsch.** { *; }
|
||||
|
||||
# AndroidX 基础组件
|
||||
-keep class androidx.appcompat.** { *; }
|
||||
-keep interface androidx.appcompat.** { *; }
|
||||
|
||||
# ============================== 优化与调试配置 ==============================
|
||||
# 优化级别(平衡混淆效果与性能)
|
||||
-optimizationpasses 5
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
|
||||
# 调试辅助(保留行号便于崩溃定位)
|
||||
-verbose
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
|
||||
@@ -9,33 +9,41 @@
|
||||
<!-- 拨打电话 -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||
|
||||
<!-- 读取手机状态和身份 -->
|
||||
<!-- 读取手机状态和身份(API 30+ 需细化权限) -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
||||
|
||||
<!-- 修改系统设置 -->
|
||||
<!-- 修改系统设置(移除无效的 protectionLevel 声明,该属性由系统定义) -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
|
||||
<!-- 重新设置外拨电话的路径 -->
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
|
||||
|
||||
<!-- 读取联系人 -->
|
||||
<!-- 联系人权限(适配 Android 13+ 细分权限) -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
|
||||
<!-- 修改您的通讯录 -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.GET_CONTACTS"/>
|
||||
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<!-- 悬浮窗权限(需动态申请) -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 更改您的音频设置 -->
|
||||
<!-- 更改音频设置 -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
|
||||
<!-- 读取通话记录 -->
|
||||
<!-- 通话记录权限(适配 Android 13+ 细分权限) -->
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
|
||||
|
||||
<!-- 录音 -->
|
||||
<!-- 录音权限 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
|
||||
<!-- 前台服务权限(按业务类型声明) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||
|
||||
<!-- API 30+ 通话筛选服务权限(替代 PROCESS_OUTGOING_CALLS) -->
|
||||
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
@@ -51,11 +59,8 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
@@ -65,7 +70,6 @@
|
||||
android:label="CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -74,89 +78,92 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="tel"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
|
||||
|
||||
<!-- 主服务:仅 dataSync 类型(与代码中 0x00000008 匹配) -->
|
||||
<service
|
||||
android:name="cc.winboll.studio.contacts.services.MainService"
|
||||
android:exported="true"/>
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<service android:name="cc.winboll.studio.contacts.services.AssistantService"/>
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<meta-data
|
||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||
android:value="true"/>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.telecom.InCallService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- 通话监听服务:phoneCall 类型(与代码中 0x00000020 匹配) -->
|
||||
<service
|
||||
android:name=".listenphonecall.CallListenerService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name=".service.CallShowService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver">
|
||||
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.APPStatusWidget"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
@@ -165,14 +172,11 @@
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener">
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
@@ -183,7 +187,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/studio_provider"/>
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
@@ -194,3 +198,4 @@
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -1,59 +1,313 @@
|
||||
package cc.winboll.studio.contacts;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe Activity 栈管理工具,统一管理应用内 Activity 生命周期
|
||||
* 适配:Java7 + Android API29-30 + 小米机型,优化并发安全与通话场景稳定性
|
||||
*/
|
||||
public class ActivityStack {
|
||||
// 常量定义(核心标识+版本兼容常量)
|
||||
public static final String TAG = "ActivityStack";
|
||||
private static final int API_VERSION_O = 26; // Android 8.0 API26(isDestroyed适配用)
|
||||
|
||||
// 单例与核心成员变量(按优先级排序)
|
||||
private static final ActivityStack INSTANCE = new ActivityStack();
|
||||
// 替换为ArrayList+同步锁:解决CopyOnWriteArrayList迭代器不能删除的崩溃,兼顾并发安全
|
||||
private final List<Activity> mActivityList = new ArrayList<Activity>();
|
||||
private final Handler mMainHandler = new Handler(Looper.getMainLooper()); // 复用主线程Handler,避免内存泄漏
|
||||
|
||||
private List<Activity> activities = new ArrayList<>();
|
||||
|
||||
// 单例对外暴露方法
|
||||
public static ActivityStack getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void addActivity(Activity activity) {
|
||||
activities.add(activity);
|
||||
// 私有构造,禁止外部实例化
|
||||
private ActivityStack() {
|
||||
LogUtils.d(TAG, "ActivityStack 初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 栈基础操作(添加/移除) ======================
|
||||
/**
|
||||
* 添加Activity到栈中,避免重复入栈
|
||||
* @param activity 待添加的Activity
|
||||
*/
|
||||
public void addActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "addActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
// 同步锁:解决多线程并发添加冲突(小米机型多线程场景适配)
|
||||
synchronized (mActivityList) {
|
||||
if (!mActivityList.contains(activity)) {
|
||||
mActivityList.add(activity);
|
||||
LogUtils.d(TAG, "addActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Activity(不销毁,用于正常退出场景)
|
||||
* @param activity 待移除的Activity
|
||||
*/
|
||||
public void removeActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "removeActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.remove(activity)) {
|
||||
LogUtils.d(TAG, "removeActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== Activity状态查询(获取/判断存活) ======================
|
||||
/**
|
||||
* 获取栈顶有效Activity(迭代遍历替代递归,避免栈溢出,适配小米多页面场景)
|
||||
* @return 栈顶有效Activity,无则返回null
|
||||
*/
|
||||
public Activity getTopActivity() {
|
||||
if (activities.isEmpty()) {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getTopActivity: stack is empty, return null");
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity validTopActivity = null;
|
||||
// 倒序遍历,优先取最顶层有效Activity,同时清理无效残留
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
// 版本兼容校验:API26+才支持isDestroyed
|
||||
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
validTopActivity = activity;
|
||||
break;
|
||||
} else {
|
||||
mActivityList.remove(i);
|
||||
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||
LogUtils.w(TAG, "getTopActivity: remove invalid activity: " + className);
|
||||
}
|
||||
}
|
||||
|
||||
if (validTopActivity != null) {
|
||||
LogUtils.d(TAG, "getTopActivity: top activity: " + validTopActivity.getClass().getSimpleName());
|
||||
}
|
||||
return validTopActivity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类的有效Activity实例(通话场景核心方法,判断页面是否存活)
|
||||
* @param activityClass 目标Activity类
|
||||
* @return 有效实例,无则返回null
|
||||
*/
|
||||
public Activity getActivity(Class<?> activityClass) {
|
||||
if (activityClass == null) {
|
||||
LogUtils.w(TAG, "getActivity: activityClass is null, return null");
|
||||
return null;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getActivity: stack empty, return null");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Activity activity : mActivityList) {
|
||||
if (activity != null && activity.getClass().equals(activityClass) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
LogUtils.d(TAG, "getActivity: find valid activity: " + activityClass.getSimpleName());
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
LogUtils.w(TAG, "getActivity: no valid activity: " + activityClass.getSimpleName());
|
||||
return null;
|
||||
}
|
||||
return activities.get(activities.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定Activity是否存活(简化通话场景调用,避免重复判空)
|
||||
* @param activityClass 目标Activity类
|
||||
* @return true:存活,false:未存活
|
||||
*/
|
||||
public boolean isActivityAlive(Class<?> activityClass) {
|
||||
boolean isAlive = getActivity(activityClass) != null;
|
||||
LogUtils.d(TAG, "isActivityAlive: " + activityClass.getSimpleName() + ", result: " + isAlive);
|
||||
return isAlive;
|
||||
}
|
||||
|
||||
// ====================== Activity销毁操作(单/批量/全部) ======================
|
||||
/**
|
||||
* 销毁栈顶Activity(主线程执行,适配小米机型线程限制)
|
||||
*/
|
||||
public void finishTopActivity() {
|
||||
if (!activities.isEmpty()) {
|
||||
activities.remove(activities.size() - 1).finish();
|
||||
}
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishTopActivity: stack is empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先移除再校验,避免并发冲突(小米多线程场景适配)
|
||||
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
||||
if (topActivity == null) {
|
||||
LogUtils.w(TAG, "finishTopActivity: top activity is null, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!topActivity.isFinishing() && (getSdkVersion() < API_VERSION_O || !topActivity.isDestroyed())) {
|
||||
topActivity.finish();
|
||||
LogUtils.d(TAG, "finishTopActivity: destroy top activity: " + topActivity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void finishActivity(Activity activity) {
|
||||
if (activity != null) {
|
||||
activities.remove(activity);
|
||||
activity.finish();
|
||||
}
|
||||
/**
|
||||
* 销毁指定Activity(主线程执行,避免跨线程异常)
|
||||
* @param activity 待销毁的Activity
|
||||
*/
|
||||
public void finishActivity(final Activity activity) {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "finishActivity: activity is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.contains(activity) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
mActivityList.remove(activity);
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: destroy activity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void finishActivity(Class activityClass) {
|
||||
for (Activity activity : activities) {
|
||||
if (activity.getClass().equals(activityClass)) {
|
||||
finishActivity(activity);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 销毁指定类的所有Activity(核心修复:迭代器删除崩溃,通话场景核心)
|
||||
* @param activityClass 目标Activity类
|
||||
*/
|
||||
public void finishActivity(final Class<?> activityClass) {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activityClass == null) {
|
||||
LogUtils.w(TAG, "finishActivity: activityClass is null, skip");
|
||||
return;
|
||||
}
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishActivity: stack empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 核心修复:用索引遍历+倒序删除,替代迭代器删除(避免UnsupportedOperationException)
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
if (activity != null && activity.getClass().equals(activityClass)) {
|
||||
if (!activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
mActivityList.remove(i); // 索引删除,支持ArrayList
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: destroy class activity: " + activityClass.getSimpleName() + ", stack size: " + mActivityList.size());
|
||||
} else {
|
||||
mActivityList.remove(i); // 清理无效残留
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁栈中所有Activity(退出应用/清空栈场景用)
|
||||
*/
|
||||
public void finishAllActivity() {
|
||||
if (!activities.isEmpty()) {
|
||||
for (Activity activity : activities) {
|
||||
activity.finish();
|
||||
activities.remove(activity);
|
||||
}
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishAllActivity: stack is empty, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历销毁所有有效Activity,逐个状态校验(小米机型稳定性适配)
|
||||
for (Activity activity : mActivityList) {
|
||||
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishAllActivity: destroy activity: " + activity.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
mActivityList.clear();
|
||||
LogUtils.d(TAG, "finishAllActivity: all activity destroyed, stack cleared");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 栈优化与工具方法 ======================
|
||||
/**
|
||||
* 清理栈中所有无效Activity(null/已销毁/已结束),优化小米机型内存占用
|
||||
*/
|
||||
public void clearInvalidActivities() {
|
||||
runOnMainThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mActivityList) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 倒序索引删除,避免遍历过程中索引错乱
|
||||
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||
Activity activity = mActivityList.get(i);
|
||||
if (activity == null || activity.isFinishing() || (getSdkVersion() >= API_VERSION_O && activity.isDestroyed())) {
|
||||
mActivityList.remove(i);
|
||||
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "clearInvalidActivities: remove invalid activity: " + className);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "clearInvalidActivities: done, stack size: " + mActivityList.size());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保任务在主线程执行(Activity操作必须主线程,小米机型严格限制)
|
||||
* @param runnable 待执行任务
|
||||
*/
|
||||
private void runOnMainThread(Runnable runnable) {
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
// 避免不必要的线程切换,优化性能(小米机型流畅度适配)
|
||||
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
mMainHandler.post(runnable);
|
||||
LogUtils.d(TAG, "runOnMainThread: post task to main thread");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:获取当前系统SDK版本(简化版本判断逻辑,统一调用)
|
||||
* @return SDK版本号
|
||||
*/
|
||||
private int getSdkVersion() {
|
||||
return android.os.Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ package cc.winboll.studio.contacts;
|
||||
* @Date 2024/12/08 15:10:51
|
||||
* @Describe 全局应用类
|
||||
*/
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
@@ -16,22 +15,19 @@ public class App extends GlobalApplication {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// 必须在调用基类前设置应用调试标志,
|
||||
// 这样可以预先设置日志与数据的存储根目录。
|
||||
//setIsDebuging(BuildConfig.DEBUG);
|
||||
super.onCreate();
|
||||
// 设置 WinBoLL 应用 UI 类型
|
||||
getWinBoLLActivityManager().setWinBoLLUI_TYPE(WinBoLLActivityManager.WinBoLLUI_TYPE.Aplication);
|
||||
|
||||
//LogUtils.d(TAG, "onCreate");
|
||||
|
||||
// 设置应用调试标志
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
// 初始化窗口管理类
|
||||
WinBoLLActivityManager.init(this);
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.toast_custom_view);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package cc.winboll.studio.contacts;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 主窗口
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -25,64 +21,79 @@ import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.activities.WinBollActivity;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||
import cc.winboll.studio.contacts.fragments.ContactsFragment;
|
||||
import cc.winboll.studio.contacts.fragments.LogFragment;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.utils.AppGoToSettingsUtil;
|
||||
import cc.winboll.studio.contacts.utils.PermissionUtils;
|
||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe Contacts 主窗口(完全适配 API 30 + Java 7 语法)
|
||||
* 核心优化:1. 移除电话状态监听 2. 移除通话筛选服务 3. 移除 MainService 所有相关逻辑 4. ViewPager 实现 Fragment 懒加载(仅首屏初始化)
|
||||
* 问题修复:解决首屏 Fragment 空白问题(删除 setPrimaryItem 冲突逻辑+延迟首屏初始化)
|
||||
*/
|
||||
public final class MainActivity extends WinBollActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||
|
||||
// ====================== 1. 常量定义区(硬编码API版本,避免高版本依赖) ======================
|
||||
public static final String TAG = "MainActivity";
|
||||
public static final int REQUEST_HOME_ACTIVITY = 0;
|
||||
public static final int REQUEST_ABOUT_ACTIVITY = 1;
|
||||
public static final int REQUEST_APP_SETTINGS = 2;
|
||||
public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS";
|
||||
|
||||
static MainActivity _MainActivity;
|
||||
LogView mLogView;
|
||||
Toolbar mToolbar;
|
||||
CheckBox cbMainService;
|
||||
MainServiceBean mMainServiceBean;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private List<View> views;
|
||||
ImageView[] imageViews;
|
||||
LinearLayout linearLayout;
|
||||
int currentPoint = 0;
|
||||
|
||||
private TelephonyManager telephonyManager;
|
||||
private MyPhoneStateListener phoneStateListener;
|
||||
List<Fragment> fragmentList;
|
||||
List<String> tabTitleList;
|
||||
|
||||
private static final int DIALER_REQUEST_CODE = 1;
|
||||
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
|
||||
// 关键修改1:新增 READ_CALL_LOG 权限到必需权限列表(解决通话记录读取崩溃)
|
||||
private String[] REQUIRED_PERMISSIONS = new String[]{
|
||||
Manifest.permission.READ_CONTACTS, // 通讯录读取(原)
|
||||
Manifest.permission.CALL_PHONE, // 电话拨号(原)
|
||||
Manifest.permission.READ_CALL_LOG // 通话记录读取(新增,核心修复)
|
||||
};
|
||||
private static final int REQUEST_OVERLAY_PERMISSION = 1003;
|
||||
|
||||
// API版本硬编码常量(Java 7兼容,杜绝Build.VERSION_CODES高版本引用)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
private static final int ANDROID_8_API = 26;
|
||||
private static final int ANDROID_10_API = 29;
|
||||
private static final int ANDROID_14_API = 34;
|
||||
|
||||
// ====================== 2. 静态成员区 ======================
|
||||
static MainActivity _MainActivity;
|
||||
|
||||
// ====================== 3. 权限常量区 ======================
|
||||
private final String[] REQUIRED_PERMISSIONS = PermissionUtils.BASE_PERMISSIONS;
|
||||
|
||||
// ====================== 4. UI控件成员区 ======================
|
||||
private ADsBannerView mADsBannerView;
|
||||
private LogView mLogView;
|
||||
private Toolbar mToolbar;
|
||||
private CheckBox cbMainService;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private List<View> views;
|
||||
private ImageView[] imageViews;
|
||||
private LinearLayout linearLayout;
|
||||
|
||||
// ====================== 5. 业务逻辑成员区 ======================
|
||||
private int currentPoint = 0;
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
// 记录已初始化的Fragment位置(避免重复初始化)
|
||||
private boolean[] isFragmentInit;
|
||||
|
||||
// ====================== 6. 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -93,102 +104,235 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 7. 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "===== onCreate: 主Activity开始创建 =====");
|
||||
_MainActivity = this;
|
||||
|
||||
// 优先检查所有必需权限(含新增的 READ_CALL_LOG)
|
||||
if (!checkAllRequiredPermissions()) {
|
||||
requestAllRequiredPermissions();
|
||||
} else {
|
||||
initUIAndLogic(savedInstanceState);
|
||||
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
|
||||
initUIAndLogic(savedInstanceState);
|
||||
|
||||
MainServiceBean mainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mainServiceBean != null && mainServiceBean.isEnable()) {
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate: 主Activity创建完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
LogUtils.d(TAG, "onResume: 广告栏资源已恢复");
|
||||
}
|
||||
|
||||
//ToastUtils.show("onCreate");
|
||||
}
|
||||
|
||||
// 权限检查方法(无需修改,自动包含新增的 READ_CALL_LOG)
|
||||
private boolean checkAllRequiredPermissions() {
|
||||
for (String permission : REQUIRED_PERMISSIONS) {
|
||||
if (ActivityCompat.checkSelfPermission(this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 主Activity开始销毁 =====");
|
||||
// 释放广告资源
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
LogUtils.d(TAG, "onDestroy: 广告栏资源已释放");
|
||||
}
|
||||
return true;
|
||||
// 清空Fragment相关引用,避免内存泄漏
|
||||
if (fragmentList != null) {
|
||||
fragmentList.clear();
|
||||
fragmentList = null;
|
||||
}
|
||||
if (tabTitleList != null) {
|
||||
tabTitleList.clear();
|
||||
tabTitleList = null;
|
||||
}
|
||||
isFragmentInit = null;
|
||||
LogUtils.d(TAG, "===== onDestroy: 主Activity销毁完成 =====");
|
||||
}
|
||||
|
||||
// 权限申请方法(无需修改,自动申请新增的 READ_CALL_LOG)
|
||||
private void requestAllRequiredPermissions() {
|
||||
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS);
|
||||
}
|
||||
|
||||
// 权限结果回调(无需修改,确保所有权限(含 READ_CALL_LOG)都通过才加载UI)
|
||||
// ====================== 8. 权限相关回调函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
|
||||
if (requestCode == REQUEST_REQUIRED_PERMISSIONS) {
|
||||
boolean allPermissionsGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allPermissionsGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPermissionsGranted) {
|
||||
initUIAndLogic(null);
|
||||
String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions);
|
||||
if (deniedPerms.length() == 0) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功");
|
||||
checkAndRequestRemainingPermissions();
|
||||
} else {
|
||||
// 关键修改2:更新提示文案,告知用户新增的“通话记录权限”
|
||||
showPermissionDeniedDialogAndExit();
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 被拒权限:" + deniedPerms);
|
||||
showPermissionDeniedDialogAndExit("应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 核心修改:新增“设置权限”按钮,点击调用 AppGoToSettingsUtil 跳转设置页
|
||||
private void showPermissionDeniedDialogAndExit() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("权限不足,无法使用")
|
||||
// 文案修改:明确新增“通话记录读取”权限
|
||||
.setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。")
|
||||
.setCancelable(false)
|
||||
// 新增:左侧“设置权限”按钮(先添加的按钮在左侧)
|
||||
.setNegativeButton("设置权限", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
// 调用工具类跳转应用设置页(按需求实现)
|
||||
AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil();
|
||||
appGoToSettingsUtil.GoToSetting(MainActivity.this);
|
||||
}
|
||||
})
|
||||
// 原有:右侧“确定退出”按钮(后添加的按钮在右侧)
|
||||
.setPositiveButton("确定退出", new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "onActivityResult: 页面回调触发,requestCode=" + requestCode + ",resultCode=" + resultCode);
|
||||
|
||||
switch (requestCode) {
|
||||
case DIALER_REQUEST_CODE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
LogUtils.d(TAG, "onActivityResult: 设为默认拨号应用成功");
|
||||
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
break;
|
||||
case REQUEST_APP_SETTINGS:
|
||||
LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity");
|
||||
recreate();
|
||||
break;
|
||||
case REQUEST_OVERLAY_PERMISSION:
|
||||
handleOverlayPermissionResult();
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onActivityResult: 未知requestCode=" + requestCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化UI和逻辑(无需修改,权限通过后才加载 CallLogFragment)
|
||||
/**
|
||||
* 处理悬浮窗权限申请结果
|
||||
*/
|
||||
private void handleOverlayPermissionResult() {
|
||||
if (PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||
LogUtils.d(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请成功");
|
||||
LogUtils.d(TAG, "handleOverlayPermissionResult: 所有权限已授予");
|
||||
initUIAndLogic(null);
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请失败");
|
||||
showPermissionDeniedDialogAndExit("应用需要悬浮窗权限才能展示来电弹窗,请授予后重新打开应用。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并申请剩余权限(仅保留悬浮窗)
|
||||
*/
|
||||
private void checkAndRequestRemainingPermissions() {
|
||||
if (!PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 悬浮窗权限未授予,跳转设置页");
|
||||
PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION);
|
||||
} else {
|
||||
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 所有权限已授予");
|
||||
initUIAndLogic(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限拒绝提示对话框(Java 7 匿名内部类实现,禁止Lambda)
|
||||
*/
|
||||
private void showPermissionDeniedDialogAndExit(String tip) {
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 弹出权限不足提示框");
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle("权限不足,无法使用");
|
||||
builder.setMessage(tip);
|
||||
builder.setCancelable(false);
|
||||
|
||||
builder.setNegativeButton("去设置", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择去设置权限");
|
||||
PermissionUtils.goAppDetailsSettings(MainActivity.this);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择退出应用");
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
// ====================== 9. UI与业务逻辑初始化区 ======================
|
||||
private void initUIAndLogic(Bundle savedInstanceState) {
|
||||
if (mToolbar != null) {
|
||||
LogUtils.d(TAG, "initUIAndLogic: UI已初始化,无需重复执行");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "===== initUIAndLogic: 开始初始化UI与业务逻辑 =====");
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// 1. 工具栏初始化
|
||||
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||
setSupportActionBar(mToolbar);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 工具栏初始化完成");
|
||||
|
||||
// 2. TabLayout与ViewPager初始化
|
||||
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
|
||||
viewPager = (ViewPager) findViewById(R.id.viewPager);
|
||||
initViewPagerAndTabs();
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
LogUtils.d(TAG, "initUIAndLogic: ViewPager与TabLayout初始化完成");
|
||||
|
||||
// 3. 广告栏初始化
|
||||
mADsBannerView = (ADsBannerView) findViewById(R.id.adsbanner);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 广告栏控件初始化完成");
|
||||
|
||||
// 左边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||
DunTemperatureView tempViewLeft = (DunTemperatureView) findViewById(R.id.dun_temp_view_left);
|
||||
tempViewLeft.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||
tempViewLeft.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||
|
||||
int[] customColors = new int[2];
|
||||
customColors[0] = Color.parseColor("#FF3366FF");
|
||||
customColors[1] = Color.parseColor("#FF9900CC");
|
||||
float[] positions = new float[2];
|
||||
positions[0] = 0.0f;
|
||||
positions[1] = 1.0f;
|
||||
tempViewLeft.setGradientColors(customColors, positions);
|
||||
// 文本放在温度条右侧(默认,可省略)
|
||||
tempViewLeft.setTextPosition(true);
|
||||
// 右边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||
DunTemperatureView tempViewRight = (DunTemperatureView) findViewById(R.id.dun_temp_view_right);
|
||||
tempViewRight.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||
tempViewRight.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||
|
||||
tempViewRight.setGradientColors(customColors, positions);
|
||||
// 文本放在温度条左侧
|
||||
tempViewRight.setTextPosition(false);
|
||||
LogUtils.d(TAG, "initUIAndLogic: 盾值视图初始化完成");
|
||||
LogUtils.d(TAG, "===== initUIAndLogic: 初始化流程全部结束 =====");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ViewPager与Tab数据(Java7规范,泛型完整声明),添加懒加载标记
|
||||
* 关键修改:延迟50ms初始化首屏,确保Fragment控件就绪;删除setPrimaryItem冲突逻辑
|
||||
*/
|
||||
private void initViewPagerAndTabs() {
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: 开始初始化ViewPager数据");
|
||||
fragmentList = new ArrayList<Fragment>();
|
||||
tabTitleList = new ArrayList<String>();
|
||||
// CallLogFragment 仅在权限通过后才实例化(避免提前触发读取)
|
||||
|
||||
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
|
||||
fragmentList.add(CallLogFragment.newInstance(0));
|
||||
fragmentList.add(ContactsFragment.newInstance(1));
|
||||
fragmentList.add(LogFragment.newInstance(2));
|
||||
@@ -196,41 +340,172 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
tabTitleList.add("联系人");
|
||||
tabTitleList.add("应用日志");
|
||||
|
||||
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||
// 初始化懒加载标记数组(默认均未初始化)
|
||||
int fragmentCount = fragmentList.size();
|
||||
isFragmentInit = new boolean[fragmentCount];
|
||||
for (int i = 0; i < fragmentCount; i++) {
|
||||
isFragmentInit[i] = false;
|
||||
}
|
||||
|
||||
// 设置自定义适配器(已删除setPrimaryItem,避免初始化冲突)
|
||||
LazyLoadPagerAdapter adapter = new LazyLoadPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(0); // 关闭预加载,避免提前初始化 CallLogFragment
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
// 关闭预加载(设为0仅加载当前页,关键)
|
||||
viewPager.setOffscreenPageLimit(0);
|
||||
viewPager.addOnPageChangeListener(this);
|
||||
|
||||
// 原有服务启动、电话监听等逻辑...
|
||||
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean == null) {
|
||||
mMainServiceBean = new MainServiceBean();
|
||||
MainServiceBean.saveBean(this, mMainServiceBean);
|
||||
}
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MainService.startMainService(MainActivity.this);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
// 关键优化:延迟50ms初始化首屏(确保Fragment已完成onCreateView,控件绑定就绪)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initFragmentByPosition(0);
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: 延迟初始化首屏Fragment,位置=0");
|
||||
}
|
||||
}, 50);
|
||||
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
LogUtils.d(TAG, "initViewPagerAndTabs: ViewPager初始化完成,等待延迟初始化首屏");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据位置初始化Fragment(调用Fragment的初始化逻辑,避免重复执行)
|
||||
* 优化:添加isAdded判断,确保Fragment已附加到Activity,防止上下文空指针
|
||||
*/
|
||||
private void initFragmentByPosition(int position) {
|
||||
// 校验位置合法性 + 避免重复初始化 + 确保Fragment已附加到Activity
|
||||
if (position < 0 || position >= fragmentList.size() || isFragmentInit[position]) {
|
||||
return;
|
||||
}
|
||||
Fragment targetFragment = fragmentList.get(position);
|
||||
if (targetFragment != null && targetFragment.isAdded()) {
|
||||
// 触发Fragment初始化(调用各Fragment的initData方法)
|
||||
if (targetFragment instanceof CallLogFragment) {
|
||||
((CallLogFragment) targetFragment).initData();
|
||||
} else if (targetFragment instanceof ContactsFragment) {
|
||||
((ContactsFragment) targetFragment).initData();
|
||||
} else if (targetFragment instanceof LogFragment) {
|
||||
((LogFragment) targetFragment).initData();
|
||||
}
|
||||
// 标记为已初始化
|
||||
isFragmentInit[position] = true;
|
||||
LogUtils.d(TAG, "initFragmentByPosition: 初始化Fragment,位置=" + position + ",标题=" + tabTitleList.get(position));
|
||||
} else {
|
||||
LogUtils.w(TAG, "initFragmentByPosition: Fragment未附加到Activity/实例为空,位置=" + position);
|
||||
}
|
||||
}
|
||||
|
||||
// 以下为原有代码(无需修改)
|
||||
private class MyPagerAdapter extends FragmentPagerAdapter {
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> tabTitleList;
|
||||
// ====================== 10. 菜单相关函数区 ======================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu: 菜单加载完成");
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
public MyPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.item_settings) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected: 用户点击设置菜单");
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
// ====================== 11. ViewPager页面回调区(切换时初始化对应Fragment) ======================
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
currentPoint = position;
|
||||
LogUtils.d(TAG, "onPageSelected: 页面切换至[" + position + "],标题=" + tabTitleList.get(position));
|
||||
// 切换页面时,初始化当前页Fragment(未初始化过才执行)
|
||||
initFragmentByPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {}
|
||||
|
||||
// ====================== 12. 工具函数区 ======================
|
||||
/**
|
||||
* 拨号工具方法(添加空指针防护)
|
||||
*/
|
||||
public static void dialPhoneNumber(String phoneNumber) {
|
||||
if (_MainActivity == null) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: MainActivity实例为空,无法拨号");
|
||||
return;
|
||||
}
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨号号码为空");
|
||||
return;
|
||||
}
|
||||
if (PermissionUtils.checkPermission(_MainActivity, Manifest.permission.CALL_PHONE)) {
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL);
|
||||
intent.setData(Uri.parse("tel:" + phoneNumber));
|
||||
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||
_MainActivity.startActivity(intent);
|
||||
} else {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨号权限不足,无法发起拨号");
|
||||
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为默认拨号应用(适配API30,硬编码版本判断)
|
||||
*/
|
||||
public boolean isDefaultPhoneCallApp() {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
TelecomManager manager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
|
||||
if (manager != null && manager.getDefaultDialerPackage() != null) {
|
||||
boolean isDefault = manager.getDefaultDialerPackage().equals(getPackageName());
|
||||
LogUtils.d(TAG, "isDefaultPhoneCallApp: 是否为默认拨号应用=" + isDefault);
|
||||
return isDefault;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isDefaultPhoneCallApp: 系统版本低于Android 6,无法判断");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否正在运行(通用工具方法,添加空指针防护)
|
||||
*/
|
||||
public boolean isServiceRunning(Class<?> serviceClass) {
|
||||
if (serviceClass == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning: 服务类参数为null");
|
||||
return false;
|
||||
}
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (manager == null) {
|
||||
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(service.service.getClassName())) {
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]正在运行");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]未运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================== 13. 内部类定义区(Java 7 规范,禁止Lambda) ======================
|
||||
/**
|
||||
* 自定义懒加载ViewPager适配器(删除setPrimaryItem方法,解决首屏初始化冲突)
|
||||
*/
|
||||
private class LazyLoadPagerAdapter extends FragmentPagerAdapter {
|
||||
private final List<Fragment> fragmentList;
|
||||
private final List<String> tabTitleList;
|
||||
|
||||
public LazyLoadPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
|
||||
super(fm);
|
||||
this.fragmentList = fragmentList;
|
||||
this.tabTitleList = tabTitleList;
|
||||
LogUtils.d(MainActivity.TAG, "LazyLoadPagerAdapter: 初始化完成,Fragment数量=" + fragmentList.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -247,108 +522,8 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return tabTitleList.get(position);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialPhoneNumber(String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(_MainActivity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
_MainActivity.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
@Override
|
||||
public void onPageSelected(int position) {}
|
||||
@Override
|
||||
public void onClick(View v) {}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
LogUtils.d(TAG, "电话已挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
LogUtils.d(TAG, "正在通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
LogUtils.d(TAG, "来电: " + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy() SOS");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.item_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
public boolean isDefaultPhoneCallApp() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
|
||||
if (manger != null && manger.getDefaultDialerPackage() != null) {
|
||||
return manger.getDefaultDialerPackage().equals(getPackageName());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isServiceRunning(Class<?> serviceClass) {
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (manager == null) return false;
|
||||
|
||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
|
||||
Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(service.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DIALER_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_APP_SETTINGS) {
|
||||
recreate();
|
||||
}
|
||||
// 【已删除】移除setPrimaryItem方法,避免与手动初始化+onPageSelected回调冲突
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:15:54
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libaes.winboll.APPInfo;
|
||||
import cc.winboll.studio.libaes.winboll.AboutView;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libaes.views.AboutView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:15:54
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AboutActivity";
|
||||
private static final String BRANCH_NAME = "contacts";
|
||||
|
||||
Context mContext;
|
||||
Toolbar mToolbar;
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Toolbar mToolbar;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -35,58 +42,75 @@ public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 关于页面开始创建");
|
||||
|
||||
mContext = this;
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 初始化关于页面视图
|
||||
initAboutView();
|
||||
// 注册Activity管理
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
|
||||
AboutView aboutView = CreateAboutView();
|
||||
// 在 Activity 的 onCreate 或其他生命周期方法中调用
|
||||
// LinearLayout layout = new LinearLayout(this);
|
||||
// layout.setOrientation(LinearLayout.VERTICAL);
|
||||
// // 创建布局参数(宽度和高度)
|
||||
// ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT
|
||||
// );
|
||||
// addContentView(aboutView, params);
|
||||
|
||||
LinearLayout layout = findViewById(R.id.aboutviewroot_ll);
|
||||
// 创建布局参数(宽度和高度)
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layout.addView(aboutView, params);
|
||||
|
||||
GlobalApplication.getWinBoLLActivityManager().add(this);
|
||||
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面开始销毁");
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
LogUtils.d(TAG, "onDestroy: 关于页面销毁完成");
|
||||
}
|
||||
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "contacts";
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||
// Java7 适配:添加强制类型转换
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
// 非空判断,避免空指针异常
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initAboutView() {
|
||||
LogUtils.d(TAG, "initAboutView: 初始化关于页面内容视图");
|
||||
AboutView aboutView = createAboutView();
|
||||
LinearLayout layout = (LinearLayout) findViewById(R.id.aboutviewroot_ll);
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layout.addView(aboutView, params);
|
||||
LogUtils.d(TAG, "initAboutView: AboutView已添加到布局");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑函数区 ======================
|
||||
private AboutView createAboutView() {
|
||||
LogUtils.d(TAG, "createAboutView: 构建APP信息并创建AboutView");
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("Contacts");
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=4&extra=page%3D1");
|
||||
appInfo.setAppGitAPPBranch(BRANCH_NAME);
|
||||
appInfo.setAppGitAPPSubProjectFolder(BRANCH_NAME);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
|
||||
appInfo.setAppAPKName("Contacts");
|
||||
appInfo.setAppAPKFolderName("Contacts");
|
||||
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 17:15:46
|
||||
* @Describe 拨号窗口
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -20,99 +15,144 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 17:15:46
|
||||
* @Describe 拨号窗口
|
||||
*/
|
||||
public class CallActivity extends AppCompatActivity {
|
||||
public static final String TAG = "CallActivity";
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallActivity";
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private EditText phoneNumberEditText;
|
||||
private TextView callStatusTextView;
|
||||
private Button dialButton;
|
||||
|
||||
// ====================== 业务成员区 ======================
|
||||
private TelephonyManager telephonyManager;
|
||||
private MyPhoneStateListener phoneStateListener;
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
//setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面开始创建");
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
phoneNumberEditText = findViewById(R.id.phone_number);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
callStatusTextView = findViewById(R.id.call_status);
|
||||
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
if (!phoneNumber.isEmpty()) {
|
||||
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(CallActivity.this,
|
||||
new String[]{Manifest.permission.CALL_PHONE},
|
||||
REQUEST_CALL_PHONE);
|
||||
} else {
|
||||
dialPhoneNumber(phoneNumber);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化TelephonyManager和PhoneStateListener
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CALL_PHONE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
dialPhoneNumber(phoneNumber);
|
||||
} else {
|
||||
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dialPhoneNumber(String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
return;
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
callStatusTextView.setText("电话已挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
callStatusTextView.setText("正在通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
callStatusTextView.setText("来电: " + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 初始化控件
|
||||
initViews();
|
||||
// 初始化电话状态监听
|
||||
initPhoneStateListener();
|
||||
LogUtils.d(TAG, "onCreate: 拨号页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 取消监听
|
||||
if (telephonyManager != null) {
|
||||
LogUtils.d(TAG, "onDestroy: 拨号页面开始销毁");
|
||||
// 取消电话状态监听,避免内存泄漏
|
||||
if (telephonyManager != null && phoneStateListener != null) {
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
LogUtils.d(TAG, "onDestroy: 电话状态监听已取消");
|
||||
}
|
||||
LogUtils.d(TAG, "onDestroy: 拨号页面销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 权限回调函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_CALL_PHONE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 拨打电话权限授予成功");
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
dialPhoneNumber(phoneNumber);
|
||||
} else {
|
||||
LogUtils.w(TAG, "onRequestPermissionsResult: 拨打电话权限被拒绝");
|
||||
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumberEditText = (EditText) findViewById(R.id.phone_number);
|
||||
dialButton = (Button) findViewById(R.id.dial_button);
|
||||
callStatusTextView = (TextView) findViewById(R.id.call_status);
|
||||
|
||||
// 设置拨号按钮点击事件
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "initViews: 拨号按钮点击,号码=" + phoneNumber);
|
||||
if (phoneNumber.isEmpty()) {
|
||||
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "initViews: 拨打电话权限未授予,发起权限申请");
|
||||
ActivityCompat.requestPermissions(CallActivity.this,
|
||||
new String[]{Manifest.permission.CALL_PHONE},
|
||||
REQUEST_CALL_PHONE);
|
||||
} else {
|
||||
dialPhoneNumber(phoneNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 电话状态监听初始化函数区 ======================
|
||||
private void initPhoneStateListener() {
|
||||
LogUtils.d(TAG, "initPhoneStateListener: 初始化电话状态监听");
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
phoneStateListener = new MyPhoneStateListener();
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
// ====================== 核心业务函数区 ======================
|
||||
private void dialPhoneNumber(String phoneNumber) {
|
||||
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.e(TAG, "dialPhoneNumber: 拨打电话权限缺失,拨号失败");
|
||||
return;
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
// ====================== 内部电话状态监听类 ======================
|
||||
private class MyPhoneStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
callStatusTextView.setText("电话已挂断");
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-挂断");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
callStatusTextView.setText("正在通话中");
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-通话中");
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
callStatusTextView.setText("来电: " + incomingNumber);
|
||||
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-来电,号码=" + incomingNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,80 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 20:18:26
|
||||
*/
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 20:18:26
|
||||
* @Describe 拨号盘窗口(跳转到系统拨号界面)
|
||||
*/
|
||||
public class DialerActivity extends AppCompatActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DialerActivity";
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private EditText phoneNumberEditText;
|
||||
private Button dialButton;
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 拨号盘页面开始创建");
|
||||
setContentView(R.layout.activity_dialer);
|
||||
|
||||
phoneNumberEditText = findViewById(R.id.phone_number_edit_text);
|
||||
Button dialButton = findViewById(R.id.dial_button);
|
||||
// 初始化UI控件与点击事件
|
||||
initViews();
|
||||
LogUtils.d(TAG, "onCreate: 拨号盘页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 拨号盘页面已销毁");
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumberEditText = (EditText) findViewById(R.id.phone_number_edit_text);
|
||||
dialButton = (Button) findViewById(R.id.dial_button);
|
||||
|
||||
// 设置拨号按钮点击事件
|
||||
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString();
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "initViews: 拨号按钮点击,输入号码=" + phoneNumber);
|
||||
|
||||
// 空号码校验
|
||||
if (phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "initViews: 拨号失败,号码为空");
|
||||
Toast.makeText(DialerActivity.this, "请输入有效电话号码", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳转到系统拨号界面
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||
if (intent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(intent);
|
||||
LogUtils.d(TAG, "initViews: 成功跳转到系统拨号界面");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initViews: 跳转失败,无可用拨号应用");
|
||||
Toast.makeText(DialerActivity.this, "未找到可用拨号应用", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 05:37:42
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@@ -24,49 +20,59 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.App;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.PhoneConnectRuleAdapter;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.views.DuInfoTextView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 05:37:42
|
||||
* @Describe Contacts 设置页面(完全适配 API 30 + Java 7 语法)
|
||||
* 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 5. LogUtils统一日志管理
|
||||
*/
|
||||
public class SettingsActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区(置顶,统一管理) ======================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
// API版本硬编码(替代Build.VERSION_CODES,适配Java7)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
|
||||
Toolbar mToolbar;
|
||||
Switch swSilent;
|
||||
SeekBar msbVolume;
|
||||
TextView mtvVolume;
|
||||
int mnStreamMaxVolume;
|
||||
int mnStreamVolume;
|
||||
Switch mswMainService;
|
||||
static DuInfoTextView _DuInfoTextView;
|
||||
// ====================== 静态成员属性区 ======================
|
||||
private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀
|
||||
|
||||
// 云盾防御层数量
|
||||
EditText etDunTotalCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
EditText etDunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
EditText etDunResumeCount;
|
||||
// 是否启用云盾
|
||||
Switch swIsEnableDun;
|
||||
// ====================== 数据业务属性区 ======================
|
||||
private int mStreamMaxVolume; // 铃音最大音量
|
||||
private int mStreamVolume; // 当前铃音音量
|
||||
private List<PhoneConnectRuleBean> mRuleList; // 通话规则列表
|
||||
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private PhoneConnectRuleAdapter adapter;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
// ====================== UI控件属性区(统一归类,规范命名) ======================
|
||||
private Toolbar mToolbar; // 顶部工具栏
|
||||
private Switch mSwMainService; // 主服务开关
|
||||
private SeekBar mSbVolume; // 音量调节条
|
||||
private TextView mTvVolume; // 音量显示文本
|
||||
private Switch mSwEnableDun; // 云盾功能开关
|
||||
private EditText mEtDunTotalCount; // 云盾总次数输入框
|
||||
private EditText mEtDunResumeSecondCount; // 云盾恢复秒数输入框
|
||||
private EditText mEtDunResumeCount; // 云盾恢复次数输入框
|
||||
private RecyclerView mRvRuleList; // 规则列表RecyclerView
|
||||
private EditText mEtBoBullToonUrl; // BoBullToon地址输入框
|
||||
private EditText mEtSearchPhone; // 号码查询输入框
|
||||
|
||||
// ====================== 接口实现区(IWinBoLLActivity规范实现) ======================
|
||||
@Override
|
||||
public AppCompatActivity getActivity() {
|
||||
return this;
|
||||
@@ -77,268 +83,531 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区(按执行顺序排列) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 设置页面启动");
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
// 初始化工具栏
|
||||
mToolbar = findViewById(R.id.activitymainToolbar1);
|
||||
// 初始化核心流程(按优先级执行)
|
||||
initToolbar(); // 工具栏初始化(优先)
|
||||
initMainServiceSwitch();// 主服务开关初始化
|
||||
initVolumeControl(); // 音量控制初始化
|
||||
initRuleRecyclerView(); // 规则列表初始化
|
||||
initDunSettings(); // 云盾设置初始化
|
||||
initBoBullToonViews(); // BoBullToon功能初始化
|
||||
|
||||
LogUtils.d(TAG, "onCreate: 设置页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 设置页面销毁");
|
||||
// 内存泄漏防护:清空所有引用(静态+成员+UI)
|
||||
sDuInfoTextView = null;
|
||||
mRuleList = null;
|
||||
mRuleAdapter = null;
|
||||
mToolbar = null;
|
||||
mSwMainService = null;
|
||||
mSbVolume = null;
|
||||
mTvVolume = null;
|
||||
mSwEnableDun = null;
|
||||
mEtDunTotalCount = null;
|
||||
mEtDunResumeSecondCount = null;
|
||||
mEtDunResumeCount = null;
|
||||
mRvRuleList = null;
|
||||
mEtBoBullToonUrl = null;
|
||||
mEtSearchPhone = null;
|
||||
LogUtils.d(TAG, "onDestroy: 设置页面资源清理完成");
|
||||
}
|
||||
|
||||
// ====================== 初始化函数区(按功能模块归类) ======================
|
||||
/**
|
||||
* 初始化顶部工具栏(后退按钮+标题)
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||
setSupportActionBar(mToolbar);
|
||||
// 显示后退按钮
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getTag());
|
||||
|
||||
mswMainService = findViewById(R.id.sw_mainservice);
|
||||
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
mswMainService.setChecked(mMainServiceBean.isEnable());
|
||||
mswMainService.setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View arg0) {
|
||||
LogUtils.d(TAG, "mswMainService onClick");
|
||||
// TODO: Implement this method
|
||||
if (mswMainService.isChecked()) {
|
||||
//ToastUtils.show("Is Checked");
|
||||
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
} else {
|
||||
//ToastUtils.show("Not Checked");
|
||||
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
msbVolume = findViewById(R.id.bellvolume);
|
||||
mtvVolume = findViewById(R.id.tv_volume);
|
||||
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// 设置SeekBar的最大值为系统铃声音量的最大刻度
|
||||
mnStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setMax(mnStreamMaxVolume);
|
||||
// 获取当前铃声音量并设置为SeekBar的初始进度
|
||||
mnStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
msbVolume.setProgress(mnStreamVolume);
|
||||
|
||||
updateStreamVolumeTextView();
|
||||
|
||||
msbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
// 设置铃声音量
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
|
||||
RingTongBean bean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
bean = new RingTongBean();
|
||||
}
|
||||
bean.setStreamVolume(progress);
|
||||
RingTongBean.saveBean(SettingsActivity.this, bean);
|
||||
mnStreamVolume = progress;
|
||||
updateStreamVolumeTextView();
|
||||
//Toast.makeText(SettingsActivity.this, "音量设置为: " + progress, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// 当开始拖动SeekBar时的操作
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// 当停止拖动SeekBar时的操作
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
ruleList = Rules.getInstance(this).getPhoneBlacRuleBeanList();
|
||||
|
||||
adapter = new PhoneConnectRuleAdapter(this, ruleList);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
// 设置参数云盾
|
||||
_DuInfoTextView = findViewById(R.id.tv_DunInfo);
|
||||
etDunTotalCount = findViewById(R.id.et_DunTotalCount);
|
||||
etDunResumeSecondCount = findViewById(R.id.et_DunResumeSecondCount);
|
||||
etDunResumeCount = findViewById(R.id.et_DunResumeCount);
|
||||
swIsEnableDun = findViewById(R.id.sw_IsEnableDun);
|
||||
SettingsModel settingsModel = Rules.getInstance(this).getSettingsModel();
|
||||
|
||||
etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount()));
|
||||
etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount()));
|
||||
etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount()));
|
||||
swIsEnableDun.setChecked(settingsModel.isEnableDun());
|
||||
|
||||
boolean isEnableDun = settingsModel.isEnableDun();
|
||||
etDunTotalCount.setEnabled(!isEnableDun);
|
||||
etDunResumeSecondCount.setEnabled(!isEnableDun);
|
||||
etDunResumeCount.setEnabled(!isEnableDun);
|
||||
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
etBoBullToonURL.setText(Rules.getInstance(this).getBoBullToonURL());
|
||||
}
|
||||
|
||||
public static void notifyDunInfoUpdate() {
|
||||
if (_DuInfoTextView != null) {
|
||||
_DuInfoTextView.notifyInfoUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSW_IsEnableDun(View view) {
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun");
|
||||
boolean isEnableDun = swIsEnableDun.isChecked();
|
||||
etDunTotalCount.setEnabled(!isEnableDun);
|
||||
etDunResumeSecondCount.setEnabled(!isEnableDun);
|
||||
etDunResumeCount.setEnabled(!isEnableDun);
|
||||
|
||||
SettingsModel settingsModel = Rules.getInstance(this).getSettingsModel();
|
||||
if (isEnableDun) {
|
||||
settingsModel.setDunTotalCount(Integer.parseInt(etDunTotalCount.getText().toString()));
|
||||
settingsModel.setDunResumeSecondCount(Integer.parseInt(etDunResumeSecondCount.getText().toString()));
|
||||
settingsModel.setDunResumeCount(Integer.parseInt(etDunResumeCount.getText().toString()));
|
||||
|
||||
// 应用效果提示
|
||||
ToastUtils.show((settingsModel.getDunTotalCount() == 1)?"电话骚扰防御力几乎为0。":String.format("以下设置将在连拨%d次后接通电话。", settingsModel.getDunTotalCount()));
|
||||
}
|
||||
settingsModel.setIsEnableDun(isEnableDun);
|
||||
Rules.getInstance(this).saveDun();
|
||||
Rules.getInstance(this).reload();
|
||||
|
||||
// 重新加载盾牌参数
|
||||
etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount()));
|
||||
etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount()));
|
||||
etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount()));
|
||||
|
||||
}
|
||||
|
||||
void updateStreamVolumeTextView() {
|
||||
mtvVolume.setText(String.format("%d/%d", mnStreamVolume, mnStreamMaxVolume));
|
||||
}
|
||||
|
||||
public void onUnitTest(View view) {
|
||||
Intent intent = new Intent(this, UnitTestActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onAddNewConnectionRule(View view) {
|
||||
Rules.getInstance(this).getPhoneBlacRuleBeanList().add(new PhoneConnectRuleModel());
|
||||
Rules.getInstance(this).saveRules();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void onDefaultPhone(View view) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onCanDrawOverlays(View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !Settings.canDrawOverlays(this)) {
|
||||
// 请求 悬浮框 权限
|
||||
askForDrawOverlay();
|
||||
} else {
|
||||
ToastUtils.show("悬浮窗已开启");
|
||||
}
|
||||
}
|
||||
|
||||
public void onResetBoBullToonURL(View view) {
|
||||
Rules.getInstance(this).resetDefaultBoBullToonURL();
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
etBoBullToonURL.setText(Rules.getInstance(this).getBoBullToonURL());
|
||||
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
tomCat.cleanBoBullToon();
|
||||
}
|
||||
|
||||
public void onDownloadBoBullToon(View view) {
|
||||
EditText etBoBullToonURL = findViewById(R.id.bobulltoonurl_et);
|
||||
if (!etBoBullToonURL.getText().toString().trim().equals(Rules.getInstance(this).getBoBullToonURL())) {
|
||||
Rules.getInstance(this).setBoBullToonURL(etBoBullToonURL.getText().toString().trim());
|
||||
// 显示后退按钮(空指针防护)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
}
|
||||
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (tomCat.downloadBoBullToon()) {
|
||||
LogUtils.d(TAG, "BoBullToon downlaod OK!");
|
||||
MainService.restartMainService(SettingsActivity.this);
|
||||
Rules.getInstance(SettingsActivity.this).reload();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void onSearchBoBullToonPhone(View view) {
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
EditText etPhone = findViewById(R.id.activitysettingsEditText1);
|
||||
String phone = etPhone.getText().toString().trim();
|
||||
if (tomCat.loadPhoneBoBullToon()) {
|
||||
if (tomCat.isPhoneBoBullToon(phone)) {
|
||||
ToastUtils.show("It is a BoBullToon Phone!");
|
||||
} else {
|
||||
ToastUtils.show("Not in BoBullToon.");
|
||||
}
|
||||
} else {
|
||||
ToastUtils.show("没有下载 BoBullToon。");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void askForDrawOverlay() {
|
||||
AlertDialog alertDialog = new AlertDialog.Builder(this)
|
||||
.setTitle("允许显示悬浮框")
|
||||
.setMessage("为了使电话监听服务正常工作,请允许这项权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
openDrawOverlaySettings();
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后再说", new DialogInterface.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
alertDialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
alertDialog.show();
|
||||
// 后退按钮点击事件(Java7匿名内部类)
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗管理设置界面
|
||||
* 初始化主服务开关(联动MainService启停)
|
||||
*/
|
||||
private void openDrawOverlaySettings() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Android M 以上引导用户去系统设置中打开允许悬浮窗
|
||||
// 使用反射是为了用尽可能少的代码保证在大部分机型上都可用
|
||||
try {
|
||||
Context context = this;
|
||||
Class clazz = Settings.class;
|
||||
Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
Intent intent = new Intent(field.get(null).toString());
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "请在悬浮窗管理中打开权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
private void initMainServiceSwitch() {
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 初始化主服务开关");
|
||||
mSwMainService = (Switch) findViewById(R.id.sw_mainservice);
|
||||
MainServiceBean serviceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
|
||||
// 加载开关状态(空指针防护)
|
||||
boolean isServiceEnable = serviceBean != null && serviceBean.isEnable();
|
||||
mSwMainService.setChecked(isServiceEnable);
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 主服务当前状态:" + (isServiceEnable ? "启用" : "禁用"));
|
||||
|
||||
// 开关点击事件
|
||||
mSwMainService.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isChecked = mSwMainService.isChecked();
|
||||
LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||
if (isChecked) {
|
||||
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
} else {
|
||||
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化音量控制(SeekBar+音量显示+配置保存)
|
||||
*/
|
||||
private void initVolumeControl() {
|
||||
LogUtils.d(TAG, "initVolumeControl: 初始化音量控制");
|
||||
mSbVolume = (SeekBar) findViewById(R.id.bellvolume);
|
||||
mTvVolume = (TextView) findViewById(R.id.tv_volume);
|
||||
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// 空指针防护:AudioManager获取失败直接返回
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化音量参数
|
||||
mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||
mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
mSbVolume.setMax(mStreamMaxVolume);
|
||||
mSbVolume.setProgress(mStreamVolume);
|
||||
updateVolumeDisplay(); // 更新音量文本显示
|
||||
|
||||
// 音量调节监听
|
||||
mSbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
LogUtils.d(TAG, "initVolumeControl: 音量调节至:" + progress + "/" + mStreamMaxVolume);
|
||||
// 实时更新系统音量+保存配置
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
|
||||
RingTongBean ringBean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
|
||||
if (ringBean == null) {
|
||||
ringBean = new RingTongBean();
|
||||
}
|
||||
ringBean.setStreamVolume(progress);
|
||||
RingTongBean.saveBean(SettingsActivity.this, ringBean);
|
||||
mStreamVolume = progress;
|
||||
updateVolumeDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话规则列表(加载黑白名单规则)
|
||||
*/
|
||||
private void initRuleRecyclerView() {
|
||||
LogUtils.d(TAG, "initRuleRecyclerView: 初始化规则列表");
|
||||
mRvRuleList = (RecyclerView) findViewById(R.id.recycler_view);
|
||||
mRvRuleList.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
// 加载规则数据
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "initRuleRecyclerView: Rules实例获取失败,列表初始化失败");
|
||||
return;
|
||||
}
|
||||
mRuleList = rules.getPhoneBlacRuleBeanList();
|
||||
mRuleAdapter = new PhoneConnectRuleAdapter(this, mRuleList);
|
||||
mRvRuleList.setAdapter(mRuleAdapter);
|
||||
LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + mRuleList.size() + "条规则");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化云盾设置(参数加载+开关联动)
|
||||
*/
|
||||
private void initDunSettings() {
|
||||
LogUtils.d(TAG, "initDunSettings: 初始化云盾设置");
|
||||
sDuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo);
|
||||
mSwEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun);
|
||||
mEtDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount);
|
||||
mEtDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount);
|
||||
mEtDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount);
|
||||
|
||||
// 加载云盾配置
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "initDunSettings: Rules实例获取失败,云盾初始化失败");
|
||||
return;
|
||||
}
|
||||
SettingsBean dunSettings = rules.getSettingsModel();
|
||||
if (dunSettings == null) {
|
||||
LogUtils.e(TAG, "initDunSettings: 云盾配置获取失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充配置参数
|
||||
mEtDunTotalCount.setText(String.valueOf(dunSettings.getDunTotalCount()));
|
||||
mEtDunResumeSecondCount.setText(String.valueOf(dunSettings.getDunResumeSecondCount()));
|
||||
mEtDunResumeCount.setText(String.valueOf(dunSettings.getDunResumeCount()));
|
||||
mSwEnableDun.setChecked(dunSettings.isEnableDun());
|
||||
|
||||
// 开关联动:启用云盾时禁用参数编辑
|
||||
boolean isDunEnable = dunSettings.isEnableDun();
|
||||
mEtDunTotalCount.setEnabled(!isDunEnable);
|
||||
mEtDunResumeSecondCount.setEnabled(!isDunEnable);
|
||||
mEtDunResumeCount.setEnabled(!isDunEnable);
|
||||
LogUtils.d(TAG, "initDunSettings: 云盾当前状态:" + (isDunEnable ? "启用" : "禁用"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化BoBullToon功能(地址配置+号码查询)
|
||||
*/
|
||||
private void initBoBullToonViews() {
|
||||
LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon功能");
|
||||
mEtBoBullToonUrl = (EditText) findViewById(R.id.bobulltoonurl_et);
|
||||
mEtSearchPhone = (EditText) findViewById(R.id.activitysettingsEditText1);
|
||||
|
||||
// 加载保存的地址
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules != null) {
|
||||
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||
LogUtils.d(TAG, "initBoBullToonViews: 加载BoBullToon地址完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initBoBullToonViews: Rules实例获取失败,地址加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
public void onAbout(View view) {
|
||||
App.getWinBoLLActivityManager().startWinBoLLActivity(this, AboutActivity.class);
|
||||
// ====================== 点击事件回调区(按功能模块归类) ======================
|
||||
/**
|
||||
* 云盾开关点击事件(联动参数编辑权限+配置保存)
|
||||
*/
|
||||
public void onSW_IsEnableDun(View view) {
|
||||
boolean isChecked = mSwEnableDun.isChecked();
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||
|
||||
// 联动参数编辑权限
|
||||
mEtDunTotalCount.setEnabled(!isChecked);
|
||||
mEtDunResumeSecondCount.setEnabled(!isChecked);
|
||||
mEtDunResumeCount.setEnabled(!isChecked);
|
||||
|
||||
// 保存配置
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: Rules实例获取失败,配置保存失败");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
SettingsBean dunSettings = rules.getSettingsModel();
|
||||
if (dunSettings == null) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾配置获取失败,保存失败");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启用云盾时校验参数合法性
|
||||
if (isChecked) {
|
||||
try {
|
||||
String totalCountStr = mEtDunTotalCount.getText().toString().trim();
|
||||
String resumeSecStr = mEtDunResumeSecondCount.getText().toString().trim();
|
||||
String resumeCountStr = mEtDunResumeCount.getText().toString().trim();
|
||||
|
||||
// 空参数校验
|
||||
if (totalCountStr.isEmpty() || resumeSecStr.isEmpty() || resumeCountStr.isEmpty()) {
|
||||
throw new NumberFormatException("参数不能为空");
|
||||
}
|
||||
|
||||
// 转换参数并保存
|
||||
int totalCount = Integer.parseInt(totalCountStr);
|
||||
int resumeSec = Integer.parseInt(resumeSecStr);
|
||||
int resumeCount = Integer.parseInt(resumeCountStr);
|
||||
dunSettings.setDunTotalCount(totalCount);
|
||||
dunSettings.setDunResumeSecondCount(resumeSec);
|
||||
dunSettings.setDunResumeCount(resumeCount);
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数保存完成,总次数:" + totalCount + ",恢复秒数:" + resumeSec);
|
||||
|
||||
// 提示信息
|
||||
String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话";
|
||||
ToastUtils.show(toastMsg);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e);
|
||||
ToastUtils.show("参数格式错误,请输入整数");
|
||||
mSwEnableDun.setChecked(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存开关状态并刷新配置
|
||||
dunSettings.setIsEnableDun(isChecked);
|
||||
rules.saveDun();
|
||||
rules.reload();
|
||||
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成");
|
||||
}
|
||||
|
||||
public void onLogView(View view) {
|
||||
App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
|
||||
/**
|
||||
* 添加新通话规则(黑白名单)
|
||||
*/
|
||||
public void onAddNewConnectionRule(View view) {
|
||||
LogUtils.d(TAG, "onAddNewConnectionRule: 添加新通话规则");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例获取失败,添加失败");
|
||||
return;
|
||||
}
|
||||
mRuleList.add(new PhoneConnectRuleBean());
|
||||
rules.saveRules();
|
||||
mRuleAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转默认电话应用设置
|
||||
*/
|
||||
public void onDefaultPhone(View view) {
|
||||
LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置");
|
||||
startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮窗权限检查与请求
|
||||
*/
|
||||
public void onCanDrawOverlays(View view) {
|
||||
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
|
||||
// API6.0+校验权限
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !Settings.canDrawOverlays(this)) {
|
||||
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
|
||||
showDrawOverlayRequestDialog();
|
||||
} else {
|
||||
ToastUtils.show("悬浮窗权限已开启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理BoBullToon本地数据
|
||||
*/
|
||||
public void onCleanBoBullToonData(View view) {
|
||||
LogUtils.d(TAG, "onCleanBoBullToonData: 清理BoBullToon数据");
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
if (tomCat != null) {
|
||||
tomCat.cleanBoBullToon();
|
||||
ToastUtils.show("BoBullToon数据已清理");
|
||||
LogUtils.d(TAG, "onCleanBoBullToonData: 数据清理完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "onCleanBoBullToonData: TomCat实例获取失败,清理失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置BoBullToon默认地址
|
||||
*/
|
||||
public void onResetBoBullToonURL(View view) {
|
||||
LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon地址");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onResetBoBullToonURL: Rules实例获取失败,重置失败");
|
||||
return;
|
||||
}
|
||||
rules.resetDefaultBoBullToonURL();
|
||||
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||
ToastUtils.show("BoBullToon地址已重置为默认");
|
||||
LogUtils.d(TAG, "onResetBoBullToonURL: 地址重置完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载BoBullToon数据(子线程执行,避免阻塞UI)
|
||||
*/
|
||||
public void onDownloadBoBullToon(View view) {
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: 开始下载BoBullToon数据");
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules == null) {
|
||||
LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例获取失败,下载失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验并更新地址
|
||||
String inputUrl = mEtBoBullToonUrl.getText().toString().trim();
|
||||
String savedUrl = rules.getBoBullToonURL();
|
||||
if (!inputUrl.equals(savedUrl)) {
|
||||
rules.setBoBullToonURL(inputUrl);
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon地址更新为:" + inputUrl);
|
||||
}
|
||||
|
||||
// 子线程下载(Java7匿名内部类)
|
||||
final TomCat tomCat = TomCat.getInstance(this);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean downloadSuccess = tomCat != null && tomCat.downloadBoBullToon();
|
||||
if (downloadSuccess) {
|
||||
LogUtils.d(TAG, "onDownloadBoBullToon: 数据下载成功");
|
||||
// 主线程更新UI
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("BoBullToon下载成功");
|
||||
}
|
||||
});
|
||||
// 重启主服务+刷新配置
|
||||
MainService.restartMainService(SettingsActivity.this);
|
||||
Rules.getInstance(SettingsActivity.this).reload();
|
||||
} else {
|
||||
LogUtils.e(TAG, "onDownloadBoBullToon: 数据下载失败");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("BoBullToon下载失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询号码是否为BoBullToon号码
|
||||
*/
|
||||
public void onSearchBoBullToonPhone(View view) {
|
||||
LogUtils.d(TAG, "onSearchBoBullToonPhone: 执行号码查询");
|
||||
String phone = mEtSearchPhone.getText().toString().trim();
|
||||
// 空号码校验
|
||||
if (phone.isEmpty()) {
|
||||
LogUtils.w(TAG, "onSearchBoBullToonPhone: 查询号码为空,取消查询");
|
||||
ToastUtils.show("请输入查询号码");
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
TomCat tomCat = TomCat.getInstance(this);
|
||||
if (tomCat == null || !tomCat.loadPhoneBoBullToon()) {
|
||||
LogUtils.w(TAG, "onSearchBoBullToonPhone: BoBullToon数据未加载,查询失败");
|
||||
ToastUtils.show("请先下载BoBullToon数据");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone);
|
||||
String resultMsg = isBoBullToon ? "是BoBullToon号码" : "非BoBullToon号码";
|
||||
ToastUtils.show(resultMsg);
|
||||
LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + resultMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转单元测试页面
|
||||
*/
|
||||
public void onUnitTest(View view) {
|
||||
LogUtils.d(TAG, "onUnitTest: 跳转单元测试页面");
|
||||
startActivity(new Intent(this, UnitTestActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转关于页面
|
||||
*/
|
||||
public void onAbout(View view) {
|
||||
LogUtils.d(TAG, "onAbout: 跳转关于页面");
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转日志查看页面
|
||||
*/
|
||||
public void onLogView(View view) {
|
||||
LogUtils.d(TAG, "onLogView: 跳转日志页面");
|
||||
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||
}
|
||||
|
||||
// ====================== 工具方法区(通用功能+权限相关) ======================
|
||||
/**
|
||||
* 更新音量显示文本(当前音量/最大音量)
|
||||
*/
|
||||
private void updateVolumeDisplay() {
|
||||
mTvVolume.setText(mStreamVolume + "/" + mStreamMaxVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示悬浮窗权限请求对话框
|
||||
*/
|
||||
private void showDrawOverlayRequestDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("权限请求")
|
||||
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
jumpToDrawOverlaySettings();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
// 解决对话框焦点问题
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗权限设置页面(反射适配低版本)
|
||||
*/
|
||||
private void jumpToDrawOverlaySettings() {
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
|
||||
try {
|
||||
// 反射获取设置页面Action(避免高版本API依赖)
|
||||
Class<?> settingsClazz = Settings.class;
|
||||
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
String action = (String) actionField.get(null);
|
||||
|
||||
// 跳转当前应用权限设置页
|
||||
Intent intent = new Intent(action);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
|
||||
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 静态通知方法区(云盾信息更新) ======================
|
||||
/**
|
||||
* 通知云盾信息刷新(外部调用)
|
||||
*/
|
||||
public static void notifyDunInfoUpdate() {
|
||||
if (sDuInfoTextView != null) {
|
||||
LogUtils.d(TAG, "notifyDunInfoUpdate: 刷新云盾信息显示");
|
||||
sDuInfoTextView.notifyInfoUpdate();
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +1,145 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 16:07:04
|
||||
* @Describe 规则单元测试页面
|
||||
*/
|
||||
public class UnitTestActivity extends Activity {
|
||||
public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "UnitTestActivity";
|
||||
|
||||
LogView logView;
|
||||
// ====================== UI控件区 ======================
|
||||
private LogView logView;
|
||||
private EditText etPhone;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public AppCompatActivity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 单元测试页面开始创建");
|
||||
setContentView(R.layout.activity_unittest);
|
||||
logView = findViewById(R.id.logview);
|
||||
|
||||
// 初始化控件
|
||||
initViews();
|
||||
LogUtils.d(TAG, "onCreate: 单元测试页面初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 单元测试页面开始销毁");
|
||||
if (logView != null) {
|
||||
// 若LogView有停止方法,建议调用避免资源泄漏
|
||||
// logView.stop();
|
||||
LogUtils.d(TAG, "onDestroy: LogView资源已处理");
|
||||
}
|
||||
LogUtils.d(TAG, "onDestroy: 单元测试页面销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 控件初始化函数区 ======================
|
||||
private void initViews() {
|
||||
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||
// Java7 适配:添加强制类型转换
|
||||
logView = (LogView) findViewById(R.id.logview);
|
||||
etPhone = (EditText) findViewById(R.id.phone_et);
|
||||
|
||||
// 启动日志视图
|
||||
logView.start();
|
||||
LogUtils.d(TAG, "initViews: LogView已启动");
|
||||
}
|
||||
|
||||
// ====================== 点击事件测试函数区 ======================
|
||||
/**
|
||||
* 测试单个号码匹配规则
|
||||
*/
|
||||
public void onTestPhone(View view) {
|
||||
// 开始测试数据
|
||||
EditText etPhone = findViewById(R.id.phone_et);
|
||||
Rules rules = Rules.getInstance(this);
|
||||
LogUtils.d(TAG, "onTestPhone: 开始测试单个号码规则匹配");
|
||||
String phone = etPhone.getText().toString().trim();
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
}
|
||||
|
||||
public void onTestMain(View view) {
|
||||
LogUtils.d(TAG, "IntUtils.unittest_getIntInRange();");
|
||||
IntUtils.unittest_getIntInRange();
|
||||
|
||||
Rules rules = Rules.getInstance(this);
|
||||
|
||||
// 如果没有规则就添加测试规则
|
||||
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
|
||||
// 手机号码允许
|
||||
// 中国手机号码正则表达式,以1开头,第二位可以是3、4、5、6、7、8、9,后面跟9位数字
|
||||
String regex = "^1[3-9]\\d{9}$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 指定区号号码允许
|
||||
regex = "^0660\\d+$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 指定区号号码允许
|
||||
regex = "^020\\d+$";
|
||||
rules.add(regex, true, true);
|
||||
|
||||
// 添加默认拒接规则
|
||||
regex = ".*";
|
||||
rules.add(regex, false, true);
|
||||
|
||||
// 保存规则到文件
|
||||
rules.saveRules();
|
||||
if (phone.isEmpty()) {
|
||||
LogUtils.w(TAG, "onTestPhone: 测试号码为空,跳过匹配");
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始测试数据
|
||||
String phone = "16769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
Rules rules = Rules.getInstance(this);
|
||||
boolean isAllowed = rules.isAllowed(phone);
|
||||
LogUtils.d(TAG, String.format("onTestPhone: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
|
||||
phone = "16856582777";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
/**
|
||||
* 批量测试预设号码规则匹配
|
||||
*/
|
||||
public void onTestMain(View view) {
|
||||
LogUtils.d(TAG, "onTestMain: 开始批量测试号码规则匹配");
|
||||
// 测试IntUtils工具类方法
|
||||
LogUtils.d(TAG, "onTestMain: 执行 IntUtils.unittest_getIntInRange() 测试");
|
||||
IntUtils.unittest_getIntInRange();
|
||||
|
||||
phone = "17519703124";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 初始化规则实例
|
||||
Rules rules = Rules.getInstance(this);
|
||||
// 无规则时添加测试规则集
|
||||
initTestRulesIfEmpty(rules);
|
||||
|
||||
phone = "0205658955";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 预设测试号码列表
|
||||
String[] testPhones = {
|
||||
"16769764848", "16856582777", "17519703124",
|
||||
"0205658955", "0108965253", "+8616769764848",
|
||||
"4005816769764848", "95566"
|
||||
};
|
||||
|
||||
phone = "0108965253";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 遍历测试号码并输出结果
|
||||
for (String phone : testPhones) {
|
||||
boolean isAllowed = rules.isAllowed(phone);
|
||||
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
|
||||
}
|
||||
|
||||
phone = "+8616769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// ====================== 私有工具函数区 ======================
|
||||
/**
|
||||
* 规则集为空时初始化测试规则
|
||||
*/
|
||||
private void initTestRulesIfEmpty(Rules rules) {
|
||||
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
|
||||
// 规则1:中国手机号允许
|
||||
rules.add("^1[3-9]\\d{9}$", true, true);
|
||||
// 规则2:0660区号号码允许
|
||||
rules.add("^0660\\d+$", true, true);
|
||||
// 规则3:020区号号码允许
|
||||
rules.add("^020\\d+$", true, true);
|
||||
// 规则4:默认拒接所有号码
|
||||
rules.add(".*", false, true);
|
||||
|
||||
phone = "4005816769764848";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
|
||||
phone = "95566";
|
||||
LogUtils.d(TAG, String.format("Test phone : %s\n%s", phone, rules.isAllowed(phone)));
|
||||
// 保存规则到本地
|
||||
rules.saveRules();
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
package cc.winboll.studio.contacts.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:16:45
|
||||
* @Describe 应用窗口基类
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libaes.beans.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/31 15:16:45
|
||||
* @Describe 应用窗口基类,统一处理主题设置与导航返回
|
||||
*/
|
||||
public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "WinBollActivity";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
||||
// ====================== 接口实现区 ======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -29,14 +33,24 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
//LogUtils.d(TAG, "onCreate: 基类页面开始创建");
|
||||
// 优先设置主题,再执行父类初始化
|
||||
// mThemeType = getThemeType();
|
||||
// setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
//LogUtils.d(TAG, "onCreate: 基类主题设置完成,当前主题类型=" + mThemeType);
|
||||
}
|
||||
|
||||
// ====================== 主题相关函数区 ======================
|
||||
/**
|
||||
* 获取当前应用主题类型
|
||||
*/
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
LogUtils.d(TAG, "getThemeType: 获取应用主题类型");
|
||||
// 注释的SharedPreferences逻辑保留,便于后续扩展
|
||||
/*SharedPreferences sharedPreferences = getSharedPreferences(
|
||||
SHAREDPREFERENCES_NAME, MODE_PRIVATE);
|
||||
return AESThemeBean.ThemeType.values()[((sharedPreferences.getInt(DRAWER_THEME_TYPE, AESThemeBean.ThemeType.DEFAULT.ordinal())))];
|
||||
@@ -44,17 +58,27 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用当前主题样式
|
||||
*/
|
||||
void setThemeStyle() {
|
||||
//setTheme(AESThemeBean.getThemeStyle(getThemeType()));
|
||||
LogUtils.d(TAG, "setThemeStyle: 开始设置应用主题");
|
||||
// 替换原注释逻辑,使用AESThemeUtil获取的主题ID
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
LogUtils.d(TAG, "setThemeStyle: 主题设置完成");
|
||||
}
|
||||
|
||||
// ====================== 菜单与导航函数区 ======================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if(item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "onOptionsItemSelected: 菜单选项点击,itemId=" + item.getItemId());
|
||||
// 处理导航栏返回按钮点击事件
|
||||
// if (item.getItemId() == android.R.id.home) {
|
||||
// LogUtils.d(TAG, "onOptionsItemSelected: 点击导航返回按钮,关闭当前页面");
|
||||
// finish();
|
||||
// return true;
|
||||
// }
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:09:32
|
||||
* @Describe CallLogAdapter
|
||||
*/
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -13,125 +8,167 @@ import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.beans.CallLogModel;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:09:32
|
||||
* @Describe 通话记录列表适配器
|
||||
*/
|
||||
public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogAdapter";
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<CallLogModel> callLogList;
|
||||
ContactUtils mContactUtils;
|
||||
Context mContext;
|
||||
private ContactUtils mContactUtils;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public CallLogAdapter(Context context, List<CallLogModel> callLogList) {
|
||||
mContext = context;
|
||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
||||
LogUtils.d(TAG, "CallLogAdapter: 初始化适配器,数据量=" + callLogList.size());
|
||||
this.mContext = context;
|
||||
this.callLogList = callLogList;
|
||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
||||
}
|
||||
|
||||
public void relaodContacts() {
|
||||
this.mContactUtils.relaodContacts();
|
||||
}
|
||||
|
||||
// ====================== 公共方法区 ======================
|
||||
/**
|
||||
* 重新加载联系人数据
|
||||
*/
|
||||
public void relaodContacts() {
|
||||
LogUtils.d(TAG, "relaodContacts: 开始重新加载联系人数据");
|
||||
this.mContactUtils.reloadContacts();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "relaodContacts: 联系人数据加载完成,列表已刷新");
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建列表项ViewHolder");
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_call_log, parent, false);
|
||||
return new CallLogViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定列表项数据,position=" + position);
|
||||
final CallLogModel callLog = callLogList.get(position);
|
||||
holder.phoneNumber.setText(callLog.getPhoneNumber() + "☎" + mContactUtils.getContactsName(callLog.getPhoneNumber()));
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
|
||||
// 绑定通话号码与联系人名称
|
||||
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
|
||||
String phoneText = callLog.getPhoneNumber() + "☎" + (contactName == null ? "" : contactName);
|
||||
holder.phoneNumber.setText(phoneText);
|
||||
|
||||
// 号码长按弹出菜单事件
|
||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View p1) {
|
||||
// 弹出复制菜单
|
||||
PopupMenu menu = new PopupMenu(mContext, holder.phoneNumber);
|
||||
//加载菜单资源
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
|
||||
//设置点击事件的响应
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int nItemId = menuItem.getItemId();
|
||||
if (nItemId == R.id.item_calllog_phonenumber_copy) {
|
||||
// Gets a handle to the clipboard service.
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
// Creates a new text clip to put on the clipboard
|
||||
ClipData clip = ClipData.newPlainText("simple text", callLog.getPhoneNumber());
|
||||
// Set the clipboard's primary clip.
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
} else if (nItemId == R.id.item_calllog_phonenumber_add_contact) {
|
||||
//ToastUtils.show(callLog.getPhoneNumber());
|
||||
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
//一定要调用show()来显示弹出式菜单
|
||||
menu.show();
|
||||
|
||||
showPhonePopupMenu(holder.phoneNumber, callLog);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
holder.callStatus.setText(callLog.getCallStatus());
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
holder.callDate.setText(dateFormat.format(callLog.getCallDate()));
|
||||
|
||||
// 初始化拉动后拨号控件
|
||||
holder.dialAOHPCTCSeekBar.setThumb(holder.itemView.getContext().getDrawable(R.drawable.ic_call));
|
||||
holder.dialAOHPCTCSeekBar.setBlurRightDP(80);
|
||||
holder.dialAOHPCTCSeekBar.setThumbOffset(0);
|
||||
holder.dialAOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
|
||||
ToastUtils.show(phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
holder.itemView.getContext().startActivity(intent);
|
||||
}
|
||||
});
|
||||
// 绑定通话状态与时间
|
||||
holder.callStatus.setText(callLog.getCallStatus());
|
||||
holder.callDate.setText(DATE_FORMAT.format(callLog.getCallDate()));
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, callLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return callLogList.size();
|
||||
return callLogList == null ? 0 : callLogList.size();
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 显示号码操作弹窗菜单
|
||||
*/
|
||||
private void showPhonePopupMenu(View anchorView, final CallLogModel callLog) {
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 弹出号码操作菜单");
|
||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
|
||||
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.item_calllog_phonenumber_copy) {
|
||||
// 复制号码到剪贴板
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("call_log_phone", callLog.getPhoneNumber());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 号码" + callLog.getPhoneNumber() + "已复制到剪贴板");
|
||||
} else if (itemId == R.id.item_calllog_phonenumber_add_contact) {
|
||||
// 跳转到添加联系人页面
|
||||
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
|
||||
LogUtils.d(TAG, "showPhonePopupMenu: 跳转添加联系人页面,号码=" + callLog.getPhoneNumber());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动拨号SeekBar
|
||||
*/
|
||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final CallLogModel callLog) {
|
||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
||||
seekBar.setBlurRightDP(80);
|
||||
seekBar.setThumbOffset(0);
|
||||
|
||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
|
||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
||||
ToastUtils.show(phoneNumber);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类 ======================
|
||||
public class CallLogViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView phoneNumber, callStatus, callDate;
|
||||
Button dialButton;
|
||||
TextView phoneNumber;
|
||||
TextView callStatus;
|
||||
TextView callDate;
|
||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
||||
|
||||
public CallLogViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
phoneNumber = itemView.findViewById(R.id.phone_number);
|
||||
callStatus = itemView.findViewById(R.id.call_status);
|
||||
callDate = itemView.findViewById(R.id.call_date);
|
||||
dialAOHPCTCSeekBar = itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
// Java7 适配:添加强制类型转换
|
||||
phoneNumber = (TextView) itemView.findViewById(R.id.phone_number);
|
||||
callStatus = (TextView) itemView.findViewById(R.id.call_status);
|
||||
callDate = (TextView) itemView.findViewById(R.id.call_date);
|
||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:35:44
|
||||
* @Describe ContactAdapter
|
||||
*/
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -20,111 +15,142 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.beans.ContactModel;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:35:44
|
||||
* @Describe 联系人列表适配器
|
||||
*/
|
||||
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactAdapter";
|
||||
// 移除未使用的 REQUEST_CALL_PHONE 常量,精简冗余代码
|
||||
|
||||
private static final int REQUEST_CALL_PHONE = 1;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<ContactModel> contactList;
|
||||
Context mContext;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public ContactAdapter(Context context, List<ContactModel> contactList) {
|
||||
mContext = context;
|
||||
LogUtils.d(TAG, "ContactAdapter: 初始化适配器,联系人数量=" + contactList.size());
|
||||
this.mContext = context;
|
||||
this.contactList = contactList;
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建联系人列表项ViewHolder");
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false);
|
||||
return new ContactViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定联系人列表项数据,position=" + position);
|
||||
final ContactModel contact = contactList.get(position);
|
||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View p1) {
|
||||
// 弹出复制菜单
|
||||
PopupMenu menu = new PopupMenu(mContext, holder.llPhoneNumberMain);
|
||||
//加载菜单资源
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
|
||||
//设置点击事件的响应
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int nItemId = menuItem.getItemId();
|
||||
if (nItemId == R.id.item_contact_phonenumber_copy) {
|
||||
// Gets a handle to the clipboard service.
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
// Creates a new text clip to put on the clipboard
|
||||
ClipData clip = ClipData.newPlainText("simple text", contact.getNumber());
|
||||
// Set the clipboard's primary clip.
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
} else if (nItemId == R.id.item_calllog_phonenumber_edit_contact) {
|
||||
//ToastUtils.show("Test");
|
||||
Long nContactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
|
||||
//ToastUtils.show(String.format("%d", nContactId));
|
||||
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), nContactId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
//一定要调用show()来显示弹出式菜单
|
||||
menu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 绑定联系人名称与号码
|
||||
holder.contactName.setText(contact.getName());
|
||||
holder.contactNumber.setText(contact.getNumber());
|
||||
|
||||
// 初始化拉动后拨号控件
|
||||
holder.dialAOHPCTCSeekBar.setThumb(holder.itemView.getContext().getDrawable(R.drawable.ic_call));
|
||||
holder.dialAOHPCTCSeekBar.setBlurRightDP(80);
|
||||
holder.dialAOHPCTCSeekBar.setThumbOffset(0);
|
||||
holder.dialAOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
|
||||
ToastUtils.show(phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
holder.itemView.getContext().startActivity(intent);
|
||||
}
|
||||
});
|
||||
// 长按联系人条目弹出操作菜单
|
||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
showContactPopupMenu(holder.llPhoneNumberMain, contact);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化滑动拨号SeekBar
|
||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return contactList.size();
|
||||
// 增加空指针判断,避免空列表崩溃
|
||||
return contactList == null ? 0 : contactList.size();
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 显示联系人操作弹窗菜单
|
||||
*/
|
||||
private void showContactPopupMenu(View anchorView, final ContactModel contact) {
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 弹出联系人操作菜单");
|
||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
|
||||
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.item_contact_phonenumber_copy) {
|
||||
// 复制联系人号码到剪贴板
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("contact_phone", contact.getNumber());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 联系人号码" + contact.getNumber() + "已复制到剪贴板");
|
||||
} else if (itemId == R.id.item_calllog_phonenumber_edit_contact) {
|
||||
// 跳转到编辑联系人页面
|
||||
Long contactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
|
||||
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), contactId);
|
||||
LogUtils.d(TAG, "showContactPopupMenu: 跳转编辑联系人页面,号码=" + contact.getNumber() + ",ID=" + contactId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动拨号SeekBar
|
||||
*/
|
||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final ContactModel contact) {
|
||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
||||
seekBar.setBlurRightDP(80);
|
||||
seekBar.setThumbOffset(0);
|
||||
|
||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
|
||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
||||
ToastUtils.show(phoneNumber);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类 ======================
|
||||
public class ContactViewHolder extends RecyclerView.ViewHolder {
|
||||
LinearLayout llPhoneNumberMain;
|
||||
LinearLayout llPhoneNumberMain;
|
||||
TextView contactName;
|
||||
TextView contactNumber;
|
||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
||||
|
||||
public ContactViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
llPhoneNumberMain = itemView.findViewById(R.id.itemcontactLinearLayout1);
|
||||
contactName = itemView.findViewById(R.id.contact_name);
|
||||
contactNumber = itemView.findViewById(R.id.contact_number);
|
||||
dialAOHPCTCSeekBar = itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
// Java7 适配:添加强制类型转换
|
||||
llPhoneNumberMain = (LinearLayout) itemView.findViewById(R.id.itemcontactLinearLayout1);
|
||||
contactName = (TextView) itemView.findViewById(R.id.contact_name);
|
||||
contactNumber = (TextView) itemView.findViewById(R.id.contact_number);
|
||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 14:20:38
|
||||
* @Describe ImagePagerAdapter
|
||||
*/
|
||||
public class ImagePagerAdapter {
|
||||
|
||||
public static final String TAG = "ImagePagerAdapter";
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.adapters;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 17:27:41
|
||||
* @Describe PhoneConnectRuleAdapter
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -17,226 +12,230 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.views.LeftScrollView;
|
||||
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 17:27:41
|
||||
* @Describe 通话规则列表适配器,支持简单查看/编辑两种视图切换
|
||||
*/
|
||||
public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneConnectRuleAdapter";
|
||||
|
||||
private static final int VIEW_TYPE_SIMPLE = 0;
|
||||
private static final int VIEW_TYPE_EDIT = 1;
|
||||
private static final String NULL_RULE_TEXT = "[NULL]";
|
||||
|
||||
private Context context;
|
||||
private List<PhoneConnectRuleModel> ruleList;
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private List<PhoneConnectRuleBean> mRuleList;
|
||||
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleModel> ruleList) {
|
||||
this.context = context;
|
||||
this.ruleList = ruleList;
|
||||
// ====================== 构造函数区 ======================
|
||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleBean> ruleList) {
|
||||
LogUtils.d(TAG, "PhoneConnectRuleAdapter: 初始化适配器,规则数量=" + ruleList.size());
|
||||
this.mContext = context;
|
||||
this.mRuleList = ruleList;
|
||||
}
|
||||
|
||||
// ====================== RecyclerView 重写方法区 ======================
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
if (viewType == VIEW_TYPE_SIMPLE) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建简单视图ViewHolder");
|
||||
View view = inflater.inflate(R.layout.view_phone_connect_rule_simple, parent, false);
|
||||
return new SimpleViewHolder(parent, view);
|
||||
} else {
|
||||
LogUtils.d(TAG, "onCreateViewHolder: 创建编辑视图ViewHolder");
|
||||
View view = inflater.inflate(R.layout.view_phone_connect_rule, parent, false);
|
||||
return new EditViewHolder(parent, view);
|
||||
return new EditViewHolder(view);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
|
||||
final PhoneConnectRuleModel model = ruleList.get(position);
|
||||
final PhoneConnectRuleBean model = mRuleList.get(position);
|
||||
LogUtils.d(TAG, "onBindViewHolder: 绑定规则数据,position=" + position + ",视图类型=" + getItemViewType(position));
|
||||
|
||||
if (holder instanceof SimpleViewHolder) {
|
||||
final SimpleViewHolder simpleViewHolder = (SimpleViewHolder) holder;
|
||||
String szView = model.getRuleText().trim().equals("") ?"[NULL]": model.getRuleText();
|
||||
simpleViewHolder.tvRuleText.setText(szView);
|
||||
simpleViewHolder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
simpleViewHolder.checkBoxAllow.setEnabled(false);
|
||||
simpleViewHolder.checkBoxEnable.setChecked(model.isEnable());
|
||||
simpleViewHolder.checkBoxEnable.setEnabled(false);
|
||||
simpleViewHolder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener(){
|
||||
|
||||
@Override
|
||||
public void onUp() {
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
if (position > 0) {
|
||||
ToastUtils.show("onUp");
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
// PhoneConnectRuleModel newBean = new PhoneConnectRuleModel();
|
||||
// newBean.setRuleText(list.get(position).getRuleText());
|
||||
// newBean.setIsAllowConnection(list.get(position).isAllowConnection());
|
||||
// newBean.setIsEnable(list.get(position).isEnable());
|
||||
// newBean.setIsSimpleView(list.get(position).isSimpleView());
|
||||
list.add(position - 1, list.get(position));
|
||||
list.remove(position + 1);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown() {
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
if (position < list.size() - 1) {
|
||||
ToastUtils.show("onDown");
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
// PhoneConnectRuleModel newBean = new PhoneConnectRuleModel();
|
||||
// newBean.setRuleText(list.get(position).getRuleText());
|
||||
// newBean.setIsAllowConnection(list.get(position).isAllowConnection());
|
||||
// newBean.setIsEnable(list.get(position).isEnable());
|
||||
// newBean.setIsSimpleView(list.get(position).isSimpleView());
|
||||
list.add(position + 2, list.get(position));
|
||||
list.remove(position);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEdit() {
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
model.setIsSimpleView(false);
|
||||
notifyDataSetChanged();
|
||||
//notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
YesNoAlertDialog.show(simpleViewHolder.scrollView.getContext(), "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
simpleViewHolder.scrollView.smoothScrollTo(0, 0);
|
||||
model.setIsSimpleView(true);
|
||||
ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
list.remove(position);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyDataSetChanged();
|
||||
//notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
// simpleViewHolder.editButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View v) {
|
||||
// model.setIsSimpleView(false);
|
||||
// notifyItemChanged(position);
|
||||
// }
|
||||
// });
|
||||
// simpleViewHolder.deleteButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View v) {
|
||||
// model.setIsSimpleView(false);
|
||||
// ArrayList<PhoneConnectRuleModel> list = Rules.getInstance(context).getPhoneBlacRuleBeanList();
|
||||
// list.remove(position);
|
||||
// Rules.getInstance(context).saveRules();
|
||||
// notifyItemChanged(position);
|
||||
// }
|
||||
// });
|
||||
// // 触摸事件处理
|
||||
// simpleViewHolder.contentLayout.setOnTouchListener(new View.OnTouchListener() {
|
||||
// @Override
|
||||
// public boolean onTouch(View v, MotionEvent event) {
|
||||
// switch (event.getAction()) {
|
||||
// case MotionEvent.ACTION_DOWN:
|
||||
// simpleViewHolder.startX = event.getX();
|
||||
// simpleViewHolder.isSwiping = true;
|
||||
// break;
|
||||
// case MotionEvent.ACTION_MOVE:
|
||||
// if (simpleViewHolder.isSwiping) {
|
||||
// float deltaX = simpleViewHolder.startX - event.getX();
|
||||
// if (deltaX > 0) { // 左滑
|
||||
// float translationX = Math.max(-simpleViewHolder.actionLayout.getWidth(), -deltaX);
|
||||
// simpleViewHolder.contentLayout.setTranslationX(translationX);
|
||||
// simpleViewHolder.actionLayout.setVisibility(View.VISIBLE);
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// case MotionEvent.ACTION_UP:
|
||||
// simpleViewHolder.isSwiping = false;
|
||||
// if (simpleViewHolder.contentLayout.getTranslationX() < -simpleViewHolder.actionLayout.getWidth() / 2) {
|
||||
// // 保持按钮显示
|
||||
// simpleViewHolder.contentLayout.setTranslationX(-actionLayout.getWidth());
|
||||
// } else {
|
||||
// // 恢复原状
|
||||
// simpleViewHolder.contentLayout.animate().translationX(0).setDuration(200).start();
|
||||
// simpleViewHolder.actionLayout.setVisibility(View.INVISIBLE);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// });
|
||||
bindSimpleViewHolder((SimpleViewHolder) holder, model, position);
|
||||
} else if (holder instanceof EditViewHolder) {
|
||||
final EditViewHolder editViewHolder = (EditViewHolder) holder;
|
||||
editViewHolder.editText.setText(model.getRuleText());
|
||||
editViewHolder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
editViewHolder.checkBoxEnable.setChecked(model.isEnable());
|
||||
editViewHolder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
model.setRuleText(editViewHolder.editText.getText().toString());
|
||||
model.setIsAllowConnection(editViewHolder.checkBoxAllow.isChecked());
|
||||
model.setIsEnable(editViewHolder.checkBoxEnable.isChecked());
|
||||
model.setIsSimpleView(true);
|
||||
Rules.getInstance(context).saveRules();
|
||||
notifyItemChanged(position);
|
||||
Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
bindEditViewHolder((EditViewHolder) holder, model, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return ruleList.size();
|
||||
return mRuleList == null ? 0 : mRuleList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
PhoneConnectRuleModel model = ruleList.get(position);
|
||||
// 这里可以根据模型的状态来决定视图类型,简单起见,假设点击按钮后进入编辑视图
|
||||
return model.isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
return mRuleList.get(position).isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
}
|
||||
|
||||
// ====================== 私有视图绑定方法区 ======================
|
||||
/**
|
||||
* 绑定简单视图数据
|
||||
*/
|
||||
private void bindSimpleViewHolder(final SimpleViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
||||
// 绑定规则文本,空值显示[NULL]
|
||||
String ruleText = model.getRuleText().trim().isEmpty() ? NULL_RULE_TEXT : model.getRuleText().trim();
|
||||
holder.tvRuleText.setText(ruleText);
|
||||
// 设置复选框状态并禁用编辑
|
||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
holder.checkBoxAllow.setEnabled(false);
|
||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
||||
holder.checkBoxEnable.setEnabled(false);
|
||||
|
||||
// 设置左滑操作监听
|
||||
holder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener() {
|
||||
@Override
|
||||
public void onUp() {
|
||||
LogUtils.d(TAG, "onUp: 规则上移,position=" + position);
|
||||
moveRuleUp(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown() {
|
||||
LogUtils.d(TAG, "onDown: 规则下移,position=" + position);
|
||||
moveRuleDown(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEdit() {
|
||||
LogUtils.d(TAG, "onEdit: 切换到编辑视图,position=" + position);
|
||||
model.setIsSimpleView(false);
|
||||
notifyItemChanged(position);
|
||||
holder.scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
LogUtils.d(TAG, "onDelete: 触发规则删除确认,position=" + position);
|
||||
showDeleteConfirmDialog(holder.scrollView.getContext(), model, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定编辑视图数据
|
||||
*/
|
||||
private void bindEditViewHolder(final EditViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
||||
// 绑定规则文本到输入框
|
||||
holder.editText.setText(model.getRuleText());
|
||||
// 绑定复选框状态
|
||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
||||
|
||||
// 确认按钮点击事件
|
||||
holder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String newRuleText = holder.editText.getText().toString().trim();
|
||||
model.setRuleText(newRuleText);
|
||||
model.setIsAllowConnection(holder.checkBoxAllow.isChecked());
|
||||
model.setIsEnable(holder.checkBoxEnable.isChecked());
|
||||
model.setIsSimpleView(true);
|
||||
|
||||
// 保存规则并刷新视图
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyItemChanged(position);
|
||||
Toast.makeText(mContext, "保存成功", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d(TAG, "bindEditViewHolder: 规则保存成功,position=" + position + ",规则内容=" + newRuleText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 私有业务工具方法区 ======================
|
||||
/**
|
||||
* 规则上移
|
||||
*/
|
||||
private void moveRuleUp(int position) {
|
||||
if (position <= 0) {
|
||||
ToastUtils.show("已到顶部,无法上移");
|
||||
return;
|
||||
}
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
swapRulePosition(ruleList, position, position - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则下移
|
||||
*/
|
||||
private void moveRuleDown(int position) {
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
if (position >= ruleList.size() - 1) {
|
||||
ToastUtils.show("已到底部,无法下移");
|
||||
return;
|
||||
}
|
||||
swapRulePosition(ruleList, position, position + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换规则位置
|
||||
*/
|
||||
private void swapRulePosition(ArrayList<PhoneConnectRuleBean> list, int fromPos, int toPos) {
|
||||
PhoneConnectRuleBean temp = list.get(fromPos);
|
||||
list.set(fromPos, list.get(toPos));
|
||||
list.set(toPos, temp);
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "swapRulePosition: 规则位置交换完成,from=" + fromPos + ",to=" + toPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示删除确认弹窗
|
||||
*/
|
||||
private void showDeleteConfirmDialog(Context dialogContext, final PhoneConnectRuleBean model, final int position) {
|
||||
YesNoAlertDialog.show(dialogContext, "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
||||
ruleList.remove(position);
|
||||
Rules.getInstance(mContext).saveRules();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 规则删除成功,position=" + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 用户取消删除规则,position=" + position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== ViewHolder 内部类区 ======================
|
||||
static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final LeftScrollView scrollView;
|
||||
private final TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
LeftScrollView scrollView;
|
||||
TextView tvRuleText;
|
||||
CheckBox checkBoxAllow;
|
||||
CheckBox checkBoxEnable;
|
||||
|
||||
|
||||
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
scrollView = itemView.findViewById(R.id.scrollView);
|
||||
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
|
||||
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
|
||||
tvRuleText = viewContent.findViewById(R.id.ruletext_tv);
|
||||
checkBoxAllow = viewContent.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = viewContent.findViewById(R.id.checkbox_enable);
|
||||
//tvRuleText = new TextView(itemView.getContext());
|
||||
scrollView = (LeftScrollView) itemView.findViewById(R.id.scrollView);
|
||||
// 初始化简单视图内容布局
|
||||
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
|
||||
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
|
||||
tvRuleText = (TextView) viewContent.findViewById(R.id.ruletext_tv);
|
||||
checkBoxAllow = (CheckBox) viewContent.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = (CheckBox) viewContent.findViewById(R.id.checkbox_enable);
|
||||
// 设置内容宽度并添加到滚动视图
|
||||
scrollView.setContentWidth(parent.getWidth());
|
||||
//scrollView.setContentWidth(600);
|
||||
scrollView.addContentLayout(viewContent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||
@@ -245,17 +244,14 @@ public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.V
|
||||
CheckBox checkBoxEnable;
|
||||
Button buttonConfirm;
|
||||
|
||||
public EditViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
||||
public EditViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
editText = itemView.findViewById(R.id.edit_text);
|
||||
checkBoxAllow = itemView.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = itemView.findViewById(R.id.checkbox_enable);
|
||||
buttonConfirm = itemView.findViewById(R.id.button_confirm);
|
||||
// Java7 适配:添加强制类型转换
|
||||
editText = (EditText) itemView.findViewById(R.id.edit_text);
|
||||
checkBoxAllow = (CheckBox) itemView.findViewById(R.id.checkbox_allow);
|
||||
checkBoxEnable = (CheckBox) itemView.findViewById(R.id.checkbox_enable);
|
||||
buttonConfirm = (Button) itemView.findViewById(R.id.button_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCheckBoxTouchListener(CheckBox checkBox) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:10:57
|
||||
* @Describe CallLogModel
|
||||
*/
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class CallLogModel {
|
||||
public static final String TAG = "CallLogModel";
|
||||
|
||||
private String phoneNumber;
|
||||
private String callStatus;
|
||||
private Date callDate;
|
||||
|
||||
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
|
||||
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
|
||||
this.callStatus = callStatus;
|
||||
this.callDate = callDate;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getCallStatus() {
|
||||
return callStatus;
|
||||
}
|
||||
|
||||
public Date getCallDate() {
|
||||
return callDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人信息数据模型
|
||||
*/
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
||||
|
||||
public class ContactModel {
|
||||
|
||||
public static final String TAG = "ContactModel";
|
||||
|
||||
private String name;
|
||||
private String number;
|
||||
private String pinyin;
|
||||
// 新增:存储姓名的拼音首字母(如"啊牛"→"an")
|
||||
private String pinyinFirstLetter;
|
||||
|
||||
public ContactModel(String name, String number) {
|
||||
this.name = name;
|
||||
this.number = number.replaceAll("\\s", "");
|
||||
this.pinyin = convertToPinyin(name);
|
||||
// 初始化时生成拼音首字母
|
||||
this.pinyinFirstLetter = convertToPinyinFirstLetter(name);
|
||||
}
|
||||
|
||||
// 原方法:转换为全拼(如"啊牛"→"aniu")
|
||||
private String convertToPinyin(String chinese) {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
|
||||
StringBuilder pinyin = new StringBuilder();
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认首选项)
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
pinyin.append(ch); // 非汉字直接拼接(如字母、数字、符号)
|
||||
}
|
||||
}
|
||||
return pinyin.toString();
|
||||
}
|
||||
|
||||
// 新增:转换为拼音首字母(如"啊牛"→"an")
|
||||
private String convertToPinyinFirstLetter(String chinese) {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
|
||||
StringBuilder firstLetters = new StringBuilder();
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
// 取拼音的第一个字母(如"a"、"niu"→"a"、"n")
|
||||
firstLetters.append(pinyinArray[0].charAt(0));
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// 非汉字可根据需求处理:此处保留原字符(如"李3"→"l3","张A"→"za")
|
||||
firstLetters.append(ch);
|
||||
}
|
||||
}
|
||||
return firstLetters.toString();
|
||||
}
|
||||
|
||||
// 新增:获取拼音首字母
|
||||
public String getPinyinFirstLetter() {
|
||||
return pinyinFirstLetter;
|
||||
}
|
||||
|
||||
// 原有getter方法
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getPinyin() {
|
||||
return pinyin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 07:06:13
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class MainServiceBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "MainServiceBean";
|
||||
|
||||
boolean isEnable;
|
||||
|
||||
public MainServiceBean() {
|
||||
this.isEnable = false;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MainServiceBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
MainServiceBean bean = this;
|
||||
jsonWriter.name("isEnable").value(bean.isEnable());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 09:52:10
|
||||
* @Describe 电话黑名单规则
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PhoneConnectRuleModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PhoneConnectRuleModel";
|
||||
|
||||
String ruleText;
|
||||
boolean isAllowConnection;
|
||||
boolean isEnable;
|
||||
boolean isSimpleView;
|
||||
|
||||
public PhoneConnectRuleModel() {
|
||||
this.ruleText = "";
|
||||
this.isAllowConnection = false;
|
||||
this.isEnable = false;
|
||||
this.isSimpleView = true;
|
||||
}
|
||||
|
||||
public PhoneConnectRuleModel(String ruleText, boolean isAllowConnection, boolean isEnable) {
|
||||
this.ruleText = ruleText;
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
this.isEnable = isEnable;
|
||||
this.isSimpleView = true;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setRuleText(String ruleText) {
|
||||
this.ruleText = ruleText;
|
||||
}
|
||||
|
||||
public String getRuleText() {
|
||||
return ruleText;
|
||||
}
|
||||
|
||||
public void setIsAllowConnection(boolean isAllowConnection) {
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
}
|
||||
|
||||
public boolean isAllowConnection() {
|
||||
return isAllowConnection;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return PhoneConnectRuleModel.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("ruleText").value(getRuleText());
|
||||
jsonWriter.name("isAllowConnection").value(isAllowConnection());
|
||||
jsonWriter.name("isEnable").value(isEnable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("ruleText")) {
|
||||
setRuleText(jsonReader.nextString());
|
||||
} else if (name.equals("isAllowConnection")) {
|
||||
setIsAllowConnection(jsonReader.nextBoolean());
|
||||
} else if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/24 18:47:11
|
||||
* @Describe 手机铃声设置参数类
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import android.util.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import android.media.AudioManager;
|
||||
import android.util.JsonReader;
|
||||
|
||||
public class RingTongBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "AudioRingTongBean";
|
||||
|
||||
// 铃声音量
|
||||
int streamVolume;
|
||||
|
||||
public RingTongBean() {
|
||||
this.streamVolume = 100;
|
||||
}
|
||||
|
||||
public RingTongBean(int streamVolume) {
|
||||
this.streamVolume = streamVolume;
|
||||
}
|
||||
|
||||
public void setStreamVolume(int streamVolume) {
|
||||
this.streamVolume = streamVolume;
|
||||
}
|
||||
|
||||
public int getStreamVolume() {
|
||||
return streamVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return RingTongBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("streamVolume").value(getStreamVolume());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("streamVolume")) {
|
||||
setStreamVolume(jsonReader.nextInt());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package cc.winboll.studio.contacts.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 19:51:40
|
||||
* @Describe SettingsModel
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
|
||||
public class SettingsModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "SettingsModel";
|
||||
|
||||
public static final int MAX_INTRANGE = 666666;
|
||||
public static final int MIN_INTRANGE = 1;
|
||||
|
||||
// 云盾防御层数量
|
||||
int dunTotalCount;
|
||||
// 当前云盾防御层
|
||||
int dunCurrentCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
int dunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
int dunResumeCount;
|
||||
// 是否启用云盾
|
||||
boolean isEnableDun;
|
||||
// BoBullToon 应用模块数据请求地址
|
||||
String szBoBullToon_URL;
|
||||
|
||||
public SettingsModel() {
|
||||
this.dunTotalCount = 6;
|
||||
this.dunCurrentCount = 6;
|
||||
this.dunResumeSecondCount = 60;
|
||||
this.dunResumeCount = 1;
|
||||
this.isEnableDun = false;
|
||||
this.szBoBullToon_URL = "";
|
||||
}
|
||||
|
||||
public SettingsModel(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount, int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
this.isEnableDun = isEnableDun;
|
||||
this.szBoBullToon_URL = szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setBoBullToon_URL(String boBullToon_URL) {
|
||||
this.szBoBullToon_URL = boBullToon_URL;
|
||||
}
|
||||
|
||||
public String getBoBullToon_URL() {
|
||||
return szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setDunTotalCount(int dunTotalCount) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
}
|
||||
|
||||
public int getDunTotalCount() {
|
||||
return dunTotalCount;
|
||||
}
|
||||
|
||||
public void setDunCurrentCount(int dunCurrentCount) {
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
}
|
||||
|
||||
public int getDunCurrentCount() {
|
||||
return dunCurrentCount;
|
||||
}
|
||||
|
||||
public void setDunResumeSecondCount(int dunResumeSecondCount) {
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
}
|
||||
|
||||
public int getDunResumeSecondCount() {
|
||||
return dunResumeSecondCount;
|
||||
}
|
||||
|
||||
public void setDunResumeCount(int dunResumeCount) {
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
}
|
||||
|
||||
public int getDunResumeCount() {
|
||||
return dunResumeCount;
|
||||
}
|
||||
|
||||
public void setIsEnableDun(boolean isEnableDun) {
|
||||
this.isEnableDun = isEnableDun;
|
||||
}
|
||||
|
||||
public boolean isEnableDun() {
|
||||
return isEnableDun;
|
||||
}
|
||||
|
||||
int getSettingsModelRangeInt(int origin) {
|
||||
return IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return SettingsModel.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("dunTotalCount").value(getDunTotalCount());
|
||||
jsonWriter.name("dunCurrentCount").value(getDunCurrentCount());
|
||||
jsonWriter.name("dunResumeSecondCount").value(getDunResumeSecondCount());
|
||||
jsonWriter.name("dunResumeCount").value(getDunResumeCount());
|
||||
jsonWriter.name("isEnableDun").value(isEnableDun());
|
||||
jsonWriter.name("szBoBullToon_URL").value(getBoBullToon_URL());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("dunTotalCount")) {
|
||||
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunCurrentCount")) {
|
||||
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunResumeSecondCount")) {
|
||||
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("dunResumeCount")) {
|
||||
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (name.equals("isEnableDun")) {
|
||||
setIsEnableDun(jsonReader.nextBoolean());
|
||||
} else if (name.equals("szBoBullToon_URL")) {
|
||||
setBoBullToon_URL(jsonReader.nextString());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import android.content.Context;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
package cc.winboll.studio.contacts.dun;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 06:15:10
|
||||
* @Describe 云盾防御规则
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.contacts.utils.RegexPPiUtils;
|
||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.regex.Pattern;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 06:15:10
|
||||
* @Describe 云盾防御规则(双重校验锁单例模式)
|
||||
*/
|
||||
public class Rules {
|
||||
|
||||
public static final String TAG = "Rules";
|
||||
|
||||
ArrayList<PhoneConnectRuleModel> _PhoneConnectRuleModelList;
|
||||
static volatile Rules _Rules;
|
||||
// 单例核心:volatile 保证多线程可见性,禁止指令重排
|
||||
private static volatile Rules sInstance;
|
||||
// 上下文需使用 ApplicationContext 避免内存泄漏
|
||||
private static Context sApplicationContext;
|
||||
|
||||
ArrayList<PhoneConnectRuleBean> _PhoneConnectRuleModelList;
|
||||
Context mContext;
|
||||
SettingsModel mSettingsModel;
|
||||
SettingsBean mSettingsModel;
|
||||
Timer mDunResumeTimer;
|
||||
|
||||
Rules(Context context) {
|
||||
mContext = context;
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleModel>();
|
||||
/**
|
||||
* 私有化构造方法,禁止外部 new 实例
|
||||
*/
|
||||
private Rules(Context context) {
|
||||
mContext = context.getApplicationContext();
|
||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleBean>();
|
||||
reload();
|
||||
}
|
||||
|
||||
public static synchronized Rules getInstance(Context context) {
|
||||
if (_Rules == null) {
|
||||
_Rules = new Rules(context);
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,线程安全)
|
||||
* @param context 上下文,建议传入 ApplicationContext
|
||||
* @return Rules 唯一实例
|
||||
*/
|
||||
public static Rules getInstance(Context context) {
|
||||
// 第一次校验:无锁,提高性能
|
||||
if (sInstance == null) {
|
||||
// 加锁:保证多线程下仅初始化一次
|
||||
synchronized (Rules.class) {
|
||||
// 第二次校验:防止多线程并发时重复创建
|
||||
if (sInstance == null) {
|
||||
sInstance = new Rules(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _Rules;
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
@@ -57,32 +77,35 @@ public class Rules {
|
||||
|
||||
// 盾牌恢复定时器
|
||||
mDunResumeTimer = new Timer();
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsModel.MIN_INTRANGE, SettingsModel.MAX_INTRANGE);
|
||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsBean.MIN_INTRANGE, SettingsBean.MAX_INTRANGE);
|
||||
mDunResumeTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
|
||||
LogUtils.d(TAG, String.format("当前防御值为%d,最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
|
||||
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
|
||||
// 设置盾值在[0,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
||||
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
|
||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
||||
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
}
|
||||
}
|
||||
}, 1000, ss);
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
|
||||
LogUtils.d(TAG, String.format("当前防御值为%d,最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
|
||||
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
|
||||
// 设置盾值在[0,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
||||
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
|
||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
||||
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
|
||||
saveDun();
|
||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
||||
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
}
|
||||
}
|
||||
}, 1000, ss);
|
||||
}
|
||||
|
||||
public void loadRules() {
|
||||
_PhoneConnectRuleModelList.clear();
|
||||
PhoneConnectRuleModel.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
PhoneConnectRuleBean.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
}
|
||||
|
||||
public void saveRules() {
|
||||
LogUtils.d(TAG, String.format("saveRules()"));
|
||||
PhoneConnectRuleModel.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleModel.class);
|
||||
PhoneConnectRuleBean.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
||||
}
|
||||
|
||||
public void resetDefaultBoBullToonURL() {
|
||||
@@ -100,16 +123,16 @@ public class Rules {
|
||||
}
|
||||
|
||||
public void loadDun() {
|
||||
mSettingsModel = SettingsModel.loadBean(mContext, SettingsModel.class);
|
||||
mSettingsModel = SettingsBean.loadBean(mContext, SettingsBean.class);
|
||||
if (mSettingsModel == null) {
|
||||
mSettingsModel = new SettingsModel();
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
mSettingsModel = new SettingsBean();
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveDun() {
|
||||
LogUtils.d(TAG, String.format("saveDun()"));
|
||||
SettingsModel.saveBean(mContext, mSettingsModel);
|
||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
||||
}
|
||||
|
||||
public boolean isAllowed(String phoneNumber) {
|
||||
@@ -119,8 +142,7 @@ public class Rules {
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// 以下是云盾防御体系
|
||||
// 云盾防御体系
|
||||
boolean isDefend = false; // 盾牌是否生效
|
||||
boolean isConnect = true; // 防御结果是否连接
|
||||
|
||||
@@ -189,10 +211,7 @@ public class Rules {
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
} else if (isDefend) {
|
||||
// 如果触发了以上某个防御模块,
|
||||
// 就减少防御盾牌层数。
|
||||
// 每校验一次规则,云盾防御层数减1
|
||||
// 当云盾防御层数为0时,再次进行以下程序段则恢复满值防御。
|
||||
// 如果触发了以上某个防御模块,减少防御盾牌层数
|
||||
int newDunCount = nDunCurrentCount;
|
||||
LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount));
|
||||
|
||||
@@ -203,7 +222,7 @@ public class Rules {
|
||||
} else {
|
||||
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
|
||||
LogUtils.d(TAG, String.format("盾值不在[0,%d]区间,恢复防御最大值%d", mSettingsModel.getDunTotalCount(), mSettingsModel.getDunTotalCount()));
|
||||
}
|
||||
}
|
||||
|
||||
saveDun();
|
||||
SettingsActivity.notifyDunInfoUpdate();
|
||||
@@ -211,18 +230,35 @@ public class Rules {
|
||||
|
||||
// 返回校验结果
|
||||
LogUtils.d(TAG, String.format("返回校验结果 isConnect == %s", isConnect));
|
||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
||||
|
||||
return isConnect;
|
||||
}
|
||||
|
||||
public void add(String szPhoneConnectRule, boolean isAllowConnection, boolean isEnable) {
|
||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleModel(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleBean(szPhoneConnectRule, isAllowConnection, isEnable));
|
||||
}
|
||||
|
||||
public ArrayList<PhoneConnectRuleModel> getPhoneBlacRuleBeanList() {
|
||||
public ArrayList<PhoneConnectRuleBean> getPhoneBlacRuleBeanList() {
|
||||
return _PhoneConnectRuleModelList;
|
||||
}
|
||||
|
||||
public SettingsModel getSettingsModel() {
|
||||
public SettingsBean getSettingsModel() {
|
||||
return mSettingsModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选:释放单例资源(如退出应用时调用)
|
||||
*/
|
||||
public static void releaseInstance() {
|
||||
if (sInstance != null) {
|
||||
sInstance.mDunResumeTimer.cancel();
|
||||
sInstance._PhoneConnectRuleModelList.clear();
|
||||
sInstance.mSettingsModel = null;
|
||||
sInstance.mContext = null;
|
||||
sInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:57:00
|
||||
* @Describe 拨号
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
@@ -24,42 +19,48 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.CallLogAdapter;
|
||||
import cc.winboll.studio.contacts.beans.CallLogModel;
|
||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:57:00
|
||||
* @Describe 通话记录区域视图(支持懒加载,仅切换到当前页才加载数据)
|
||||
*/
|
||||
public class CallLogFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "CallFragment";
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogFragment";
|
||||
public static final int MSG_UPDATE = 1;
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
||||
|
||||
// ====================== 静态成员区 ======================
|
||||
static volatile CallLogFragment _CallLogFragment;
|
||||
|
||||
public static final int MSG_UPDATE = 1; // 添加消息常量
|
||||
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
|
||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
||||
// ====================== UI控件与适配器区 ======================
|
||||
private RecyclerView recyclerView;
|
||||
private CallLogAdapter callLogAdapter;
|
||||
private List<CallLogModel> callLogList = new ArrayList<>();
|
||||
private List<CallLogModel> callLogList = new ArrayList<CallLogModel>();
|
||||
|
||||
// 添加Handler
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
readCallLog(); // 接收到消息时更新通话记录
|
||||
}
|
||||
}
|
||||
};
|
||||
// ====================== 业务逻辑成员区 ======================
|
||||
private Handler mHandler;
|
||||
// 懒加载标记:记录当前Fragment是否已初始化数据(避免重复加载)
|
||||
private boolean isDataInited = false;
|
||||
|
||||
// ====================== 单例与实例化函数区 ======================
|
||||
CallLogFragment() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static CallLogFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建通话记录Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
CallLogFragment fragment = new CallLogFragment();
|
||||
@@ -68,67 +69,159 @@ public class CallLogFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_call_log, container, false);
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
// Java7 兼容:移除Lambda,使用匿名内部类初始化Handler
|
||||
mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到更新消息,开始读取通话记录");
|
||||
readCallLog();
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
return inflater.inflate(R.layout.fragment_call_log, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
recyclerView = view.findViewById(R.id.recyclerView);
|
||||
LogUtils.d(TAG, "onViewCreated: 视图创建完成,仅初始化控件(不加载数据)");
|
||||
// 初始化RecyclerView(仅绑定控件、设置布局管理器,不设置数据/发起请求)
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
// 初始化适配器(传入空列表,后续懒加载时更新数据)
|
||||
callLogAdapter = new CallLogAdapter(getContext(), callLogList);
|
||||
recyclerView.setAdapter(callLogAdapter);
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
|
||||
} else {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE); // 通过Handler触发更新
|
||||
}
|
||||
LogUtils.d(TAG, "onViewCreated: RecyclerView控件初始化完成(未加载数据)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已初始化过数据 → 仅刷新(避免重复初始化,优化性能)
|
||||
if (isDataInited && callLogAdapter != null) {
|
||||
LogUtils.d(TAG, "onResume: 数据已初始化,仅刷新列表");
|
||||
callLogAdapter.relaodContacts();
|
||||
readCallLog(); // 刷新最新通话记录
|
||||
LogUtils.d(TAG, "onResume: 通话记录数据刷新完成");
|
||||
}
|
||||
// 未初始化 → 不操作(等待MainActivity调用initData触发初始化)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
if (mHandler != null) {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDestroy: Handler消息已清空");
|
||||
}
|
||||
// 释放资源,避免内存泄漏
|
||||
if (callLogList != null) {
|
||||
callLogList.clear();
|
||||
callLogList = null;
|
||||
}
|
||||
callLogAdapter = null;
|
||||
recyclerView = null;
|
||||
_CallLogFragment = null;
|
||||
isDataInited = false;
|
||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 权限回调函数区 ======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_READ_CALL_LOG) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE); // 通过Handler触发更新
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 通话记录权限授予成功,开始加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 通话记录权限被拒绝,无法加载数据");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 避免重复初始化(双重防护:标记+判断)
|
||||
if (isDataInited || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 数据已初始化/上下文为空,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化通话记录数据");
|
||||
// 权限检查与数据加载(原onViewCreated中的核心逻辑迁移至此)
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "initData: 读取通话记录权限未授予,发起权限申请");
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
|
||||
} else {
|
||||
LogUtils.d(TAG, "initData: 权限已授予,发送更新消息加载数据");
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
// 标记为已初始化(后续仅刷新,不重复初始化)
|
||||
isDataInited = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化流程完成");
|
||||
}
|
||||
|
||||
// ====================== 业务核心函数区 ======================
|
||||
private void readCallLog() {
|
||||
callLogList.clear(); // 清空原有数据
|
||||
Cursor cursor = requireContext().getContentResolver().query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC");
|
||||
LogUtils.d(TAG, "readCallLog: 开始读取系统通话记录");
|
||||
// 避免空指针(懒加载场景下,控件可能未初始化完成)
|
||||
if (callLogList == null || callLogAdapter == null || getContext() == null) {
|
||||
LogUtils.w(TAG, "readCallLog: 控件/列表为空,跳过读取");
|
||||
return;
|
||||
}
|
||||
callLogList.clear();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = requireContext().getContentResolver().query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC"
|
||||
);
|
||||
if (cursor != null) {
|
||||
LogUtils.d(TAG, "readCallLog: 成功获取通话记录游标,数据条数=" + cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
|
||||
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
|
||||
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
|
||||
Date callDate = new Date(callDateLong);
|
||||
String callStatus = getCallStatus(callType);
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
|
||||
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
|
||||
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
|
||||
Date callDate = new Date(callDateLong);
|
||||
|
||||
String callStatus = getCallStatus(callType);
|
||||
|
||||
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
|
||||
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
|
||||
}
|
||||
callLogAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "readCallLog: 通话记录数据解析完成,共" + callLogList.size() + "条");
|
||||
} else {
|
||||
LogUtils.w(TAG, "readCallLog: 通话记录游标为空");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readCallLog: 读取通话记录异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, "readCallLog: 游标已关闭");
|
||||
}
|
||||
cursor.close();
|
||||
callLogAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,27 +238,21 @@ public class CallLogFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mHandler.removeCallbacksAndMessages(null); // 清理Handler防止内存泄漏
|
||||
}
|
||||
|
||||
// ====================== 外部调用函数区 ======================
|
||||
public void triggerUpdate() {
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
LogUtils.d(TAG, "triggerUpdate: 外部触发通话记录更新");
|
||||
if (isDataInited) { // 已初始化才触发更新(避免未加载时调用)
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateCallLogFragment() {
|
||||
if (_CallLogFragment != null) {
|
||||
LogUtils.d(TAG, "updateCallLogFragment: 静态方法触发Fragment更新");
|
||||
_CallLogFragment.triggerUpdate();
|
||||
} else {
|
||||
LogUtils.w(TAG, "updateCallLogFragment: Fragment实例为空,无法更新");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//ToastUtils.show("onResume");
|
||||
callLogAdapter.relaodContacts();
|
||||
readCallLog(); // 窗口回显时更新通话记录
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人视图
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -29,42 +23,55 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.adapters.ContactAdapter;
|
||||
import cc.winboll.studio.contacts.beans.ContactModel;
|
||||
import cc.winboll.studio.contacts.model.ContactModel;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人区域视图(支持懒加载,仅切换到当前页才加载数据)
|
||||
*/
|
||||
public class ContactsFragment extends Fragment {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactsFragment";
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private static final int REQUEST_READ_CONTACTS = 1;
|
||||
private static final long DEBOUNCE_DELAY = 300; // 搜索防抖延迟
|
||||
|
||||
// ====================== 静态缓存区 ======================
|
||||
// 全局复用联系人数据,减少重复查询
|
||||
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
|
||||
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
private boolean isViewInitialized = false; // 视图初始化标记(控件绑定完成)
|
||||
private boolean isDataLoaded = false; // 数据加载标记(数据+功能初始化完成)
|
||||
private boolean isLazyInitCompleted = false; // 懒加载总标记(供MainActivity判断)
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private RecyclerView recyclerView;
|
||||
private ContactAdapter contactAdapter;
|
||||
private EditText searchEditText;
|
||||
private Button btnDial;
|
||||
private boolean isViewInitialized = false; // 标记视图是否已初始化
|
||||
|
||||
// 静态缓存:全局复用联系人数据
|
||||
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
|
||||
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
|
||||
|
||||
// 当前页面数据容器
|
||||
// ====================== 数据容器区 ======================
|
||||
private List<ContactModel> contactList = new ArrayList<ContactModel>();
|
||||
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
|
||||
|
||||
// 异步工具
|
||||
// ====================== 异步工具区 ======================
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean isDataLoaded = false;
|
||||
|
||||
|
||||
// ====================== 实例化函数区 ======================
|
||||
public static ContactsFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建联系人Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
ContactsFragment fragment = new ContactsFragment();
|
||||
@@ -72,61 +79,154 @@ public class ContactsFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
// 加载布局(已移除进度条相关代码)
|
||||
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
return view;
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
return inflater.inflate(R.layout.fragment_contacts, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// 初始化RecyclerView
|
||||
LogUtils.d(TAG, "onViewCreated: 开始初始化UI控件(仅绑定,不加载数据/功能)");
|
||||
// 初始化RecyclerView(仅绑定控件、设适配器,隐藏列表)
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
contactList = new ArrayList<ContactModel>();
|
||||
contactAdapter = new ContactAdapter(getActivity(), contactList);
|
||||
recyclerView.setAdapter(contactAdapter);
|
||||
// 初始隐藏列表,数据加载后显示
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
|
||||
// 绑定搜索框和拨号按钮
|
||||
// 绑定搜索框和拨号按钮(仅赋值,不显示、不绑定事件)
|
||||
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
|
||||
btnDial = (Button) view.findViewById(R.id.btn_dial);
|
||||
// 初始隐藏搜索相关控件,延迟到首次可见时显示
|
||||
searchEditText.setVisibility(View.GONE);
|
||||
btnDial.setVisibility(View.GONE);
|
||||
|
||||
// 标记视图控件绑定完成
|
||||
isViewInitialized = true;
|
||||
LogUtils.d(TAG, "onViewCreated: UI控件初始化完成(未加载数据/功能)");
|
||||
}
|
||||
|
||||
// 首次可见时初始化资源
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!isViewInitialized) {
|
||||
initSearchAndDial(); // 初始化搜索和拨号功能
|
||||
checkContactPermission(); // 检查权限并加载数据
|
||||
isViewInitialized = true;
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已完成懒加载 → 仅恢复缓存数据(切回页面时刷新)
|
||||
if (isLazyInitCompleted && isDataLoaded) {
|
||||
LogUtils.d(TAG, "onResume: 懒加载已完成,恢复缓存数据");
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
executor.shutdown(); // 关闭线程池
|
||||
mainHandler.removeCallbacksAndMessages(null); // 清空Handler任务
|
||||
// 释放本地数据引用(保留静态缓存,全局复用)
|
||||
if (contactList != null) {
|
||||
contactList.clear();
|
||||
contactList = null;
|
||||
}
|
||||
if (originalContactList != null) {
|
||||
originalContactList.clear();
|
||||
originalContactList = null;
|
||||
}
|
||||
// 重置标记
|
||||
isViewInitialized = false;
|
||||
isDataLoaded = false;
|
||||
isLazyInitCompleted = false;
|
||||
LogUtils.d(TAG, "onDestroy: 异步工具+本地资源已释放");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
LogUtils.d(TAG, "onHiddenChanged: Fragment隐藏状态变更,hidden=" + hidden);
|
||||
// 已完成懒加载+显示状态 → 恢复缓存数据(兼容Tab切换场景)
|
||||
if (!hidden && isLazyInitCompleted && isDataLoaded) {
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "onHiddenChanged: 恢复缓存数据,列表已显示");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化搜索框和拨号按钮
|
||||
// ====================== 权限相关函数区 ======================
|
||||
private void checkContactPermission() {
|
||||
LogUtils.d(TAG, "checkContactPermission: 检查联系人读取权限");
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "checkContactPermission: 权限未授予,发起申请");
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||
} else {
|
||||
LogUtils.d(TAG, "checkContactPermission: 权限已授予,开始加载数据");
|
||||
loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 权限回调触发,requestCode=" + requestCode);
|
||||
if (requestCode == REQUEST_READ_CONTACTS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "onRequestPermissionsResult: 联系人权限授予成功");
|
||||
loadContacts();
|
||||
} else {
|
||||
LogUtils.e(TAG, "onRequestPermissionsResult: 联系人权限被拒绝");
|
||||
ToastUtils.show("请授予联系人权限以查看联系人列表");
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
// 权限拒绝也标记懒加载完成(避免重复触发)
|
||||
isLazyInitCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 双重防护:避免重复初始化(标记+视图就绪判断)
|
||||
if (isLazyInitCompleted || !isViewInitialized || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化(功能+数据)");
|
||||
// 1. 初始化搜索、拨号功能(原onResume首次进入逻辑迁移至此)
|
||||
initSearchAndDial();
|
||||
// 2. 检查权限+加载数据(原onResume首次进入逻辑迁移至此)
|
||||
checkContactPermission();
|
||||
// 标记懒加载总流程完成(无论权限是否授予,仅执行一次)
|
||||
isLazyInitCompleted = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化流程启动完成");
|
||||
}
|
||||
|
||||
// ====================== UI功能初始化区 ======================
|
||||
private void initSearchAndDial() {
|
||||
// 显示搜索相关控件
|
||||
LogUtils.d(TAG, "initSearchAndDial: 初始化搜索和拨号功能");
|
||||
// 显示控件
|
||||
searchEditText.setVisibility(View.VISIBLE);
|
||||
btnDial.setVisibility(View.VISIBLE);
|
||||
|
||||
// 搜索框防抖监听
|
||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(300) {
|
||||
// 搜索防抖监听
|
||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(DEBOUNCE_DELAY) {
|
||||
@Override
|
||||
public void onDebounceTextChanged(String query) {
|
||||
filterContacts(query);
|
||||
@@ -142,68 +242,58 @@ public class ContactsFragment extends Fragment {
|
||||
ToastUtils.show("请输入号码");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initSearchAndDial: 发起拨号,号码=" + phoneNumber);
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initSearchAndDial: 功能初始化完成");
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
private void checkContactPermission() {
|
||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||
} else {
|
||||
loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载联系人(延迟到首次可见时)
|
||||
// ====================== 数据加载与处理区 ======================
|
||||
private void loadContacts() {
|
||||
// 若有缓存,直接复用
|
||||
// 优先使用缓存数据(保留原有缓存逻辑,提升性能)
|
||||
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
|
||||
LogUtils.d(TAG, "loadContacts: 存在缓存数据,直接复用");
|
||||
originalContactList.clear();
|
||||
originalContactList.addAll(sCachedOriginalList);
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE); // 显示列表
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
isDataLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 无缓存时异步加载
|
||||
// 无缓存时异步加载(保留原有异步逻辑,避免主线程阻塞)
|
||||
if (!isDataLoaded) {
|
||||
recyclerView.setVisibility(View.GONE); // 加载中隐藏列表
|
||||
|
||||
LogUtils.d(TAG, "loadContacts: 无缓存,异步读取联系人数据");
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 子线程读取联系人
|
||||
final List<ContactModel> tempList = readContactsInBackground();
|
||||
|
||||
// 主线程更新UI
|
||||
// 主线程更新UI和缓存
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 更新缓存
|
||||
sCachedOriginalList.clear();
|
||||
sCachedOriginalList.addAll(tempList);
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(tempList);
|
||||
|
||||
// 更新当前列表
|
||||
originalContactList.clear();
|
||||
originalContactList.addAll(sCachedOriginalList);
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, String.format("联系人加载完成,共%d条数据", contactList.size()));
|
||||
|
||||
// 数据加载后显示列表
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
isDataLoaded = true;
|
||||
|
||||
LogUtils.d(TAG, "loadContacts: 联系人数据加载完成,共" + contactList.size() + "条");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,12 +301,11 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// 子线程读取联系人
|
||||
private List<ContactModel> readContactsInBackground() {
|
||||
LogUtils.d(TAG, "readContactsInBackground: 子线程读取联系人");
|
||||
List<ContactModel> tempList = new ArrayList<ContactModel>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
// 查询联系人姓名和号码
|
||||
cursor = requireContext().getContentResolver().query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
new String[]{
|
||||
@@ -231,66 +320,52 @@ public class ContactsFragment extends Fragment {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
|
||||
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
|
||||
|
||||
do {
|
||||
String name = cursor.getString(nameIndex);
|
||||
String number = cursor.getString(numberIndex).replaceAll("\\s", ""); // 去除空格
|
||||
String number = cursor.getString(numberIndex).replaceAll("\\s", "");
|
||||
tempList.add(new ContactModel(name, number));
|
||||
} while (cursor.moveToNext());
|
||||
LogUtils.d(TAG, "readContactsInBackground: 成功读取" + tempList.size() + "条联系人数据");
|
||||
} else {
|
||||
LogUtils.w(TAG, "readContactsInBackground: 未读取到联系人数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "读取联系人失败:" + e);
|
||||
LogUtils.e(TAG, "readContactsInBackground: 读取联系人异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, "readContactsInBackground: 游标已关闭");
|
||||
}
|
||||
}
|
||||
return tempList;
|
||||
}
|
||||
|
||||
// 过滤联系人
|
||||
private void filterContacts(String query) {
|
||||
LogUtils.d(TAG, "filterContacts: 搜索过滤,关键词=" + query);
|
||||
contactList.clear();
|
||||
sCachedFilteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
contactList.addAll(originalContactList);
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(originalContactList);
|
||||
} else {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
for (ContactModel contact : originalContactList) {
|
||||
// 匹配姓名、全拼、简拼、号码
|
||||
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
|
||||
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
|
||||
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
|
||||
boolean matchNumber = contact.getNumber().contains(lowerQuery);
|
||||
|
||||
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
|
||||
contactList.add(contact);
|
||||
}
|
||||
}
|
||||
sCachedFilteredList.clear();
|
||||
sCachedFilteredList.addAll(contactList);
|
||||
}
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
// 过滤后确保列表可见
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "filterContacts: 过滤完成,显示" + contactList.size() + "条数据");
|
||||
}
|
||||
|
||||
// 权限回调
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_READ_CONTACTS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadContacts(); // 授权后加载联系人
|
||||
} else {
|
||||
ToastUtils.show("请授予联系人权限以查看联系人列表");
|
||||
recyclerView.setVisibility(View.VISIBLE); // 显示空列表
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖TextWatcher(Java 7实现)
|
||||
// ====================== 内部防抖监听类 ======================
|
||||
public abstract static class DebounceTextWatcher implements TextWatcher {
|
||||
private final long debounceDelay;
|
||||
private Handler handler = new Handler(Looper.getMainLooper());
|
||||
@@ -301,17 +376,13 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// 无需处理
|
||||
}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, int start, int before, int count) {
|
||||
// 移除之前的延迟任务
|
||||
if (pendingRunnable != null) {
|
||||
handler.removeCallbacks(pendingRunnable);
|
||||
}
|
||||
// 延迟执行过滤
|
||||
pendingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -322,33 +393,9 @@ public class ContactsFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 无需处理
|
||||
}
|
||||
public void afterTextChanged(Editable s) {}
|
||||
|
||||
// 抽象方法:防抖后的回调
|
||||
public abstract void onDebounceTextChanged(String query);
|
||||
}
|
||||
|
||||
// 资源释放
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
executor.shutdown(); // 关闭线程池
|
||||
mainHandler.removeCallbacksAndMessages(null); // 清除未执行任务
|
||||
}
|
||||
|
||||
// Fragment隐藏/显示时的处理
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden && isDataLoaded) {
|
||||
// 复用缓存数据并显示列表
|
||||
contactList.clear();
|
||||
contactList.addAll(sCachedFilteredList);
|
||||
contactAdapter.notifyDataSetChanged();
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.fragments;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:58:15
|
||||
* @Describe 应用日志
|
||||
*/
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -13,18 +8,34 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/20 12:58:15
|
||||
* @Describe 应用日志区域视图(支持懒加载,仅切换到当前页才启动日志)
|
||||
*/
|
||||
public class LogFragment extends Fragment {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "LogFragment";
|
||||
|
||||
private static final String ARG_PAGE = "ARG_PAGE";
|
||||
private int mPage;
|
||||
|
||||
LogView mLogView;
|
||||
|
||||
// ====================== 页面参数区 ======================
|
||||
private int mPage;
|
||||
|
||||
// ====================== UI控件区 ======================
|
||||
private LogView mLogView;
|
||||
|
||||
// ====================== 懒加载标记区 ======================
|
||||
private boolean isViewInitialized = false; // 视图控件绑定完成标记
|
||||
private boolean isLazyInitCompleted = false; // 懒加载总流程完成标记
|
||||
private boolean isLogViewStarted = false; // LogView启动状态标记
|
||||
|
||||
// ====================== 实例化函数区 ======================
|
||||
public static LogFragment newInstance(int page) {
|
||||
LogUtils.d(TAG, "newInstance: 创建日志Fragment实例,页码=" + page);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_PAGE, page);
|
||||
LogFragment fragment = new LogFragment();
|
||||
@@ -32,30 +43,76 @@ public class LogFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// ====================== 生命周期函数区 ======================
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
||||
if (getArguments() != null) {
|
||||
mPage = getArguments().getInt(ARG_PAGE);
|
||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
||||
View view = inflater.inflate(R.layout.fragment_log, container, false);
|
||||
mLogView = view.findViewById(R.id.logview);
|
||||
mLogView.start();
|
||||
// Java7 适配:添加强制类型转换,仅初始化LogView控件(不启动)
|
||||
mLogView = (LogView) view.findViewById(R.id.logview);
|
||||
LogUtils.d(TAG, "onCreateView: LogView控件初始化完成(未启动)");
|
||||
// 标记视图控件绑定完成
|
||||
isViewInitialized = true;
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//ToastUtils.show("onResume");
|
||||
mLogView.start();
|
||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
||||
// 已完成懒加载 → 仅重启LogView(切回页面时恢复日志显示)
|
||||
if (isLazyInitCompleted && mLogView != null && !isLogViewStarted) {
|
||||
mLogView.start();
|
||||
isLogViewStarted = true;
|
||||
LogUtils.d(TAG, "onResume: LogView已重启,恢复日志显示");
|
||||
}
|
||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
||||
if (mLogView != null) {
|
||||
// 若LogView有停止方法,必须调用(避免后台持续占用资源,根据实际API调整)
|
||||
// mLogView.stop(); // 关键:释放LogView资源,防止内存泄漏
|
||||
LogUtils.d(TAG, "onDestroy: LogView资源已释放");
|
||||
}
|
||||
// 重置所有标记,避免重建时状态异常
|
||||
mLogView = null;
|
||||
isViewInitialized = false;
|
||||
isLazyInitCompleted = false;
|
||||
isLogViewStarted = false;
|
||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
||||
public void initData() {
|
||||
// 双重防护:避免重复初始化(标记+视图就绪+控件非空)
|
||||
if (isLazyInitCompleted || !isViewInitialized || mLogView == null || getContext() == null) {
|
||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "initData: 开始懒加载初始化,启动LogView");
|
||||
// 核心:启动LogView(原onCreateView中的start逻辑迁移至此)
|
||||
mLogView.start();
|
||||
isLogViewStarted = true;
|
||||
// 标记懒加载总流程完成(仅执行一次)
|
||||
isLazyInitCompleted = true;
|
||||
LogUtils.d(TAG, "initData: 懒加载初始化完成,LogView正常启动");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
@@ -14,198 +16,377 @@ import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.Nullable;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity;
|
||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Describe 通话监听服务(无前台服务),负责监听通话状态、显示通话悬浮窗、跳转通话界面
|
||||
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 避免内存泄漏
|
||||
*/
|
||||
public class CallListenerService extends Service {
|
||||
|
||||
private View phoneCallView;
|
||||
private TextView tvCallNumber;
|
||||
private Button btnOpenApp;
|
||||
// ====================== 常量定义区(精准适配API29-30,无冗余) ======================
|
||||
public static final String TAG = "CallListenerService";
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams params;
|
||||
// Android版本常量(仅保留适配必需版本,精简无用定义)
|
||||
private static final int ANDROID_8_API = 26; // 悬浮窗类型适配(API26+必需)
|
||||
private static final int ANDROID_10_API = 29; // API29+ 悬浮窗权限/参数适配
|
||||
private static final int ANDROID_19_API = 19; // 透明状态栏/导航栏适配
|
||||
|
||||
private PhoneStateListener phoneStateListener;
|
||||
private TelephonyManager telephonyManager;
|
||||
// 延迟初始化参数(让出主线程,避免启动阻塞)
|
||||
private static final long DELAY_INIT_MS = 100L;
|
||||
|
||||
private String callNumber;
|
||||
private boolean hasShown;
|
||||
private boolean isCallingIn;
|
||||
// ====================== 成员属性区(按功能归类,命名规范) ======================
|
||||
// 延迟初始化核心
|
||||
private Handler mDelayHandler; // 延迟处理器(避免onCreate阻塞)
|
||||
|
||||
// 通话监听核心
|
||||
private TelephonyManager mTelephonyManager; // 电话管理器(监听通话状态)
|
||||
private PhoneStateListener mPhoneStateListener;// 通话状态监听回调
|
||||
private String mCallNumber; // 当前通话号码
|
||||
private boolean mIsCallingIn; // 是否为来电(true=来电,false=去电)
|
||||
|
||||
// 悬浮窗核心
|
||||
private WindowManager mWindowManager; // 窗口管理器(添加/移除悬浮窗)
|
||||
private WindowManager.LayoutParams mWindowParams;// 悬浮窗参数配置
|
||||
private View mPhoneCallView; // 通话悬浮窗根视图
|
||||
private TextView mTvCallNumber; // 悬浮窗号码显示控件
|
||||
private Button mBtnOpenApp; // 悬浮窗跳转APP按钮
|
||||
private boolean mHasShown; // 悬浮窗显示状态标记(避免重复操作)
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 =====");
|
||||
|
||||
initPhoneStateListener();
|
||||
// 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度)
|
||||
initDelayHandlerAndLogic();
|
||||
|
||||
initPhoneCallView();
|
||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动完成 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId);
|
||||
|
||||
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
|
||||
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
|
||||
return startMode;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务无需绑定,返回null");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化来电状态监听器
|
||||
*/
|
||||
private void initPhoneStateListener() {
|
||||
phoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
|
||||
callNumber = incomingNumber;
|
||||
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE: // 待机,即无电话时,挂断时触发
|
||||
dismiss();
|
||||
break;
|
||||
|
||||
case TelephonyManager.CALL_STATE_RINGING: // 响铃,来电时触发
|
||||
isCallingIn = true;
|
||||
updateUI();
|
||||
show();
|
||||
break;
|
||||
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK: // 摘机,接听或拨出电话时触发
|
||||
updateUI();
|
||||
show();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 设置来电监听器
|
||||
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
if (telephonyManager != null) {
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void initPhoneCallView() {
|
||||
windowManager = (WindowManager) getApplicationContext()
|
||||
.getSystemService(Context.WINDOW_SERVICE);
|
||||
int width = windowManager.getDefaultDisplay().getWidth();
|
||||
int height = windowManager.getDefaultDisplay().getHeight();
|
||||
|
||||
params = new WindowManager.LayoutParams();
|
||||
params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
|
||||
params.width = width;
|
||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
|
||||
// 设置图片格式,效果为背景透明
|
||||
params.format = PixelFormat.TRANSLUCENT;
|
||||
// 设置 Window flag 为系统级弹框 | 覆盖表层
|
||||
params.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
|
||||
WindowManager.LayoutParams.TYPE_PHONE;
|
||||
|
||||
// 不可聚集(不响应返回键)| 全屏
|
||||
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
// API 19 以上则还可以开启透明状态栏与导航栏
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
params.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
|
||||
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
}
|
||||
|
||||
FrameLayout interceptorLayout = new FrameLayout(this) {
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
phoneCallView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.view_phone_call, interceptorLayout);
|
||||
tvCallNumber = phoneCallView.findViewById(R.id.tv_call_number);
|
||||
btnOpenApp = phoneCallView.findViewById(R.id.btn_open_app);
|
||||
btnOpenApp.setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// CallListenerService.this.startActivity(intent);
|
||||
|
||||
PhoneCallService.CallType callType = isCallingIn ? PhoneCallService.CallType.CALL_IN: PhoneCallService.CallType.CALL_OUT;
|
||||
PhoneCallActivity.actionStart(CallListenerService.this, callNumber, callType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示顶级弹框展示通话信息
|
||||
*/
|
||||
private void show() {
|
||||
if (!hasShown) {
|
||||
windowManager.addView(phoneCallView, params);
|
||||
hasShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消显示
|
||||
*/
|
||||
private void dismiss() {
|
||||
if (hasShown) {
|
||||
windowManager.removeView(phoneCallView);
|
||||
isCallingIn = false;
|
||||
hasShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
tvCallNumber.setText(formatPhoneNumber(callNumber));
|
||||
|
||||
int callTypeDrawable = isCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
|
||||
tvCallNumber.setCompoundDrawablesWithIntrinsicBounds(null, null,
|
||||
getResources().getDrawable(callTypeDrawable), null);
|
||||
}
|
||||
|
||||
public static String formatPhoneNumber(String phoneNum) {
|
||||
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
|
||||
return phoneNum.substring(0, 3) + "-"
|
||||
+ phoneNum.substring(3, 7) + "-"
|
||||
+ phoneNum.substring(7);
|
||||
}
|
||||
return phoneNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务开始销毁 =====");
|
||||
|
||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
// 全量清理资源,彻底避免内存泄漏
|
||||
dismissFloatWindow(); // 移除悬浮窗
|
||||
unregisterPhoneStateListener();// 注销通话监听
|
||||
clearDelayHandler(); // 清空延迟任务
|
||||
resetAllReferences(); // 置空所有成员属性
|
||||
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务销毁完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 延迟初始化方法区(非阻塞启动,提升稳定性) ======================
|
||||
/**
|
||||
* 初始化延迟处理器,执行核心逻辑(通话监听+悬浮窗)
|
||||
*/
|
||||
private void initDelayHandlerAndLogic() {
|
||||
mDelayHandler = new Handler(Looper.getMainLooper());
|
||||
mDelayHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 开始延迟初始化核心逻辑");
|
||||
initPhoneStateListener(); // 初始化通话状态监听
|
||||
initFloatWindow(); // 初始化通话悬浮窗
|
||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 延迟初始化完成,服务就绪");
|
||||
}
|
||||
}, DELAY_INIT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话状态监听(注册TelephonyManager,响应通话状态变化)
|
||||
*/
|
||||
private void initPhoneStateListener() {
|
||||
// 1. 创建通话状态监听回调
|
||||
mPhoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int callState, String incomingNumber) {
|
||||
super.onCallStateChanged(callState, incomingNumber);
|
||||
mCallNumber = incomingNumber;
|
||||
LogUtils.d(TAG, "onCallStateChanged: 通话状态变化,状态=" + getCallStateDesc(callState) + ",号码=" + incomingNumber);
|
||||
|
||||
// 响应不同通话状态
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// 通话空闲(挂断/未通话):隐藏悬浮窗
|
||||
dismissFloatWindow();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
// 来电响铃:标记来电状态,更新UI并显示悬浮窗
|
||||
mIsCallingIn = true;
|
||||
updateFloatWindowUI();
|
||||
showFloatWindow();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
// 通话中(接听/拨号):更新UI并显示悬浮窗
|
||||
updateFloatWindowUI();
|
||||
showFloatWindow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 注册通话监听(非空校验,避免崩溃)
|
||||
mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
if (mTelephonyManager != null) {
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
LogUtils.d(TAG, "initPhoneStateListener: 通话状态监听注册成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败,监听注册失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通话悬浮窗(配置参数+加载布局+绑定事件,适配API29-30)
|
||||
*/
|
||||
private void initFloatWindow() {
|
||||
// 1. 获取窗口管理器(非空校验,避免后续崩溃)
|
||||
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
|
||||
if (mWindowManager == null) {
|
||||
LogUtils.e(TAG, "initFloatWindow: WindowManager获取失败,悬浮窗初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 配置悬浮窗参数(精准适配API29+,兼容悬浮窗权限)
|
||||
initFloatWindowParams();
|
||||
|
||||
// 3. 加载悬浮窗布局(添加返回键拦截,避免误关闭)
|
||||
FrameLayout keyInterceptorLayout = new FrameLayout(this) {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// 拦截返回键,保障通话时悬浮窗正常显示
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件");
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
mPhoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, keyInterceptorLayout);
|
||||
|
||||
// 4. 绑定悬浮窗控件,设置跳转按钮事件
|
||||
bindFloatWindowViews();
|
||||
|
||||
LogUtils.d(TAG, "initFloatWindow: 悬浮窗初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置悬浮窗参数(适配API29+窗口类型,确保正常显示)
|
||||
*/
|
||||
private void initFloatWindowParams() {
|
||||
mWindowParams = new WindowManager.LayoutParams();
|
||||
// 窗口位置:顶部居中
|
||||
mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
|
||||
// 窗口大小:宽度全屏,高度自适应
|
||||
mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 固定竖屏显示
|
||||
mWindowParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
// 窗口格式:半透明
|
||||
mWindowParams.format = PixelFormat.TRANSLUCENT;
|
||||
|
||||
// 窗口类型(API29+ 强制用 TYPE_APPLICATION_OVERLAY,需悬浮窗权限)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
LogUtils.d(TAG, "initFloatWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY(需开启悬浮窗权限)");
|
||||
} else if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
} else {
|
||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
|
||||
}
|
||||
|
||||
// 窗口标志:无焦点(不抢占输入)、全屏布局、兼容透明状态栏/导航栏
|
||||
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_19_API) {
|
||||
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定悬浮窗控件,设置跳转通话详情页事件
|
||||
*/
|
||||
private void bindFloatWindowViews() {
|
||||
mTvCallNumber = (TextView) mPhoneCallView.findViewById(R.id.tv_call_number);
|
||||
mBtnOpenApp = (Button) mPhoneCallView.findViewById(R.id.btn_open_app);
|
||||
|
||||
// 跳转按钮点击事件
|
||||
mBtnOpenApp.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (TextUtils.isEmpty(mCallNumber)) {
|
||||
LogUtils.w(TAG, "bindFloatWindowViews: 通话号码为空,跳过跳转");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "bindFloatWindowViews: 点击跳转通话详情页,号码=" + mCallNumber);
|
||||
PhoneCallService.CallType callType = mIsCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT;
|
||||
PhoneCallActivity.actionStart(CallListenerService.this, mCallNumber, callType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 悬浮窗功能逻辑区(显示/隐藏/更新UI) ======================
|
||||
/**
|
||||
* 显示通话悬浮窗(避免重复添加,防止窗口泄露)
|
||||
*/
|
||||
private void showFloatWindow() {
|
||||
if (!mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
||||
try {
|
||||
mWindowManager.addView(mPhoneCallView, mWindowParams);
|
||||
mHasShown = true;
|
||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗显示成功");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示失败(无悬浮窗权限,需引导用户开启)", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示异常", e);
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗已显示/组件未初始化,跳过显示");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏通话悬浮窗(避免重复移除,防止崩溃)
|
||||
*/
|
||||
private void dismissFloatWindow() {
|
||||
if (mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
||||
try {
|
||||
mWindowManager.removeView(mPhoneCallView);
|
||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗隐藏成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "dismissFloatWindow: 悬浮窗隐藏异常", e);
|
||||
} finally {
|
||||
mHasShown = false;
|
||||
mIsCallingIn = false; // 重置来电状态标记
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗已隐藏/组件未初始化,跳过隐藏");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新悬浮窗UI(显示格式化号码+通话类型图标)
|
||||
*/
|
||||
private void updateFloatWindowUI() {
|
||||
if (mTvCallNumber == null || TextUtils.isEmpty(mCallNumber)) {
|
||||
LogUtils.w(TAG, "updateFloatWindowUI: 控件未初始化/号码为空,更新失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化11位手机号(3-4-4分隔,提升可读性)
|
||||
String formattedNumber = formatPhoneNumber(mCallNumber);
|
||||
mTvCallNumber.setText(formattedNumber);
|
||||
|
||||
// 设置通话类型图标(来电/去电区分)
|
||||
int iconResId = mIsCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
|
||||
mTvCallNumber.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null, null, getResources().getDrawable(iconResId), null
|
||||
);
|
||||
LogUtils.d(TAG, "updateFloatWindowUI: 悬浮窗UI更新完成,号码=" + formattedNumber + ",类型=" + (mIsCallingIn ? "来电" : "去电"));
|
||||
}
|
||||
|
||||
// ====================== 资源清理方法区(服务销毁时全量释放) ======================
|
||||
/**
|
||||
* 注销通话状态监听(释放TelephonyManager资源)
|
||||
*/
|
||||
private void unregisterPhoneStateListener() {
|
||||
if (mTelephonyManager != null && mPhoneStateListener != null) {
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
LogUtils.d(TAG, "unregisterPhoneStateListener: 通话监听已注销");
|
||||
}
|
||||
mTelephonyManager = null;
|
||||
mPhoneStateListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空延迟处理器(移除未执行任务,避免内存泄漏)
|
||||
*/
|
||||
private void clearDelayHandler() {
|
||||
if (mDelayHandler != null) {
|
||||
mDelayHandler.removeCallbacksAndMessages(null);
|
||||
mDelayHandler = null;
|
||||
LogUtils.d(TAG, "clearDelayHandler: 延迟处理器已清空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 置空所有成员属性(彻底释放引用,避免内存泄漏)
|
||||
*/
|
||||
private void resetAllReferences() {
|
||||
mCallNumber = null;
|
||||
mPhoneCallView = null;
|
||||
mWindowParams = null;
|
||||
mWindowManager = null;
|
||||
mTvCallNumber = null;
|
||||
mBtnOpenApp = null;
|
||||
}
|
||||
|
||||
// ====================== 工具方法区(通用辅助功能,独立归类) ======================
|
||||
/**
|
||||
* 格式化手机号(11位手机号:3-4-4分隔,非11位保持原格式)
|
||||
* @param phoneNum 待格式化的手机号
|
||||
* @return 格式化后的号码
|
||||
*/
|
||||
public static String formatPhoneNumber(String phoneNum) {
|
||||
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
|
||||
String formatted = phoneNum.substring(0, 3) + "-"
|
||||
+ phoneNum.substring(3, 7) + "-"
|
||||
+ phoneNum.substring(7);
|
||||
LogUtils.d(TAG, "formatPhoneNumber: 号码格式化,原=" + phoneNum + ",新=" + formatted);
|
||||
return formatted;
|
||||
}
|
||||
LogUtils.d(TAG, "formatPhoneNumber: 非11位号码,无需格式化,号码=" + phoneNum);
|
||||
return phoneNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换通话状态为文字描述(便于日志查看,快速定位问题)
|
||||
* @param callState 通话状态(TelephonyManager常量)
|
||||
* @return 状态描述文字
|
||||
*/
|
||||
private String getCallStateDesc(int callState) {
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲(挂断/未通话)";
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃(来电)";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中(接听/拨号)";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 13:10:57
|
||||
* @Describe 通话记录数据模型
|
||||
*/
|
||||
public class CallLogModel {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "CallLogModel";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String phoneNumber;
|
||||
private String callStatus;
|
||||
private Date callDate;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
|
||||
// 去除号码中的空格并初始化
|
||||
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
|
||||
this.callStatus = callStatus;
|
||||
this.callDate = callDate;
|
||||
|
||||
LogUtils.d(TAG, "CallLogModel: 初始化通话记录模型 | 号码=" + this.phoneNumber
|
||||
+ " | 状态=" + this.callStatus + " | 时间=" + this.callDate);
|
||||
}
|
||||
|
||||
// ====================== Getter 方法区 ======================
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getCallStatus() {
|
||||
return callStatus;
|
||||
}
|
||||
|
||||
public Date getCallDate() {
|
||||
return callDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人信息数据模型,支持姓名转全拼和拼音首字母
|
||||
*/
|
||||
public class ContactModel {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactModel";
|
||||
// 汉字匹配正则常量,避免重复创建
|
||||
private static final String CHINESE_CHAR_REGEX = "[\\u4e00-\\u9fa5]";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String name;
|
||||
private String number;
|
||||
private String pinyin;
|
||||
private String pinyinFirstLetter;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public ContactModel(String name, String number) {
|
||||
LogUtils.d(TAG, "ContactModel: 开始初始化联系人模型");
|
||||
this.name = name == null ? "" : name;
|
||||
// 去除号码空格,空值处理为""
|
||||
this.number = number == null ? "" : number.replaceAll("\\s", "");
|
||||
// 初始化拼音和拼音首字母
|
||||
this.pinyin = convertToPinyin(this.name);
|
||||
this.pinyinFirstLetter = convertToPinyinFirstLetter(this.name);
|
||||
|
||||
LogUtils.d(TAG, "ContactModel: 联系人初始化完成 | 姓名=" + this.name
|
||||
+ " | 号码=" + this.number + " | 全拼=" + this.pinyin
|
||||
+ " | 拼音首字母=" + this.pinyinFirstLetter);
|
||||
}
|
||||
|
||||
// ====================== 拼音转换工具方法区 ======================
|
||||
/**
|
||||
* 姓名转为全拼(多音字默认取首个拼音)
|
||||
*/
|
||||
private String convertToPinyin(String chinese) {
|
||||
LogUtils.d(TAG, "convertToPinyin: 开始转换姓名为全拼,姓名=" + chinese);
|
||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
||||
StringBuilder pinyinSb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
// 仅处理汉字
|
||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
pinyinSb.append(pinyinArray[0]);
|
||||
LogUtils.v(TAG, "convertToPinyin: 字符[" + ch + "]转为拼音[" + pinyinArray[0] + "]");
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "convertToPinyin: 拼音转换异常,字符=" + ch, e);
|
||||
}
|
||||
} else {
|
||||
pinyinSb.append(ch);
|
||||
LogUtils.v(TAG, "convertToPinyin: 非汉字字符直接拼接,字符=" + ch);
|
||||
}
|
||||
}
|
||||
|
||||
String result = pinyinSb.toString();
|
||||
LogUtils.d(TAG, "convertToPinyin: 全拼转换完成,结果=" + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名转为拼音首字母(多音字默认取首个拼音首字母)
|
||||
*/
|
||||
private String convertToPinyinFirstLetter(String chinese) {
|
||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 开始转换姓名为拼音首字母,姓名=" + chinese);
|
||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
||||
StringBuilder firstLetterSb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < chinese.length(); i++) {
|
||||
char ch = chinese.charAt(i);
|
||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
char firstChar = pinyinArray[0].charAt(0);
|
||||
firstLetterSb.append(firstChar);
|
||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 字符[" + ch + "]转为首字母[" + firstChar + "]");
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "convertToPinyinFirstLetter: 拼音首字母转换异常,字符=" + ch, e);
|
||||
}
|
||||
} else {
|
||||
firstLetterSb.append(ch);
|
||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 非汉字字符直接拼接,字符=" + ch);
|
||||
}
|
||||
}
|
||||
|
||||
String result = firstLetterSb.toString();
|
||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 拼音首字母转换完成,结果=" + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统一的拼音输出格式(小写、无音调)
|
||||
* 抽离为公共方法,避免重复创建对象
|
||||
*/
|
||||
private HanyuPinyinOutputFormat getPinyinOutputFormat() {
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
return format;
|
||||
}
|
||||
|
||||
// ====================== Getter 方法区 ======================
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getPinyin() {
|
||||
return pinyin;
|
||||
}
|
||||
|
||||
public String getPinyinFirstLetter() {
|
||||
return pinyinFirstLetter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 07:06:13
|
||||
* @Describe 主服务配置实体类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class MainServiceBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainServiceBean";
|
||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private boolean isEnable;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainServiceBean() {
|
||||
this.isEnable = false;
|
||||
LogUtils.d(TAG, "MainServiceBean: 初始化实体类,默认状态为禁用");
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
LogUtils.d(TAG, "setIsEnable: 服务状态设置为" + isEnable);
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = MainServiceBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将实体类写入JSON");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入服务启用状态字段
|
||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(this.isEnable);
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON写入完成,isEnable=" + this.isEnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类方法处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_IS_ENABLE.equals(name)) {
|
||||
this.isEnable = jsonReader.nextBoolean();
|
||||
LogUtils.d(TAG, "initObjectsFromJsonReader: 读取字段[" + name + "]值=" + this.isEnable);
|
||||
return true;
|
||||
}
|
||||
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别字段=" + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON读取实体类数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + name);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON读取完成,当前实体状态=" + this.isEnable);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/21 09:52:10
|
||||
* @Describe 电话黑名单规则实体类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class PhoneConnectRuleBean extends BaseBean {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneConnectRuleModel";
|
||||
// JSON字段名常量,避免硬编码错误
|
||||
private static final String JSON_KEY_RULE_TEXT = "ruleText";
|
||||
private static final String JSON_KEY_ALLOW_CONNECTION = "isAllowConnection";
|
||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private String ruleText;
|
||||
private boolean isAllowConnection;
|
||||
private boolean isEnable;
|
||||
private boolean isSimpleView;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,初始化默认值
|
||||
*/
|
||||
public PhoneConnectRuleBean() {
|
||||
this.ruleText = "";
|
||||
this.isAllowConnection = false;
|
||||
this.isEnable = false;
|
||||
this.isSimpleView = true;
|
||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 默认构造初始化完成 | 规则文本空串,默认禁用状态");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化核心规则参数
|
||||
*/
|
||||
public PhoneConnectRuleBean(String ruleText, boolean isAllowConnection, boolean isEnable) {
|
||||
this.ruleText = ruleText == null ? "" : ruleText;
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
this.isEnable = isEnable;
|
||||
this.isSimpleView = true;
|
||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 带参构造初始化完成 | 规则文本=" + this.ruleText
|
||||
+ " | 允许连接=" + this.isAllowConnection + " | 规则启用=" + this.isEnable);
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public String getRuleText() {
|
||||
return ruleText;
|
||||
}
|
||||
|
||||
public void setRuleText(String ruleText) {
|
||||
String oldValue = this.ruleText;
|
||||
this.ruleText = ruleText == null ? "" : ruleText;
|
||||
LogUtils.d(TAG, "setRuleText: 规则文本更新 | 旧值=" + oldValue + " | 新值=" + this.ruleText);
|
||||
}
|
||||
|
||||
public boolean isAllowConnection() {
|
||||
return isAllowConnection;
|
||||
}
|
||||
|
||||
public void setIsAllowConnection(boolean isAllowConnection) {
|
||||
LogUtils.d(TAG, "setIsAllowConnection: 允许连接状态更新为" + isAllowConnection);
|
||||
this.isAllowConnection = isAllowConnection;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
LogUtils.d(TAG, "setIsEnable: 规则启用状态更新为" + isEnable);
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
LogUtils.d(TAG, "setIsSimpleView: 视图模式更新 | 简洁模式=" + isSimpleView);
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = PhoneConnectRuleBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化规则数据");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 序列化核心字段
|
||||
jsonWriter.name(JSON_KEY_RULE_TEXT).value(getRuleText());
|
||||
jsonWriter.name(JSON_KEY_ALLOW_CONNECTION).value(isAllowConnection());
|
||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(isEnable());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先让父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_RULE_TEXT.equals(name)) {
|
||||
setRuleText(jsonReader.nextString());
|
||||
} else if (JSON_KEY_ALLOW_CONNECTION.equals(name)) {
|
||||
setIsAllowConnection(jsonReader.nextBoolean());
|
||||
} else if (JSON_KEY_IS_ENABLE.equals(name)) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析规则数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 解析后规则=" + getRuleText());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/24 18:47:11
|
||||
* @Describe 手机铃声设置参数类,支持JSON序列化与反序列化
|
||||
*/
|
||||
public class RingTongBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AudioRingTongBean";
|
||||
private static final String JSON_KEY_STREAM_VOLUME = "streamVolume";
|
||||
// 铃声音量范围常量(参考AudioManager标准)
|
||||
private static final int VOLUME_MIN = 0;
|
||||
private static final int VOLUME_MAX = 100;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private int streamVolume;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,铃声音量初始化为最大值
|
||||
*/
|
||||
public RingTongBean() {
|
||||
this.streamVolume = VOLUME_MAX;
|
||||
LogUtils.d(TAG, "RingTongBean: 默认构造初始化 | 铃声音量=" + this.streamVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化指定铃声音量
|
||||
*/
|
||||
public RingTongBean(int streamVolume) {
|
||||
// 音量值范围校验,避免非法值
|
||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
||||
LogUtils.d(TAG, "RingTongBean: 带参构造初始化 | 原始音量=" + streamVolume + " | 校正后=" + this.streamVolume);
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public int getStreamVolume() {
|
||||
return streamVolume;
|
||||
}
|
||||
|
||||
public void setStreamVolume(int streamVolume) {
|
||||
int oldVolume = this.streamVolume;
|
||||
// 音量值范围校验
|
||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
||||
LogUtils.d(TAG, "setStreamVolume: 铃声音量更新 | 旧值=" + oldVolume + " | 新值=" + this.streamVolume);
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = RingTongBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化铃声音量参数");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name(JSON_KEY_STREAM_VOLUME).value(getStreamVolume());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成 | 音量值=" + getStreamVolume());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属字段
|
||||
if (JSON_KEY_STREAM_VOLUME.equals(name)) {
|
||||
setStreamVolume(jsonReader.nextInt());
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 解析字段[" + name + "]值=" + this.streamVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析铃声音量参数");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 最终音量值=" + this.streamVolume);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package cc.winboll.studio.contacts.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 19:51:40
|
||||
* @Describe 应用设置数据模型,支持云盾防御配置与JSON序列化
|
||||
*/
|
||||
public class SettingsBean extends BaseBean {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "SettingsModel";
|
||||
// 数值范围常量
|
||||
public static final int MAX_INTRANGE = 666666;
|
||||
public static final int MIN_INTRANGE = 1;
|
||||
// JSON字段名常量,消除硬编码
|
||||
private static final String JSON_KEY_DUN_TOTAL = "dunTotalCount";
|
||||
private static final String JSON_KEY_DUN_CURRENT = "dunCurrentCount";
|
||||
private static final String JSON_KEY_DUN_RESUME_SECOND = "dunResumeSecondCount";
|
||||
private static final String JSON_KEY_DUN_RESUME_COUNT = "dunResumeCount";
|
||||
private static final String JSON_KEY_DUN_ENABLE = "isEnableDun";
|
||||
private static final String JSON_KEY_URL = "szBoBullToon_URL";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 云盾防御层数量
|
||||
private int dunTotalCount;
|
||||
// 当前云盾防御层
|
||||
private int dunCurrentCount;
|
||||
// 防御层恢复时间间隔(秒钟)
|
||||
private int dunResumeSecondCount;
|
||||
// 每次恢复防御层数
|
||||
private int dunResumeCount;
|
||||
// 是否启用云盾
|
||||
private boolean isEnableDun;
|
||||
// BoBullToon 应用模块数据请求地址
|
||||
private String szBoBullToon_URL;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
/**
|
||||
* 默认构造,初始化默认配置
|
||||
*/
|
||||
public SettingsBean() {
|
||||
this.dunTotalCount = 6;
|
||||
this.dunCurrentCount = 6;
|
||||
this.dunResumeSecondCount = 60;
|
||||
this.dunResumeCount = 1;
|
||||
this.isEnableDun = false;
|
||||
this.szBoBullToon_URL = "";
|
||||
LogUtils.d(TAG, "SettingsModel: 默认构造初始化完成 | 云盾默认配置加载完毕");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造,初始化自定义配置并校验数值范围
|
||||
*/
|
||||
public SettingsBean(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount,
|
||||
int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
this.isEnableDun = isEnableDun;
|
||||
this.szBoBullToon_URL = szBoBullToon_URL == null ? "" : szBoBullToon_URL;
|
||||
|
||||
LogUtils.d(TAG, "SettingsModel: 带参构造初始化完成 | 总层数=" + this.dunTotalCount
|
||||
+ " | 当前层数=" + this.dunCurrentCount + " | 恢复间隔=" + this.dunResumeSecondCount
|
||||
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun);
|
||||
}
|
||||
|
||||
// ====================== 私有工具方法区 ======================
|
||||
/**
|
||||
* 数值范围校验,确保参数在 MIN~MAX 区间内
|
||||
*/
|
||||
private int getSettingsModelRangeInt(int origin) {
|
||||
int result = IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
|
||||
if (result != origin) {
|
||||
LogUtils.w(TAG, "getSettingsModelRangeInt: 数值校正 | 原始值=" + origin + " | 校正后=" + result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====================== Getter & Setter 方法区 ======================
|
||||
public int getDunTotalCount() {
|
||||
return dunTotalCount;
|
||||
}
|
||||
|
||||
public void setDunTotalCount(int dunTotalCount) {
|
||||
int oldValue = this.dunTotalCount;
|
||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
||||
LogUtils.d(TAG, "setDunTotalCount: 总防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunTotalCount);
|
||||
}
|
||||
|
||||
public int getDunCurrentCount() {
|
||||
return dunCurrentCount;
|
||||
}
|
||||
|
||||
public void setDunCurrentCount(int dunCurrentCount) {
|
||||
int oldValue = this.dunCurrentCount;
|
||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
||||
LogUtils.d(TAG, "setDunCurrentCount: 当前防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunCurrentCount);
|
||||
}
|
||||
|
||||
public int getDunResumeSecondCount() {
|
||||
return dunResumeSecondCount;
|
||||
}
|
||||
|
||||
public void setDunResumeSecondCount(int dunResumeSecondCount) {
|
||||
int oldValue = this.dunResumeSecondCount;
|
||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
||||
LogUtils.d(TAG, "setDunResumeSecondCount: 恢复间隔更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeSecondCount);
|
||||
}
|
||||
|
||||
public int getDunResumeCount() {
|
||||
return dunResumeCount;
|
||||
}
|
||||
|
||||
public void setDunResumeCount(int dunResumeCount) {
|
||||
int oldValue = this.dunResumeCount;
|
||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
||||
LogUtils.d(TAG, "setDunResumeCount: 恢复层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeCount);
|
||||
}
|
||||
|
||||
public boolean isEnableDun() {
|
||||
return isEnableDun;
|
||||
}
|
||||
|
||||
public void setIsEnableDun(boolean isEnableDun) {
|
||||
LogUtils.d(TAG, "setIsEnableDun: 云盾启用状态更新为" + isEnableDun);
|
||||
this.isEnableDun = isEnableDun;
|
||||
}
|
||||
|
||||
public String getBoBullToon_URL() {
|
||||
return szBoBullToon_URL;
|
||||
}
|
||||
|
||||
public void setBoBullToon_URL(String boBullToon_URL) {
|
||||
String oldValue = this.szBoBullToon_URL;
|
||||
this.szBoBullToon_URL = boBullToon_URL == null ? "" : boBullToon_URL;
|
||||
LogUtils.d(TAG, "setBoBullToon_URL: 请求地址更新 | 旧值=" + oldValue + " | 新值=" + this.szBoBullToon_URL);
|
||||
}
|
||||
|
||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = SettingsBean.class.getName();
|
||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化设置数据");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入所有配置字段
|
||||
jsonWriter.name(JSON_KEY_DUN_TOTAL).value(getDunTotalCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_CURRENT).value(getDunCurrentCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_RESUME_SECOND).value(getDunResumeSecondCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_RESUME_COUNT).value(getDunResumeCount());
|
||||
jsonWriter.name(JSON_KEY_DUN_ENABLE).value(isEnableDun());
|
||||
jsonWriter.name(JSON_KEY_URL).value(getBoBullToon_URL());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
// 优先调用父类处理通用字段
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理当前类专属配置字段
|
||||
if (JSON_KEY_DUN_TOTAL.equals(name)) {
|
||||
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_CURRENT.equals(name)) {
|
||||
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_RESUME_SECOND.equals(name)) {
|
||||
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_RESUME_COUNT.equals(name)) {
|
||||
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
||||
} else if (JSON_KEY_DUN_ENABLE.equals(name)) {
|
||||
setIsEnableDun(jsonReader.nextBoolean());
|
||||
} else if (JSON_KEY_URL.equals(name)) {
|
||||
setBoBullToon_URL(jsonReader.nextString());
|
||||
} else {
|
||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
||||
return false;
|
||||
}
|
||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析设置数据");
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 云盾配置加载完毕");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,362 @@
|
||||
package cc.winboll.studio.contacts.phonecallui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
||||
|
||||
|
||||
/**
|
||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
||||
*
|
||||
* @author aJIEw
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/14 21:01
|
||||
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
|
||||
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
public class PhoneCallActivity extends Activity implements View.OnClickListener {
|
||||
// 常量定义区(核心常量+小米适配标识)
|
||||
public static final String TAG = "PhoneCallActivity";
|
||||
private static final int MSG_CLOSE_ACTIVITY = 0x001;
|
||||
private static final String MI_ADAPT_TAG = "MiAdapt";
|
||||
private static final String TOAST_CALLING = "通话进行中,无法重复创建通话窗口";
|
||||
private static final long CLOSE_DELAY_MS = 100; // 小米机型关闭延迟时间
|
||||
|
||||
private TextView tvCallNumberLabel;
|
||||
private TextView tvCallNumber;
|
||||
private TextView tvPickUp;
|
||||
private TextView tvCallingTime;
|
||||
private TextView tvHangUp;
|
||||
// 静态属性区(单例核心+全局工具对象)
|
||||
private static volatile boolean sIsActivityAlive = false;
|
||||
private static Handler sCloseHandler;
|
||||
|
||||
private PhoneCallManager phoneCallManager;
|
||||
private PhoneCallService.CallType callType;
|
||||
private String phoneNumber;
|
||||
// 控件属性区(按界面布局顺序排列)
|
||||
private TextView mTvCallNumberLabel;
|
||||
private TextView mTvCallNumber;
|
||||
private TextView mTvPickUp;
|
||||
private TextView mTvCallingTime;
|
||||
private TextView mTvHangUp;
|
||||
|
||||
private Timer onGoingCallTimer;
|
||||
private int callingTime;
|
||||
// 业务属性区(按依赖优先级排列)
|
||||
private PhoneCallManager mPhoneCallManager;
|
||||
private PhoneCallService.CallType mCallType;
|
||||
private String mPhoneNumber;
|
||||
private Timer mOnGoingCallTimer;
|
||||
private int mCallingTime;
|
||||
private boolean isClosing = false; // 新增:避免重复关闭页面
|
||||
|
||||
public static void actionStart(Context context, String phoneNumber,
|
||||
PhoneCallService.CallType callType) {
|
||||
// 对外静态接口(单例启动+外部关闭)
|
||||
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
|
||||
if (context == null || phoneNumber == null || callType == null) {
|
||||
LogUtils.e(TAG, "actionStart: 入参为空,启动失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sIsActivityAlive) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 已有活跃通话窗口,拒绝重复启动");
|
||||
Toast.makeText(context, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 启动通话界面,号码=" + phoneNumber + ",类型=" + callType.name());
|
||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||
intent.putExtra("call_type", callType);
|
||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void closePhoneCallActivity() {
|
||||
LogUtils.d(TAG, "closePhoneCallActivity: 收到外部关闭指令");
|
||||
if (sIsActivityAlive && sCloseHandler != null) {
|
||||
sCloseHandler.sendEmptyMessage(MSG_CLOSE_ACTIVITY);
|
||||
LogUtils.d(TAG, "closePhoneCallActivity: 关闭消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "closePhoneCallActivity: 页面已销毁或Handler未初始化,关闭跳过");
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期方法区(按执行流程排序)
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始创建,SDK版本=" + Build.VERSION.SDK_INT);
|
||||
|
||||
// 单例双重校验,防止异常场景多实例
|
||||
if (sIsActivityAlive) {
|
||||
Toast.makeText(this, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 拦截重复创建,即将关闭当前实例");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
sIsActivityAlive = false;
|
||||
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
ActivityStack.getInstance().addActivity(this);
|
||||
adaptLockScreenAndXiaomi();
|
||||
initHandler();
|
||||
initData();
|
||||
initView();
|
||||
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
phoneCallManager = new PhoneCallManager(this);
|
||||
onGoingCallTimer = new Timer();
|
||||
if (getIntent() != null) {
|
||||
phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
|
||||
}
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
|
||||
tvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||
tvCallNumber = findViewById(R.id.tv_call_number);
|
||||
tvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||
tvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||
tvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||
|
||||
tvCallNumber.setText(formatPhoneNumber(phoneNumber));
|
||||
tvPickUp.setOnClickListener(this);
|
||||
tvHangUp.setOnClickListener(this);
|
||||
|
||||
// 打进的电话
|
||||
if (callType == PhoneCallService.CallType.CALL_IN) {
|
||||
tvCallNumberLabel.setText("来电号码");
|
||||
tvPickUp.setVisibility(View.VISIBLE);
|
||||
} else if (callType == PhoneCallService.CallType.CALL_OUT) {
|
||||
tvCallNumberLabel.setText("呼叫号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
phoneCallManager.openSpeaker();
|
||||
}
|
||||
|
||||
showOnLockScreen();
|
||||
}
|
||||
|
||||
public void showOnLockScreen() {
|
||||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.tv_phone_pick_up) {
|
||||
phoneCallManager.answer();
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
tvCallingTime.setVisibility(View.VISIBLE);
|
||||
onGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void run() {
|
||||
callingTime++;
|
||||
tvCallingTime.setText("通话中:" + getCallingTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
} else if (v.getId() == R.id.tv_phone_hang_up) {
|
||||
phoneCallManager.disconnect();
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private String getCallingTime() {
|
||||
int minute = callingTime / 60;
|
||||
int second = callingTime % 60;
|
||||
return (minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
if (onGoingCallTimer != null) {
|
||||
onGoingCallTimer.cancel();
|
||||
}
|
||||
|
||||
callingTime = 0;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//MainActivity.updateCallLogFragment();
|
||||
phoneCallManager.destroy();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始销毁");
|
||||
|
||||
sIsActivityAlive = false;
|
||||
isClosing = false;
|
||||
stopTimer();
|
||||
// 销毁通话管理器
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.destroy();
|
||||
mPhoneCallManager = null;
|
||||
LogUtils.d(TAG, "销毁通话管理器资源");
|
||||
}
|
||||
// 销毁Handler避免内存泄漏
|
||||
if (sCloseHandler != null) {
|
||||
sCloseHandler.removeCallbacksAndMessages(null);
|
||||
sCloseHandler = null;
|
||||
LogUtils.d(TAG, "销毁关闭Handler");
|
||||
}
|
||||
ActivityStack.getInstance().removeActivity(this);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面销毁完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (isFinishing()) {
|
||||
sIsActivityAlive = false;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 页面即将关闭,重置单例标记");
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件回调
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == null) {
|
||||
LogUtils.w(TAG, "onClick: 点击控件为空,忽略操作");
|
||||
return;
|
||||
}
|
||||
switch (v.getId()) {
|
||||
case R.id.tv_phone_pick_up:
|
||||
LogUtils.d(TAG, "onClick: 触发接听操作");
|
||||
answerCall();
|
||||
break;
|
||||
case R.id.tv_phone_hang_up:
|
||||
LogUtils.d(TAG, "onClick: 触发挂断操作,当前通话时长=" + mCallingTime + "秒");
|
||||
hangUpCall();
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onClick: 未知点击事件,控件ID=" + v.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化方法区(按初始化顺序排列)
|
||||
private void initHandler() {
|
||||
sCloseHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_CLOSE_ACTIVITY) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到关闭消息,执行挂断逻辑");
|
||||
hangUpCall();
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initHandler: 关闭Handler初始化完成");
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
LogUtils.d(TAG, "initData: 开始初始化业务数据");
|
||||
mPhoneCallManager = PhoneCallManager.getInstance(this);
|
||||
Intent intent = getIntent();
|
||||
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "initData: 启动Intent为空,终止初始化");
|
||||
removeFromRecentsAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
mPhoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
mCallType = (PhoneCallService.CallType) intent.getSerializableExtra("call_type");
|
||||
if (mPhoneNumber == null || mCallType == null) {
|
||||
LogUtils.e(TAG, "initData: 通话号码或类型解析失败");
|
||||
removeFromRecentsAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
mOnGoingCallTimer = new Timer();
|
||||
mCallingTime = 0;
|
||||
LogUtils.d(TAG, "initData: 业务数据初始化完成,号码=" + mPhoneNumber);
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
LogUtils.d(TAG, "initView: 开始初始化界面控件");
|
||||
// 修复沉浸式导航栏语法,适配小米全面屏
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||
|
||||
// 绑定控件
|
||||
mTvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||
mTvCallNumber = findViewById(R.id.tv_call_number);
|
||||
mTvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||
mTvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||
mTvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||
|
||||
// 设置控件属性
|
||||
mTvCallNumber.setText(formatPhoneNumber(mPhoneNumber));
|
||||
mTvPickUp.setOnClickListener(this);
|
||||
mTvHangUp.setOnClickListener(this);
|
||||
|
||||
// 区分来电/去电UI样式
|
||||
if (PhoneCallService.CallType.CALL_IN == mCallType) {
|
||||
mTvCallNumberLabel.setText("来电号码");
|
||||
mTvPickUp.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setVisibility(View.GONE);
|
||||
} else if (PhoneCallService.CallType.CALL_OUT == mCallType) {
|
||||
mTvCallNumberLabel.setText("呼叫号码");
|
||||
mTvPickUp.setVisibility(View.GONE);
|
||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setText("通话中:00:00");
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.openSpeaker();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 去电模式自动开启免提");
|
||||
}
|
||||
startCallTimer();
|
||||
}
|
||||
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
||||
}
|
||||
|
||||
// 小米机型专属适配方法
|
||||
private void adaptLockScreenAndXiaomi() {
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 执行锁屏适配逻辑");
|
||||
Window window = getWindow();
|
||||
if (window == null) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " Window对象为空,适配失败");
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
|
||||
|
||||
// 小米机型额外添加解锁屏标志,解决MIUI锁屏拦截问题
|
||||
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
|
||||
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 已添加小米机型专属锁屏适配标志");
|
||||
}
|
||||
window.addFlags(flags);
|
||||
|
||||
// 适配API29+锁屏新接口
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setShowWhenLocked(true);
|
||||
setTurnScreenOn(true);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 适配API29+锁屏接口完成");
|
||||
}
|
||||
}
|
||||
|
||||
// 通话核心业务方法
|
||||
private void answerCall() {
|
||||
LogUtils.d(TAG, "answerCall: 执行接听操作");
|
||||
if (mPhoneCallManager == null) {
|
||||
LogUtils.e(TAG, "answerCall: 通话管理器为空,接听失败");
|
||||
return;
|
||||
}
|
||||
mPhoneCallManager.answer();
|
||||
mTvPickUp.setVisibility(View.GONE);
|
||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||
mTvCallingTime.setText("通话中:00:00");
|
||||
startCallTimer();
|
||||
LogUtils.d(TAG, "answerCall: 接听操作完成,启动通话计时");
|
||||
}
|
||||
|
||||
private void hangUpCall() {
|
||||
if (isClosing) {
|
||||
LogUtils.w(TAG, "hangUpCall: 挂断操作已执行,无需重复调用");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "hangUpCall: 执行挂断操作,当前时长=" + mCallingTime + "秒");
|
||||
isClosing = true;
|
||||
stopTimer();
|
||||
if (mPhoneCallManager != null) {
|
||||
mPhoneCallManager.disconnect();
|
||||
LogUtils.d(TAG, "hangUpCall: 通话连接已断开");
|
||||
}
|
||||
// 延迟关闭页面,适配小米机型通话时序
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
removeFromRecentsAndFinish();
|
||||
}
|
||||
}, CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
// 任务栈清理方法
|
||||
private void removeFromRecentsAndFinish() {
|
||||
if (isFinishing()) {
|
||||
LogUtils.d(TAG, "removeFromRecentsAndFinish: 页面已在关闭中,无需重复操作");
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 移除任务栈并关闭页面");
|
||||
} else {
|
||||
finish();
|
||||
LogUtils.d(TAG, "兼容低版本,关闭页面");
|
||||
}
|
||||
}
|
||||
|
||||
// 计时工具方法
|
||||
private void startCallTimer() {
|
||||
LogUtils.d(TAG, "startCallTimer: 启动通话计时器");
|
||||
if (mOnGoingCallTimer == null) {
|
||||
mOnGoingCallTimer = new Timer();
|
||||
}
|
||||
mOnGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallingTime++;
|
||||
mTvCallingTime.setText("通话中:" + formatCallingTime(mCallingTime));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
LogUtils.d(TAG, "stopTimer: 停止通话计时器");
|
||||
if (mOnGoingCallTimer != null) {
|
||||
mOnGoingCallTimer.cancel();
|
||||
mOnGoingCallTimer = null;
|
||||
}
|
||||
mCallingTime = 0;
|
||||
}
|
||||
|
||||
// 辅助工具方法:格式化通话时长
|
||||
private String formatCallingTime(int seconds) {
|
||||
int minute = seconds / 60;
|
||||
int second = seconds % 60;
|
||||
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
|
||||
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
|
||||
return minuteStr + ":" + secondStr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,57 +6,199 @@ import android.os.Build;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.VideoProfile;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/15 20:11
|
||||
* @Describe 通话核心管理类
|
||||
* 功能:接听/挂断通话、免提控制、资源释放,适配API29-30及小米机型
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
|
||||
public class PhoneCallManager {
|
||||
// 常量定义区
|
||||
public static final String TAG = "PhoneCallManager";
|
||||
private static final String MI_ADAPT_TAG = "MiDeviceAdapt"; // 小米适配标识
|
||||
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
|
||||
private static final int AUDIO_MODE_BACKUP = -1; // 音频模式备份默认值
|
||||
|
||||
public static Call call;
|
||||
// 成员属性区(按依赖优先级排序,移除静态call避免跨组件冲突)
|
||||
private Context mContext;
|
||||
private AudioManager mAudioManager;
|
||||
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
|
||||
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
|
||||
|
||||
private Context context;
|
||||
private AudioManager audioManager;
|
||||
|
||||
public PhoneCallManager(Context context) {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
// 构造方法(单例化改造,避免多实例冲突)
|
||||
private static volatile PhoneCallManager sInstance;
|
||||
public static PhoneCallManager getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为空,初始化失败");
|
||||
return null;
|
||||
}
|
||||
if (sInstance == null) {
|
||||
synchronized (PhoneCallManager.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PhoneCallManager(context.getApplicationContext()); // 用应用上下文,避免内存泄漏
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// 私有构造,禁止外部实例化
|
||||
private PhoneCallManager(Context context) {
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 初始化通话管理类");
|
||||
this.mContext = context;
|
||||
this.mAudioModeBackup = AUDIO_MODE_BACKUP;
|
||||
this.mIsSpeakerOpened = false;
|
||||
initAudioManager();
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理类初始化完成");
|
||||
}
|
||||
|
||||
// 初始化辅助方法
|
||||
private void initAudioManager() {
|
||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (mAudioManager != null) {
|
||||
// 备份原始音频模式(小米机型切换后需恢复,避免外放异常)
|
||||
mAudioModeBackup = mAudioManager.getMode();
|
||||
LogUtils.d(TAG, "音频管理器初始化成功,原始模式备份:" + mAudioModeBackup);
|
||||
} else {
|
||||
LogUtils.e(TAG, "音频管理器初始化失败,将影响通话音频控制");
|
||||
}
|
||||
}
|
||||
|
||||
// 核心业务方法(按使用场景排序,强化小米适配+容错)
|
||||
/**
|
||||
* 接听电话
|
||||
* 接听电话,默认音频通话模式
|
||||
*/
|
||||
public void answer() {
|
||||
if (call != null) {
|
||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
||||
openSpeaker();
|
||||
LogUtils.d(TAG, "执行接听通话操作");
|
||||
// 从PhoneCallService的静态管理器获取通话对象,统一数据源
|
||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||
if (currentCall == null) {
|
||||
LogUtils.e(TAG, "接听失败:通话对象为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验通话状态,避免重复接听(小米机型状态变更延迟)
|
||||
if (currentCall.getState() != Call.STATE_RINGING) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 非响铃状态,无需接听,当前状态:" + currentCall.getState());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
|
||||
openSpeaker(); // 接听后自动开免提
|
||||
LogUtils.d(TAG, "通话接听成功,自动开启免提");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 接听权限不足(需android.permission.ANSWER_PHONE_CALLS)", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法接听", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "接听通话异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开电话,包括来电时的拒接以及接听后的挂断
|
||||
* 断开通话(支持来电拒接、通话中挂断)
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (call != null) {
|
||||
call.disconnect();
|
||||
LogUtils.d(TAG, "执行断开通话操作");
|
||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||
if (currentCall == null) {
|
||||
LogUtils.e(TAG, "挂断失败:通话对象为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验通话状态,避免重复挂断
|
||||
if (currentCall.getState() == Call.STATE_DISCONNECTED) {
|
||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 通话已断开,无需重复操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentCall.disconnect();
|
||||
closeSpeaker(); // 挂断后关闭免提+恢复音频模式
|
||||
LogUtils.d(TAG, "通话断开成功");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 挂断权限不足(需android.permission.CALL_PHONE)", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法挂断", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "断开通话异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开免提
|
||||
* 打开免提,适配小米机型音频通道切换(解决MIUI音频混乱)
|
||||
*/
|
||||
public void openSpeaker() {
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
LogUtils.d(TAG, "执行打开免提操作");
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, "打开免提失败:音频管理器未初始化");
|
||||
return;
|
||||
}
|
||||
if (mIsSpeakerOpened) {
|
||||
LogUtils.w(TAG, "免提已开启,无需重复操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 小米机型适配步骤:1. 设置通话模式 2. 关闭静音 3. 开启免提(固定顺序)
|
||||
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
mAudioManager.setStreamMute(AudioManager.STREAM_VOICE_CALL, false); // 确保通话音频不静音
|
||||
mAudioManager.setSpeakerphoneOn(true);
|
||||
|
||||
mIsSpeakerOpened = true;
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 免提开启成功,当前模式:" + mAudioManager.getMode());
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 音频控制权限不足", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "打开免提异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源
|
||||
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
|
||||
*/
|
||||
public void closeSpeaker() {
|
||||
LogUtils.d(TAG, "执行关闭免提操作");
|
||||
if (mAudioManager == null || !mIsSpeakerOpened) {
|
||||
LogUtils.w(TAG, "免提未开启或音频管理器为空,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mAudioManager.setSpeakerphoneOn(false);
|
||||
// 恢复原始音频模式(关键:小米机型不恢复会导致其他应用外放异常)
|
||||
if (mAudioModeBackup != AUDIO_MODE_BACKUP) {
|
||||
mAudioManager.setMode(mAudioModeBackup);
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 恢复原始音频模式:" + mAudioModeBackup);
|
||||
}
|
||||
mIsSpeakerOpened = false;
|
||||
LogUtils.d(TAG, "免提关闭成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 关闭免提异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源,避免内存泄漏+音频残留(适配小米内存管理)
|
||||
*/
|
||||
public void destroy() {
|
||||
call = null;
|
||||
context = null;
|
||||
audioManager = null;
|
||||
LogUtils.d(TAG, "开始销毁通话管理资源");
|
||||
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
|
||||
// 释放资源(应用上下文无需主动置空,避免空指针)
|
||||
mAudioManager = null;
|
||||
sInstance = null; // 单例置空,下次重新初始化
|
||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:获取当前免提状态(供UI层同步显示)
|
||||
*/
|
||||
public boolean isSpeakerOpened() {
|
||||
return mIsSpeakerOpened;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,215 +1,284 @@
|
||||
package cc.winboll.studio.contacts.phonecallui;
|
||||
|
||||
/**
|
||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
||||
*
|
||||
* @author aJIEw
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaRecorder;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.CallLog;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.InCallService;
|
||||
import android.telephony.TelephonyManager;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
||||
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
* 适配:Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
|
||||
*/
|
||||
@RequiresApi(api = 29)
|
||||
public class PhoneCallService extends InCallService {
|
||||
|
||||
// 常量定义区
|
||||
public static final String TAG = "PhoneCallService";
|
||||
// 小米设备适配标识,便于日志区分
|
||||
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
|
||||
|
||||
MediaRecorder mediaRecorder;
|
||||
// 成员属性区(按依赖顺序排列)
|
||||
private Call.Callback mCallCallback;
|
||||
private AudioManager mAudioManager;
|
||||
|
||||
private final Call.Callback callback = new Call.Callback() {
|
||||
@Override
|
||||
public void onStateChanged(Call call, int state) {
|
||||
super.onStateChanged(call, state);
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
{
|
||||
long callId = getCurrentCallId();
|
||||
if (callId != -1) {
|
||||
// 在这里可以对获取到的通话记录ID进行处理
|
||||
//System.out.println("当前通话记录ID: " + callId);
|
||||
// 内部枚举类(通话类型定义)
|
||||
public enum CallType {
|
||||
CALL_IN, // 来电
|
||||
CALL_OUT // 去电
|
||||
}
|
||||
|
||||
// 电话接通,开始录音
|
||||
startRecording(callId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// 电话挂断,停止录音
|
||||
stopRecording();
|
||||
break;
|
||||
case Call.STATE_ACTIVE: {
|
||||
break;
|
||||
}
|
||||
|
||||
case Call.STATE_DISCONNECTED: {
|
||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
// Service生命周期方法区(按执行流程排序)
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
|
||||
initAudioManager();
|
||||
initCallCallback();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAdded(Call call) {
|
||||
super.onCallAdded(call);
|
||||
|
||||
call.registerCallback(callback);
|
||||
PhoneCallManager.call = call;
|
||||
CallType callType = null;
|
||||
|
||||
if (call.getState() == Call.STATE_RINGING) {
|
||||
callType = CallType.CALL_IN;
|
||||
} else if (call.getState() == Call.STATE_CONNECTING) {
|
||||
callType = CallType.CALL_OUT;
|
||||
LogUtils.d(TAG, "检测到新通话");
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "通话对象为空,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重校验回调,避免重复注册
|
||||
if (mCallCallback != null) {
|
||||
call.registerCallback(mCallCallback);
|
||||
}
|
||||
// 绑定通话对象到管理器,供UI层调用
|
||||
PhoneCallManager.call = call;
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
|
||||
|
||||
CallType callType = judgeCallType(call);
|
||||
if (callType != null) {
|
||||
Call.Details details = call.getDetails();
|
||||
String phoneNumber = details.getHandle().getSchemeSpecificPart();
|
||||
|
||||
// 记录原始铃声音量
|
||||
//
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
// 恢复铃声音量,预防其他意外条件导致的音量变化问题
|
||||
//
|
||||
|
||||
// 读取应用配置,未配置就初始化配置文件
|
||||
RingTongBean bean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
// 初始化配置
|
||||
bean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, bean);
|
||||
}
|
||||
// 如果当前音量和应用保存的不一致就恢复为应用设定值
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
if (ringerVolume != bean.getStreamVolume()) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
}
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
// 检查电话接收规则
|
||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||
// 调低音量
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_SILENT);
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
// 断开电话
|
||||
call.disconnect();
|
||||
// 停顿1秒,预防第一声铃声响动
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, "");
|
||||
}
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
// 屏蔽电话结束
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常接听电话
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
handleValidCall(call, callType);
|
||||
} else {
|
||||
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallRemoved(Call call) {
|
||||
super.onCallRemoved(call);
|
||||
call.unregisterCallback(callback);
|
||||
PhoneCallManager.call = null;
|
||||
LogUtils.d(TAG, "通话结束,开始清理资源");
|
||||
if (call != null && mCallCallback != null) {
|
||||
call.unregisterCallback(mCallCallback);
|
||||
LogUtils.d(TAG, "通话回调已注销");
|
||||
}
|
||||
|
||||
// 延迟置空通话对象,避免UI层挂断时对象已被释放(适配小米机型时序)
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 延迟200ms,确保PhoneCallActivity挂断逻辑执行完成
|
||||
Thread.sleep(200);
|
||||
PhoneCallManager.call = null;
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 延迟置空通话对象异常", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
PhoneCallActivity.closePhoneCallActivity();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话资源清理完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "服务开始销毁");
|
||||
CallLogFragment.updateCallLogFragment();
|
||||
// 释放资源,适配小米设备内存管理,避免内存泄漏
|
||||
mCallCallback = null;
|
||||
mAudioManager = null;
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务销毁完成");
|
||||
}
|
||||
|
||||
public enum CallType {
|
||||
CALL_IN,
|
||||
CALL_OUT,
|
||||
}
|
||||
|
||||
|
||||
private void startRecording(long callId) {
|
||||
LogUtils.d(TAG, "startRecording(...)");
|
||||
mediaRecorder = new MediaRecorder();
|
||||
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
|
||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||
mediaRecorder.setOutputFile(getOutputFilePath(callId));
|
||||
try {
|
||||
mediaRecorder.prepare();
|
||||
mediaRecorder.start();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// 初始化方法区
|
||||
private void initAudioManager() {
|
||||
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 音频管理器初始化成功");
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutputFilePath(long callId) {
|
||||
LogUtils.d(TAG, "getOutputFilePath(...)");
|
||||
// 设置录音文件的保存路径
|
||||
File file = new File(getExternalFilesDir(TAG), String.format("call_%d.mp4", callId));
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
private void initCallCallback() {
|
||||
mCallCallback = new Call.Callback() {
|
||||
@Override
|
||||
public void onStateChanged(Call call, int state) {
|
||||
super.onStateChanged(call, state);
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "onStateChanged: 通话对象为空");
|
||||
return;
|
||||
}
|
||||
String stateDesc = getCallStateDesc(state);
|
||||
LogUtils.d(TAG, "通话状态变更:" + stateDesc + "(状态码:" + state + ")");
|
||||
|
||||
private void stopRecording() {
|
||||
LogUtils.d(TAG, "stopRecording()");
|
||||
if (mediaRecorder != null) {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.release();
|
||||
mediaRecorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private long getCurrentCallId() {
|
||||
LogUtils.d(TAG, "getCurrentCallId()");
|
||||
ContentResolver contentResolver = getApplicationContext().getContentResolver();
|
||||
Uri callLogUri = Uri.parse("content://call_log/calls");
|
||||
String[] projection = {"_id", "number", "call_type", "date"};
|
||||
String selection = "call_type = " + CallLog.Calls.OUTGOING_TYPE + " OR call_type = " + CallLog.Calls.INCOMING_TYPE;
|
||||
String sortOrder = "date DESC";
|
||||
|
||||
try {
|
||||
Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex("_id"));
|
||||
switch (state) {
|
||||
case Call.STATE_DISCONNECTED:
|
||||
// 双重校验,避免重复关闭页面
|
||||
if (ActivityStack.getInstance().getActivity(PhoneCallActivity.class) != null) {
|
||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||
LogUtils.d(TAG, "通话界面已关闭");
|
||||
}
|
||||
break;
|
||||
case Call.STATE_ACTIVE:
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话进入活跃状态,适配音频通道");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
};
|
||||
LogUtils.d(TAG, "通话状态回调初始化完成");
|
||||
}
|
||||
|
||||
// 核心业务处理方法区
|
||||
private CallType judgeCallType(Call call) {
|
||||
if (call == null) {
|
||||
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
|
||||
return null;
|
||||
}
|
||||
int callState = call.getState();
|
||||
if (callState == Call.STATE_RINGING) {
|
||||
LogUtils.d(TAG, "识别为来电");
|
||||
return CallType.CALL_IN;
|
||||
} else if (callState == Call.STATE_CONNECTING) {
|
||||
LogUtils.d(TAG, "识别为去电");
|
||||
return CallType.CALL_OUT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean handleValidCall(Call call, CallType callType) {
|
||||
if (call == null || callType == null) {
|
||||
LogUtils.e(TAG, "handleValidCall: 通话对象或类型为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
return -1;
|
||||
Call.Details callDetails = call.getDetails();
|
||||
if (callDetails == null || callDetails.getHandle() == null) {
|
||||
LogUtils.e(TAG, "通话详情缺失,处理终止");
|
||||
return false;
|
||||
}
|
||||
|
||||
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
|
||||
LogUtils.d(TAG, "处理通话:号码=" + phoneNumber + ",类型=" + callType.name());
|
||||
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, "音频管理器未初始化");
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkRulesAndHandleRingerVolumeControl(phoneNumber, call)) {
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话界面启动成功");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkRulesAndHandleRingerVolumeControl(String phoneNumber, Call call) {
|
||||
if (mAudioManager == null || phoneNumber == null || call == null) {
|
||||
LogUtils.e(TAG, "checkRulesAndHandleRingerVolumeControl: 入参为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
LogUtils.d(TAG, "当前铃声音量:" + currentVolume);
|
||||
|
||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringTongBean == null) {
|
||||
ringTongBean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringTongBean);
|
||||
LogUtils.d(TAG, "初始化默认铃音配置");
|
||||
}
|
||||
final int configVolume = ringTongBean.getStreamVolume();
|
||||
|
||||
try {
|
||||
// 小米机型适配:调整音量时添加权限校验
|
||||
if (currentVolume != configVolume) {
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "音量调整失败,权限不足", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验拦截规则
|
||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||
LogUtils.d(TAG, "号码" + phoneNumber + "命中拦截规则");
|
||||
try {
|
||||
// 拦截时静音并挂断
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||
call.disconnect();
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 拦截通话已挂断并静音");
|
||||
|
||||
// 延迟恢复音量,适配小米机型音频通道延迟
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
if (mAudioManager != null) {
|
||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 延迟恢复铃音配置");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "恢复音量线程中断", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "拦截静音失败", e);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 辅助工具方法区:解析通话状态描述
|
||||
private String getCallStateDesc(int state) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃中";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中";
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
case Call.STATE_ACTIVE:
|
||||
return "通话活跃";
|
||||
case Call.STATE_CONNECTING:
|
||||
return "连接中";
|
||||
case Call.STATE_DISCONNECTED:
|
||||
return "已断开";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
// 静态内部类:统一管理通话对象,避免跨组件对象混乱
|
||||
public static class PhoneCallManager {
|
||||
public static Call call;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,98 @@
|
||||
package cc.winboll.studio.contacts.receivers;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 主要广播接收器
|
||||
*/
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 主要广播接收器,监听系统开机广播并自动启动主服务
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainReceiver";
|
||||
public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
WeakReference<MainService> mwrService;
|
||||
// 监听的系统广播 Action
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 使用弱引用关联 MainService,避免内存泄漏
|
||||
private WeakReference<MainService> mMainServiceWeakRef;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainReceiver(MainService service) {
|
||||
mwrService = new WeakReference<MainService>(service);
|
||||
this.mMainServiceWeakRef = new WeakReference<>(service);
|
||||
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
|
||||
}
|
||||
|
||||
// ====================== 重写 BroadcastReceiver 核心方法 ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
ToastUtils.show("ACTION_BOOT_COMPLETED");
|
||||
// 空值校验,避免空指针异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "onReceive: Context 为 null,无法处理广播");
|
||||
return;
|
||||
}
|
||||
if (intent == null || intent.getAction() == null) {
|
||||
LogUtils.w(TAG, "onReceive: 接收到空 Intent 或空 Action");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, "onReceive: 接收到广播 | Action=" + action);
|
||||
|
||||
// 处理开机完成广播
|
||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
LogUtils.i(TAG, "onReceive: 监听到开机完成广播,自动启动 MainService");
|
||||
ToastUtils.show("设备开机,启动拨号主服务");
|
||||
MainService.startMainService(context);
|
||||
} else {
|
||||
ToastUtils.show(szAction);
|
||||
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
|
||||
ToastUtils.show("收到广播:" + action);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
// ====================== 广播注册/注销方法区 ======================
|
||||
/**
|
||||
* 注册广播接收器,监听指定系统广播
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(ACTION_BOOT_COMPLETED);
|
||||
//filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
context.registerReceiver(this, filter);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "registerAction: Context 为 null,注册失败");
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_BOOT_COMPLETED);
|
||||
// 可按需添加其他监听的 Action
|
||||
// intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
|
||||
context.registerReceiver(this, intentFilter);
|
||||
LogUtils.d(TAG, "registerAction: 广播接收器注册成功 | 监听 Action=" + ACTION_BOOT_COMPLETED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器,释放资源(解决 mMainReceiver.unregisterAction(this) 调用缺失问题)
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "unregisterAction: Context 为 null,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播接收器注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播接收器未注册,无需注销", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +1,271 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务,用于监控并保活主服务 MainService
|
||||
* 适配 Android 12+ 后台服务启动限制,支持前台服务运行
|
||||
* 兼容 Java 7 语法 & 低版本 SDK 编译
|
||||
* 移除无关的 microphone 类型配置,修复前台服务类型不匹配崩溃
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AssistantService";
|
||||
// 前台服务通知配置
|
||||
private static final String FOREGROUND_CHANNEL_ID = "assistant_service_foreground_channel";
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1002;
|
||||
// 修复:前台服务类型改为 dataSync(0x00000001),与 Manifest 保持一致,移除 microphone 类型
|
||||
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001;
|
||||
// Android 版本常量硬编码(Java 7 兼容)
|
||||
private static final int ANDROID_8_API = 26; // 通知渠道最低版本
|
||||
private static final int ANDROID_10_API = 29; // 前台服务类型最低支持版本
|
||||
private static final int ANDROID_12_API = 31; // 后台启动限制最低版本
|
||||
// 重试延迟时间(避免频繁触发后台启动限制)
|
||||
private static final long RETRY_DELAY_MS = 3000L;
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
MainService mMainService;
|
||||
boolean isBound = false;
|
||||
volatile boolean isThreadAlive = false;
|
||||
// ====================== 成员变量区 ======================
|
||||
private MainServiceBean mMainServiceBean;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private MainService mMainService;
|
||||
private boolean mIsBound = false;
|
||||
private volatile boolean mIsThreadAlive = false;
|
||||
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
LogUtils.d(TAG, "setIsThreadAlive(...)");
|
||||
LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive));
|
||||
this.isThreadAlive = isThreadAlive;
|
||||
// ====================== Binder 内部类 ======================
|
||||
/**
|
||||
* 对外暴露服务实例的 Binder
|
||||
*/
|
||||
public class MyBinder extends Binder {
|
||||
public AssistantService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 获取 AssistantService 实例");
|
||||
return AssistantService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== ServiceConnection 内部类 ======================
|
||||
/**
|
||||
* 主服务连接状态监听回调
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
|
||||
mIsBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
mIsBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 主服务绑定成功 | MainService=" + mMainService);
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
|
||||
mIsBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务连接断开");
|
||||
mMainService = null;
|
||||
mIsBound = false;
|
||||
|
||||
// 尝试重新绑定主服务(如果配置为启用)
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务");
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外方法区 ======================
|
||||
/**
|
||||
* 设置线程存活状态
|
||||
*/
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
this.mIsThreadAlive = isThreadAlive;
|
||||
LogUtils.d(TAG, "setIsThreadAlive: 线程存活状态变更 | " + isThreadAlive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程存活状态
|
||||
*/
|
||||
public boolean isThreadAlive() {
|
||||
return isThreadAlive;
|
||||
return mIsThreadAlive;
|
||||
}
|
||||
|
||||
// ====================== 前台服务辅助方法 ======================
|
||||
/**
|
||||
* 创建前台服务通知(Android 8.0+ 必须配置渠道)
|
||||
*/
|
||||
private Notification createForegroundNotification() {
|
||||
// 1. 创建通知渠道(API 26+ 必需)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
FOREGROUND_CHANNEL_ID,
|
||||
"守护服务",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("守护服务后台运行,保障主服务存活");
|
||||
// 空指针防护
|
||||
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建通知(Java 7 分步设置,取消链式调用简化)
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
|
||||
} else {
|
||||
builder = new Notification.Builder(this);
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_launcher);
|
||||
builder.setContentTitle("守护服务运行中");
|
||||
builder.setContentText("正在监控主服务状态");
|
||||
builder.setPriority(Notification.PRIORITY_LOW);
|
||||
builder.setOngoing(true); // 不可手动取消
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// ====================== Service 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
||||
|
||||
// 适配 Android 12+ 后台启动限制:应用后台时启动为前台服务
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_12_API) {
|
||||
Notification notification = createForegroundNotification();
|
||||
// 修复:使用 dataSync 类型,添加异常捕获防止崩溃
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
} else {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, notification);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: 守护服务已启动为前台服务(dataSync 类型)");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "onCreate: 启动前台服务失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主服务连接回调
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
LogUtils.d(TAG, "onCreate: 初始化 MyServiceConnection 完成");
|
||||
}
|
||||
|
||||
// 初始化运行状态
|
||||
setIsThreadAlive(false);
|
||||
// 启动守护逻辑
|
||||
assistantService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
super.onCreate();
|
||||
|
||||
//mMyBinder = new MyBinder();
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
setIsThreadAlive(false);
|
||||
assistantService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "call onStartCommand(...)");
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
||||
// 每次启动都执行守护逻辑,确保主服务存活
|
||||
assistantService();
|
||||
// START_STICKY:服务被杀死后系统尝试重启
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
setIsThreadAlive(false);
|
||||
// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁");
|
||||
|
||||
// 停止线程并解除主服务绑定
|
||||
setIsThreadAlive(false);
|
||||
if (mIsBound && mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 解除主服务绑定成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
|
||||
}
|
||||
mIsBound = false;
|
||||
}
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, String.format("mMainServiceBean.isEnable() %s", mMainServiceBean.isEnable()));
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, String.format("mIsThreadAlive %s", isThreadAlive()));
|
||||
if (isThreadAlive() == false) {
|
||||
// 设置运行状态
|
||||
setIsThreadAlive(true);
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
// ====================== 核心守护逻辑方法区 ======================
|
||||
/**
|
||||
* 守护服务核心逻辑:检查配置并保活主服务
|
||||
*/
|
||||
private void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService: 执行守护逻辑");
|
||||
|
||||
// 加载主服务配置
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean == null) {
|
||||
LogUtils.e(TAG, "assistantService: 主服务配置加载失败,终止守护逻辑");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "assistantService: 主服务启用状态 | " + mMainServiceBean.isEnable());
|
||||
// 配置启用且线程未存活时,唤醒并绑定主服务
|
||||
if (mMainServiceBean.isEnable() && !isThreadAlive()) {
|
||||
setIsThreadAlive(true);
|
||||
wakeupAndBindMain();
|
||||
} else if (!mMainServiceBean.isEnable()) {
|
||||
setIsThreadAlive(false);
|
||||
LogUtils.d(TAG, "assistantService: 主服务已禁用,停止保活");
|
||||
}
|
||||
}
|
||||
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain()");
|
||||
// 绑定服务的Intent
|
||||
/**
|
||||
* 唤醒并绑定主服务 MainService(适配后台启动限制)
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
if (mMyServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
startService(new Intent(this, MainService.class));
|
||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||
startForegroundService(intent);
|
||||
|
||||
// BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// startService(new Intent(this, MainService.class));
|
||||
// bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
mMainServiceBean = MainServiceBean.loadBean(AssistantService.this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
isBound = false;
|
||||
mMainService = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
public class MyBinder extends Binder {
|
||||
AssistantService getService() {
|
||||
LogUtils.d(TAG, "AssistantService MyBinder getService()");
|
||||
return AssistantService.this;
|
||||
}
|
||||
// ====================== 辅助方法区 ======================
|
||||
/**
|
||||
* 重新加载主服务配置
|
||||
*/
|
||||
private void reloadMainServiceConfig() {
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务
|
||||
* 参考:
|
||||
* 进程保活-双进程守护的正确姿势
|
||||
* https://blog.csdn.net/sinat_35159441/article/details/75267380
|
||||
* Android Service之onStartCommand方法研究
|
||||
* https://blog.csdn.net/cyp331203/article/details/38920491
|
||||
*/
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -17,309 +11,586 @@ import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.contacts.App;
|
||||
import cc.winboll.studio.contacts.beans.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.beans.RingTongBean;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.receivers.MainReceiver;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.threads.MainServiceThread;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.sos.SOS;
|
||||
import cc.winboll.studio.libappbase.winboll.WinBoLL;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
|
||||
* 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃
|
||||
* 核心优化:1. 移除延迟启动逻辑 2. 标准化日志管理 3. 强化资源清理 4. 结构分层重构
|
||||
*/
|
||||
public class MainService extends Service {
|
||||
|
||||
// ====================== 常量定义区(全硬编码,无高版本API依赖) ======================
|
||||
public static final String TAG = "MainService";
|
||||
|
||||
public static final int MSG_UPDATE_STATUS = 0;
|
||||
|
||||
static MainService _mControlCenterService;
|
||||
// 铃声音量监控参数(定时检查+恢复)
|
||||
private static final long VOLUME_CHECK_DELAY = 1000L; // 首次检查延迟1s
|
||||
private static final long VOLUME_CHECK_PERIOD = 60000L; // 后续每60s检查一次
|
||||
|
||||
volatile boolean isServiceRunning;
|
||||
// 前台服务配置(固定ID+渠道,避免重复创建)
|
||||
private static final String FOREGROUND_CHANNEL_ID = "main_service_foreground_channel";
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1001;
|
||||
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; // dataSync类型硬编码
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MainServiceThread mMainServiceThread;
|
||||
MainServiceHandler mMainServiceHandler;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
AssistantService mAssistantService;
|
||||
boolean isBound = false;
|
||||
MainReceiver mMainReceiver;
|
||||
Timer mStreamVolumeCheckTimer;
|
||||
static volatile TomCat _TomCat;
|
||||
// Android版本常量(替代Build.VERSION_CODES,适配Java7)
|
||||
private static final int ANDROID_8_API = 26; // Android 8.0
|
||||
private static final int ANDROID_10_API = 29; // Android 10
|
||||
private static final int ANDROID_12_API = 31; // Android 12
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new MyBinder();
|
||||
}
|
||||
// 守护服务重绑定延迟(仅保留核心重试逻辑)
|
||||
private static final long RETRY_DELAY_MS = 3000L;
|
||||
|
||||
public MainServiceThread getRemindThread() {
|
||||
return mMainServiceThread;
|
||||
}
|
||||
// ====================== 静态成员属性区(全局共享实例,统一前缀s) ======================
|
||||
private static MainService sMainServiceInstance; // 主服务全局实例
|
||||
private static volatile TomCat sTomCatInstance; // 号码识别核心实例(volatile保证可见性)
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate()");
|
||||
_mControlCenterService = MainService.this;
|
||||
isServiceRunning = false;
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
// ====================== 成员属性区(业务+UI+资源,统一前缀m) ======================
|
||||
private volatile boolean mIsServiceRunning; // 服务运行状态标记(volatile防指令重排)
|
||||
private MainServiceBean mMainServiceBean; // 服务配置实体(启用状态存储)
|
||||
private MainServiceHandler mMainServiceHandler; // 服务消息处理器(主线程通信)
|
||||
private MyServiceConnection mServiceConnection; // 守护服务连接实例
|
||||
private AssistantService mAssistantService; // 绑定的守护服务实例
|
||||
private boolean mIsAssistantBound; // 守护服务绑定状态标记
|
||||
private MainReceiver mMainReceiver; // 全局广播接收器(监听系统事件)
|
||||
private Timer mVolumeCheckTimer; // 铃声音量检查定时器(定时恢复配置)
|
||||
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 铃声检查定时器
|
||||
mStreamVolumeCheckTimer = new Timer();
|
||||
mStreamVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
// 恢复铃声音量,预防其他意外条件导致的音量变化问题
|
||||
//
|
||||
|
||||
// 读取应用配置,未配置就初始化配置文件
|
||||
RingTongBean bean = RingTongBean.loadBean(MainService.this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
// 初始化配置
|
||||
bean = new RingTongBean();
|
||||
RingTongBean.saveBean(MainService.this, bean);
|
||||
}
|
||||
// 如果当前音量和应用保存的不一致就恢复为应用设定值
|
||||
// 恢复铃声音量
|
||||
try {
|
||||
if (ringerVolume != bean.getStreamVolume()) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
//audioManager.setMode(AudioManager.RINGER_MODE_NORMAL);
|
||||
}
|
||||
} catch (java.lang.SecurityException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
}, 1000, 60000);
|
||||
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand(...)");
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void mainService() {
|
||||
LogUtils.d(TAG, "mainService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable() && isServiceRunning == false) {
|
||||
LogUtils.d(TAG, "mainService() start running");
|
||||
isServiceRunning = true;
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
// 召唤 WinBoLL APP 绑定本服务
|
||||
if (App.isDebuging()) {
|
||||
WinBoLL.bindToAPPBaseBeta(this, MainService.class.getName());
|
||||
} else {
|
||||
WinBoLL.bindToAPPBase(this, MainService.class.getName());
|
||||
}
|
||||
|
||||
// 初始化服务运行参数
|
||||
_TomCat = TomCat.getInstance(this);
|
||||
if (!_TomCat.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "没有下载 BoBullToon 数据。BoBullToon 参数无法加载。");
|
||||
}
|
||||
|
||||
if (mMainReceiver == null) {
|
||||
// 注册广播接收器
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
}
|
||||
|
||||
Rules.getInstance(this).loadRules();
|
||||
|
||||
startPhoneCallListener();
|
||||
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).start();
|
||||
|
||||
LogUtils.i(TAG, "Main Service Is Start.");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (_TomCat != null) {
|
||||
return _TomCat.isPhoneBoBullToon(phone);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 唤醒和绑定守护进程
|
||||
//
|
||||
void wakeupAndBindAssistant() {
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistant()");
|
||||
// if (ServiceUtils.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) {
|
||||
// startService(new Intent(MainService.this, AssistantService.class));
|
||||
// //LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService");
|
||||
// bindService(new Intent(MainService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
// }
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
startService(intent);
|
||||
// 绑定服务的Intent
|
||||
//Intent intent = new Intent(this, AssistantService.class);
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// Intent intent = new Intent(this, AssistantService.class);
|
||||
// startService(intent);
|
||||
// LogUtils.d(TAG, "startService(intent)");
|
||||
// bindService(new Intent(this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
void startPhoneCallListener() {
|
||||
Intent callListener = new Intent(this, CallListenerService.class);
|
||||
startService(callListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
//LogUtils.d(TAG, "onDestroy done");
|
||||
if (mMainServiceBean.isEnable() == false) {
|
||||
// 设置运行状态
|
||||
isServiceRunning = false;// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
// 停止守护进程
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
stopService(intent);
|
||||
// 停止Receiver
|
||||
if (mMainReceiver != null) {
|
||||
unregisterReceiver(mMainReceiver);
|
||||
mMainReceiver = null;
|
||||
}
|
||||
// 停止前台通知栏
|
||||
stopForeground(true);
|
||||
|
||||
// 停止主要进程
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).setIsExit(true);
|
||||
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
if (App.isDebuging()) {
|
||||
SOS.sosToAppBase(getApplicationContext(), MainService.class.getName());
|
||||
} else {
|
||||
SOS.sosToAppBaseBeta(getApplicationContext(), MainService.class.getName());
|
||||
}
|
||||
}
|
||||
isBound = false;
|
||||
mAssistantService = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
// ====================== 内部类:Binder(服务绑定通信,优先定义) ======================
|
||||
public class MyBinder extends Binder {
|
||||
MainService getService() {
|
||||
LogUtils.d(TAG, "MainService MyBinder getService()");
|
||||
/**
|
||||
* 外部组件绑定服务时,获取主服务实例
|
||||
* @return MainService 主服务实例
|
||||
*/
|
||||
public MainService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 外部获取主服务实例");
|
||||
return MainService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// //
|
||||
// // 启动服务
|
||||
// //
|
||||
// public static void startControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.startForegroundService(intent);
|
||||
// }
|
||||
//
|
||||
// //
|
||||
// // 停止服务
|
||||
// //
|
||||
// public static void stopControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.stopService(intent);
|
||||
// }
|
||||
// ====================== 内部类:ServiceConnection(守护服务绑定回调) ======================
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的IBinder为空,绑定失败");
|
||||
mIsAssistantBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public void appenMessage(String message) {
|
||||
LogUtils.d(TAG, String.format("Message : %s", message));
|
||||
}
|
||||
try {
|
||||
// 类型转换获取守护服务实例
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
mIsAssistantBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功");
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder类型转换失败", e);
|
||||
mIsAssistantBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainService(Context context) {
|
||||
LogUtils.d(TAG, "stopMainService");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开");
|
||||
mAssistantService = null;
|
||||
mIsAssistantBound = false;
|
||||
|
||||
public static void startMainService(Context context) {
|
||||
LogUtils.d(TAG, "startMainService");
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
public static void restartMainService(Context context) {
|
||||
LogUtils.d(TAG, "restartMainService");
|
||||
|
||||
MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (bean != null && bean.isEnable()) {
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
// try {
|
||||
// Thread.sleep(1000);
|
||||
// } catch (InterruptedException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// }
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
LogUtils.d(TAG, "已重启 MainService");
|
||||
// 服务启用状态下,重试绑定守护服务(主服务存活核心保障)
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: " + RETRY_DELAY_MS + "ms后重试绑定守护服务");
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
wakeupAndBindAssistantService();
|
||||
}
|
||||
}, RETRY_DELAY_MS);
|
||||
} else {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务已禁用,跳过重试绑定");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
// ====================== 对外静态方法区(服务启停/重启/状态查询,全局调用) ======================
|
||||
/**
|
||||
* 检查号码是否在BoBullToon库中(外部组件调用,静态入口)
|
||||
* @param phone 待查询号码
|
||||
* @return true=是BoBullToon号码,false=否/初始化失败
|
||||
*/
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (sTomCatInstance != null && phone != null && !phone.isEmpty()) {
|
||||
boolean result = sTomCatInstance.isPhoneBoBullToon(phone);
|
||||
LogUtils.d(TAG, "isPhoneInBoBullToon: 号码" + phone + "查询结果:" + (result ? "是" : "否"));
|
||||
return result;
|
||||
}
|
||||
LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat未初始化或号码为空,查询失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止主服务(仅停止,不修改配置)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void stopMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainService: 上下文为空,无法停止服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainService: 执行停止主服务操作");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动主服务(仅启动,不修改配置)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void startMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainService: 上下文为空,无法启动服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainService: 执行启动主服务操作(前台服务模式)");
|
||||
Intent intent = new Intent(context, MainService.class);
|
||||
context.startForegroundService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启主服务(先停后启,需服务已启用)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void restartMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "restartMainService: 上下文为空,无法重启服务");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "restartMainService: 执行主服务重启流程");
|
||||
|
||||
MainServiceBean config = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (config != null && config.isEnable()) {
|
||||
stopMainService(context);
|
||||
startMainService(context);
|
||||
LogUtils.i(TAG, "restartMainService: 主服务重启完成");
|
||||
} else {
|
||||
LogUtils.w(TAG, "restartMainService: 服务未启用或配置为空,跳过重启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务并保存禁用状态(更新配置+停止服务)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainServiceAndSaveStatus: 上下文为空,操作失败");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 保存禁用状态并停止服务");
|
||||
MainServiceBean config = new MainServiceBean();
|
||||
config.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, config);
|
||||
stopMainService(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务并保存启用状态(更新配置+启动服务,先停后启避免重复)
|
||||
* @param context 上下文(非空校验)
|
||||
*/
|
||||
public static void startMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainServiceAndSaveStatus: 上下文为空,操作失败");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus: 保存启用状态并启动服务");
|
||||
MainServiceBean config = new MainServiceBean();
|
||||
config.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, config);
|
||||
stopMainService(context); // 先停止旧服务,避免冲突
|
||||
startMainService(context);
|
||||
}
|
||||
|
||||
// ====================== 核心工具方法区(服务状态检查+前台通知创建,通用功能) ======================
|
||||
/**
|
||||
* 补充消息追加方法(外部组件向服务发送消息)
|
||||
* @param message 待追加消息(空值防护)
|
||||
*/
|
||||
public void appenMessage(String message) {
|
||||
String msg = message == null ? "null" : message;
|
||||
LogUtils.d(TAG, "appenMessage: 接收外部消息:" + msg);
|
||||
|
||||
if (mMainServiceHandler != null) {
|
||||
android.os.Message handlerMsg = android.os.Message.obtain();
|
||||
handlerMsg.what = MSG_UPDATE_STATUS;
|
||||
handlerMsg.obj = msg;
|
||||
mMainServiceHandler.sendMessage(handlerMsg);
|
||||
LogUtils.d(TAG, "appenMessage: 消息已发送至Handler处理");
|
||||
} else {
|
||||
LogUtils.w(TAG, "appenMessage: MainServiceHandler未初始化,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建前台服务通知(Android8.0+需渠道,低版本兼容)
|
||||
* @return Notification 前台服务通知实例
|
||||
*/
|
||||
private Notification createForegroundNotification() {
|
||||
// 1. Android8.0+创建通知渠道(必需,否则通知不显示)
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
FOREGROUND_CHANNEL_ID,
|
||||
"拨号主服务",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("主服务后台运行,保障通话监听与号码识别功能正常");
|
||||
channel.setSound(null, null); // 关闭通知声音
|
||||
channel.enableVibration(false); // 关闭振动
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "createForegroundNotification: Android8.0+通知渠道创建成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "createForegroundNotification: NotificationManager获取失败,渠道创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建通知实例(分版本兼容Builder)
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
||||
builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
|
||||
} else {
|
||||
builder = new Notification.Builder(this);
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_launcher);
|
||||
builder.setContentTitle("拨号服务运行中");
|
||||
builder.setContentText("后台保障通话监听与号码识别,请勿手动关闭");
|
||||
builder.setPriority(Notification.PRIORITY_LOW); // 低优先级,不打扰用户
|
||||
builder.setOngoing(true); // 不可手动清除,保障服务存活
|
||||
|
||||
LogUtils.d(TAG, "createForegroundNotification: 前台服务通知构建完成");
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定服务是否正在运行(通过ActivityManager查询)
|
||||
* @param serviceClass 待检查服务类
|
||||
* @return true=运行中,false=未运行/查询失败
|
||||
*/
|
||||
private boolean isServiceRunning(Class<?> serviceClass) {
|
||||
if (serviceClass == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning: 服务类为空,检查失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager == null) {
|
||||
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败,检查失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 遍历运行中服务,匹配类名
|
||||
for (ActivityManager.RunningServiceInfo serviceInfo : activityManager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "正在运行");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "未运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序:创建→绑定→启动→销毁) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "===== onCreate: 主服务开始创建 =====");
|
||||
sMainServiceInstance = this;
|
||||
mIsServiceRunning = false;
|
||||
|
||||
// 初始化核心组件(无延迟,直接初始化)
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
mServiceConnection = new MyServiceConnection();
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 初始化音量监控定时器(服务启动即开启,保障音量配置)
|
||||
initVolumeCheckTimer();
|
||||
|
||||
// 执行核心业务启动逻辑(无延迟,优先启动)
|
||||
startCoreBusiness();
|
||||
|
||||
LogUtils.d(TAG, "===== onCreate: 主服务创建完成 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 主服务被外部组件绑定,Intent=" + (intent != null ? intent.getAction() : "null"));
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 主服务被启动,startId=" + startId);
|
||||
// 重复启动时再次执行核心业务(避免服务被杀死后重启失败)
|
||||
startCoreBusiness();
|
||||
|
||||
// 服务启用则返回START_STICKY(系统杀死后自动重启),否则默认行为
|
||||
int result = (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (result == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁,全量清理资源 =====");
|
||||
|
||||
// 1. 停止音量监控定时器(释放线程资源)
|
||||
cancelVolumeCheckTimer();
|
||||
|
||||
// 2. 解除守护服务绑定(避免内存泄漏)
|
||||
if (mIsAssistantBound && mServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务绑定已解除");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除守护服务绑定失败(服务未绑定)", e);
|
||||
}
|
||||
mIsAssistantBound = false;
|
||||
mServiceConnection = null;
|
||||
}
|
||||
|
||||
// 3. 注销广播接收器(释放系统资源)
|
||||
if (mMainReceiver != null) {
|
||||
mMainReceiver.unregisterAction(this);
|
||||
mMainReceiver = null;
|
||||
LogUtils.d(TAG, "onDestroy: 全局广播接收器已注销");
|
||||
}
|
||||
|
||||
// 4. 停止所有子服务(强制停止,避免子服务残留)
|
||||
stopService(new Intent(this, AssistantService.class));
|
||||
stopService(new Intent(this, CallListenerService.class));
|
||||
stopService(new Intent(this, MyCallScreeningService.class));
|
||||
LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止");
|
||||
|
||||
// 5. 清空所有引用(静态+成员,彻底避免内存泄漏,不修改配置Bean)
|
||||
sMainServiceInstance = null;
|
||||
sTomCatInstance = null;
|
||||
mMainServiceHandler = null;
|
||||
mAssistantService = null;
|
||||
mMainServiceBean = null;
|
||||
|
||||
// 标记服务为未运行
|
||||
mIsServiceRunning = false;
|
||||
LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成,资源全清理 =====");
|
||||
}
|
||||
|
||||
// ====================== 核心业务逻辑区(服务启动核心流程,无延迟) ======================
|
||||
/**
|
||||
* 启动核心业务(主服务核心入口,避免重复启动)
|
||||
*/
|
||||
private synchronized void startCoreBusiness() {
|
||||
// 服务已运行则直接返回,避免重复执行启动流程
|
||||
if (mIsServiceRunning) {
|
||||
LogUtils.d(TAG, "startCoreBusiness: 主服务已运行,跳过重复启动");
|
||||
return;
|
||||
}
|
||||
mIsServiceRunning = true;
|
||||
|
||||
// 重新加载最新配置(避免配置修改后未生效)
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean == null || !mMainServiceBean.isEnable()) {
|
||||
LogUtils.w(TAG, "startCoreBusiness: 服务配置未启用或配置为空,启动流程终止");
|
||||
mIsServiceRunning = false;
|
||||
stopSelf(); // 未启用则主动停止服务
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.i(TAG, "startCoreBusiness: 服务配置已启用,启动核心流程");
|
||||
|
||||
// 1. 优先启动前台服务(避免前台服务启动超时崩溃,核心优先级)
|
||||
try {
|
||||
Notification foregroundNotification = createForegroundNotification();
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
LogUtils.i(TAG, "startCoreBusiness: 前台服务启动成功,通知ID=" + FOREGROUND_NOTIFICATION_ID);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动失败(服务类型不匹配)", e);
|
||||
mIsServiceRunning = false;
|
||||
stopSelf();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动异常", e);
|
||||
mIsServiceRunning = false;
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 绑定守护服务(保障主服务存活,防系统杀死)
|
||||
wakeupAndBindAssistantService();
|
||||
|
||||
// 3. 初始化核心业务组件(号码识别+黑白名单规则)
|
||||
initTomCatComponent();
|
||||
initRulesConfig();
|
||||
|
||||
// 4. 启动通话监听相关服务(无延迟,直接启动,保障功能生效)
|
||||
startCallRelatedServices();
|
||||
|
||||
LogUtils.i(TAG, "startCoreBusiness: 主服务核心流程启动完成,服务正常运行");
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒并绑定守护服务(分版本适配启动方式,Android10+前台服务启动)
|
||||
*/
|
||||
private void wakeupAndBindAssistantService() {
|
||||
if (mServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindAssistantService: 服务连接实例未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent assistantIntent = new Intent(this, AssistantService.class);
|
||||
// Android10+应用后台启动服务需用前台服务模式
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistantService: Android10+,前台服务模式启动守护服务");
|
||||
startForegroundService(assistantIntent);
|
||||
|
||||
// 绑定守护服务(BIND_IMPORTANT:高优先级绑定,断开时回调)
|
||||
bindService(assistantIntent, mServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistantService: 守护服务启动并发起绑定请求");
|
||||
}
|
||||
|
||||
// ====================== 铃声音量监控方法区(定时检查+恢复配置) ======================
|
||||
/**
|
||||
* 初始化音量检查定时器(先取消旧定时器,避免重复创建)
|
||||
*/
|
||||
private void initVolumeCheckTimer() {
|
||||
cancelVolumeCheckTimer();
|
||||
mVolumeCheckTimer = new Timer();
|
||||
mVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkAndRestoreRingerVolume();
|
||||
}
|
||||
}, VOLUME_CHECK_DELAY, VOLUME_CHECK_PERIOD);
|
||||
LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量监控定时器启动,周期" + VOLUME_CHECK_PERIOD + "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并恢复铃声音量(对比配置值,不一致则恢复,保障用户配置生效)
|
||||
*/
|
||||
private void checkAndRestoreRingerVolume() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: AudioManager获取失败,音量检查终止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载音量配置(无配置则初始化默认值)
|
||||
RingTongBean ringConfig = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringConfig == null) {
|
||||
ringConfig = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringConfig);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 音量配置未存在,初始化默认配置");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
int configVolume = ringConfig.getStreamVolume();
|
||||
// 音量不一致则恢复配置值
|
||||
if (currentVolume != configVolume) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复,配置值=" + configVolume + ",原当前值=" + currentVolume);
|
||||
} else {
|
||||
LogUtils.v(TAG, "checkAndRestoreRingerVolume: 铃声音量与配置一致,无需调整");
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足,恢复失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消音量检查定时器(释放Timer资源,避免内存泄漏)
|
||||
*/
|
||||
private void cancelVolumeCheckTimer() {
|
||||
if (mVolumeCheckTimer != null) {
|
||||
mVolumeCheckTimer.cancel();
|
||||
mVolumeCheckTimer = null;
|
||||
LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量监控定时器已取消");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助初始化方法区(业务组件初始化,统一归类) ======================
|
||||
/**
|
||||
* 初始化TomCat组件(号码识别核心,加载本地数据)
|
||||
*/
|
||||
private void initTomCatComponent() {
|
||||
sTomCatInstance = TomCat.getInstance(this);
|
||||
if (sTomCatInstance.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "initTomCatComponent: BoBullToon号码库加载成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "initTomCatComponent: BoBullToon号码库未下载,加载失败(不影响服务运行)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑白名单规则配置(加载本地规则,保障通话筛选生效)
|
||||
*/
|
||||
private void initRulesConfig() {
|
||||
Rules rules = Rules.getInstance(this);
|
||||
if (rules != null) {
|
||||
rules.loadRules();
|
||||
LogUtils.d(TAG, "initRulesConfig: 黑白名单通话规则加载完成");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initRulesConfig: Rules实例获取失败,通话规则加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化广播接收器(监听系统事件,如开机启动、网络变化)
|
||||
*/
|
||||
private void initMainReceiver() {
|
||||
if (mMainReceiver == null) {
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
LogUtils.d(TAG, "initMainReceiver: 全局广播接收器注册完成");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initMainReceiver: 广播接收器已初始化,跳过重复注册");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动通话相关服务(通话监听+通话筛选,核心功能服务)
|
||||
*/
|
||||
private void startCallRelatedServices() {
|
||||
// 启动通话监听服务
|
||||
try {
|
||||
Intent callListenerIntent = new Intent(this, CallListenerService.class);
|
||||
startService(callListenerIntent);
|
||||
LogUtils.d(TAG, "startCallRelatedServices: CallListenerService启动成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCallRelatedServices: CallListenerService启动失败", e);
|
||||
}
|
||||
|
||||
// 启动通话筛选服务(API10+生效,低版本兼容)
|
||||
try {
|
||||
Intent screeningIntent = new Intent(this, MyCallScreeningService.class);
|
||||
startService(screeningIntent);
|
||||
LogUtils.d(TAG, "startCallRelatedServices: MyCallScreeningService启动成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startCallRelatedServices: MyCallScreeningService启动失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.telecom.CallScreeningService;
|
||||
import android.telephony.TelephonyManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Describe 通话筛选服务(无前台服务),负责识别通话类型、拦截指定号码、处理正常通话逻辑
|
||||
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 强化空指针防护 | 无冗余代码
|
||||
*/
|
||||
@RequiresApi(api = 29) // 父类 CallScreeningService 最低要求 API29,明确标注
|
||||
public class MyCallScreeningService extends CallScreeningService {
|
||||
|
||||
// ====================== 常量定义区(全硬编码,无高版本API依赖) ======================
|
||||
public static final String TAG = "MyCallScreeningService";
|
||||
|
||||
// 通话方向常量(硬编码替代 Call.Details 高版本字段,适配API29-30)
|
||||
private static final int CALL_DIRECTION_INCOMING = 1; // 来电
|
||||
private static final int CALL_DIRECTION_OUTGOING = 2; // 外拨
|
||||
|
||||
// ====================== 成员属性区(精简必要属性,命名规范) ======================
|
||||
private Context mContext; // 上下文对象,避免重复调用 getApplicationContext()
|
||||
|
||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mContext = this;
|
||||
LogUtils.d(TAG, "===== onCreate: 通话筛选服务启动 =====");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId);
|
||||
|
||||
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
|
||||
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
||||
|
||||
return startMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 置空上下文,释放引用,避免内存泄漏
|
||||
mContext = null;
|
||||
LogUtils.d(TAG, "===== onDestroy: 通话筛选服务销毁完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 核心通话筛选方法区(父类抽象方法实现) ======================
|
||||
/**
|
||||
* 核心:通话筛选入口(API29-30标准方法,100%兼容)
|
||||
* 功能:识别通话号码/类型、拦截指定号码、处理正常通话
|
||||
*/
|
||||
@Override
|
||||
@RequiresApi(api = 29)
|
||||
public void onScreenCall(@NonNull android.telecom.Call.Details details) {
|
||||
LogUtils.d(TAG, "===== onScreenCall: 开始筛选通话 =====");
|
||||
|
||||
// 1. 安全获取通话号码(多层空指针防护,Java7规范写法)
|
||||
String phoneNumber = getSafePhoneNumber(details);
|
||||
// 2. 识别通话方向(来电/外拨/未知)
|
||||
int callDirection = details.getCallDirection();
|
||||
String callTypeDesc = getCallDirectionDesc(callDirection);
|
||||
int callState = getCallStateByDirection(callDirection);
|
||||
|
||||
LogUtils.d(TAG, "筛选结果:通话类型=" + callTypeDesc + ",号码=" + phoneNumber);
|
||||
|
||||
// 3. 自定义拦截逻辑(示例:拦截指定号码10086,可按需扩展黑白名单)
|
||||
boolean isNeedBlock = isTargetBlockNumber(phoneNumber);
|
||||
|
||||
// 4. 构建筛选响应(Java7分步调用,不使用链式写法,避免兼容问题)
|
||||
CallResponse callResponse = buildCallScreeningResponse(isNeedBlock);
|
||||
|
||||
// 5. 提交筛选结果(必须调用父类方法,完成拦截/放行逻辑)
|
||||
respondToCall(details, callResponse);
|
||||
|
||||
// 6. 分场景处理后续逻辑(拦截日志/正常通话业务)
|
||||
handleCallAfterScreening(phoneNumber, callTypeDesc, isNeedBlock);
|
||||
|
||||
LogUtils.d(TAG, "===== onScreenCall: 通话筛选完成 =====");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法区(按功能拆分,低耦合) ======================
|
||||
/**
|
||||
* 安全获取通话号码(多层空指针+空字符串防护,避免崩溃)
|
||||
*/
|
||||
private String getSafePhoneNumber(android.telecom.Call.Details details) {
|
||||
String phoneNumber = "未知号码";
|
||||
if (details == null) {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 通话详情为空,无法获取号码");
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
Uri handle = details.getHandle();
|
||||
if (handle != null) {
|
||||
String schemePart = handle.getSchemeSpecificPart();
|
||||
if (schemePart != null && !schemePart.trim().isEmpty()) {
|
||||
phoneNumber = schemePart.trim();
|
||||
LogUtils.d(TAG, "getSafePhoneNumber: 成功获取号码,原始值=" + schemePart + ",处理后=" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 号码格式异常,schemePart=" + schemePart);
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, "getSafePhoneNumber: 通话 handle 为空,无法获取号码");
|
||||
}
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为目标拦截号码(可扩展黑白名单逻辑,当前示例拦截10086)
|
||||
*/
|
||||
private boolean isTargetBlockNumber(String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "isTargetBlockNumber: 号码为空,不拦截");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 示例拦截逻辑:拦截 10086(实际可替换为黑白名单查询)
|
||||
boolean isBlock = "10086".equals(phoneNumber.trim());
|
||||
if (isBlock) {
|
||||
LogUtils.d(TAG, "isTargetBlockNumber: 命中拦截规则,号码=" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.d(TAG, "isTargetBlockNumber: 未命中拦截规则,号码=" + phoneNumber);
|
||||
}
|
||||
return isBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通话筛选响应(按需配置拦截/放行参数,适配API29-30)
|
||||
*/
|
||||
private CallResponse buildCallScreeningResponse(boolean isNeedBlock) {
|
||||
CallResponse.Builder responseBuilder = new CallResponse.Builder();
|
||||
// 拦截配置:是否禁止通话+是否拒绝通话(两者配合实现拦截)
|
||||
responseBuilder.setDisallowCall(isNeedBlock);
|
||||
responseBuilder.setRejectCall(isNeedBlock);
|
||||
// 日志/通知配置:拦截的通话跳过日志和通知,正常通话保留
|
||||
responseBuilder.setSkipCallLog(isNeedBlock);
|
||||
responseBuilder.setSkipNotification(isNeedBlock);
|
||||
|
||||
CallResponse response = responseBuilder.build();
|
||||
LogUtils.d(TAG, "buildCallScreeningResponse: 响应构建完成,拦截状态=" + isNeedBlock);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选后分场景处理(拦截日志/正常通话业务扩展)
|
||||
*/
|
||||
private void handleCallAfterScreening(String phoneNumber, String callTypeDesc, boolean isNeedBlock) {
|
||||
if (isNeedBlock) {
|
||||
// 拦截场景:仅打日志(可扩展:添加拦截记录、本地存储等)
|
||||
LogUtils.d(TAG, "handleCallAfterScreening: 已拦截通话,类型=" + callTypeDesc + ",号码=" + phoneNumber);
|
||||
} else {
|
||||
// 正常通话场景:处理业务逻辑(可扩展:通话记录、广播通知、号码识别等)
|
||||
int callState = getCallStateByDirection(getCallDirectionFromDesc(callTypeDesc));
|
||||
handleNormalCallBusiness(phoneNumber, callState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常通话业务处理(核心业务扩展入口,强化空指针防护)
|
||||
*/
|
||||
private void handleNormalCallBusiness(String phoneNumber, int callState) {
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "handleNormalCallBusiness: 号码为空,跳过业务处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String callStateDesc = getCallStateDesc(callState);
|
||||
LogUtils.d(TAG, "handleNormalCallBusiness: 处理正常通话业务,号码=" + phoneNumber + ",状态=" + callStateDesc);
|
||||
|
||||
// 此处可扩展业务逻辑(示例):
|
||||
// 1. 保存通话记录到本地
|
||||
// 2. 发送广播通知其他组件(如通话监听服务)
|
||||
// 3. 调用号码识别接口,匹配联系人信息
|
||||
}
|
||||
|
||||
// ====================== 工具辅助方法区(统一归类,复用性强) ======================
|
||||
/**
|
||||
* 通话方向转文字描述(便于日志查看,快速定位场景)
|
||||
*/
|
||||
private String getCallDirectionDesc(int callDirection) {
|
||||
switch (callDirection) {
|
||||
case CALL_DIRECTION_INCOMING:
|
||||
return "来电";
|
||||
case CALL_DIRECTION_OUTGOING:
|
||||
return "外拨";
|
||||
default:
|
||||
return "未知通话";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字描述转通话方向(配合业务逻辑反向匹配,避免重复判断)
|
||||
*/
|
||||
private int getCallDirectionFromDesc(String callTypeDesc) {
|
||||
if ("来电".equals(callTypeDesc)) {
|
||||
return CALL_DIRECTION_INCOMING;
|
||||
} else if ("外拨".equals(callTypeDesc)) {
|
||||
return CALL_DIRECTION_OUTGOING;
|
||||
} else {
|
||||
return -1; // 未知方向
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通话方向转 TelephonyManager 状态(统一状态标准,便于业务复用)
|
||||
*/
|
||||
private int getCallStateByDirection(int callDirection) {
|
||||
switch (callDirection) {
|
||||
case CALL_DIRECTION_INCOMING:
|
||||
return TelephonyManager.CALL_STATE_RINGING; // 来电=响铃中
|
||||
case CALL_DIRECTION_OUTGOING:
|
||||
return TelephonyManager.CALL_STATE_OFFHOOK; // 外拨=通话中
|
||||
default:
|
||||
return TelephonyManager.CALL_STATE_IDLE; // 未知=空闲
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TelephonyManager 状态转文字描述(统一日志格式,提升可读性)
|
||||
*/
|
||||
private String getCallStateDesc(int callState) {
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
return "响铃中";
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
return "通话中";
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,104 @@
|
||||
package cc.winboll.studio.contacts.threads;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
* @Describe 主服务后台工作线程,负责定时轮询与消息调度
|
||||
*/
|
||||
public class MainServiceThread extends Thread {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainServiceThread";
|
||||
|
||||
volatile static MainServiceThread _MainServiceThread;
|
||||
// 控制线程是否退出的标志
|
||||
volatile boolean isExit = false;
|
||||
volatile boolean isStarted = false;
|
||||
Context mContext;
|
||||
// 服务Handler, 用于线程发送消息使用
|
||||
WeakReference<MainServiceHandler> mwrMainServiceHandler;
|
||||
// 线程休眠周期(1秒)
|
||||
private static final long THREAD_SLEEP_INTERVAL = 1000L;
|
||||
|
||||
MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
mContext = context;
|
||||
mwrMainServiceHandler = new WeakReference<MainServiceHandler>(handler);
|
||||
// ====================== 静态成员变量区 ======================
|
||||
private static volatile MainServiceThread sInstance;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 线程运行控制标记
|
||||
private volatile boolean mIsExit;
|
||||
private volatile boolean mIsStarted;
|
||||
// 弱引用持有上下文和Handler,避免内存泄漏
|
||||
private WeakReference<Context> mContextWeakRef;
|
||||
private WeakReference<MainServiceHandler> mHandlerWeakRef;
|
||||
|
||||
// ====================== 私有构造函数 ======================
|
||||
private MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
this.mContextWeakRef = new WeakReference<>(context);
|
||||
this.mHandlerWeakRef = new WeakReference<>(handler);
|
||||
this.mIsExit = false;
|
||||
this.mIsStarted = false;
|
||||
LogUtils.d(TAG, "MainServiceThread: 线程实例初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 单例获取方法 ======================
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
// 若已有实例,先标记退出并销毁旧实例
|
||||
if (sInstance != null) {
|
||||
LogUtils.d(TAG, "getInstance: 存在旧线程实例,标记退出");
|
||||
sInstance.setIsExit(true);
|
||||
sInstance = null;
|
||||
}
|
||||
// 创建新线程实例
|
||||
sInstance = new MainServiceThread(context, handler);
|
||||
LogUtils.d(TAG, "getInstance: 新线程实例已创建");
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ====================== 运行状态控制方法 ======================
|
||||
public void setIsExit(boolean isExit) {
|
||||
this.isExit = isExit;
|
||||
this.mIsExit = isExit;
|
||||
LogUtils.d(TAG, "setIsExit: 线程退出标记已更新 | " + isExit);
|
||||
}
|
||||
|
||||
public boolean isExit() {
|
||||
return isExit;
|
||||
return mIsExit;
|
||||
}
|
||||
|
||||
public void setIsStarted(boolean isStarted) {
|
||||
this.isStarted = isStarted;
|
||||
this.mIsStarted = isStarted;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
if (_MainServiceThread != null) {
|
||||
_MainServiceThread.setIsExit(true);
|
||||
}
|
||||
_MainServiceThread = new MainServiceThread(context, handler);
|
||||
return _MainServiceThread;
|
||||
return mIsStarted;
|
||||
}
|
||||
|
||||
// ====================== 线程核心执行方法 ======================
|
||||
@Override
|
||||
public void run() {
|
||||
if (isStarted == false) {
|
||||
isStarted = true;
|
||||
LogUtils.d(TAG, "run()");
|
||||
|
||||
while (!isExit()) {
|
||||
//ToastUtils.show("run");
|
||||
//LogUtils.d(TAG, "run()");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
_MainServiceThread = null;
|
||||
LogUtils.d(TAG, "run() exit");
|
||||
// 防止重复启动
|
||||
if (mIsStarted) {
|
||||
LogUtils.w(TAG, "run: 线程已启动,避免重复执行");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记线程启动状态
|
||||
mIsStarted = true;
|
||||
LogUtils.i(TAG, "run: 线程开始运行");
|
||||
|
||||
// 线程主循环
|
||||
while (!mIsExit) {
|
||||
try {
|
||||
// 此处可添加业务逻辑(如定时任务、消息分发)
|
||||
Thread.sleep(THREAD_SLEEP_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "run: 线程休眠被中断", e);
|
||||
// 恢复线程中断状态
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// 线程退出清理
|
||||
mIsStarted = false;
|
||||
mContextWeakRef.clear();
|
||||
mHandlerWeakRef.clear();
|
||||
sInstance = null;
|
||||
LogUtils.i(TAG, "run: 线程正常退出");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,270 +1,268 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 调用应用属性设置页工具类
|
||||
* 来源:https://blog.csdn.net/zhuhai__yizhi/article/details/78737593
|
||||
* Created by zyy on 2018/3/12.
|
||||
* 直接跳转到权限后返回,可以监控权限授权情况,但是,跳转到应用详情页,无法监测权限情况
|
||||
* 是否要加以区分,若是应用详情页,则跳转回来后,onRestart检测所求权限,如果授权,则收回提示,如果没授权,则继续提示
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 应用权限设置页跳转工具类,适配主流手机厂商的权限页路径,跳转失败时降级到应用详情页
|
||||
*/
|
||||
public class AppGoToSettingsUtil {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AppGoToSettingsUtil";
|
||||
|
||||
// 跳转设置页的 Activity 结果码,复用 MainActivity 的请求码
|
||||
public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS;
|
||||
|
||||
/**
|
||||
* Build.MANUFACTURER判断各大手机厂商品牌
|
||||
*/
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";//华为
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";//魅族
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";//小米
|
||||
private static final String MANUFACTURER_SONY = "Sony";//索尼
|
||||
// 主流手机厂商品牌常量
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";
|
||||
private static final String MANUFACTURER_SONY = "Sony";
|
||||
private static final String MANUFACTURER_OPPO = "OPPO";
|
||||
private static final String MANUFACTURER_LG = "LG";
|
||||
private static final String MANUFACTURER_VIVO = "vivo";
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";//三星
|
||||
private static final String MANUFACTURER_LETV = "Letv";//乐视
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";//中兴
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";//酷派
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";//联想
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";
|
||||
private static final String MANUFACTURER_LETV = "Letv";
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";
|
||||
|
||||
public static boolean isAppSettingOpen=false;
|
||||
// ====================== 成员变量区 ======================
|
||||
// 标记当前跳转的是应用详情页(true)还是厂商权限页(false)
|
||||
public static boolean isAppSettingOpen = false;
|
||||
|
||||
// ====================== 核心跳转方法区 ======================
|
||||
/**
|
||||
* 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页
|
||||
* 这里需要改造成返回true或者false,应用详情页:true,应用权限页:false
|
||||
* @param activity
|
||||
* 跳转到对应品牌手机的系统权限设置页,跳转失败则降级到应用详情页
|
||||
* @param activity 上下文 Activity
|
||||
*/
|
||||
public static void GoToSetting(Activity activity) {
|
||||
switch (Build.MANUFACTURER) {
|
||||
case MANUFACTURER_HUAWEI://华为
|
||||
Huawei(activity);
|
||||
public static void goToSetting(Activity activity) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "goToSetting: Activity 为 null,无法跳转设置页");
|
||||
return;
|
||||
}
|
||||
|
||||
String manufacturer = Build.MANUFACTURER;
|
||||
LogUtils.d(TAG, "goToSetting: 当前设备厂商 | " + manufacturer);
|
||||
|
||||
// 根据厂商跳转对应权限页
|
||||
switch (manufacturer) {
|
||||
case MANUFACTURER_HUAWEI:
|
||||
gotoHuaweiSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_MEIZU://魅族
|
||||
Meizu(activity);
|
||||
case MANUFACTURER_MEIZU:
|
||||
gotoMeizuSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_XIAOMI://小米
|
||||
Xiaomi(activity);
|
||||
case MANUFACTURER_XIAOMI:
|
||||
gotoXiaomiSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_SONY://索尼
|
||||
Sony(activity);
|
||||
case MANUFACTURER_SONY:
|
||||
gotoSonySetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_OPPO://oppo
|
||||
OPPO(activity);
|
||||
case MANUFACTURER_OPPO:
|
||||
gotoOppoSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LG://lg
|
||||
LG(activity);
|
||||
case MANUFACTURER_LG:
|
||||
gotoLgSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LETV://乐视
|
||||
Letv(activity);
|
||||
case MANUFACTURER_LETV:
|
||||
gotoLetvSetting(activity);
|
||||
break;
|
||||
default://其他
|
||||
try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
} catch (Exception e) {
|
||||
SystemConfig(activity);
|
||||
}
|
||||
default:
|
||||
LogUtils.w(TAG, "goToSetting: 未适配当前厂商,跳转应用详情页");
|
||||
openAppDetailSetting(activity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 各厂商权限页跳转方法区 ======================
|
||||
/**
|
||||
* 华为跳转权限设置页
|
||||
* @param activity
|
||||
* 跳转华为手机权限设置页
|
||||
*/
|
||||
public static void Huawei(Activity activity) {
|
||||
private static void gotoHuaweiSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.huawei.systemmanager",
|
||||
"com.huawei.permissionmanager.ui.MainActivity"));
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoHuaweiSetting: 跳转华为权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoHuaweiSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 魅族跳转权限设置页,测试时,点击无反应,具体原因不明
|
||||
* @param activity
|
||||
* 跳转魅族手机权限设置页
|
||||
*/
|
||||
public static void Meizu(Activity activity) {
|
||||
private static void gotoMeizuSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoMeizuSetting: 跳转魅族权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoMeizuSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小米,功能正常
|
||||
* @param activity
|
||||
* 跳转小米手机权限设置页
|
||||
*/
|
||||
public static void Xiaomi(Activity activity) {
|
||||
try { //MIUI 8 9
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
private static void gotoXiaomiSetting(Activity activity) {
|
||||
try {
|
||||
// 适配 MIUI 8/9 及以上版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI8+)成功");
|
||||
} catch (Exception e) {
|
||||
try { //MIUI 5/6/7
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
try {
|
||||
// 适配 MIUI 5/6/7 版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
} catch (Exception e1) { //否则跳转到应用详情
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI5-7)成功");
|
||||
} catch (Exception e1) {
|
||||
LogUtils.e(TAG, "gotoXiaomiSetting: 所有版本适配失败,降级到应用详情页", e1);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
//这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果
|
||||
//activity.startActivity(getAppDetailSettingIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索尼,6.0以上的手机非常少,基本没看见
|
||||
* @param activity
|
||||
* 跳转索尼手机权限设置页
|
||||
*/
|
||||
public static void Sony(Activity activity) {
|
||||
private static void gotoSonySetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.sonymobile.cta",
|
||||
"com.sonymobile.cta.SomcCTAMainActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoSonySetting: 跳转索尼权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoSonySetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPPO
|
||||
* @param activity
|
||||
* 跳转OPPO手机权限设置页
|
||||
*/
|
||||
public static void OPPO(Activity activity) {
|
||||
private static void gotoOppoSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.color.safecenter",
|
||||
"com.color.safecenter.permission.PermissionManagerActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoOppoSetting: 跳转OPPO权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoOppoSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LG经过测试,正常使用
|
||||
* @param activity
|
||||
* 跳转LG手机权限设置页
|
||||
*/
|
||||
public static void LG(Activity activity) {
|
||||
private static void gotoLgSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.android.settings",
|
||||
"com.android.settings.Settings$AccessLockSummaryActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLgSetting: 跳转LG权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLgSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 乐视6.0以上很少,基本都可以忽略了,现在乐视手机不多
|
||||
* @param activity
|
||||
* 跳转乐视手机权限设置页
|
||||
*/
|
||||
public static void Letv(Activity activity) {
|
||||
private static void gotoLetvSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.letv.android.letvsafe",
|
||||
"com.letv.android.letvsafe.PermissionAndApps"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLetvSetting: 跳转乐视权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLetvSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 降级跳转方法区 ======================
|
||||
/**
|
||||
* 只能打开到自带安全软件
|
||||
* @param activity
|
||||
* 跳转系统设置主界面
|
||||
*/
|
||||
public static void _360(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
public static void gotoSystemConfig(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "gotoSystemConfig: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 系统设置界面
|
||||
* @param activity
|
||||
*/
|
||||
public static void SystemConfig(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
/**
|
||||
* 获取应用详情页面
|
||||
* @return
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//if (Build.VERSION.SDK_INT >= 9) {
|
||||
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
/*} else if (Build.VERSION.SDK_INT <= 8) {
|
||||
localIntent.setAction(Intent.ACTION_VIEW);
|
||||
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
|
||||
localIntent.putExtra("com.android.settings.ApplicationPkgName", activity.getPackageName());
|
||||
}*/
|
||||
return localIntent;
|
||||
LogUtils.d(TAG, "gotoSystemConfig: 跳转系统设置主界面成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用详情页的 Intent
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
Intent intent = new Intent();
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开应用详情设置页
|
||||
*/
|
||||
public static void openAppDetailSetting(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "openAppDetailSetting: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = true;
|
||||
LogUtils.d(TAG, "openAppDetailSetting: 跳转应用详情设置页成功");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
@@ -13,203 +8,344 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集:提供联系人查询、添加、编辑、号码格式化等功能,适配主流机型
|
||||
*/
|
||||
public class ContactUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactUtils";
|
||||
// 手机号正则(11位中国大陆手机号)
|
||||
private static final String REGEX_CHINA_MOBILE = "^1[0-9]{10}$";
|
||||
|
||||
Map<String, String> contactMap = new HashMap<>();
|
||||
// ====================== 单例与成员变量区 ======================
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile ContactUtils sInstance;
|
||||
// 上下文(弱引用避免内存泄漏,Java7 兼容)
|
||||
private final Context mContext;
|
||||
// 缓存联系人:key=纯数字号码,value=联系人姓名
|
||||
private final Map<String, String> mContactMap = new HashMap<>();
|
||||
|
||||
static volatile ContactUtils _ContactUtils;
|
||||
Context mContext;
|
||||
ContactUtils(Context context) {
|
||||
mContext = context;
|
||||
relaodContacts();
|
||||
// ====================== 单例构造区 ======================
|
||||
/**
|
||||
* 私有构造器:初始化上下文并加载联系人
|
||||
*/
|
||||
private ContactUtils(Context context) {
|
||||
// 传入应用上下文,避免Activity上下文泄漏
|
||||
this.mContext = context.getApplicationContext();
|
||||
LogUtils.d(TAG, "ContactUtils 初始化,开始加载联系人");
|
||||
reloadContacts();
|
||||
}
|
||||
public synchronized static ContactUtils getInstance(Context context) {
|
||||
if (_ContactUtils == null) {
|
||||
_ContactUtils = new ContactUtils(context);
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,Java7 安全)
|
||||
*/
|
||||
public static ContactUtils getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为null,无法创建实例");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
}
|
||||
return _ContactUtils;
|
||||
}
|
||||
|
||||
public void relaodContacts() {
|
||||
readContacts();
|
||||
}
|
||||
|
||||
private void readContacts() {
|
||||
contactMap.clear();
|
||||
ContentResolver contentResolver = mContext.getContentResolver();
|
||||
Cursor cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
null, null, null, null);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
//Map<String, String> contactMap = new HashMap<>();
|
||||
contactMap.put(formatToSimplePhoneNumber(phoneNumber), displayName);
|
||||
if (sInstance == null) {
|
||||
synchronized (ContactUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ContactUtils(context);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
// 此时 contactList 就是存储联系人信息的 Map 列表
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public String getContactsName(String phone) {
|
||||
String result = contactMap.get(formatToSimplePhoneNumber(phone));
|
||||
return result == null ? "[NotInContacts]" : result;
|
||||
// ====================== 联系人缓存与查询区 ======================
|
||||
/**
|
||||
* 重新加载联系人到缓存
|
||||
*/
|
||||
public void reloadContacts() {
|
||||
LogUtils.d(TAG, "reloadContacts: 开始刷新联系人缓存");
|
||||
mContactMap.clear();
|
||||
readContactsFromSystem();
|
||||
LogUtils.d(TAG, "reloadContacts: 联系人缓存刷新完成,共缓存 " + mContactMap.size() + " 个联系人");
|
||||
}
|
||||
|
||||
// static String getSimplePhone(String phone) {
|
||||
// return phone.replaceAll("[+\\s]", "");
|
||||
// }
|
||||
/**
|
||||
* 从系统通讯录读取所有联系人(核心方法)
|
||||
*/
|
||||
private void readContactsFromSystem() {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
// 只查询姓名和号码字段,减少IO开销
|
||||
String[] projection = {
|
||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||
};
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor == null) {
|
||||
LogUtils.w(TAG, "readContactsFromSystem: 通讯录查询Cursor为null,可能缺少权限");
|
||||
return;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
|
||||
if (phone != null) {
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
mContactMap.put(simplePhone, name != null ? name : "[UnknownName]");
|
||||
LogUtils.v(TAG, "readContactsFromSystem: 缓存联系人 - 号码:" + simplePhone + ",姓名:" + name);
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录失败,缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 确保游标关闭,避免内存泄漏
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取联系人姓名
|
||||
*/
|
||||
public String getContactName(String phone) {
|
||||
if (phone == null) {
|
||||
LogUtils.w(TAG, "getContactName: 输入号码为null");
|
||||
return "[NotInContacts]";
|
||||
}
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
String name = mContactMap.get(simplePhone);
|
||||
LogUtils.d(TAG, "getContactName: 查询号码 " + simplePhone + ",姓名:" + (name == null ? "[NotInContacts]" : name));
|
||||
return name == null ? "[NotInContacts]" : name;
|
||||
}
|
||||
|
||||
// ====================== 号码格式化工具区 ======================
|
||||
/**
|
||||
* 格式化号码为纯数字(去除所有非数字字符)
|
||||
*/
|
||||
public static String formatToSimplePhoneNumber(String number) {
|
||||
// 去除所有空格和非数字字符
|
||||
return number.replaceAll("[^0-9]", "");
|
||||
if (number == null || number.isEmpty()) {
|
||||
LogUtils.w(TAG, "formatToSimplePhoneNumber: 输入号码为空");
|
||||
return "";
|
||||
}
|
||||
String simpleNumber = number.replaceAll("[^0-9]", "");
|
||||
LogUtils.v(TAG, "formatToSimplePhoneNumber: 原号码 " + number + " → 纯数字号码 " + simpleNumber);
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化11位手机号为带空格格式(如:138 0000 1234)
|
||||
*/
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
if (simpleNumber == null || !simpleNumber.matches(REGEX_CHINA_MOBILE)) {
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 号码不符合11位手机号格式,无需格式化");
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(simpleNumber.substring(0, 3))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(3, 7))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(7, 11));
|
||||
|
||||
String formatted = sb.toString();
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 纯数字号码 " + simpleNumber + " → 带空格号码 " + formatted);
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// ====================== 联系人查询(直接查系统,不走缓存)区 ======================
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按原始号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhone(Context context, String phoneNumber) {
|
||||
String displayName = null;
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhone: 上下文或号码为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{phoneNumber}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
Cursor cursor = null;
|
||||
String displayName = null;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER + "=?",
|
||||
new String[]{phoneNumber},
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
}
|
||||
LogUtils.d(TAG, "getDisplayNameByPhone: 按原始号码 " + phoneNumber + " 查询,姓名:" + displayName);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按纯数字号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhoneSimple(Context context, String phoneNumber) {
|
||||
String displayName = null;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{formatToSimplePhoneNumber(phoneNumber)}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
if (phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhoneSimple: 输入号码为null");
|
||||
return null;
|
||||
}
|
||||
return displayName;
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
LogUtils.d(TAG, "getDisplayNameByPhoneSimple: 按纯数字号码 " + simplePhone + " 查询");
|
||||
return getDisplayNameByPhone(context, simplePhone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断号码是否在系统通讯录中
|
||||
*/
|
||||
public static boolean isPhoneInContacts(Context context, String phoneNumber) {
|
||||
String szPhoneNumber = formatToSimplePhoneNumber(phoneNumber);
|
||||
String szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
szPhoneNumber = formatToSpacePhoneNumber(szPhoneNumber);
|
||||
szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "isPhoneInContacts: 上下文或号码为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
String displayName = getDisplayNameByPhone(context, simplePhone);
|
||||
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 未找到联系人(纯数字匹配)");
|
||||
String spacePhone = formatToSpacePhoneNumber(simplePhone);
|
||||
displayName = getDisplayNameByPhone(context, spacePhone);
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + spacePhone + " 未找到联系人(带空格匹配)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, String.format("Phone %s is found in contacts %s.", szPhoneNumber, szDisplayName));
|
||||
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 已在联系人中,姓名:" + displayName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
// 去除所有空格和非数字字符
|
||||
StringBuilder sbSpaceNumber = new StringBuilder();
|
||||
String regex = "^1[0-9]{10}$";
|
||||
if (simpleNumber.matches(regex)) {
|
||||
sbSpaceNumber.append(simpleNumber.substring(0, 3));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(3, 7));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(7, 11));
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配定制机型)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "getContactIdByPhone: 上下文或号码为空");
|
||||
return -1L;
|
||||
}
|
||||
return sbSpaceNumber.toString();
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
Long contactId = -1L;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
LogUtils.d(TAG, "getContactIdByPhone: 号码 " + phoneNumber + " 对应的联系人ID:" + contactId);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return contactId;
|
||||
}
|
||||
|
||||
// ====================== 联系人跳转工具区 ======================
|
||||
/**
|
||||
* 跳转至系统添加联系人界面
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 预填号码(可为null)
|
||||
*/
|
||||
public static void jumpToAddContact(Context context, String phoneNumber) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToAddContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至系统添加联系人界面的工具函数
|
||||
* @param context 上下文(如 PhoneCallService、Activity、Fragment 均可,需传入有效上下文)
|
||||
* @param phoneNumber 可选参数:预填的联系人电话(传 null 则跳转空表单)
|
||||
*/
|
||||
public static void jumpToAddContact(Context mContext, String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
intent.putExtra(android.provider.ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
if (phoneNumber != null) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,预填号码:" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,无预填号码");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文(Activity/Service/Fragment)
|
||||
* @param phoneNumber 待编辑联系人的电话号码(用于匹配已有联系人,必传)
|
||||
* @param contactId 可选:已有联系人的ID(通过 ContactsContract 获取,传null则自动匹配号码)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
// 关键:小米等机型需明确设置数据类型为“单个联系人”,避免参数丢失
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 支持非Activity上下文调用
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
// 场景A:已知联系人ID(精准定位,优先用此方式,参数传递最稳定)
|
||||
if (contactId != null && contactId > 0) {
|
||||
// 构建联系人的Uri(格式:content://contacts/people/[contactId],系统标准格式)
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
//ToastUtils.show("1");
|
||||
} else if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
// 方式1:小米等机型兼容的“通过号码定位联系人”参数(部分系统认此参数)
|
||||
//intent.putExtra(ContactsContract.Intents.Insert.PHONE_NUMBER, phoneNumber);
|
||||
// 方式2:补充系统标准的“数据Uri”,强化匹配(避免参数被定制系统忽略)
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
} else {
|
||||
LogUtils.d(TAG, "编辑联系人失败:电话号码和联系人ID均为空");
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待编辑号码(必传)
|
||||
* @param contactId 联系人ID(可选,优先使用)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新)
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
// 校验必要参数
|
||||
if (contactId == null || contactId <= 0) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 联系人ID和号码均为空,无法编辑");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动活动(加防护,避免无联系人应用崩溃)
|
||||
// 小米机型在Service/非Activity中调用,需加NEW_TASK标志,否则可能无法启动
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配小米机型,解决编辑时匹配不稳定问题)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待查询的电话号码
|
||||
* @return 联系人ID(无匹配时返回-1)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
return -1L;
|
||||
}
|
||||
// 优先通过ID定位(精准)
|
||||
if (contactId != null && contactId > 0) {
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过ID " + contactId + " 定位联系人,准备编辑");
|
||||
} else {
|
||||
// 通过号码定位
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过号码 " + phoneNumber + " 定位联系人,准备编辑");
|
||||
}
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
// 1. 构建电话查询Uri(系统标准:通过号码过滤联系人数据)
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
// 2. 只查询“联系人ID”字段(高效,避免冗余数据)
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = cr.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
// 3. 读取联系人ID(返回Long类型,避免int溢出)
|
||||
return cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "查询联系人ID失败。" + e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
}
|
||||
}
|
||||
return -1L; // 无匹配联系人
|
||||
}
|
||||
// 预填最新号码
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import android.widget.EditText;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 00:59:13
|
||||
* @Describe Int类型数字输入框工具集
|
||||
* @Describe Int类型数字输入框工具集:安全读取 EditText 中的整数内容
|
||||
*/
|
||||
public class EditTextIntUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "EditTextIntUtils";
|
||||
// 默认返回值:读取失败时返回
|
||||
private static final int DEFAULT_INT_VALUE = 0;
|
||||
|
||||
// ====================== 工具方法区 ======================
|
||||
/**
|
||||
* 从 EditText 中安全读取整数
|
||||
* @param editText 目标输入框
|
||||
* @return 输入框中的整数,读取失败返回 0
|
||||
*/
|
||||
public static int getIntFromEditText(EditText editText) {
|
||||
// 空值校验:防止 EditText 为 null 导致空指针
|
||||
if (editText == null) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: EditText 实例为 null,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 获取并去除首尾空格
|
||||
String inputStr = editText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "getIntFromEditText: 输入框原始内容 | " + inputStr);
|
||||
|
||||
// 校验空字符串
|
||||
if (inputStr.isEmpty()) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: 输入框内容为空,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 安全转换整数,捕获格式异常
|
||||
try {
|
||||
String sz = editText.getText().toString().trim();
|
||||
return Integer.parseInt(sz);
|
||||
int result = Integer.parseInt(inputStr);
|
||||
LogUtils.d(TAG, "getIntFromEditText: 转换成功 | 结果=" + result);
|
||||
return result;
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
return 0;
|
||||
LogUtils.e(TAG, "getIntFromEditText: 内容不是有效整数 | 输入内容=" + inputStr, e);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,64 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 01:16:28
|
||||
* @Describe Int数字操作工具集
|
||||
* @Describe Int数字操作工具集:提供整数范围限制、数值边界校准功能
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class IntUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "IntUtils";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 将整数限制在指定区间内,自动校准超出边界的数值
|
||||
* @param origin 原始整数
|
||||
* @param range_a 区间端点1(无需区分大小)
|
||||
* @param range_b 区间端点2(无需区分大小)
|
||||
* @return 校准后的整数,结果始终在 [min(range_a,range_b), max(range_a,range_b)] 内
|
||||
*/
|
||||
public static int getIntInRange(int origin, int range_a, int range_b) {
|
||||
int min = Math.min(range_a, range_b);
|
||||
int max = Math.max(range_a, range_b);
|
||||
int res = Math.min(origin, max);
|
||||
res = Math.max(res, min);
|
||||
|
||||
// 打印调试日志,记录参数与计算结果
|
||||
LogUtils.d(TAG, String.format("getIntInRange: 原始值=%d, 区间=[%d,%d], 校准后=%d",
|
||||
origin, min, max, res));
|
||||
return res;
|
||||
}
|
||||
|
||||
// ====================== 单元测试方法区 ======================
|
||||
/**
|
||||
* 单元测试:验证 getIntInRange 方法在不同场景下的正确性
|
||||
*/
|
||||
public static void unittest_getIntInRange() {
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, 5, 10); %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(8, 5, 10); %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(200, 5, 10); %d", getIntInRange(200, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, -5, 10); %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(9, -5, 10); %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(100, -5, 10); %d", getIntInRange(100, -5, 10)));
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 开始执行单元测试");
|
||||
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 5, -10); %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(4, 5, -10); %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-20, 5, -10); %d", getIntInRange(-20, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 50, 10); %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(30, 50, 10); %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(6, 50, 10); %d", getIntInRange(6, 50, 10)));
|
||||
// 正数区间测试
|
||||
LogUtils.d(TAG, String.format("测试1: getIntInRange(-100, 5, 10) = %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试2: getIntInRange(8, 5, 10) = %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试3: getIntInRange(200, 5, 10) = %d", getIntInRange(200, 5, 10)));
|
||||
|
||||
// 跨正负区间测试
|
||||
LogUtils.d(TAG, String.format("测试4: getIntInRange(-100, -5, 10) = %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试5: getIntInRange(9, -5, 10) = %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试6: getIntInRange(100, -5, 10) = %d", getIntInRange(100, -5, 10)));
|
||||
|
||||
// 端点顺序颠倒测试
|
||||
LogUtils.d(TAG, String.format("测试7: getIntInRange(500, 5, -10) = %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试8: getIntInRange(4, 5, -10) = %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试9: getIntInRange(-20, 5, -10) = %d", getIntInRange(-20, 5, -10)));
|
||||
|
||||
// 大数区间测试
|
||||
LogUtils.d(TAG, String.format("测试10: getIntInRange(500, 50, 10) = %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试11: getIntInRange(30, 50, 10) = %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试12: getIntInRange(6, 50, 10) = %d", getIntInRange(6, 50, 10)));
|
||||
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 单元测试执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.telecom.TelecomManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/12 16:28
|
||||
* @Describe 敏感权限申请工具类(完全适配 Android API 30 + Java 7 语法)
|
||||
* 修复 ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP / EXTRA_PACKAGE_NAME 未定义问题
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static final String TAG = "PermissionUtils";
|
||||
|
||||
// API 版本硬编码常量(Java 7 兼容,不依赖 Build.VERSION_CODES 高版本字段)
|
||||
private static final int ANDROID_6_API = 23;
|
||||
private static final int ANDROID_10_API = 29;
|
||||
private static final int ANDROID_13_API = 33;
|
||||
private static final int ANDROID_14_API = 34;
|
||||
|
||||
// 硬编码系统常量字符串,解决 API 30 下未定义问题
|
||||
private static final String ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP =
|
||||
"android.telecom.action.CHANGE_DEFAULT_CALL_SCREENING_APP";
|
||||
private static final String EXTRA_PACKAGE_NAME =
|
||||
"android.telecom.extra.PACKAGE_NAME";
|
||||
|
||||
// 基础权限组(严格适配 API 30,移除废弃/不存在的权限)
|
||||
public static final String[] BASE_PERMISSIONS = {
|
||||
android.Manifest.permission.READ_CONTACTS,
|
||||
android.Manifest.permission.WRITE_CONTACTS,
|
||||
android.Manifest.permission.READ_CALL_LOG,
|
||||
android.Manifest.permission.CALL_PHONE,
|
||||
android.Manifest.permission.RECORD_AUDIO,
|
||||
android.Manifest.permission.MODIFY_AUDIO_SETTINGS
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有需要申请的权限(Java 7 传统 for 循环,无菱形运算符)
|
||||
*/
|
||||
public static String[] getAllNeedPermissions() {
|
||||
List<String> permissions = new ArrayList<String>();
|
||||
// Java 7 传统循环遍历数组
|
||||
for (int i = 0; i < BASE_PERMISSIONS.length; i++) {
|
||||
permissions.add(BASE_PERMISSIONS[i]);
|
||||
}
|
||||
// 显式创建数组并转换,避免 Java 7 泛型转换警告
|
||||
String[] permissionArray = new String[permissions.size()];
|
||||
return permissions.toArray(permissionArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个权限是否授予(使用 PackageManager 标准常量)
|
||||
*/
|
||||
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
|
||||
return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限组是否全部授予(Java 7 传统循环)
|
||||
*/
|
||||
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
// Java 7 遍历数组,避免增强 for 循环的语法糖问题
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
if (!checkPermission(context, permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请权限组(Activity 中调用,Java 7 兼容)
|
||||
*/
|
||||
public static void requestPermissions(@NonNull FragmentActivity activity,
|
||||
@NonNull String[] permissions,
|
||||
int requestCode) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请权限组(Fragment 中调用,Java 7 兼容)
|
||||
*/
|
||||
public static void requestPermissions(@NonNull Fragment fragment,
|
||||
@NonNull String[] permissions,
|
||||
int requestCode) {
|
||||
fragment.requestPermissions(permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查悬浮窗权限(API 30 适配)
|
||||
*/
|
||||
public static boolean isOverlayPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
}
|
||||
// 6.0 以下默认授予
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请悬浮窗权限(Java 7 规范,拆分 Intent 创建步骤)
|
||||
*/
|
||||
public static void requestOverlayPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !isOverlayPermissionGranted(context)) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
if (context instanceof FragmentActivity) {
|
||||
((FragmentActivity) context).startActivityForResult(intent, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查修改系统设置权限(API 30 适配)
|
||||
*/
|
||||
public static boolean isWriteSettingsPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||
return Settings.System.canWrite(context);
|
||||
}
|
||||
// 6.0 以下默认授予
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请修改系统设置权限(Java 7 规范)
|
||||
*/
|
||||
public static void requestWriteSettingsPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !isWriteSettingsPermissionGranted(context)) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
if (context instanceof FragmentActivity) {
|
||||
((FragmentActivity) context).startActivityForResult(intent, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查通话筛选权限(适配 API 30,优化反射逻辑 + 异常捕获)
|
||||
*/
|
||||
public static boolean isCallScreeningPermissionGranted(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||
TelecomManager telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
|
||||
if (telecomManager == null) {
|
||||
return false;
|
||||
}
|
||||
String defaultPackage = null;
|
||||
// 反射调用高版本方法,捕获所有异常避免崩溃(Java 7 必须显式捕获 Exception)
|
||||
try {
|
||||
Method method = TelecomManager.class.getMethod("getDefaultCallScreeningAppPackage");
|
||||
defaultPackage = (String) method.invoke(telecomManager);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// API 30-32 无此方法,返回 false
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
// 其他反射异常,返回 false
|
||||
return false;
|
||||
}
|
||||
return defaultPackage != null && defaultPackage.equals(context.getPackageName());
|
||||
}
|
||||
// 10.0 以下无此权限,默认返回 true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请通话筛选权限(完全适配 API 30,解决 ActivityNotFoundException 崩溃)
|
||||
*/
|
||||
public static void requestCallScreeningPermission(@NonNull Context context, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API && !isCallScreeningPermissionGranted(context)) {
|
||||
FragmentActivity activity = null;
|
||||
if (context instanceof FragmentActivity) {
|
||||
activity = (FragmentActivity) context;
|
||||
}
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = null;
|
||||
// 版本分级处理:避免高版本 ACTION 失效
|
||||
if (Build.VERSION.SDK_INT >= ANDROID_14_API) {
|
||||
// Android 14+:跳转默认应用设置页
|
||||
intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
intent.setData(uri);
|
||||
} else if (Build.VERSION.SDK_INT >= ANDROID_13_API) {
|
||||
// Android 13:使用硬编码 ACTION
|
||||
intent = new Intent(ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP);
|
||||
intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
} else {
|
||||
// API 30-32:直接跳转应用详情页
|
||||
goAppDetailsSettings(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 捕获 Activity 找不到异常,兜底处理(Java 7 必须显式捕获)
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (android.content.ActivityNotFoundException e) {
|
||||
// 兜底:跳转应用详情页
|
||||
goAppDetailsSettings(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转应用详情页(权限兜底引导,Java 7 规范)
|
||||
*/
|
||||
public static void goAppDetailsSettings(@NonNull Context context) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析被拒绝的权限(Java 7 字符串操作,无 Lambda)
|
||||
*/
|
||||
public static String getDeniedPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
StringBuilder deniedPerms = new StringBuilder();
|
||||
// Java 7 传统循环遍历权限数组
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
if (!checkPermission(context, permission)) {
|
||||
// 截取权限名称,优化展示
|
||||
int lastDotIndex = permission.lastIndexOf(".");
|
||||
if (lastDotIndex != -1 && lastDotIndex < permission.length() - 1) {
|
||||
String permName = permission.substring(lastDotIndex + 1);
|
||||
deniedPerms.append(permName).append("、");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 移除最后一个分隔符(Java 7 字符串操作)
|
||||
if (deniedPerms.length() > 0) {
|
||||
deniedPerms.deleteCharAt(deniedPerms.length() - 1);
|
||||
}
|
||||
return deniedPerms.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,58 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe PhoneUtils
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe 拨打电话工具类:封装拨打电话逻辑与权限校验
|
||||
*/
|
||||
public class PhoneUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneUtils";
|
||||
|
||||
// 拨打电话 Action 与 Uri 前缀
|
||||
private static final String CALL_ACTION = Intent.ACTION_CALL;
|
||||
private static final String TEL_URI_PREFIX = "tel:";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 直接拨打电话(需申请 CALL_PHONE 权限)
|
||||
* @param context 上下文对象
|
||||
* @param phoneNumber 目标电话号码
|
||||
*/
|
||||
public static void call(Context context, String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
// 空值校验:防止上下文或号码为空导致异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "call: Context 为 null,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
context.startActivity(intent);
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "call: 电话号码为空,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
String targetPhone = phoneNumber.trim();
|
||||
LogUtils.d(TAG, "call: 准备拨打号码 | " + targetPhone);
|
||||
|
||||
// 权限校验:检查是否持有拨打电话权限
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "call: 缺少 CALL_PHONE 权限,无法直接拨打电话");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建拨打电话 Intent 并启动
|
||||
Intent callIntent = new Intent(CALL_ACTION);
|
||||
callIntent.setData(Uri.parse(TEL_URI_PREFIX + targetPhone));
|
||||
// 添加 FLAG 支持非 Activity 上下文启动
|
||||
callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
context.startActivity(callIntent);
|
||||
LogUtils.i(TAG, "call: 拨打电话 Intent 已发送 | 号码=" + targetPhone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe 正则前置校验工具类(RegexPPi):检验文本是否满足基础正则匹配要求
|
||||
*/
|
||||
public class RegexPPiUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "RegexPPiUtils";
|
||||
// 基础匹配正则:匹配任意文本(包括空字符串)
|
||||
private static final String BASE_REGEX = ".*";
|
||||
// 预编译正则 Pattern,提升重复调用效率
|
||||
private static final Pattern BASE_PATTERN = Pattern.compile(BASE_REGEX);
|
||||
|
||||
//
|
||||
// 检验文本是否满足适合正则表达式模式计算
|
||||
//
|
||||
// ====================== 核心校验方法区 ======================
|
||||
/**
|
||||
* 检验文本是否满足基础正则表达式模式(.*)匹配要求
|
||||
* @param text 待校验的文本内容
|
||||
* @return 匹配结果,文本为null时返回false
|
||||
*/
|
||||
public static boolean isPPiOK(String text) {
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = ".*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
// 空值校验,避免空指针异常
|
||||
if (text == null) {
|
||||
LogUtils.w(TAG, "isPPiOK: 待校验文本为 null,返回 false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行正则匹配
|
||||
Matcher matcher = BASE_PATTERN.matcher(text);
|
||||
boolean isMatch = matcher.matches();
|
||||
|
||||
// 打印调试日志,记录校验结果
|
||||
LogUtils.d(TAG, String.format("isPPiOK: 文本=[%s],匹配结果=%b", text, isMatch));
|
||||
return isMatch;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,117 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 21:11:03
|
||||
* @Describe 云盾防御信息
|
||||
* @Describe 云盾防御信息视图控件:展示云盾防御值统计,并支持消息驱动更新
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.beans.SettingsModel;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class DuInfoTextView extends TextView {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DuInfoTextView";
|
||||
|
||||
public static final int MSG_NOTIFY_INFO_UPDATE = 0;
|
||||
|
||||
Context mContext;
|
||||
|
||||
public DuInfoTextView(android.content.Context context) {
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Handler mHandler;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DuInfoTextView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
void initView(android.content.Context context) {
|
||||
mContext = context;
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView: 开始初始化云盾信息控件");
|
||||
this.mContext = context;
|
||||
initHandler();
|
||||
updateInfo();
|
||||
LogUtils.d(TAG, "initView: 云盾信息控件初始化完成");
|
||||
}
|
||||
|
||||
void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo()");
|
||||
SettingsModel settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", settingsModel.getDunCurrentCount(), settingsModel.getDunTotalCount());
|
||||
setText(info);
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if(msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
updateInfo();
|
||||
|
||||
/**
|
||||
* 初始化 Handler,处理信息更新消息
|
||||
*/
|
||||
private void initHandler() {
|
||||
mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到信息更新消息,开始刷新视图");
|
||||
updateInfo();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 视图更新方法区 ======================
|
||||
/**
|
||||
* 更新云盾防御信息显示
|
||||
*/
|
||||
private void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo: 开始更新云盾防御信息");
|
||||
// 空值校验,避免上下文为空导致异常
|
||||
if (mContext == null) {
|
||||
LogUtils.w(TAG, "updateInfo: 上下文为空,跳过信息更新");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
SettingsBean settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
// 校验 SettingsBean 非空,防止空指针
|
||||
if (settingsModel == null) {
|
||||
LogUtils.w(TAG, "updateInfo: SettingsBean 为空,显示默认值");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentCount = settingsModel.getDunCurrentCount();
|
||||
int totalCount = settingsModel.getDunTotalCount();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", currentCount, totalCount);
|
||||
setText(info);
|
||||
LogUtils.d(TAG, "updateInfo: 云盾防御信息更新完成 | " + info);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "updateInfo: 信息更新异常", e);
|
||||
setText("(云盾防御值【--/--】)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供的信息更新通知方法
|
||||
*/
|
||||
public void notifyInfoUpdate() {
|
||||
LogUtils.d(TAG, "notifyInfoUpdate()");
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
LogUtils.d(TAG, "notifyInfoUpdate: 发送信息更新通知");
|
||||
if (mHandler != null) {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyInfoUpdate: Handler 未初始化,无法发送更新消息");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/19 14:04:20
|
||||
* @Describe 云盾华氏度热力视图,垂直盾值温度视图控件(带颜色渐变+静态Handler更新)
|
||||
* 采用绘图方式展示盾值温度,填充色随盾值比例渐变,支持设置文本在温度条左侧/右侧,底部对齐竖排显示
|
||||
* 温度条宽度=5dp,文本区宽度固定=5dp,整体左右边距=0,无任何多余空白间距
|
||||
*/
|
||||
public class DunTemperatureView extends View {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DunTemperatureView";
|
||||
// 控件默认高度
|
||||
private static final int DEFAULT_HEIGHT = 200;
|
||||
// 温度条宽度(5dp)、文本区宽度(固定5dp)
|
||||
private static final int THERMOMETER_WIDTH_DP = 5;
|
||||
private static final int TEXT_AREA_WIDTH_DP = 5;
|
||||
// 填充区域内边距(左右=0,上下=2避免贴边)
|
||||
private static final int FILL_PADDING_HORIZONTAL = 0;
|
||||
private static final int FILL_PADDING_VERTICAL = 2;
|
||||
// 竖排文本字间距
|
||||
private static final float TEXT_CHAR_SPACING = 8f;
|
||||
// Handler消息标识
|
||||
public static final int MSG_UPDATE_DUN_VALUE = 0x01;
|
||||
// 消息参数Key
|
||||
public static final String KEY_MAX_VALUE = "max_value";
|
||||
public static final String KEY_CURRENT_VALUE = "current_value";
|
||||
|
||||
// ====================== 静态成员区 ======================
|
||||
// 弱引用缓存控件实例,避免内存泄漏
|
||||
private static WeakHashMap<DunTemperatureView, Object> sViewCache = new WeakHashMap<>();
|
||||
// 静态Handler,处理跨线程更新消息
|
||||
private static Handler sStaticHandler;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 画笔相关
|
||||
private Paint mThermometerPaint;
|
||||
private Paint mFillPaint;
|
||||
private Paint mTextPaint;
|
||||
// 尺寸参数(dp转px后的值)
|
||||
private int mThermometerWidth;
|
||||
private int mTextAreaWidth;
|
||||
private int mMaxValue = 100; // 最高盾值
|
||||
private int mCurrentValue = 0; // 当前盾值
|
||||
private RectF mThermometerRect; // 温度条矩形区域
|
||||
// 渐变颜色配置(低→中→高 对应绿→黄→红)
|
||||
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
|
||||
private float[] mGradientPositions = {0.0f, 0.5f, 1.0f};
|
||||
// 布局配置:true=文本在温度条右侧(默认),false=文本在温度条左侧
|
||||
private boolean isTextOnRight = true;
|
||||
// 其他颜色配置
|
||||
private int mBorderColor = Color.parseColor("#FF444444");
|
||||
private int mTextColor = Color.parseColor("#FF000000");
|
||||
|
||||
// ====================== 静态代码块 ======================
|
||||
static {
|
||||
// 初始化静态Handler,绑定主线程Looper
|
||||
sStaticHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_UPDATE_DUN_VALUE) {
|
||||
// 获取消息中的盾值参数
|
||||
int maxValue = msg.getData().getInt(KEY_MAX_VALUE, 100);
|
||||
int currentValue = msg.getData().getInt(KEY_CURRENT_VALUE, 0);
|
||||
LogUtils.d(TAG, "sStaticHandler: 收到更新消息,max=" + maxValue + ", current=" + currentValue);
|
||||
|
||||
// 遍历缓存的控件实例,更新所有实例
|
||||
for (DunTemperatureView view : sViewCache.keySet()) {
|
||||
if (view != null && view.isShown()) {
|
||||
view.setMaxValue(maxValue);
|
||||
view.setCurrentValue(currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DunTemperatureView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public DunTemperatureView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public DunTemperatureView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
// ====================== 初始化方法区 ======================
|
||||
/**
|
||||
* 初始化画笔和参数
|
||||
*/
|
||||
private void init() {
|
||||
LogUtils.d(TAG, "init: 开始初始化云盾温度视图控件");
|
||||
// dp转px(适配不同分辨率)
|
||||
mThermometerWidth = dp2px(getContext(), THERMOMETER_WIDTH_DP);
|
||||
mTextAreaWidth = dp2px(getContext(), TEXT_AREA_WIDTH_DP);
|
||||
LogUtils.d(TAG, "init: 温度条宽度5dp转px=" + mThermometerWidth + ",文本区宽度5dp转px=" + mTextAreaWidth);
|
||||
|
||||
// 温度条边框画笔(宽度1px,适配5dp宽度)
|
||||
mThermometerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mThermometerPaint.setColor(mBorderColor);
|
||||
mThermometerPaint.setStyle(Paint.Style.STROKE);
|
||||
mThermometerPaint.setStrokeWidth(1);
|
||||
|
||||
// 温度条填充画笔(支持渐变)
|
||||
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mFillPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
// 文本画笔(适配5dp窄文本区,文字居中绘制)
|
||||
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mTextPaint.setColor(mTextColor);
|
||||
mTextPaint.setTextSize(18); // 缩小字号适配5dp窄文本区(避免文字超出)
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setFakeBoldText(true); // 文字加粗,提升窄区域可读性
|
||||
|
||||
// 初始化温度条矩形
|
||||
mThermometerRect = new RectF();
|
||||
|
||||
// 将当前实例加入静态缓存
|
||||
sViewCache.put(this, null);
|
||||
LogUtils.d(TAG, "init: 云盾温度视图控件初始化完成,实例已加入缓存");
|
||||
}
|
||||
|
||||
// ====================== 工具方法区 ======================
|
||||
/**
|
||||
* dp 转 px(适配不同屏幕分辨率)
|
||||
*/
|
||||
private int dp2px(Context context, float dpValue) {
|
||||
return (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dpValue,
|
||||
context.getResources().getDisplayMetrics()
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== 对外控制方法区 ======================
|
||||
/**
|
||||
* 设置文本相对于温度条的位置
|
||||
* @param isOnRight true=文本在温度条右侧(默认),false=文本在温度条左侧
|
||||
*/
|
||||
public void setTextPosition(boolean isOnRight) {
|
||||
this.isTextOnRight = isOnRight;
|
||||
invalidate(); // 刷新布局绘制
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文本位置配置
|
||||
* @return true=右侧,false=左侧
|
||||
*/
|
||||
public boolean isTextOnRight() {
|
||||
return isTextOnRight;
|
||||
}
|
||||
|
||||
// ====================== 对外静态方法区 ======================
|
||||
/**
|
||||
* 静态外部方法:发送消息更新所有 DunTemperatureView 实例的盾值
|
||||
* 可在子线程中调用
|
||||
* @param maxValue 最高盾值
|
||||
* @param currentValue 当前盾值
|
||||
*/
|
||||
public static void updateDunValue(int maxValue, int currentValue) {
|
||||
if (sStaticHandler == null) {
|
||||
LogUtils.w(TAG, "updateDunValue: 静态Handler未初始化");
|
||||
return;
|
||||
}
|
||||
// 封装参数到消息
|
||||
Message msg = sStaticHandler.obtainMessage(MSG_UPDATE_DUN_VALUE);
|
||||
msg.getData().putInt(KEY_MAX_VALUE, maxValue);
|
||||
msg.getData().putInt(KEY_CURRENT_VALUE, currentValue);
|
||||
// 发送消息
|
||||
sStaticHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
// ====================== 对外实例方法区 ======================
|
||||
/**
|
||||
* 设置最高盾值
|
||||
* @param maxValue 最高盾值(需大于0)
|
||||
*/
|
||||
public void setMaxValue(int maxValue) {
|
||||
if (maxValue <= 0) {
|
||||
LogUtils.w(TAG, "setMaxValue: 最高盾值必须大于0,当前值=" + maxValue);
|
||||
return;
|
||||
}
|
||||
this.mMaxValue = maxValue;
|
||||
// 限制当前值不超过最大值
|
||||
mCurrentValue = Math.min(mCurrentValue, maxValue);
|
||||
invalidate(); // 重绘控件
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前盾值
|
||||
* @param currentValue 当前盾值(范围 0~maxValue)
|
||||
*/
|
||||
public void setCurrentValue(int currentValue) {
|
||||
int oldValue = this.mCurrentValue;
|
||||
this.mCurrentValue = Math.max(0, Math.min(currentValue, mMaxValue));
|
||||
if (oldValue != this.mCurrentValue) {
|
||||
LogUtils.d(TAG, "setCurrentValue: 当前盾值从" + oldValue + "更新为" + mCurrentValue);
|
||||
invalidate(); // 重绘控件
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前盾值
|
||||
*/
|
||||
public int getCurrentValue() {
|
||||
return mCurrentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最高盾值
|
||||
*/
|
||||
public int getMaxValue() {
|
||||
return mMaxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义渐变颜色
|
||||
* @param colors 渐变颜色数组(至少2种颜色)
|
||||
* @param positions 颜色位置数组(与colors长度一致,0.0~1.0)
|
||||
*/
|
||||
public void setGradientColors(int[] colors, float[] positions) {
|
||||
if (colors == null || colors.length < 2 || positions == null || positions.length != colors.length) {
|
||||
LogUtils.w(TAG, "setGradientColors: 渐变颜色参数不合法,颜色数组长度=" + (colors == null ? "null" : colors.length));
|
||||
return;
|
||||
}
|
||||
this.mGradientColors = colors;
|
||||
this.mGradientPositions = positions;
|
||||
LogUtils.d(TAG, "setGradientColors: 自定义渐变颜色已设置");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置温度条边框颜色
|
||||
*/
|
||||
public void setBorderColor(int color) {
|
||||
this.mBorderColor = color;
|
||||
mThermometerPaint.setColor(color);
|
||||
LogUtils.d(TAG, "setBorderColor: 边框颜色已更新");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文本颜色
|
||||
*/
|
||||
public void setTextColor(int color) {
|
||||
this.mTextColor = color;
|
||||
mTextPaint.setColor(color);
|
||||
LogUtils.d(TAG, "setTextColor: 文本颜色已更新");
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
// 控件从窗口移除时,从缓存中清除,避免内存泄漏
|
||||
sViewCache.remove(this);
|
||||
LogUtils.d(TAG, "onDetachedFromWindow: 控件实例已从缓存移除");
|
||||
}
|
||||
|
||||
// ====================== 测量与绘制区 ======================
|
||||
/**
|
||||
* 测量辅助函数
|
||||
*/
|
||||
private int measureSize(int defaultSize, int measureSpec) {
|
||||
int result = defaultSize;
|
||||
int specMode = MeasureSpec.getMode(measureSpec);
|
||||
int specSize = MeasureSpec.getSize(measureSpec);
|
||||
if (specMode == MeasureSpec.EXACTLY) {
|
||||
result = specSize;
|
||||
} else if (specMode == MeasureSpec.AT_MOST) {
|
||||
result = Math.min(defaultSize, specSize);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
// 强制控件整体左右内边距=0,彻底消除外部边距
|
||||
setPadding(0, getPaddingTop(), 0, getPaddingBottom());
|
||||
|
||||
// 控件宽度=温度条宽度 + 文本区宽度(均为5dp转px,无额外空白)
|
||||
int defaultWidth = mThermometerWidth + mTextAreaWidth;
|
||||
int width = measureSize(defaultWidth, widthMeasureSpec);
|
||||
int height = measureSize(DEFAULT_HEIGHT, heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
|
||||
// 根据文本位置配置,计算温度条矩形坐标
|
||||
float thermometerLeft, thermometerRight;
|
||||
if (isTextOnRight) {
|
||||
// 文本在右侧:温度条靠左,右接文本区
|
||||
thermometerLeft = 0;
|
||||
thermometerRight = thermometerLeft + mThermometerWidth;
|
||||
} else {
|
||||
// 文本在左侧:温度条靠右,左接文本区
|
||||
thermometerLeft = width - mThermometerWidth;
|
||||
thermometerRight = width;
|
||||
}
|
||||
float thermometerTop = getPaddingTop();
|
||||
float thermometerBottom = height - getPaddingBottom();
|
||||
mThermometerRect.set(thermometerLeft, thermometerTop, thermometerRight, thermometerBottom);
|
||||
|
||||
LogUtils.v(TAG, "onMeasure: 文本位置=" + (isTextOnRight ? "右侧" : "左侧") + ",控件尺寸=" + width + "x" + height + ",温度条区域=" + mThermometerRect.toShortString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
// 1. 绘制温度条边框(5dp宽度,小圆角适配)
|
||||
canvas.drawRoundRect(mThermometerRect, 3, 3, mThermometerPaint);
|
||||
|
||||
// 2. 计算填充高度(根据当前值占最大值的比例)
|
||||
float fillRatio = (float) mCurrentValue / mMaxValue;
|
||||
float fillHeight = mThermometerRect.height() * fillRatio;
|
||||
float fillTop = mThermometerRect.bottom - fillHeight;
|
||||
|
||||
// 3. 绘制渐变填充部分(左右贴边框,无间距)
|
||||
if (fillHeight > 0) {
|
||||
RectF fillRect = new RectF(
|
||||
mThermometerRect.left + FILL_PADDING_HORIZONTAL,
|
||||
fillTop + FILL_PADDING_VERTICAL,
|
||||
mThermometerRect.right - FILL_PADDING_HORIZONTAL,
|
||||
mThermometerRect.bottom - FILL_PADDING_VERTICAL
|
||||
);
|
||||
LinearGradient gradient = new LinearGradient(
|
||||
fillRect.centerX(), fillRect.bottom,
|
||||
fillRect.centerX(), fillRect.top,
|
||||
mGradientColors,
|
||||
mGradientPositions,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
mFillPaint.setShader(gradient);
|
||||
canvas.drawRoundRect(fillRect, 2, 2, mFillPaint);
|
||||
mFillPaint.setShader(null);
|
||||
}
|
||||
|
||||
// 4. 绘制文本(5dp固定宽度文本区,底部对齐竖排,文字居中)
|
||||
String text = String.format("%d/%d", mCurrentValue, mMaxValue);
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
float textBaseX;
|
||||
if (isTextOnRight) {
|
||||
// 文本在右侧:X=温度条右边缘 + 文本区宽度的一半(紧贴温度条)
|
||||
textBaseX = mThermometerRect.right + (mTextAreaWidth / 2f);
|
||||
} else {
|
||||
// 文本在左侧:X=文本区宽度的一半(紧贴控件左边缘)
|
||||
textBaseX = mTextAreaWidth / 2f;
|
||||
}
|
||||
|
||||
// 文本绘制参数(适配5dp窄区域,缩小字间距提升紧凑度)
|
||||
float singleCharHeight = mTextPaint.getTextSize() + (TEXT_CHAR_SPACING - 2f); // 字间距减为6f
|
||||
float totalTextHeight = (singleCharHeight * text.length()) - (TEXT_CHAR_SPACING - 2f);
|
||||
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
|
||||
float charBottomOffset = fontMetrics.bottom;
|
||||
|
||||
// 文本起始Y(底部对齐控件底部,无间距)
|
||||
float startTextY = getHeight() - getPaddingBottom() - charBottomOffset;
|
||||
|
||||
// 逐字竖排绘制(文字居中于5dp文本区,无超出)
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char singleChar = text.charAt(i);
|
||||
float currentTextY = startTextY - (i * singleCharHeight);
|
||||
canvas.drawText(String.valueOf(singleChar), textBaseX, currentTextY, mTextPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe CustomHorizontalScrollView
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
@@ -16,10 +11,17 @@ import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe 左滑显示操作按钮的自定义滚动视图,支持编辑、删除、上移、下移功能
|
||||
*/
|
||||
public class LeftScrollView extends HorizontalScrollView {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "LeftScrollView";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 布局控件
|
||||
private LinearLayout contentLayout;
|
||||
private LinearLayout toolLayout;
|
||||
private TextView textView;
|
||||
@@ -27,11 +29,15 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
private Button deleteButton;
|
||||
private Button upButton;
|
||||
private Button downButton;
|
||||
// 滑动事件相关
|
||||
private float mStartX;
|
||||
private float mEndX;
|
||||
private boolean isScrolling = false;
|
||||
private int nScrollAcceptSize;
|
||||
// 回调接口
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public LeftScrollView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
@@ -47,174 +53,254 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
init();
|
||||
}
|
||||
|
||||
public void addContentLayout(View viewContent) {
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void init() {
|
||||
LogUtils.d(TAG, "init: 开始初始化左滑滚动视图");
|
||||
// 加载布局
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
if (viewMain == null) {
|
||||
LogUtils.e(TAG, "init: 布局加载失败,无法初始化控件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定布局控件
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 校验控件是否绑定成功
|
||||
if (contentLayout == null || toolLayout == null) {
|
||||
LogUtils.e(TAG, "init: 核心布局控件绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加主布局到当前视图
|
||||
addView(viewMain);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListener();
|
||||
|
||||
LogUtils.d(TAG, "init: 左滑滚动视图初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作按钮的点击事件
|
||||
*/
|
||||
private void setButtonClickListener() {
|
||||
// 编辑按钮
|
||||
if (editButton != null) {
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击编辑按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
if (deleteButton != null) {
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击删除按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 上移按钮
|
||||
if (upButton != null) {
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击上移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 下移按钮
|
||||
if (downButton != null) {
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击下移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供的方法区 ======================
|
||||
/**
|
||||
* 添加内容视图到容器
|
||||
* @param viewContent 待添加的内容视图
|
||||
*/
|
||||
public void addContentLayout(View viewContent) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 内容布局未初始化,无法添加视图");
|
||||
return;
|
||||
}
|
||||
if (viewContent == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 待添加视图为null");
|
||||
return;
|
||||
}
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
LogUtils.d(TAG, "addContentLayout: 内容视图添加成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容布局的宽度
|
||||
* @param contentWidth 目标宽度
|
||||
*/
|
||||
public void setContentWidth(int contentWidth) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "setContentWidth: 内容布局未初始化,无法设置宽度");
|
||||
return;
|
||||
}
|
||||
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) contentLayout.getLayoutParams();
|
||||
layoutParams.width = contentWidth;
|
||||
contentLayout.setLayoutParams(layoutParams);
|
||||
|
||||
LogUtils.d(TAG, "setContentWidth: 内容布局宽度设置为 " + contentWidth);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
|
||||
// 创建内容布局
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
|
||||
//LogUtils.d(TAG, String.format("getWidth() %d", getWidth()));
|
||||
|
||||
addView(viewMain);
|
||||
|
||||
// 创建编辑按钮
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
// 创建删除按钮
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
// 向上按钮
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
// 向下按钮
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 编辑按钮点击事件
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 编辑按钮点击事件
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 设置文本内容(原代码未初始化textView,添加空校验)
|
||||
* @param text 待显示的文本
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
if (textView == null) {
|
||||
LogUtils.w(TAG, "setText: 文本控件未初始化,无法设置文本");
|
||||
return;
|
||||
}
|
||||
textView.setText(text);
|
||||
LogUtils.d(TAG, "setText: 文本设置为 " + text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件回调监听器
|
||||
* @param listener 回调接口实例
|
||||
*/
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
LogUtils.d(TAG, "setOnActionListener: 事件监听器已设置");
|
||||
}
|
||||
|
||||
// ====================== 滑动事件处理区 ======================
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (event == null) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
LogUtils.d(TAG, "ACTION_DOWN");
|
||||
mStartX = event.getX();
|
||||
// isScrolling = false;
|
||||
LogUtils.d(TAG, "onTouchEvent: ACTION_DOWN,起始X坐标 = " + mStartX);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
//LogUtils.d(TAG, "ACTION_MOVE");
|
||||
// float currentX = event.getX();
|
||||
// float deltaX = mStartX - currentX;
|
||||
// //mLastX = currentX;
|
||||
// if (Math.abs(deltaX) > 0) {
|
||||
// isScrolling = true;
|
||||
// }
|
||||
// 可根据需求添加滑动中逻辑
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (getScrollX() > 0) {
|
||||
LogUtils.d(TAG, "ACTION_UP");
|
||||
mEndX = event.getX();
|
||||
LogUtils.d(TAG, String.format("mStartX %f, mEndX %f", mStartX, mEndX));
|
||||
if (mEndX < mStartX) {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > editButton.getWidth()) {
|
||||
if (Math.abs(mStartX - mEndX) > editButton.getWidth()) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > deleteButton.getWidth()) {
|
||||
if (Math.abs(mEndX - mStartX) > deleteButton.getWidth()) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
}
|
||||
mEndX = event.getX();
|
||||
int scrollX = getScrollX();
|
||||
LogUtils.d(TAG, String.format("onTouchEvent: ACTION_UP/CANCEL,起始X=%f 结束X=%f 滚动距离=%d",
|
||||
mStartX, mEndX, scrollX));
|
||||
|
||||
if (scrollX > 0) {
|
||||
handleScrollLogic();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
void smoothScrollToRight() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
// 计算需要滑动到最右边的距离
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
// 确保滑动距离不小于0
|
||||
final int scrollToX2 = Math.max(0, scrollToX);
|
||||
// 平滑滑动到最右边
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(scrollToX2, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "smoothScrollTo(scrollToX, 0);");
|
||||
/**
|
||||
* 处理滑动结束后的逻辑,判断滑动方向并执行滚动
|
||||
*/
|
||||
private void handleScrollLogic() {
|
||||
float deltaX = Math.abs(mStartX - mEndX);
|
||||
// 校验按钮是否存在,避免空指针
|
||||
float threshold = editButton != null ? editButton.getWidth() : 50;
|
||||
|
||||
if (mEndX < mStartX) {
|
||||
// 向左滑,显示操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
// 向右滑,隐藏操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void smoothScrollToLeft() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
// 在手指抬起时,使用 post 方法调用 smoothScrollTo(0, 0)
|
||||
/**
|
||||
* 平滑滚动到右侧(显示操作按钮)
|
||||
*/
|
||||
private void smoothScrollToRight() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void run() {
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
int targetX = Math.max(0, scrollToX);
|
||||
smoothScrollTo(targetX, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToRight: 滚动到右侧,目标X坐标 = " + targetX);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
// 设置文本内容
|
||||
public void setText(CharSequence text) {
|
||||
textView.setText(text);
|
||||
/**
|
||||
* 平滑滚动到左侧(隐藏操作按钮)
|
||||
*/
|
||||
private void smoothScrollToLeft() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToLeft: 滚动到左侧");
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
// 定义回调接口
|
||||
/**
|
||||
* 重置滑动坐标
|
||||
*/
|
||||
private void resetScrollCoordinate() {
|
||||
mStartX = 0;
|
||||
mEndX = 0;
|
||||
}
|
||||
|
||||
// ====================== 回调接口定义区 ======================
|
||||
public interface OnActionListener {
|
||||
void onEdit();
|
||||
void onDelete();
|
||||
void onUp();
|
||||
void onDown();
|
||||
}
|
||||
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/19 14:04:20
|
||||
* @Describe 云盾滑视度热备控件
|
||||
*/
|
||||
public class ScrollDoView {
|
||||
|
||||
public static final String TAG = "ScrollDoView";
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import android.content.Intent;
|
||||
import android.widget.RemoteViews;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.utils.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
public class APPStatusWidget extends AppWidgetProvider {
|
||||
|
||||
|
||||
@@ -12,26 +12,52 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitymainToolbar1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0">
|
||||
android:layout_weight="1.0"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<cc.winboll.studio.contacts.views.DunTemperatureView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/dun_temp_view_left"
|
||||
android:layout_alignParentLeft="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewPager"/>
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainLinearLayout1"
|
||||
android:layout_toRightOf="@id/dun_temp_view_left"
|
||||
android:layout_toLeftOf="@id/dun_temp_view_right">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:id="@+id/tabLayout"/>
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewPager"/>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:id="@+id/tabLayout"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.contacts.views.DunTemperatureView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/dun_temp_view_right"
|
||||
android:layout_alignParentRight="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -195,30 +195,48 @@
|
||||
android:text="拨不通电话记录查询:"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<EditText
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/bobulltoonurl_et"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="重置地址"
|
||||
android:onClick="onResetBoBullToonURL"/>
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="下载数据"
|
||||
android:onClick="onDownloadBoBullToon"/>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="下载数据"
|
||||
android:onClick="onDownloadBoBullToon"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="清空 BoBullToon 数据"
|
||||
android:onClick="onCleanBoBullToonData"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="重置地址"
|
||||
android:onClick="onResetBoBullToonURL"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -318,6 +336,11 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="其他:"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsControlView
|
||||
android:id="@+id/ads_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Contacts</string>
|
||||
<string name="default_bobulltoon_url">https://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip</string>
|
||||
<string name="default_bobulltoon_url">https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="MyAppTheme" parent="AESTheme">
|
||||
<!-- 方案1:无 ActionBar 主题(推荐,适合自定义标题栏) -->
|
||||
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
|
||||
</style>
|
||||
|
||||
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorTittle">@color/colorAccent</item>
|
||||
<item name="colorTittleBackgound">@color/colorPrimary</item>
|
||||
<item name="colorText">@color/colorAccent</item>
|
||||
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
|
||||
|
||||
</style>
|
||||
|
||||
<!-- 方案2:带 ActionBar 主题(如需系统默认标题栏,启用此方案) -->
|
||||
<!--
|
||||
<style name="MyAppTheme" parent="Theme.MaterialComponents.Light">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
|
||||
<item name="android:textSizeHeadline">18sp</item>
|
||||
<item name="android:textSizeBody">16sp</item>
|
||||
<item name="android:textSizeSubtitle">14sp</item>
|
||||
<item name="android:textSizeCaption">12sp</item>
|
||||
</style>
|
||||
|
||||
<style name="GlobalCrashActivityTheme" parent="AESTheme">
|
||||
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light">
|
||||
<item name="colorTittle">@color/colorAccent</item>
|
||||
<item name="colorTittleBackgound">@color/colorPrimary</item>
|
||||
<item name="colorText">@color/colorAccent</item>
|
||||
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
<item name="android:textSizeHeadline">20sp</item>
|
||||
<item name="android:textSizeBody">14sp</item>
|
||||
<item name="android:textSizeButton">16sp</item>
|
||||
</style>
|
||||
-->
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Dec 07 03:22:51 HKT 2025
|
||||
stageCount=7
|
||||
#Sun Dec 07 04:17:43 GMT 2025
|
||||
stageCount=8
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.7
|
||||
publishVersion=15.11.7
|
||||
buildCount=1
|
||||
baseBetaVersion=15.11.8
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher_stage"
|
||||
android:roundIcon="@drawable/ic_launcher_stage"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:roundIcon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
@@ -37,7 +37,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher_stage"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -280,4 +280,4 @@
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,12V14H5V12H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3,15V17H5V15H3M14,15H20V19H14V15M3,18V20H5V18H3M6,18V20H8V18H6M9,18V20H11V18H9Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3.88,13.46L2.46,14.88L4.59,17L2.46,19.12L3.88,20.54L6,18.41L8.12,20.54L9.54,19.12L7.41,17L9.54,14.88L8.12,13.46L6,15.59L3.88,13.46M14,15H20V19H14V15Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M22,6C22,4.9 21.1,4 20,4H4C2.9,4 2,4.9 2,6V18C2,19.1 2.9,20 4,20H20C21.1,20 22,19.1 22,18V6M20,6L12,11L4,6H20M20,18H4V8L12,13L20,8V18Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M24,7H22V13H24V7M24,15H22V17H24V15M20,6C20,4.9 19.1,4 18,4H2C0.9,4 0,4.9 0,6V18C0,19.1 0.9,20 2,20H18C19.1,20 20,19.1 20,18V6M18,6L10,11L2,6H18M18,18H2V8L10,13L18,8V18Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="1565dp"
|
||||
android:height="1565dp"
|
||||
android:viewportWidth="1565"
|
||||
android:viewportHeight="1565">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M793.8 222.7C782.5 239.2 736.3 298.6 685.6 361.9 647 410.2 633.3 428.9 633.6 432.7 633.7 433.4 633.8 434.6 633.9 435.3 634.1 437.8 647.9 440.7 663.8 441.5 706.7 443.8 717 445.3 724.9 450.6 733.7 456.4 735.9 467.4 736 504.2 736 504.2 736 521.9 736 521.9 736 521.9 730.8 522.4 730.8 522.4 727.9 522.8 691.8 525.3 650.5 528 609.3 530.8 571.9 533.3 567.5 533.6 531.8 536 503.4 540.5 487.4 546 475 550.3 442.2 566.5 428.2 575.2 392.1 597.6 360.1 629.2 338 664.3 317 697.7 304.6 729.6 292.5 781 287.8 801 283.9 805.6 267.2 811 255.1 814.9 248.7 818.3 243.4 823.5 239.7 827.2 239.2 828.4 237.7 836.5 236.7 841.5 235.5 850 235 855.5 234.3 863.9 232.6 911.1 232.6 924.5 232.6 934.6 234.2 959.8 235 963 237.4 972 242.3 975.8 258.7 981.7 271.7 986.3 277.4 989.5 280.4 993.9 283.1 997.9 286.5 1008.7 287.4 1016.5 289.3 1031.7 293.3 1050.2 297.7 1064 306.4 1091.9 317.6 1107.7 330 1109.5 332.4 1109.9 334.7 1110.3 335 1110.5 335.3 1110.6 339.1 1111.1 343.5 1111.4 347.9 1111.8 353.8 1112.3 356.5 1112.5 370 1113.7 407.8 1115.8 430 1116.5 439.6 1116.8 453.4 1117.3 460.5 1117.5 467.7 1117.8 482.3 1118.2 493 1118.5 503.7 1118.8 520.8 1119.2 531 1119.5 587.8 1121.1 683 1122 794.5 1122 918.8 1122 996.1 1121 1075 1118.5 1083.5 1118.2 1098.6 1117.8 1108.5 1117.4 1118.4 1117.1 1129.4 1116.7 1133 1116.5 1136.6 1116.3 1144.7 1115.8 1151 1115.5 1183.3 1114 1207.5 1110.9 1221.5 1106.3 1235.3 1101.9 1244.8 1094.1 1249.9 1083.2 1253.5 1075.4 1253.7 1074.4 1259 1044 1266.4 1001.7 1269.1 993.5 1276.9 989.2 1278.9 988.1 1284.7 986.1 1289.9 984.6 1305.8 980.1 1313.3 975.5 1314.4 969.7 1316.2 959.5 1317.1 881.4 1315.7 859.5 1314 834.8 1313.1 831.4 1307 824.7 1301.2 818.3 1295 814.8 1283 811 1278 809.5 1272.4 807 1270.5 805.6 1264 800.6 1259.5 790.1 1252.5 763 1243.5 728.4 1235.9 706.8 1224.6 683.5 1202.3 637.7 1167.5 602.5 1111.8 569.1 1087.6 554.7 1054.4 542.7 1028.1 539 1024.1 538.4 1020.5 537.8 1020.1 537.6 1019.8 537.4 1016.4 536.9 1012.7 536.5 1009 536.2 1005.1 535.7 1004.2 535.5 999.7 534.7 988.4 533.6 973.5 532.5 969.7 532.2 964.5 531.8 962 531.5 959.5 531.2 940.4 530.1 919.5 529 851.7 525.4 836.1 523.7 829.5 519.4 821.2 514 825.2 495.6 837.6 481.8 843.8 474.9 844.2 474.3 856.8 448 862.6 436 868.9 425.2 879.9 408.7 888.2 396.2 895 385.5 895 385.1 895 384.6 889.7 383.3 883.3 382 851.1 375.9 812.7 367.8 806.9 365.9 792.4 361.2 790.7 358.7 790.2 342.5 789.7 326.8 792.4 303.4 800 258.5 802.1 246.1 803.3 220.9 801.9 219.6 801.7 219.4 800.5 218.9 799.3 218.6 797.4 218 796.5 218.7 793.8 222.7Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M570.4 704.1C553.5 706.1 539.7 713 532.9 722.7 523.5 736.3 519.9 764.2 519.6 826 519.5 859.4 519.7 860.6 530.1 872.9 543.4 888.6 560.4 900 574.1 902.1 586.7 904 601.5 898.9 615.7 887.5 632.5 874.1 633.5 868.9 632.7 793.6 632.2 742.5 631.6 736 627 725.2 620.5 710 596.2 700.9 570.4 704.1Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M949.2 705.5C935.1 708.7 927 716.8 922.6 732.4 918.4 746.7 917.8 757.3 917.3 816.5 917.1 847.8 917.3 874 917.7 874.6 919.2 876.7 945.3 892.3 955.5 897.1 973.4 905.5 983.5 905.4 978.9 896.8 977.1 893.4 979.1 892.6 985.9 893.9 993.6 895.4 997.8 894.3 1005.2 889.1 1010.9 885 1020.2 873.6 1023.8 866.5 1029.6 854.9 1031.1 841.6 1031.1 800.5 1031.1 750.1 1028.6 737.6 1014.9 720.4 1009.4 713.5 1004.6 709.8 996.9 706.8 992.2 704.9 989.1 704.6 973.5 704.4 960.6 704.1 953.7 704.5 949.2 705.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M717 988.7C671.1 990.4 640.9 994.2 635.6 998.8 631.5 1002.5 636.3 1017.5 643.6 1024 650.9 1030.3 661.7 1032.5 694 1034 721.9 1035.3 829.3 1035.3 857 1034 891.4 1032.4 901.4 1029.9 908.9 1021.3 914.5 1015 917.9 1003.4 915.2 999.4 910.4 992.1 856 987.9 772 988.2 745.9 988.3 721.1 988.5 717 988.7Z"/>
|
||||
</vector>
|
||||
@@ -9,5 +9,5 @@
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/ic_iw"/>
|
||||
android:drawable="@drawable/ic_launcher"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp">
|
||||
<item android:drawable="@drawable/ic_launcher_background"/>
|
||||
<item
|
||||
android:left="0dp"
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/ic_launcher_foreground_disable"/>
|
||||
</layer-list>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FF808080"
|
||||
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -9,7 +9,7 @@
|
||||
android:id="@+id/toolbar_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_iw"
|
||||
android:src="@drawable/ic_launcher"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="6dp"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |