From 5614848a65a7eae3350dfd4c53419c468f4cc380 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Fri, 12 Dec 2025 15:58:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=9B=BE=E5=8A=9B=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contacts/build.properties | 4 +- contacts/src/main/AndroidManifest.xml | 16 +- .../winboll/studio/contacts/MainActivity.java | 17 +- .../cc/winboll/studio/contacts/dun/Rules.java | 101 ++++-- .../contacts/services/AssistantService.java | 84 ++++- .../studio/contacts/services/MainService.java | 100 +++++- .../contacts/utils/AppForegroundUtils.java | 52 +++ .../contacts/views/DunTemperatureView.java | 328 ++++++++++++++++++ .../studio/contacts/views/ScrollDoView.java | 14 - .../src/main/res/layout/activity_main.xml | 44 ++- 10 files changed, 679 insertions(+), 81 deletions(-) create mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/utils/AppForegroundUtils.java create mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/views/DunTemperatureView.java delete mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/views/ScrollDoView.java diff --git a/contacts/build.properties b/contacts/build.properties index 7718a05..dfb4dd1 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Dec 12 06:22:08 GMT 2025 +#Fri Dec 12 07:55:42 GMT 2025 stageCount=1 libraryProject= baseVersion=15.12 publishVersion=15.12.0 -buildCount=10 +buildCount=18 baseBetaVersion=15.12.1 diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml index 238961a..8405c3b 100644 --- a/contacts/src/main/AndroidManifest.xml +++ b/contacts/src/main/AndroidManifest.xml @@ -36,6 +36,10 @@ + + + + - + - + @@ -234,6 +237,18 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct // 电话状态监听初始化 initPhoneStateListener(); LogUtils.d(TAG, "initUIAndLogic: 电话状态监听器初始化完成"); + + // 布局中引用控件 + DunTemperatureView tempView = findViewById(R.id.dun_temp_view); + // 设置最高盾值 + tempView.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount()); + // 设置当前盾值 + tempView.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount()); + // 自定义蓝紫渐变 + int[] customColors = {Color.parseColor("#FF3366FF"), Color.parseColor("#FF9900CC")}; + float[] positions = {0.0f, 1.0f}; + tempView.setGradientColors(customColors, positions); + } private void initViewPagerAndTabs() { diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java b/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java index e31885b..3728e4c 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java @@ -1,46 +1,66 @@ package cc.winboll.studio.contacts.dun; -/** - * @Author ZhanGSKen - * @Date 2025/02/21 06:15:10 - * @Describe 云盾防御规则 - */ import android.content.Context; import cc.winboll.studio.contacts.activities.SettingsActivity; +import cc.winboll.studio.contacts.bobulltoon.TomCat; import cc.winboll.studio.contacts.model.PhoneConnectRuleBean; import cc.winboll.studio.contacts.model.SettingsBean; import cc.winboll.studio.contacts.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&豆包大模型 + * @Date 2025/02/21 06:15:10 + * @Describe 云盾防御规则(双重校验锁单例模式) + */ public class Rules { public static final String TAG = "Rules"; + // 单例核心:volatile 保证多线程可见性,禁止指令重排 + private static volatile Rules sInstance; + // 上下文需使用 ApplicationContext 避免内存泄漏 + private static Context sApplicationContext; + ArrayList _PhoneConnectRuleModelList; - static volatile Rules _Rules; Context mContext; SettingsBean mSettingsModel; Timer mDunResumeTimer; - Rules(Context context) { - mContext = context; + /** + * 私有化构造方法,禁止外部 new 实例 + */ + private Rules(Context context) { + mContext = context.getApplicationContext(); _PhoneConnectRuleModelList = new ArrayList(); 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() { @@ -59,20 +79,20 @@ public class Rules { mDunResumeTimer = new Timer(); 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(); + SettingsActivity.notifyDunInfoUpdate(); + } + } + }, 1000, ss); } public void loadRules() { @@ -119,8 +139,7 @@ public class Rules { return true; } - // - // 以下是云盾防御体系 + // 云盾防御体系 boolean isDefend = false; // 盾牌是否生效 boolean isConnect = true; // 防御结果是否连接 @@ -189,10 +208,7 @@ public class Rules { saveDun(); SettingsActivity.notifyDunInfoUpdate(); } else if (isDefend) { - // 如果触发了以上某个防御模块, - // 就减少防御盾牌层数。 - // 每校验一次规则,云盾防御层数减1 - // 当云盾防御层数为0时,再次进行以下程序段则恢复满值防御。 + // 如果触发了以上某个防御模块,减少防御盾牌层数 int newDunCount = nDunCurrentCount; LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount)); @@ -203,7 +219,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,6 +227,9 @@ public class Rules { // 返回校验结果 LogUtils.d(TAG, String.format("返回校验结果 isConnect == %s", isConnect)); + // 一键更新所有 DunTemperatureView 实例的盾值 + DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), nDunCurrentCount); + return isConnect; } @@ -225,4 +244,18 @@ public class Rules { 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; + } + } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java index 3145728..59c5543 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java @@ -1,23 +1,43 @@ package cc.winboll.studio.contacts.services; +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.Handler; import android.os.IBinder; +import android.os.Looper; + +import cc.winboll.studio.contacts.R; import cc.winboll.studio.contacts.model.MainServiceBean; +import cc.winboll.studio.contacts.utils.AppForegroundUtils; import cc.winboll.studio.libappbase.LogUtils; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/02/14 03:38:31 * @Describe 守护进程服务,用于监控并保活主服务 MainService + * 适配 Android 12+ 后台服务启动限制,支持前台服务运行 + * 兼容 Java 7 语法 & 低版本 SDK 编译 */ 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; + // 前台服务类型:FOREGROUND_SERVICE_TYPE_DATA_SYNC(API 34)硬编码值 + private static final int FOREGROUND_SERVICE_TYPE = 0x00000008; + // Android 12 对应 API 等级(替换 Build.VERSION_CODES.S) + private static final int ANDROID_12_API = 31; + // 重试延迟时间(避免频繁触发后台启动限制) + private static final long RETRY_DELAY_MS = 3000L; // ====================== 成员变量区 ====================== private MainServiceBean mMainServiceBean; @@ -70,8 +90,14 @@ public class AssistantService extends Service { // 尝试重新绑定主服务(如果配置为启用) reloadMainServiceConfig(); if (mMainServiceBean != null && mMainServiceBean.isEnable()) { - LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定主服务"); - wakeupAndBindMain(); + LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务"); + // Java 7 替换 Lambda 为匿名 Runnable + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + wakeupAndBindMain(); + } + }, RETRY_DELAY_MS); } } } @@ -92,12 +118,55 @@ public class AssistantService extends Service { return mIsThreadAlive; } + // ====================== 前台服务辅助方法 ====================== + /** + * 创建前台服务通知 + */ + private Notification createForegroundNotification() { + // 1. 创建通知渠道(Android 8.0+ 必须,API 26) + if (Build.VERSION.SDK_INT >= 26) { + NotificationChannel channel = new NotificationChannel( + FOREGROUND_CHANNEL_ID, + "守护服务", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("守护服务后台运行,保障主服务存活"); + // Java 7 兼容:强制类型转换获取 NotificationManager + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + + // 2. 构建通知:Java 7 不支持三元运算符直接初始化复杂对象,改用 if-else + Notification.Builder builder; + if (Build.VERSION.SDK_INT >= 26) { + builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID); + } else { + builder = new Notification.Builder(this); + } + + return builder + .setSmallIcon(R.drawable.ic_launcher) // 替换为应用实际图标资源 + .setContentTitle("守护服务运行中") + .setContentText("正在监控主服务状态") + .setPriority(Notification.PRIORITY_LOW) + .setOngoing(true) // 不可手动取消 + .build(); + } + // ====================== Service 生命周期方法区 ====================== @Override public void onCreate() { super.onCreate(); LogUtils.d(TAG, "onCreate: 守护服务创建"); + // 适配 Android 12+ 后台启动限制:应用后台时启动为前台服务 + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), FOREGROUND_SERVICE_TYPE); + LogUtils.d(TAG, "onCreate: 守护服务已启动为前台服务"); + } + // 初始化主服务连接回调 if (mMyServiceConnection == null) { mMyServiceConnection = new MyServiceConnection(); @@ -170,7 +239,7 @@ public class AssistantService extends Service { } /** - * 唤醒并绑定主服务 MainService + * 唤醒并绑定主服务 MainService(适配后台启动限制) */ private void wakeupAndBindMain() { if (mMyServiceConnection == null) { @@ -179,8 +248,13 @@ public class AssistantService extends Service { } Intent intent = new Intent(this, MainService.class); - // 先启动主服务,再绑定(确保服务进程存在) - startService(intent); + // 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService) + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + LogUtils.d(TAG, "wakeupAndBindMain: 应用后台,启动主服务为前台服务"); + startForegroundService(intent); + } else { + startService(intent); + } // BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开 bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService"); diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java index ce0dfde..a8b9c8d 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java @@ -1,5 +1,8 @@ package cc.winboll.studio.contacts.services; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.Service; import android.content.ComponentName; import android.content.Context; @@ -7,7 +10,12 @@ import android.content.Intent; import android.content.ServiceConnection; import android.media.AudioManager; import android.os.Binder; +import android.os.Build; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; + +import cc.winboll.studio.contacts.R; import cc.winboll.studio.contacts.bobulltoon.TomCat; import cc.winboll.studio.contacts.dun.Rules; import cc.winboll.studio.contacts.handlers.MainServiceHandler; @@ -16,7 +24,9 @@ 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.threads.MainServiceThread; +import cc.winboll.studio.contacts.utils.AppForegroundUtils; import cc.winboll.studio.libappbase.LogUtils; + import java.util.Timer; import java.util.TimerTask; @@ -27,6 +37,8 @@ import java.util.TimerTask; * 参考: * 进程保活-双进程守护的正确姿势 * Android Service之onStartCommand方法研究 + * 适配 Android 12+ 后台服务启动限制,改造为前台服务 + * 兼容 Java 7 语法 & 低版本 SDK 编译 */ public class MainService extends Service { // ====================== 常量定义区 ====================== @@ -35,6 +47,16 @@ public class MainService extends Service { // 铃声音量检查定时器参数:延迟1秒启动,每分钟检查一次 private static final long VOLUME_CHECK_DELAY = 1000L; private static final long VOLUME_CHECK_PERIOD = 60000L; + // 前台服务通知配置 + private static final String FOREGROUND_CHANNEL_ID = "main_service_foreground_channel"; + private static final int FOREGROUND_NOTIFICATION_ID = 1001; + // 前台服务类型硬编码:DATA_SYNC(0x00000008) + PHONE_CALL(0x00000020) + private static final int FOREGROUND_SERVICE_TYPE = 0x00000008 | 0x00000020; + // 版本常量硬编码(解决低 SDK 找不到符号问题) + private static final int ANDROID_12_API = 31; + private static final int ANDROID_8_API = 26; + // 重试延迟时间 + private static final long RETRY_DELAY_MS = 3000L; // ====================== 静态成员变量区 ====================== private static MainService sMainServiceInstance; @@ -95,7 +117,13 @@ public class MainService extends Service { // 尝试重新绑定守护服务(如果主服务配置为启用) if (mMainServiceBean != null && mMainServiceBean.isEnable()) { LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定守护服务"); - wakeupAndBindAssistant(); + // Java 7 兼容:匿名 Runnable 替代 Lambda + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + wakeupAndBindAssistant(); + } + }, RETRY_DELAY_MS); } } } @@ -125,7 +153,7 @@ public class MainService extends Service { } /** - * 启动主服务 + * 启动主服务(适配 Android 12+ 后台启动限制) */ public static void startMainService(Context context) { if (context == null) { @@ -133,7 +161,14 @@ public class MainService extends Service { return; } LogUtils.d(TAG, "startMainService: 执行启动主服务操作"); - context.startService(new Intent(context, MainService.class)); + Intent intent = new Intent(context, MainService.class); + // 替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(context)) { + LogUtils.d(TAG, "startMainService: 应用后台,使用 startForegroundService 启动"); + context.startForegroundService(intent); + } else { + context.startService(intent); + } } /** @@ -201,6 +236,42 @@ public class MainService extends Service { LogUtils.d(TAG, "Message : " + (message == null ? "null" : message)); } + /** + * 创建前台服务通知(Android 8.0+ 必须) + */ + private Notification createForegroundNotification() { + // 1. 创建通知渠道:替换 Build.VERSION_CODES.O 为硬编码 ANDROID_8_API + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + NotificationChannel channel = new NotificationChannel( + FOREGROUND_CHANNEL_ID, + "拨号主服务", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("主服务后台运行,保障拨号功能正常"); + // Java 7 兼容:强制类型转换获取 NotificationManager + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + + // 2. 构建通知:if-else 替代三元运算符,兼容 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); + } + + return builder + .setSmallIcon(R.drawable.ic_launcher) // 替换为应用实际图标资源 + .setContentTitle("拨号服务运行中") + .setContentText("正在后台保障通话监听与号码识别") + .setPriority(Notification.PRIORITY_LOW) + .setOngoing(true) // 不可手动取消 + .build(); + } + // ====================== Service 生命周期方法区 ====================== @Override public void onCreate() { @@ -214,6 +285,12 @@ public class MainService extends Service { mMyServiceConnection = new MyServiceConnection(); mMainServiceHandler = new MainServiceHandler(this); + // 启动前台服务:替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), FOREGROUND_SERVICE_TYPE); + LogUtils.d(TAG, "onCreate: 主服务已启动为前台服务"); + } + // 初始化铃声音量检查定时器 initVolumeCheckTimer(); @@ -320,7 +397,7 @@ public class MainService extends Service { } /** - * 唤醒并绑定守护服务 + * 唤醒并绑定守护服务(适配后台启动限制) */ private void wakeupAndBindAssistant() { if (mMyServiceConnection == null) { @@ -328,7 +405,13 @@ public class MainService extends Service { return; } Intent intent = new Intent(this, AssistantService.class); - startService(intent); + // 替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + LogUtils.d(TAG, "wakeupAndBindAssistant: 应用后台,启动 AssistantService 为前台服务"); + startForegroundService(intent); + } else { + startService(intent); + } bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); LogUtils.d(TAG, "wakeupAndBindAssistant: 已启动并绑定守护服务"); } @@ -338,7 +421,12 @@ public class MainService extends Service { */ private void startPhoneCallListener() { Intent callListenerIntent = new Intent(this, CallListenerService.class); - startService(callListenerIntent); + // 替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API + if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + startForegroundService(callListenerIntent); + } else { + startService(callListenerIntent); + } LogUtils.d(TAG, "startPhoneCallListener: 通话监听服务已启动"); } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppForegroundUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppForegroundUtils.java new file mode 100644 index 0000000..3f7a78b --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppForegroundUtils.java @@ -0,0 +1,52 @@ +package cc.winboll.studio.contacts.utils; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Build; +import android.text.TextUtils; + +import java.util.List; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/12 15:20 + * @Describe AppForegroundUtils 应用前后台状态判断工具类(兼容 Java 7) + */ +public class AppForegroundUtils { + + public static final String TAG = "AppForegroundUtils"; + + /** + * 判断应用是否处于前台状态 + * @param context 上下文 + * @return true-前台,false-后台/无上下文 + */ + public static boolean isAppForeground(Context context) { + if (context == null) { + return false; + } + + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + return false; + } + + String packageName = context.getPackageName(); + List processList = activityManager.getRunningAppProcesses(); + if (processList == null || processList.isEmpty()) { + return false; + } + + // 遍历进程列表,匹配当前应用包名并判断前台状态 + for (ActivityManager.RunningAppProcessInfo processInfo : processList) { + // Java 7 兼容:避免 Lambda,使用普通循环 + TextUtils 判等 + if (!TextUtils.isEmpty(processInfo.processName) + && TextUtils.equals(processInfo.processName, packageName) + && processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return true; + } + } + return false; + } +} + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/DunTemperatureView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/DunTemperatureView.java new file mode 100644 index 0000000..02d98f6 --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/views/DunTemperatureView.java @@ -0,0 +1,328 @@ +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.view.View; +import cc.winboll.studio.libappbase.LogUtils; +import java.util.WeakHashMap; + +/** + * @Author ZhanGSKen + * @Date 2025/03/19 14:04:20 + * @Describe 云盾华氏度热力视图,垂直盾值温度视图控件(带颜色渐变+静态Handler更新) + * 采用绘图方式展示盾值温度,填充色随盾值比例渐变,底部显示 (当前盾值/最高盾值) 文本 + * 支持静态方法跨线程发送消息更新视图 + */ +public class DunTemperatureView extends View { + // ====================== 常量定义区 ====================== + public static final String TAG = "DunTemperatureView"; + // 控件默认尺寸 + private static final int DEFAULT_WIDTH = 80; + private static final int DEFAULT_HEIGHT = 200; + // 文本预留高度 + private static final int TEXT_RESERVED_HEIGHT = 40; + // 填充区域内边距 + private static final int FILL_PADDING = 2; + // 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 sViewCache = new WeakHashMap<>(); + // 静态Handler,处理跨线程更新消息 + private static Handler sStaticHandler; + + // ====================== 成员变量区 ====================== + // 画笔相关 + private Paint mThermometerPaint; + private Paint mFillPaint; + private Paint mTextPaint; + // 温度条相关参数 + private int mMaxValue = 100; // 最高盾值 + private int mCurrentValue = 0; // 当前盾值 + private int mThermometerWidth = 40; // 温度条宽度 + private RectF mThermometerRect; // 温度条矩形区域 + // 渐变颜色配置(低→中→高 对应绿→黄→红) + private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED}; + private float[] mGradientPositions = {0.0f, 0.5f, 1.0f}; + // 其他颜色配置 + 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: 开始初始化云盾温度视图控件"); + // 初始化温度条边框画笔 + mThermometerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mThermometerPaint.setColor(mBorderColor); + mThermometerPaint.setStyle(Paint.Style.STROKE); + mThermometerPaint.setStrokeWidth(2); + + // 初始化温度条填充画笔(支持渐变) + mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFillPaint.setStyle(Paint.Style.FILL); + + // 初始化文本画笔 + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mTextColor); + mTextPaint.setTextSize(30); + mTextPaint.setTextAlign(Paint.Align.CENTER); + + // 初始化温度条矩形 + mThermometerRect = new RectF(); + + // 将当前实例加入静态缓存 + sViewCache.put(this, null); + LogUtils.d(TAG, "init: 云盾温度视图控件初始化完成,实例已加入缓存"); + } + + // ====================== 对外静态方法区 ====================== + /** + * 静态外部方法:发送消息更新所有 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); + LogUtils.d(TAG, "setMaxValue: 最高盾值设置为" + maxValue + ",当前值校准为" + mCurrentValue); + 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); + // 设置控件默认尺寸 + int width = measureSize(DEFAULT_WIDTH, widthMeasureSpec); + int height = measureSize(DEFAULT_HEIGHT, heightMeasureSpec); + setMeasuredDimension(width, height); + + // 计算温度条矩形区域(居中绘制) + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + + int contentWidth = width - paddingLeft - paddingRight; + int contentHeight = height - paddingTop - paddingBottom - TEXT_RESERVED_HEIGHT; + + float left = paddingLeft + (contentWidth - mThermometerWidth) / 2f; + float right = left + mThermometerWidth; + float top = paddingTop; + float bottom = top + contentHeight; + + mThermometerRect.set(left, top, right, bottom); + LogUtils.v(TAG, "onMeasure: 温度条矩形区域设置为" + mThermometerRect.toShortString()); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + // 绘制温度条边框 + canvas.drawRoundRect(mThermometerRect, 10, 10, mThermometerPaint); + + // 计算填充高度(根据当前值占最大值的比例) + float fillRatio = (float) mCurrentValue / mMaxValue; + float fillHeight = mThermometerRect.height() * fillRatio; + float fillTop = mThermometerRect.bottom - fillHeight; + + // 绘制渐变填充部分 + if (fillHeight > 0) { + RectF fillRect = new RectF( + mThermometerRect.left + FILL_PADDING, + fillTop, + mThermometerRect.right - FILL_PADDING, + mThermometerRect.bottom + ); + // 创建线性渐变(从下到上,对应盾值低到高) + LinearGradient gradient = new LinearGradient( + fillRect.centerX(), fillRect.bottom, + fillRect.centerX(), fillRect.top, + mGradientColors, + mGradientPositions, + Shader.TileMode.CLAMP + ); + mFillPaint.setShader(gradient); + canvas.drawRoundRect(fillRect, 8, 8, mFillPaint); + // 用完渐变后清空shader,避免影响后续绘制 + mFillPaint.setShader(null); + LogUtils.v(TAG, "onDraw: 渐变填充绘制完成,填充高度=" + fillHeight + ",比例=" + fillRatio); + } + + // 绘制底部文本 (当前盾值/最高盾值) + String text = String.format("(%d/%d)", mCurrentValue, mMaxValue); + float textX = getWidth() / 2f; + float textY = mThermometerRect.bottom + 30; + canvas.drawText(text, textX, textY, mTextPaint); + LogUtils.v(TAG, "onDraw: 底部文本绘制完成,内容=" + text); + } +} + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/ScrollDoView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/ScrollDoView.java deleted file mode 100644 index 252a2af..0000000 --- a/contacts/src/main/java/cc/winboll/studio/contacts/views/ScrollDoView.java +++ /dev/null @@ -1,14 +0,0 @@ -package cc.winboll.studio.contacts.views; - -/** - * @Author ZhanGSKen - * @Date 2025/03/19 14:04:20 - * @Describe 云盾滑视度热备控件 - */ -public class ScrollDoView { - - public static final String TAG = "ScrollDoView"; - - - -} diff --git a/contacts/src/main/res/layout/activity_main.xml b/contacts/src/main/res/layout/activity_main.xml index 34db61a..9b30a71 100644 --- a/contacts/src/main/res/layout/activity_main.xml +++ b/contacts/src/main/res/layout/activity_main.xml @@ -16,27 +16,41 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/adsbanner"/> - - + android:layout_weight="1.0" + android:padding="10dp"> - + + + android:layout_height="match_parent" + android:id="@+id/activitymainLinearLayout1" + android:layout_toRightOf="@id/dun_temp_view" + android:layout_alignParentRight="true"> - - - + + + + + +