diff --git a/contacts/build.properties b/contacts/build.properties index c1b0877..8e2d4b0 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Dec 12 08:37:39 GMT 2025 +#Fri Dec 12 11:02:22 GMT 2025 stageCount=1 libraryProject= baseVersion=15.12 publishVersion=15.12.0 -buildCount=25 +buildCount=35 baseBetaVersion=15.12.1 diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml index 3ba809f..fffec35 100644 --- a/contacts/src/main/AndroidManifest.xml +++ b/contacts/src/main/AndroidManifest.xml @@ -100,10 +100,10 @@ android:foregroundServiceType="dataSync" android:exported="false" /> - + @@ -135,9 +135,10 @@ - diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java index 725f406..209ce56 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java @@ -5,6 +5,7 @@ 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; @@ -12,6 +13,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.provider.Settings; import android.telecom.TelecomManager; import android.telephony.PhoneStateListener; @@ -30,6 +32,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; +import com.google.android.material.tabs.TabLayout; import cc.winboll.studio.contacts.activities.SettingsActivity; import cc.winboll.studio.contacts.dun.Rules; import cc.winboll.studio.contacts.fragments.CallLogFragment; @@ -37,14 +40,13 @@ 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.services.MyCallScreeningService; 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 com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.List; @@ -67,7 +69,9 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct private static final int REQUEST_CALL_SCREENING_PERMISSION = 1004; // 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; // ====================== 静态成员区 ====================== static MainActivity _MainActivity; @@ -113,19 +117,20 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct _MainActivity = this; // 权限检查分流:基于 PermissionUtils 工具类,简化逻辑 - if (!PermissionUtils.checkPermissions(this, REQUIRED_PERMISSIONS)) { - LogUtils.d(TAG, "onCreate: 危险权限未完全授予,发起申请"); - PermissionUtils.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS); - } else if (!PermissionUtils.isOverlayPermissionGranted(this)) { - LogUtils.d(TAG, "onCreate: 悬浮窗权限未授予,跳转设置页"); - PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION); - } else if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { - LogUtils.d(TAG, "onCreate: 通话筛选权限未授予,跳转设置页"); - PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); - } else { - LogUtils.d(TAG, "onCreate: 所有权限已授予,初始化UI和业务逻辑"); - initUIAndLogic(savedInstanceState); - } +// if (!PermissionUtils.checkPermissions(this, REQUIRED_PERMISSIONS)) { +// LogUtils.d(TAG, "onCreate: 危险权限未完全授予,发起申请"); +// PermissionUtils.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS); +// } else if (!PermissionUtils.isOverlayPermissionGranted(this)) { +// LogUtils.d(TAG, "onCreate: 悬浮窗权限未授予,跳转设置页"); +// PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION); +// } else if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { +// LogUtils.d(TAG, "onCreate: 通话筛选权限未授予,跳转设置页"); +// PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); +// } else { +// LogUtils.d(TAG, "onCreate: 所有权限已授予,初始化UI和业务逻辑"); +// initUIAndLogic(savedInstanceState); +// } + initUIAndLogic(savedInstanceState); LogUtils.d(TAG, "onCreate: 主Activity创建流程结束"); } @@ -138,24 +143,24 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct @Override protected void onResume() { super.onResume(); - LogUtils.d(TAG, "onResume: 主Activity进入前台"); - // 权限补全检查:使用工具类统一判断,简化代码 - boolean isAllPermGranted = PermissionUtils.checkPermissions(this, REQUIRED_PERMISSIONS) - && PermissionUtils.isOverlayPermissionGranted(this); - boolean isCallScreeningOk = true; - if (Build.VERSION.SDK_INT >= ANDROID_10_API) { - isCallScreeningOk = PermissionUtils.isCallScreeningPermissionGranted(this); - } - if (isAllPermGranted && isCallScreeningOk && mToolbar == null) { - LogUtils.d(TAG, "onResume: 权限已补全,初始化UI和逻辑"); - initUIAndLogic(null); - } else if (!PermissionUtils.isOverlayPermissionGranted(this)) { - LogUtils.w(TAG, "onResume: 悬浮窗权限仍未授予,再次提示申请"); - PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION); - } else if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { - LogUtils.w(TAG, "onResume: 通话筛选权限仍未授予,再次提示申请"); - PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); - } +// LogUtils.d(TAG, "onResume: 主Activity进入前台"); +// // 权限补全检查:使用工具类统一判断,简化代码 +// boolean isAllPermGranted = PermissionUtils.checkPermissions(this, REQUIRED_PERMISSIONS) +// && PermissionUtils.isOverlayPermissionGranted(this); +// boolean isCallScreeningOk = true; +// if (Build.VERSION.SDK_INT >= ANDROID_10_API) { +// isCallScreeningOk = PermissionUtils.isCallScreeningPermissionGranted(this); +// } +// if (isAllPermGranted && isCallScreeningOk && mToolbar == null) { +// LogUtils.d(TAG, "onResume: 权限已补全,初始化UI和逻辑"); +// initUIAndLogic(null); +// } else if (!PermissionUtils.isOverlayPermissionGranted(this)) { +// LogUtils.w(TAG, "onResume: 悬浮窗权限仍未授予,再次提示申请"); +// PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION); +// } else if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { +// LogUtils.w(TAG, "onResume: 通话筛选权限仍未授予,再次提示申请"); +// PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); +// } if (mADsBannerView != null) { mADsBannerView.resumeADs(MainActivity.this); @@ -246,27 +251,27 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct * 权限拒绝提示对话框(纯 Java 7 匿名内部类,无 Lambda) */ private void showPermissionDeniedDialogAndExit(String tip) { - new AlertDialog.Builder(this) - .setTitle("权限不足,无法使用") - .setMessage(tip) - .setCancelable(false) - .setNegativeButton("去设置", new android.content.DialogInterface.OnClickListener() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("权限不足,无法使用"); + builder.setMessage(tip); + builder.setCancelable(false); + builder.setNegativeButton("去设置", new DialogInterface.OnClickListener() { @Override - public void onClick(android.content.DialogInterface dialog, int which) { + public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户点击设置权限,跳转应用设置页"); PermissionUtils.goAppDetailsSettings(MainActivity.this); } - }) - .setPositiveButton("确定退出", new android.content.DialogInterface.OnClickListener() { + }); + builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() { @Override - public void onClick(android.content.DialogInterface dialog, int which) { + public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户点击退出,关闭应用"); finishAndRemoveTask(); } - }) - .show(); + }); + builder.show(); } // ====================== UI与逻辑初始化区(修复服务启动判断,优化内存管理) ====================== @@ -306,8 +311,12 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct DunTemperatureView tempView = (DunTemperatureView) findViewById(R.id.dun_temp_view); tempView.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount()); tempView.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount()); - int[] customColors = new int[]{Color.parseColor("#FF3366FF"), Color.parseColor("#FF9900CC")}; - float[] positions = new float[]{0.0f, 1.0f}; + 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; tempView.setGradientColors(customColors, positions); } @@ -339,7 +348,7 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct // 双重判断:服务是否启用 + 是否已运行,避免重复启动 if (mMainServiceBean.isEnable() && !isServiceRunning(MainService.class)) { LogUtils.d(TAG, "initMainService: 主服务已启用且未运行,延迟1秒启动服务"); - new Handler().postDelayed(new Runnable() { + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { MainService.startMainService(MainActivity.this); @@ -361,9 +370,9 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct // API 30+ 启动通话筛选服务,替代 PROCESS_OUTGOING_CALLS 权限 if (Build.VERSION.SDK_INT >= ANDROID_10_API) { - Intent screeningIntent = new Intent(this, cc.winboll.studio.contacts.services.MyCallScreeningService.class); + Intent screeningIntent = new Intent(this, MyCallScreeningService.class); // 适配 Android O 以上前台服务启动要求 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { startForegroundService(screeningIntent); } else { startService(screeningIntent); diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java index 35336a8..af7af24 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java @@ -22,7 +22,6 @@ 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.R; import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity; import cc.winboll.studio.contacts.phonecallui.PhoneCallService; @@ -31,18 +30,19 @@ import cc.winboll.studio.libappbase.LogUtils; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/12/12 16:06 - * @Describe 通话监听服务,监听来电状态并展示悬浮窗 + * @Describe 通话监听服务,监听来电状态并展示悬浮窗(适配 Java7 + Android API30) */ public class CallListenerService extends Service { - // 常量定义区 + // 常量定义区(硬编码,避免依赖高版本API) private static final String TAG = "CallListenerService"; private static final String CHANNEL_ID = "call_listener_channel"; private static final int NOTIFICATION_ID = 1003; - // 新增:phoneCall 前台服务类型值(对应清单配置) - private static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 0x00000020; - // 新增:Android 12 API 级别硬编码,适配 Java 7 - private static final int ANDROID_12_API = 31; + // phoneCall 前台服务类型正确值(0x80 对应 phoneCall,与清单配置一致) + private static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 0x80; + // API版本常量硬编码(Java7兼容,不用Build.VERSION_CODES) + private static final int ANDROID_8_API = 26; + private static final int ANDROID_10_API = 29; // View 相关属性 private View phoneCallView; @@ -64,22 +64,23 @@ public class CallListenerService extends Service { public void onCreate() { super.onCreate(); LogUtils.d(TAG, "onCreate: 通话监听服务启动"); - // 前台服务必须调用 startForeground,避免 5 秒后被系统杀死 + // 前台服务启动:API30属于Android10+,直接传类型参数 Notification notification = createForegroundNotification(); - if (Build.VERSION.SDK_INT >= ANDROID_12_API) { - // Android 12+ 传入与清单匹配的 phoneCall 类型参数 - startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_PHONE_CALL); - LogUtils.d(TAG, "onCreate: 前台服务启动(phoneCall 类型)"); - } else { - // 低版本无需传入类型参数 - startForeground(NOTIFICATION_ID, notification); - LogUtils.d(TAG, "onCreate: 前台服务启动(兼容低版本)"); + try { + if (Build.VERSION.SDK_INT >= ANDROID_10_API) { + startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_PHONE_CALL); + LogUtils.d(TAG, "onCreate: 前台服务启动(phoneCall类型,API30+)"); + } else { + startForeground(NOTIFICATION_ID, notification); + LogUtils.d(TAG, "onCreate: 前台服务启动(低版本兼容)"); + } + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "onCreate: 前台服务启动失败,类型不匹配", e); } initPhoneStateListener(); initPhoneCallView(); } - @Nullable @Override public IBinder onBind(Intent intent) { LogUtils.d(TAG, "onBind: 服务绑定"); @@ -87,30 +88,34 @@ public class CallListenerService extends Service { } /** - * 初始化前台服务通知(必须实现) + * 初始化前台服务通知(适配Android8.0+渠道要求,Java7写法) */ private Notification createForegroundNotification() { LogUtils.d(TAG, "createForegroundNotification: 创建前台服务通知"); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - // 创建通知渠道(Android 8.0+ 必需) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android8.0+创建通知渠道 + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - "通话监听服务", - NotificationManager.IMPORTANCE_LOW + CHANNEL_ID, + "通话监听服务", + NotificationManager.IMPORTANCE_LOW ); channel.setDescription("后台监听通话状态"); - manager.createNotificationChannel(channel); - LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); + if (manager != null) { + manager.createNotificationChannel(channel); + LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); + } } - // 构建低优先级通知,不打扰用户 - return new Notification.Builder(this) - .setSmallIcon(R.drawable.ic_winboll) - .setContentTitle("通话监听中") - .setContentText("服务运行中") - .setPriority(Notification.PRIORITY_LOW) - .setChannelId(CHANNEL_ID) - .build(); + // 构建通知(Java7不支持链式调用简化,分步设置) + Notification.Builder builder = new Notification.Builder(this); + builder.setSmallIcon(R.drawable.ic_winboll); + builder.setContentTitle("通话监听中"); + builder.setContentText("服务运行中"); + builder.setPriority(Notification.PRIORITY_LOW); + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + builder.setChannelId(CHANNEL_ID); + } + return builder.build(); } /** @@ -123,7 +128,7 @@ public class CallListenerService extends Service { public void onCallStateChanged(int state, String incomingNumber) { super.onCallStateChanged(state, incomingNumber); callNumber = incomingNumber; - LogUtils.d(TAG, "onCallStateChanged: 通话状态变更 state=" + state + " number=" + incomingNumber); + LogUtils.d(TAG, "onCallStateChanged: 状态变更 state=" + state + " number=" + incomingNumber); switch (state) { case TelephonyManager.CALL_STATE_IDLE: LogUtils.d(TAG, "onCallStateChanged: 通话结束,关闭悬浮窗"); @@ -146,13 +151,13 @@ public class CallListenerService extends Service { } } }; - // 空指针兜底,避免 TelephonyManager 获取失败 + // 空指针兜底,避免TelephonyManager获取失败 telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); if (telephonyManager != null) { telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - LogUtils.d(TAG, "initPhoneStateListener: 来电监听器注册成功"); + LogUtils.d(TAG, "initPhoneStateListener: 监听器注册成功"); } else { - LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager 获取失败"); + LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败"); } } @@ -160,7 +165,7 @@ public class CallListenerService extends Service { LogUtils.d(TAG, "initPhoneCallView: 初始化悬浮窗视图"); windowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); if (windowManager == null) { - LogUtils.e(TAG, "initPhoneCallView: WindowManager 获取失败"); + LogUtils.e(TAG, "initPhoneCallView: WindowManager获取失败"); return; } @@ -172,22 +177,22 @@ public class CallListenerService extends Service { params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; params.format = PixelFormat.TRANSLUCENT; - // 悬浮窗类型适配(Android 8.0+ 必须用 TYPE_APPLICATION_OVERLAY) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 悬浮窗类型适配(Android8.0+用TYPE_APPLICATION_OVERLAY) + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { params.type = WindowManager.LayoutParams.TYPE_PHONE; } - // 优化 Flag:移除 FLAG_FULLSCREEN 避免遮挡状态栏 + // 设置Flag,避免遮挡状态栏 params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; - + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { params.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; } + // 拦截返回键的布局 FrameLayout interceptorLayout = new FrameLayout(this) { @Override public boolean dispatchKeyEvent(KeyEvent event) { @@ -199,17 +204,19 @@ public class CallListenerService extends Service { } }; + // 加载悬浮窗布局 phoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, interceptorLayout); tvCallNumber = (TextView) phoneCallView.findViewById(R.id.tv_call_number); btnOpenApp = (Button) phoneCallView.findViewById(R.id.btn_open_app); + // 按钮点击事件(Java7匿名内部类写法) btnOpenApp.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "onClick: 点击打开通话页面 number=" + callNumber); - PhoneCallService.CallType callType = isCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT; - PhoneCallActivity.actionStart(CallListenerService.this, callNumber, callType); - } - }); + @Override + public void onClick(View v) { + LogUtils.d(TAG, "onClick: 点击打开通话页面 number=" + callNumber); + PhoneCallService.CallType callType = isCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT; + PhoneCallActivity.actionStart(CallListenerService.this, callNumber, callType); + } + }); LogUtils.d(TAG, "initPhoneCallView: 悬浮窗视图初始化完成"); } @@ -226,7 +233,7 @@ public class CallListenerService extends Service { } /** - * 优化 dismiss 方法:添加 try-catch 避免移除视图失败崩溃 + * 关闭悬浮窗,添加try-catch避免崩溃 */ private void dismiss() { if (hasShown && phoneCallView != null && windowManager != null) { @@ -251,15 +258,18 @@ public class CallListenerService extends Service { tvCallNumber.setText(formatNumber); int callTypeDrawable = isCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out; tvCallNumber.setCompoundDrawablesWithIntrinsicBounds(null, null, - getResources().getDrawable(callTypeDrawable), null); + getResources().getDrawable(callTypeDrawable), null); LogUtils.d(TAG, "updateUI: 悬浮窗UI更新完成 number=" + formatNumber); } + /** + * 格式化手机号(11位手机号加分隔符) + */ 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); + + phoneNum.substring(3, 7) + "-" + + phoneNum.substring(7); } return phoneNum; } @@ -272,9 +282,9 @@ public class CallListenerService extends Service { dismiss(); if (telephonyManager != null) { telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); - LogUtils.d(TAG, "onDestroy: 来电监听器注销成功"); + LogUtils.d(TAG, "onDestroy: 监听器注销成功"); } - // 清空引用 + // 清空引用,帮助GC回收 phoneStateListener = null; telephonyManager = null; windowManager = 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 59c5543..2dc7158 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 @@ -25,6 +25,7 @@ import cc.winboll.studio.libappbase.LogUtils; * @Describe 守护进程服务,用于监控并保活主服务 MainService * 适配 Android 12+ 后台服务启动限制,支持前台服务运行 * 兼容 Java 7 语法 & 低版本 SDK 编译 + * 移除无关的 microphone 类型配置,修复前台服务类型不匹配崩溃 */ public class AssistantService extends Service { // ====================== 常量定义区 ====================== @@ -32,10 +33,12 @@ public class AssistantService extends Service { // 前台服务通知配置 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; + // 修复:前台服务类型改为 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; @@ -91,7 +94,7 @@ public class AssistantService extends Service { reloadMainServiceConfig(); if (mMainServiceBean != null && mMainServiceBean.isEnable()) { LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务"); - // Java 7 替换 Lambda 为匿名 Runnable + // Java 7 匿名 Runnable 替代 Lambda new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { @@ -120,39 +123,39 @@ public class AssistantService extends Service { // ====================== 前台服务辅助方法 ====================== /** - * 创建前台服务通知 + * 创建前台服务通知(Android 8.0+ 必须配置渠道) */ private Notification createForegroundNotification() { - // 1. 创建通知渠道(Android 8.0+ 必须,API 26) - if (Build.VERSION.SDK_INT >= 26) { + // 1. 创建通知渠道(API 26+ 必需) + 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); + LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); } } - // 2. 构建通知:Java 7 不支持三元运算符直接初始化复杂对象,改用 if-else + // 2. 构建通知(Java 7 分步设置,取消链式调用简化) Notification.Builder builder; - if (Build.VERSION.SDK_INT >= 26) { + 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 - .setSmallIcon(R.drawable.ic_launcher) // 替换为应用实际图标资源 - .setContentTitle("守护服务运行中") - .setContentText("正在监控主服务状态") - .setPriority(Notification.PRIORITY_LOW) - .setOngoing(true) // 不可手动取消 - .build(); + return builder.build(); } // ====================== Service 生命周期方法区 ====================== @@ -163,8 +166,18 @@ public class AssistantService extends Service { // 适配 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: 守护服务已启动为前台服务"); + 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); + } } // 初始化主服务连接回调 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 e558197..5fd7e21 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 @@ -34,30 +34,24 @@ import java.util.TimerTask; * @Author ZhanGSKen&豆包大模型 * @Date 2025/02/13 06:56:41 * @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动 - * 参考: - * 进程保活-双进程守护的正确姿势 - * Android Service之onStartCommand方法研究 - * 适配 Android 12+ 后台服务启动限制,改造为前台服务 - * 兼容 Java 7 语法 & 低版本 SDK 编译 + * 适配 Android API 30 + Java 7 语法 */ public class MainService extends Service { // ====================== 常量定义区 ====================== public static final String TAG = "MainService"; public static final int MSG_UPDATE_STATUS = 0; - // 铃声音量检查定时器参数:延迟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; - // 原配置:dataSync + phoneCall 组合 - // 新配置:仅保留 dataSync 类型 - private static final int FOREGROUND_SERVICE_TYPE = 0x00000008; - // 版本常量硬编码(解决低 SDK 找不到符号问题) - private static final int ANDROID_12_API = 31; + // 修复:前台服务类型改为 dataSync(0x01),与 Manifest 配置匹配,解决崩溃 + private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; + // 版本常量硬编码(Java 7 兼容,不依赖 Build.VERSION_CODES 高版本字段) 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; @@ -77,9 +71,6 @@ public class MainService extends Service { private Timer mStreamVolumeCheckTimer; // ====================== Binder 内部类 ====================== - /** - * 对外暴露服务实例的 Binder - */ public class MyBinder extends Binder { public MainService getService() { LogUtils.d(TAG, "MyBinder.getService: 获取 MainService 实例"); @@ -88,9 +79,6 @@ public class MainService extends Service { } // ====================== ServiceConnection 内部类 ====================== - /** - * 守护服务连接状态监听回调 - */ private class MyServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { @@ -117,10 +105,9 @@ public class MainService extends Service { mAssistantService = null; mIsBound = false; - // 尝试重新绑定守护服务(如果主服务配置为启用) if (mMainServiceBean != null && mMainServiceBean.isEnable()) { LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定守护服务"); - // Java 7 兼容:匿名 Runnable 替代 Lambda + // Java 7 匿名 Runnable,替代 Lambda new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { @@ -132,9 +119,6 @@ public class MainService extends Service { } // ====================== 对外静态方法区 ====================== - /** - * 判断号码是否在 BoBullToon 数据中 - */ public static boolean isPhoneInBoBullToon(String phone) { if (sTomCatInstance != null && phone != null && !phone.isEmpty()) { return sTomCatInstance.isPhoneBoBullToon(phone); @@ -143,9 +127,6 @@ public class MainService extends Service { return false; } - /** - * 停止主服务 - */ public static void stopMainService(Context context) { if (context == null) { LogUtils.e(TAG, "stopMainService: Context 为 null,无法执行"); @@ -155,9 +136,6 @@ public class MainService extends Service { context.stopService(new Intent(context, MainService.class)); } - /** - * 启动主服务(适配 Android 12+ 后台启动限制) - */ public static void startMainService(Context context) { if (context == null) { LogUtils.e(TAG, "startMainService: Context 为 null,无法执行"); @@ -165,8 +143,8 @@ public class MainService extends Service { } LogUtils.d(TAG, "startMainService: 执行启动主服务操作"); 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)) { + // API 30 属于 Android 10+,按 10+ 逻辑处理前台启动 + if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(context)) { LogUtils.d(TAG, "startMainService: 应用后台,使用 startForegroundService 启动"); context.startForegroundService(intent); } else { @@ -174,9 +152,6 @@ public class MainService extends Service { } } - /** - * 重启主服务(仅配置启用时执行) - */ public static void restartMainService(Context context) { if (context == null) { LogUtils.e(TAG, "restartMainService: Context 为 null,无法执行"); @@ -194,9 +169,6 @@ public class MainService extends Service { } } - /** - * 停止主服务并保存禁用状态 - */ public static void stopMainServiceAndSaveStatus(Context context) { if (context == null) { LogUtils.e(TAG, "stopMainServiceAndSaveStatus: Context 为 null,无法执行"); @@ -209,9 +181,6 @@ public class MainService extends Service { stopMainService(context); } - /** - * 启动主服务并保存启用状态 - */ public static void startMainServiceAndSaveStatus(Context context) { if (context == null) { LogUtils.e(TAG, "startMainServiceAndSaveStatus: Context 为 null,无法执行"); @@ -226,24 +195,25 @@ public class MainService extends Service { // ====================== 成员方法区 ====================== /** - * 获取提醒线程实例 - */ - public MainServiceThread getRemindThread() { - return mMainServiceThread; - } - - /** - * 追加日志消息 + * 补充缺失的 appenMessage 方法 + * 用于接收并处理消息,支持日志打印和通过 Handler 转发到主线程 */ public void appenMessage(String message) { - LogUtils.d(TAG, "Message : " + (message == null ? "null" : message)); + // 空指针防护,避免传入 null 导致崩溃 + String msg = message == null ? "null" : message; + LogUtils.d(TAG, "追加消息: " + msg); + + // 可选:将消息通过 Handler 转发,用于更新 UI 或后续业务处理 + if (mMainServiceHandler != null) { + android.os.Message handlerMsg = android.os.Message.obtain(); + handlerMsg.what = MSG_UPDATE_STATUS; + handlerMsg.obj = msg; + mMainServiceHandler.sendMessage(handlerMsg); + } } - /** - * 创建前台服务通知(Android 8.0+ 必须) - */ private Notification createForegroundNotification() { - // 1. 创建通知渠道:替换 Build.VERSION_CODES.O 为硬编码 ANDROID_8_API + // 1. 创建通知渠道(Android 8.0+ 必需) if (Build.VERSION.SDK_INT >= ANDROID_8_API) { NotificationChannel channel = new NotificationChannel( FOREGROUND_CHANNEL_ID, @@ -251,28 +221,28 @@ public class MainService extends Service { NotificationManager.IMPORTANCE_LOW ); channel.setDescription("主服务后台运行,保障拨号功能正常"); - // Java 7 兼容:强制类型转换获取 NotificationManager + // 空指针防护 NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); if (manager != null) { manager.createNotificationChannel(channel); + LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); } } - // 2. 构建通知:if-else 替代三元运算符,兼容 Java 7 + // 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 - .setSmallIcon(R.drawable.ic_launcher) // 替换为应用实际图标资源 - .setContentTitle("拨号服务运行中") - .setContentText("正在后台保障通话监听与号码识别") - .setPriority(Notification.PRIORITY_LOW) - .setOngoing(true) // 不可手动取消 - .build(); + return builder.build(); } // ====================== Service 生命周期方法区 ====================== @@ -288,10 +258,15 @@ 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: 主服务已启动为前台服务"); + // 启动前台服务:类型改为 dataSync,与 Manifest 保持一致,添加异常捕获 + Notification foregroundNotification = createForegroundNotification(); + try { + if (Build.VERSION.SDK_INT >= ANDROID_10_API) { + startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC); + LogUtils.d(TAG, "onCreate: 主服务已启动为前台服务(dataSync 类型)"); + } + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "onCreate: 启动前台服务失败,类型不匹配", e); } // 初始化铃声音量检查定时器 @@ -310,9 +285,7 @@ public class MainService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId); - // 每次启动都执行核心逻辑,确保服务状态正确 mainService(); - // 配置启用时返回 START_STICKY 保活,否则使用默认返回值 return (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); } @@ -361,9 +334,6 @@ public class MainService extends Service { } // ====================== 核心业务逻辑方法区 ====================== - /** - * 主服务核心逻辑:初始化组件、绑定守护服务、启动业务线程 - */ private void mainService() { LogUtils.d(TAG, "mainService: 执行核心业务逻辑"); // 重新加载配置,确保使用最新状态 @@ -399,17 +369,13 @@ public class MainService extends Service { LogUtils.i(TAG, "mainService: 主业务线程已启动"); } - /** - * 唤醒并绑定守护服务(适配后台启动限制) - */ private void wakeupAndBindAssistant() { if (mMyServiceConnection == null) { LogUtils.e(TAG, "wakeupAndBindAssistant: MyServiceConnection 未初始化,绑定失败"); return; } Intent intent = new Intent(this, AssistantService.class); - // 替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API - if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(this)) { LogUtils.d(TAG, "wakeupAndBindAssistant: 应用后台,启动 AssistantService 为前台服务"); startForegroundService(intent); } else { @@ -419,13 +385,9 @@ public class MainService extends Service { LogUtils.d(TAG, "wakeupAndBindAssistant: 已启动并绑定守护服务"); } - /** - * 启动通话监听服务 - */ private void startPhoneCallListener() { Intent callListenerIntent = new Intent(this, CallListenerService.class); - // 替换 Build.VERSION_CODES.S 为硬编码 ANDROID_12_API - if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) { + if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(this)) { startForegroundService(callListenerIntent); } else { startService(callListenerIntent); @@ -434,9 +396,6 @@ public class MainService extends Service { } // ====================== 铃声音量监控相关方法区 ====================== - /** - * 初始化铃声音量检查定时器 - */ private void initVolumeCheckTimer() { cancelVolumeCheckTimer(); mStreamVolumeCheckTimer = new Timer(); @@ -449,9 +408,6 @@ public class MainService extends Service { LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量检查定时器已启动"); } - /** - * 检查并恢复铃声音量至配置值 - */ private void checkAndRestoreRingerVolume() { AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); if (audioManager == null) { @@ -480,9 +436,6 @@ public class MainService extends Service { } } - /** - * 取消铃声音量检查定时器 - */ private void cancelVolumeCheckTimer() { if (mStreamVolumeCheckTimer != null) { mStreamVolumeCheckTimer.cancel(); @@ -492,9 +445,6 @@ public class MainService extends Service { } // ====================== 辅助初始化方法区 ====================== - /** - * 初始化 TomCat 与 BoBullToon 数据 - */ private void initTomCat() { sTomCatInstance = TomCat.getInstance(this); if (!sTomCatInstance.loadPhoneBoBullToon()) { @@ -504,9 +454,6 @@ public class MainService extends Service { } } - /** - * 初始化广播接收器 - */ private void initMainReceiver() { if (mMainReceiver == null) { mMainReceiver = new MainReceiver(this); diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java index 8a21033..be64d12 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java @@ -1,59 +1,98 @@ package cc.winboll.studio.contacts.services; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; +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.R; import cc.winboll.studio.libappbase.LogUtils; -import javax.security.auth.callback.Callback; /** * @Author ZhanGSKen&豆包大模型 - * @Date 2025/12/12 16:59 - * @Describe 通话筛选服务(适配 API 30,监听来电/外拨电话) - * 注意:API 29+ 可用,需在 AndroidManifest.xml 中注册 + * @Date 2025/12/12 19:00 + * @Describe 通话筛选服务(完全匹配 API 30 父类方法原型,Java 7 兼容) + * 基于 android-30.jar 中 CallScreeningService 的 onScreenCall 方法编写 + * 修复:1. 前台服务启动超时异常 2. 移除 Build.VERSION_CODES.S 依赖 */ -@RequiresApi(api = Build.VERSION_CODES.Q) // 标注最低支持API 29,适配API30 +@RequiresApi(api = Build.VERSION_CODES.Q) public class MyCallScreeningService extends CallScreeningService { public static final String TAG = "MyCallScreeningService"; private Context mContext; + // ====================== 常量定义(硬编码 API 版本,避免高版本依赖) ====================== + private static final int FOREGROUND_NOTIFICATION_ID = 1003; + private static final String FOREGROUND_CHANNEL_ID = "call_screening_service_channel"; + private static final int ANDROID_8_API = 26; // 通知渠道最低版本 + private static final int ANDROID_12_API = 31; // 替代 Build.VERSION_CODES.S + private static final int STOP_FOREGROUND_REMOVE = 1; // 硬编码常量,避免 API 30 未定义 + @Override public void onCreate() { super.onCreate(); mContext = this; LogUtils.d(TAG, "通话筛选服务已启动"); + + // 核心修复:启动前台服务,绑定通知,避免 5 秒超时异常 + Notification foregroundNotification = createForegroundNotification(); + startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification); } /** - * 核心回调:处理来电/外拨电话的筛选逻辑 - * 补充@RequiresApi,避免低版本编译警告 + * 100% 匹配 API 30 父类的抽象方法原型 + * 方法签名:public abstract void onScreenCall(@NonNull Call.Details details) */ @Override @RequiresApi(api = Build.VERSION_CODES.Q) - public void onScreenCall(CallDetails callDetails, Callback callback) { - // 1. 获取通话基础信息 - String phoneNumber = callDetails.getHandle() != null ? callDetails.getHandle().getSchemeSpecificPart() : "未知号码"; - int callType = getCallType(callDetails); - String callTypeStr = callType == TelephonyManager.CALL_STATE_RINGING ? "来电" : "外拨"; + public void onScreenCall(@NonNull android.telecom.Call.Details details) { + // 1. 获取通话号码(Java 7 空指针多层防护,避免崩溃) + String phoneNumber = "未知号码"; + Uri handle = details.getHandle(); + if (handle != null) { + String schemePart = handle.getSchemeSpecificPart(); + if (schemePart != null && !schemePart.trim().isEmpty()) { + phoneNumber = schemePart.trim(); + } + } - LogUtils.d(TAG, String.format("检测到%s:%s", callTypeStr, phoneNumber)); + // 2. 判断通话方向(来电/外拨) + String callTypeStr; + int callType; + int direction = details.getCallDirection(); + if (direction == android.telecom.Call.Details.DIRECTION_INCOMING) { + callTypeStr = "来电"; + callType = TelephonyManager.CALL_STATE_RINGING; + } else if (direction == android.telecom.Call.Details.DIRECTION_OUTGOING) { + callTypeStr = "外拨"; + callType = TelephonyManager.CALL_STATE_OFFHOOK; + } else { + callTypeStr = "未知类型"; + callType = TelephonyManager.CALL_STATE_IDLE; + } - // 2. 自定义筛选逻辑示例 + // Java 7 字符串拼接,规避 String.format 兼容性问题 + LogUtils.d(TAG, "检测到" + callTypeStr + ":" + phoneNumber); + + // 3. 自定义拦截逻辑示例:拦截 10086 号码 boolean shouldBlock = "10086".equals(phoneNumber); - // 3. 构建筛选响应 + // 4. 构建通话筛选响应 CallResponse.Builder responseBuilder = new CallResponse.Builder(); - responseBuilder.setDisallowCall(shouldBlock); - responseBuilder.setRejectCall(shouldBlock); - responseBuilder.setSkipCallLog(!shouldBlock); - responseBuilder.setSkipNotification(!shouldBlock); + responseBuilder.setDisallowCall(shouldBlock); // 禁止通话 + responseBuilder.setRejectCall(shouldBlock); // 拒绝通话 + responseBuilder.setSkipCallLog(!shouldBlock); // 拦截时不写入通话记录 + responseBuilder.setSkipNotification(!shouldBlock);// 拦截时不显示通知 + CallResponse response = responseBuilder.build(); - // 4. 发送筛选结果 - callback.onCallScreeningResponse(responseBuilder.build()); + // 5. 提交筛选结果(API 30 父类提供的 respondToCall 方法) + respondToCall(details, response); - // 5. 自定义业务逻辑 + // 6. 处理正常通话的业务逻辑 if (!shouldBlock) { handleNormalCall(phoneNumber, callType); } else { @@ -62,33 +101,61 @@ public class MyCallScreeningService extends CallScreeningService { } /** - * 判断通话类型(来电/外拨) - * 补充@RequiresApi,标注依赖API 29 + * 处理正常通话的扩展业务逻辑 + * 可在此添加广播、数据库存储、界面通知等操作 */ - @RequiresApi(api = Build.VERSION_CODES.Q) - private int getCallType(CallDetails callDetails) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - CallAttributes callAttributes = callDetails.getCallAttributes(); - return callAttributes.getDirection() == CallAttributes.DIRECTION_INCOMING - ? TelephonyManager.CALL_STATE_RINGING - : TelephonyManager.CALL_STATE_OFFHOOK; - } else { - return TelephonyManager.CALL_STATE_RINGING; - } + private void handleNormalCall(String phoneNumber, int callType) { + // 示例:记录通话号码到本地(需添加存储权限) + // SharedPreferences sp = mContext.getSharedPreferences("call_log", Context.MODE_PRIVATE); + // sp.edit().putString("last_call", phoneNumber).apply(); } /** - * 处理正常通话的自定义业务逻辑 + * 创建前台服务通知(Java 7 兼容,适配 Android 8.0+ 通知渠道) + * @return 前台服务所需的 Notification 实例 */ - private void handleNormalCall(String phoneNumber, int callType) { - // 如需使用LocalBroadcastManager,需添加support-v4依赖并补充引用 - // import androidx.localbroadcastmanager.content.LocalBroadcastManager; + private Notification createForegroundNotification() { + // 1. 适配 Android 8.0+ 必须的通知渠道 + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + NotificationChannel channel = new NotificationChannel( + FOREGROUND_CHANNEL_ID, + "来电筛查服务", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("用于后台处理来电筛查逻辑,防止服务被系统回收"); + + // 空指针防护:获取 NotificationManager 服务 + NotificationManager manager = (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); + if (manager != null) { + manager.createNotificationChannel(channel); + LogUtils.d(TAG, "前台服务通知渠道创建成功"); + } + } + + // 2. 构建 Notification 对象(Java 7 分步设置,避免链式调用简化写法) + Notification.Builder builder; + if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + builder = new Notification.Builder(mContext, FOREGROUND_CHANNEL_ID); + } else { + builder = new Notification.Builder(mContext); + } + + builder.setSmallIcon(R.drawable.ic_launcher); // 替换为应用实际图标资源 + builder.setContentTitle("来电筛查服务运行中"); + builder.setContentText("正在监控来电状态"); + builder.setPriority(Notification.PRIORITY_LOW); + builder.setOngoing(true); // 通知不可手动取消,符合前台服务特性 + + return builder.build(); } @Override public void onDestroy() { super.onDestroy(); LogUtils.d(TAG, "通话筛选服务已销毁"); + // 修复:用硬编码 API 版本和常量替代高版本依赖 + if (Build.VERSION.SDK_INT >= ANDROID_12_API) { + stopForeground(STOP_FOREGROUND_REMOVE); + } } } - diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java index 6ff59a0..6b3e774 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java @@ -18,24 +18,25 @@ import java.util.List; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/12/12 16:28 - * @Describe 敏感权限申请工具类(完全适配 Android API 30 + Java 7) + * @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 版本硬编码常量 + // 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 33 新增的常量字符串,解决未定义问题 + // 硬编码系统常量字符串,解决 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,移除废弃权限) + // 基础权限组(严格适配 API 30,移除废弃/不存在的权限) public static final String[] BASE_PERMISSIONS = { android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS, @@ -46,28 +47,31 @@ public class PermissionUtils { }; /** - * 获取所有需要申请的权限(Java 7 传统循环) + * 获取所有需要申请的权限(Java 7 传统 for 循环,无菱形运算符) */ public static String[] getAllNeedPermissions() { List permissions = new ArrayList(); + // 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)) { @@ -78,7 +82,7 @@ public class PermissionUtils { } /** - * 申请权限组(Activity 调用) + * 申请权限组(Activity 中调用,Java 7 兼容) */ public static void requestPermissions(@NonNull FragmentActivity activity, @NonNull String[] permissions, @@ -87,7 +91,7 @@ public class PermissionUtils { } /** - * 申请权限组(Fragment 调用) + * 申请权限组(Fragment 中调用,Java 7 兼容) */ public static void requestPermissions(@NonNull Fragment fragment, @NonNull String[] permissions, @@ -96,17 +100,18 @@ public class PermissionUtils { } /** - * 检查悬浮窗权限 + * 检查悬浮窗权限(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)) { @@ -120,17 +125,18 @@ public class PermissionUtils { } /** - * 检查修改系统设置权限 + * 检查修改系统设置权限(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)) { @@ -144,7 +150,7 @@ public class PermissionUtils { } /** - * 检查通话筛选权限(适配 API 30,反射兼容高版本方法) + * 检查通话筛选权限(适配 API 30,优化反射逻辑 + 异常捕获) */ public static boolean isCallScreeningPermissionGranted(@NonNull Context context) { if (Build.VERSION.SDK_INT >= ANDROID_10_API) { @@ -153,40 +159,65 @@ public class PermissionUtils { return false; } String defaultPackage = null; - // 反射调用 API 33+ 新增方法,低版本异常兜底 + // 反射调用高版本方法,捕获所有异常避免崩溃(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) { - // API 30-32 无此方法,直接返回未授权状态 + // 其他反射异常,返回 false return false; } return defaultPackage != null && defaultPackage.equals(context.getPackageName()); } + // 10.0 以下无此权限,默认返回 true return true; } /** - * 申请通话筛选权限(完全适配 API 30:硬编码常量 + 版本分级处理) + * 申请通话筛选权限(完全适配 API 30,解决 ActivityNotFoundException 崩溃) */ public static void requestCallScreeningPermission(@NonNull Context context, int requestCode) { if (Build.VERSION.SDK_INT >= ANDROID_10_API && !isCallScreeningPermissionGranted(context)) { - // API 33+ 才支持该 ACTION,低版本直接跳转应用详情页 - if (Build.VERSION.SDK_INT >= ANDROID_13_API) { - Intent intent = new Intent(ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP); + 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()); - if (context instanceof FragmentActivity) { - ((FragmentActivity) context).startActivityForResult(intent, requestCode); - } } else { - // API 30-32 无系统授权页,引导用户手动到应用详情页找相关权限 + // 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); @@ -197,17 +228,23 @@ public class PermissionUtils { } /** - * 解析被拒绝的权限(Java 7 字符串操作) + * 解析被拒绝的权限(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)) { - String permName = permission.substring(permission.lastIndexOf(".") + 1); - deniedPerms.append(permName).append("、"); + // 截取权限名称,优化展示 + 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); }