From 181e3e8a3472da9b49cbe098a63c512c0bdd736b Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Fri, 12 Dec 2025 17:15:52 +0800 Subject: [PATCH] =?UTF-8?q?20251212=5F171425=5F295=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=94=B3=E8=AF=B7=E9=83=A8=E5=88=86=E6=94=B9?= =?UTF-8?q?=E9=80=A0=EF=BC=8C=E7=89=88=E6=9C=AC=E5=8F=AF=E8=83=BD=E4=BC=9A?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E3=80=82=E3=80=82=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contacts/build.properties | 4 +- contacts/src/main/AndroidManifest.xml | 96 +++---- .../winboll/studio/contacts/MainActivity.java | 245 +++++++++++------- .../listenphonecall/CallListenerService.java | 233 +++++++++++------ .../studio/contacts/services/MainService.java | 5 +- .../services/MyCallScreeningService.java | 94 +++++++ .../contacts/utils/PermissionUtils.java | 217 ++++++++++++++++ 7 files changed, 669 insertions(+), 225 deletions(-) create mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java create mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java diff --git a/contacts/build.properties b/contacts/build.properties index dfb4dd1..c1b0877 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Dec 12 07:55:42 GMT 2025 +#Fri Dec 12 08:37:39 GMT 2025 stageCount=1 libraryProject= baseVersion=15.12 publishVersion=15.12.0 -buildCount=18 +buildCount=25 baseBetaVersion=15.12.1 diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml index 8405c3b..3ba809f 100644 --- a/contacts/src/main/AndroidManifest.xml +++ b/contacts/src/main/AndroidManifest.xml @@ -9,36 +9,40 @@ - + + - + - - - - + - - + - + - + - + + + - + - - - + + + + + + + + - - - @@ -69,7 +70,6 @@ android:label="CallActivity" android:launchMode="singleTask" android:exported="true"> - - - - - - - - - - - + + - + + + - - + + android:exported="false" + android:foregroundServiceType="phoneCall"> - - + + + + + + - - - - - - - - - - - - + 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 2a89a7d..725f406 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java @@ -8,9 +8,11 @@ import android.content.Context; 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.provider.Settings; import android.telecom.TelecomManager; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; @@ -29,12 +31,14 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import cc.winboll.studio.contacts.activities.SettingsActivity; +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; @@ -43,16 +47,15 @@ import cc.winboll.studio.libappbase.LogView; import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.List; -import cc.winboll.studio.contacts.dun.Rules; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/08/30 14:32 - * @Describe Contacts 主窗口 + * @Describe Contacts 主窗口(完全适配 API 30 + Java 7 语法) */ public final class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener { - // ====================== 常量定义区 ====================== + // ====================== 常量定义区(Java 7 硬编码 API 版本,避免高版本依赖) ====================== public static final String TAG = "MainActivity"; public static final int REQUEST_HOME_ACTIVITY = 0; public static final int REQUEST_ABOUT_ACTIVITY = 1; @@ -60,16 +63,17 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS"; private static final int DIALER_REQUEST_CODE = 1; private static final int REQUEST_REQUIRED_PERMISSIONS = 1002; + private static final int REQUEST_OVERLAY_PERMISSION = 1003; + 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_10_API = 29; // ====================== 静态成员区 ====================== static MainActivity _MainActivity; - // ====================== 权限常量区 ====================== - private final String[] REQUIRED_PERMISSIONS = new String[]{ - Manifest.permission.READ_CONTACTS, - Manifest.permission.CALL_PHONE, - Manifest.permission.READ_CALL_LOG - }; + // ====================== 权限常量区(移除废弃权限,直接复用 PermissionUtils 常量) ====================== + private final String[] REQUIRED_PERMISSIONS = PermissionUtils.BASE_PERMISSIONS; // ====================== UI控件成员区 ====================== private ADsBannerView mADsBannerView; @@ -101,19 +105,25 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct return TAG; } - // ====================== 生命周期函数区 ====================== + // ====================== 生命周期函数区(优化权限检查逻辑,接入 PermissionUtils 工具类) ====================== @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LogUtils.d(TAG, "onCreate: 主Activity开始创建"); _MainActivity = this; - // 权限检查与初始化分流 - if (!checkAllRequiredPermissions()) { - LogUtils.d(TAG, "onCreate: 检测到权限未完全授予,发起权限申请"); - requestAllRequiredPermissions(); + // 权限检查分流:基于 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和业务逻辑"); + LogUtils.d(TAG, "onCreate: 所有权限已授予,初始化UI和业务逻辑"); initUIAndLogic(savedInstanceState); } LogUtils.d(TAG, "onCreate: 主Activity创建流程结束"); @@ -129,6 +139,24 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct 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); + } + if (mADsBannerView != null) { mADsBannerView.resumeADs(MainActivity.this); LogUtils.d(TAG, "onResume: 广告栏资源已恢复"); @@ -143,59 +171,91 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct mADsBannerView.releaseAdResources(); LogUtils.d(TAG, "onDestroy: 广告栏资源已释放"); } + // 移除电话状态监听,避免内存泄漏(Java 7 规范) + if (telephonyManager != null && phoneStateListener != null) { + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + } LogUtils.d(TAG, "onDestroy: 主Activity销毁完成"); } - // ====================== 权限相关函数区 ====================== - private boolean checkAllRequiredPermissions() { - for (String permission : REQUIRED_PERMISSIONS) { - if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { - LogUtils.w(TAG, "checkAllRequiredPermissions: 权限[" + permission + "]未授予"); - return false; - } - } - return true; - } - - private void requestAllRequiredPermissions() { - ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS); - } - + // ====================== 权限相关函数区(完全接入 PermissionUtils,移除冗余代码) ====================== @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 i = 0; i < grantResults.length; i++) { - if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { - allPermissionsGranted = false; - LogUtils.e(TAG, "onRequestPermissionsResult: 权限[" + permissions[i] + "]被拒绝"); + // 使用工具类解析被拒绝的权限,简化字符串拼接逻辑 + String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions); + if (deniedPerms.length() == 0) { + LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功"); + if (PermissionUtils.isOverlayPermissionGranted(this)) { + if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { + PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); + } else { + initUIAndLogic(null); + } + } else { + PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION); } - } - - if (allPermissionsGranted) { - LogUtils.d(TAG, "onRequestPermissionsResult: 所有权限授予成功,初始化UI和逻辑"); - initUIAndLogic(null); } else { LogUtils.e(TAG, "onRequestPermissionsResult: 存在权限被拒绝,弹出提示对话框"); - showPermissionDeniedDialogAndExit(); + String tip = "应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。"; + showPermissionDeniedDialogAndExit(tip); } } } - private void showPermissionDeniedDialogAndExit() { + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "onActivityResult: 回调触发,requestCode=" + requestCode + ",resultCode=" + resultCode); + if (requestCode == 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(); + } + } else if (requestCode == REQUEST_APP_SETTINGS) { + LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity"); + recreate(); + } else if (requestCode == REQUEST_OVERLAY_PERMISSION) { + LogUtils.d(TAG, "onActivityResult: 从悬浮窗设置页返回"); + if (PermissionUtils.isOverlayPermissionGranted(this)) { + LogUtils.d(TAG, "onActivityResult: 悬浮窗权限申请成功"); + if (Build.VERSION.SDK_INT >= ANDROID_10_API && !PermissionUtils.isCallScreeningPermissionGranted(this)) { + PermissionUtils.requestCallScreeningPermission(this, REQUEST_CALL_SCREENING_PERMISSION); + } else { + initUIAndLogic(null); + } + } else { + LogUtils.e(TAG, "onActivityResult: 悬浮窗权限申请失败"); + showPermissionDeniedDialogAndExit("应用需要悬浮窗权限才能展示来电弹窗,请授予后重新打开应用。"); + } + } else if (requestCode == REQUEST_CALL_SCREENING_PERMISSION) { + LogUtils.d(TAG, "onActivityResult: 从通话筛选设置页返回"); + if (PermissionUtils.isCallScreeningPermissionGranted(this)) { + LogUtils.d(TAG, "onActivityResult: 通话筛选权限申请成功"); + initUIAndLogic(null); + } else { + LogUtils.e(TAG, "onActivityResult: 通话筛选权限申请失败"); + showPermissionDeniedDialogAndExit("应用需要通话筛选权限监听外拨电话,请授予后重新打开应用。"); + } + } + } + + /** + * 权限拒绝提示对话框(纯 Java 7 匿名内部类,无 Lambda) + */ + private void showPermissionDeniedDialogAndExit(String tip) { new AlertDialog.Builder(this) .setTitle("权限不足,无法使用") - .setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。") + .setMessage(tip) .setCancelable(false) - .setNegativeButton("设置权限", new android.content.DialogInterface.OnClickListener() { + .setNegativeButton("去设置", new android.content.DialogInterface.OnClickListener() { @Override public void onClick(android.content.DialogInterface dialog, int which) { dialog.dismiss(); LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户点击设置权限,跳转应用设置页"); - AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil(); - appGoToSettingsUtil.goToSetting(MainActivity.this); + PermissionUtils.goAppDetailsSettings(MainActivity.this); } }) .setPositiveButton("确定退出", new android.content.DialogInterface.OnClickListener() { @@ -209,8 +269,12 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct .show(); } - // ====================== UI与逻辑初始化区 ====================== + // ====================== 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); @@ -234,21 +298,17 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct initMainService(); LogUtils.d(TAG, "initUIAndLogic: 主服务初始化完成"); - // 电话状态监听初始化 + // 电话状态监听初始化(适配 API 30,启动通话筛选服务) 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); - + // 盾值视图初始化(Java 7 数组初始化规范) + 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}; + tempView.setGradientColors(customColors, positions); } private void initViewPagerAndTabs() { @@ -264,7 +324,7 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList); viewPager.setAdapter(adapter); - viewPager.setOffscreenPageLimit(0); + viewPager.setOffscreenPageLimit(2); // 优化:保留2个缓存页,减少重建 viewPager.addOnPageChangeListener(this); } @@ -276,8 +336,9 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct MainServiceBean.saveBean(this, mMainServiceBean); } - if (mMainServiceBean.isEnable()) { - LogUtils.d(TAG, "initMainService: 主服务已启用,延迟1秒启动服务"); + // 双重判断:服务是否启用 + 是否已运行,避免重复启动 + if (mMainServiceBean.isEnable() && !isServiceRunning(MainService.class)) { + LogUtils.d(TAG, "initMainService: 主服务已启用且未运行,延迟1秒启动服务"); new Handler().postDelayed(new Runnable() { @Override public void run() { @@ -285,14 +346,30 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct } }, 1000); } else { - LogUtils.d(TAG, "initMainService: 主服务未启用,跳过启动流程"); + LogUtils.d(TAG, "initMainService: 主服务未启用或已运行,跳过启动流程"); } } + /** + * 初始化通话监听(API 30 适配,启动通话筛选服务) + */ private void initPhoneStateListener() { - telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); phoneStateListener = new MyPhoneStateListener(); + // 仅监听通话状态,避免冗余监听 telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + + // 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); + // 适配 Android O 以上前台服务启动要求 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(screeningIntent); + } else { + startService(screeningIntent); + } + LogUtils.d(TAG, "initPhoneStateListener: API 30+ 通话筛选服务已启动"); + } } // ====================== 菜单相关函数区 ====================== @@ -328,22 +405,22 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct @Override public void onClick(View v) {} - // ====================== 电话相关工具函数区 ====================== + // ====================== 电话相关工具函数区(优化权限检查,接入工具类) ====================== 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) { + 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(); - return; } - LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber); - _MainActivity.startActivity(intent); } public boolean isDefaultPhoneCallApp() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - TelecomManager manager = (TelecomManager) getSystemService(TELECOM_SERVICE); + 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); @@ -372,23 +449,7 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct return false; } - // ====================== 活动结果回调区 ====================== - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - LogUtils.d(TAG, "onActivityResult: 回调触发,requestCode=" + requestCode + ",resultCode=" + resultCode); - if (requestCode == 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(); - } - } else if (requestCode == REQUEST_APP_SETTINGS) { - LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity"); - recreate(); - } - } - - // ====================== 内部类定义区 ====================== + // ====================== 内部类定义区(Java 7 规范,无 Lambda) ====================== private class MyPagerAdapter extends FragmentPagerAdapter { private final List fragmentList; private final List tabTitleList; @@ -419,6 +480,7 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct private class MyPhoneStateListener extends PhoneStateListener { @Override public void onCallStateChanged(int state, String incomingNumber) { + super.onCallStateChanged(state, incomingNumber); switch (state) { case TelephonyManager.CALL_STATE_IDLE: LogUtils.d(TAG, "onCallStateChanged: 电话状态-空闲"); @@ -429,6 +491,9 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct case TelephonyManager.CALL_STATE_RINGING: LogUtils.d(TAG, "onCallStateChanged: 电话状态-来电,号码=" + incomingNumber); break; + default: + LogUtils.d(TAG, "onCallStateChanged: 未知通话状态,state=" + state); + break; } } } 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 42e98aa..35336a8 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 @@ -1,5 +1,8 @@ package cc.winboll.studio.contacts.listenphonecall; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -14,29 +17,45 @@ 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.phonecallui.PhoneCallActivity; import cc.winboll.studio.contacts.phonecallui.PhoneCallService; +import cc.winboll.studio.libappbase.LogUtils; - +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/12 16:06 + * @Describe 通话监听服务,监听来电状态并展示悬浮窗 + */ public class CallListenerService extends Service { + // 常量定义区 + 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; + + // View 相关属性 private View phoneCallView; private TextView tvCallNumber; private Button btnOpenApp; + // 系统服务相关属性 private WindowManager windowManager; private WindowManager.LayoutParams params; - private PhoneStateListener phoneStateListener; private TelephonyManager telephonyManager; + // 业务逻辑相关属性 private String callNumber; private boolean hasShown; private boolean isCallingIn; @@ -44,160 +63,203 @@ public class CallListenerService extends Service { @Override public void onCreate() { super.onCreate(); - + LogUtils.d(TAG, "onCreate: 通话监听服务启动"); + // 前台服务必须调用 startForeground,避免 5 秒后被系统杀死 + 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: 前台服务启动(兼容低版本)"); + } initPhoneStateListener(); - initPhoneCallView(); } @Nullable @Override public IBinder onBind(Intent intent) { + LogUtils.d(TAG, "onBind: 服务绑定"); return null; } /** - * 初始化来电状态监听器 + * 初始化前台服务通知(必须实现) + */ + 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) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "通话监听服务", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("后台监听通话状态"); + 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(); + } + + /** + * 初始化来电状态监听器,添加空指针兜底 */ private void initPhoneStateListener() { + LogUtils.d(TAG, "initPhoneStateListener: 初始化来电状态监听器"); phoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { super.onCallStateChanged(state, incomingNumber); - callNumber = incomingNumber; - + LogUtils.d(TAG, "onCallStateChanged: 通话状态变更 state=" + state + " number=" + incomingNumber); switch (state) { - case TelephonyManager.CALL_STATE_IDLE: // 待机,即无电话时,挂断时触发 + case TelephonyManager.CALL_STATE_IDLE: + LogUtils.d(TAG, "onCallStateChanged: 通话结束,关闭悬浮窗"); dismiss(); break; - - case TelephonyManager.CALL_STATE_RINGING: // 响铃,来电时触发 + case TelephonyManager.CALL_STATE_RINGING: + LogUtils.d(TAG, "onCallStateChanged: 来电响铃,展示悬浮窗"); isCallingIn = true; updateUI(); show(); break; - - case TelephonyManager.CALL_STATE_OFFHOOK: // 摘机,接听或拨出电话时触发 + case TelephonyManager.CALL_STATE_OFFHOOK: + LogUtils.d(TAG, "onCallStateChanged: 通话接通,展示悬浮窗"); updateUI(); show(); break; - default: + LogUtils.d(TAG, "onCallStateChanged: 未知通话状态"); break; - } } }; - - // 设置来电监听器 + // 空指针兜底,避免 TelephonyManager 获取失败 telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); if (telephonyManager != null) { telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + LogUtils.d(TAG, "initPhoneStateListener: 来电监听器注册成功"); + } else { + LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager 获取失败"); } - } private void initPhoneCallView() { - windowManager = (WindowManager) getApplicationContext() - .getSystemService(Context.WINDOW_SERVICE); - int width = windowManager.getDefaultDisplay().getWidth(); - int height = windowManager.getDefaultDisplay().getHeight(); + LogUtils.d(TAG, "initPhoneCallView: 初始化悬浮窗视图"); + windowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + if (windowManager == null) { + LogUtils.e(TAG, "initPhoneCallView: WindowManager 获取失败"); + return; + } + int width = windowManager.getDefaultDisplay().getWidth(); 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; - // 不可聚集(不响应返回键)| 全屏 + // 悬浮窗类型适配(Android 8.0+ 必须用 TYPE_APPLICATION_OVERLAY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + params.type = WindowManager.LayoutParams.TYPE_PHONE; + } + + // 优化 Flag:移除 FLAG_FULLSCREEN 避免遮挡状态栏 params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; - // API 19 以上则还可以开启透明状态栏与导航栏 + | 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_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + params.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; } 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; - } + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + LogUtils.d(TAG, "dispatchKeyEvent: 拦截返回键"); + 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); - } - }); + 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); + 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); + } + }); + LogUtils.d(TAG, "initPhoneCallView: 悬浮窗视图初始化完成"); } - /** - * 显示顶级弹框展示通话信息 - */ private void show() { - if (!hasShown) { - windowManager.addView(phoneCallView, params); - hasShown = true; + if (!hasShown && phoneCallView != null && windowManager != null) { + try { + windowManager.addView(phoneCallView, params); + hasShown = true; + LogUtils.d(TAG, "show: 悬浮窗展示成功"); + } catch (Exception e) { + LogUtils.e(TAG, "show: 悬浮窗展示失败", e); + } } } /** - * 取消显示 + * 优化 dismiss 方法:添加 try-catch 避免移除视图失败崩溃 */ private void dismiss() { - if (hasShown) { - windowManager.removeView(phoneCallView); - isCallingIn = false; - hasShown = false; + if (hasShown && phoneCallView != null && windowManager != null) { + try { + windowManager.removeView(phoneCallView); + LogUtils.d(TAG, "dismiss: 悬浮窗关闭成功"); + } catch (Exception e) { + LogUtils.e(TAG, "dismiss: 悬浮窗关闭失败", e); + } finally { + isCallingIn = false; + hasShown = false; + } } } private void updateUI() { - tvCallNumber.setText(formatPhoneNumber(callNumber)); - + if (tvCallNumber == null) { + LogUtils.e(TAG, "updateUI: 号码显示控件为空"); + return; + } + String formatNumber = formatPhoneNumber(callNumber); + 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); } 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; } @@ -205,7 +267,20 @@ public class CallListenerService extends Service { @Override public void onDestroy() { super.onDestroy(); - - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + LogUtils.d(TAG, "onDestroy: 通话监听服务销毁"); + // 释放资源,避免内存泄漏 + dismiss(); + if (telephonyManager != null) { + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + LogUtils.d(TAG, "onDestroy: 来电监听器注销成功"); + } + // 清空引用 + phoneStateListener = null; + telephonyManager = null; + windowManager = null; + phoneCallView = null; + tvCallNumber = null; + btnOpenApp = null; } } + 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 a8b9c8d..e558197 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 @@ -51,7 +51,10 @@ public class MainService extends Service { 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; + //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; private static final int ANDROID_8_API = 26; 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 new file mode 100644 index 0000000..8a21033 --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java @@ -0,0 +1,94 @@ +package cc.winboll.studio.contacts.services; + +import android.content.Context; +import android.os.Build; +import android.telecom.CallScreeningService; +import android.telephony.TelephonyManager; +import androidx.annotation.RequiresApi; +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 中注册 + */ +@RequiresApi(api = Build.VERSION_CODES.Q) // 标注最低支持API 29,适配API30 +public class MyCallScreeningService extends CallScreeningService { + public static final String TAG = "MyCallScreeningService"; + private Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + LogUtils.d(TAG, "通话筛选服务已启动"); + } + + /** + * 核心回调:处理来电/外拨电话的筛选逻辑 + * 补充@RequiresApi,避免低版本编译警告 + */ + @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 ? "来电" : "外拨"; + + LogUtils.d(TAG, String.format("检测到%s:%s", callTypeStr, phoneNumber)); + + // 2. 自定义筛选逻辑示例 + boolean shouldBlock = "10086".equals(phoneNumber); + + // 3. 构建筛选响应 + CallResponse.Builder responseBuilder = new CallResponse.Builder(); + responseBuilder.setDisallowCall(shouldBlock); + responseBuilder.setRejectCall(shouldBlock); + responseBuilder.setSkipCallLog(!shouldBlock); + responseBuilder.setSkipNotification(!shouldBlock); + + // 4. 发送筛选结果 + callback.onCallScreeningResponse(responseBuilder.build()); + + // 5. 自定义业务逻辑 + if (!shouldBlock) { + handleNormalCall(phoneNumber, callType); + } else { + LogUtils.d(TAG, "已拦截通话:" + phoneNumber); + } + } + + /** + * 判断通话类型(来电/外拨) + * 补充@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) { + // 如需使用LocalBroadcastManager,需添加support-v4依赖并补充引用 + // import androidx.localbroadcastmanager.content.LocalBroadcastManager; + } + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "通话筛选服务已销毁"); + } +} + 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 new file mode 100644 index 0000000..6ff59a0 --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/PermissionUtils.java @@ -0,0 +1,217 @@ +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&豆包大模型 + * @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 版本硬编码常量 + private static final int ANDROID_6_API = 23; + private static final int ANDROID_10_API = 29; + private static final int ANDROID_13_API = 33; + + // 硬编码 API 33 新增的常量字符串,解决未定义问题 + 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 传统循环) + */ + public static String[] getAllNeedPermissions() { + List permissions = new ArrayList(); + for (int i = 0; i < BASE_PERMISSIONS.length; i++) { + permissions.add(BASE_PERMISSIONS[i]); + } + String[] permissionArray = new String[permissions.size()]; + return permissions.toArray(permissionArray); + } + + /** + * 检查单个权限是否授予 + */ + public static boolean checkPermission(@NonNull Context context, @NonNull String permission) { + return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; + } + + /** + * 检查权限组是否全部授予 + */ + public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) { + for (int i = 0; i < permissions.length; i++) { + String permission = permissions[i]; + if (!checkPermission(context, permission)) { + return false; + } + } + return true; + } + + /** + * 申请权限组(Activity 调用) + */ + public static void requestPermissions(@NonNull FragmentActivity activity, + @NonNull String[] permissions, + int requestCode) { + ActivityCompat.requestPermissions(activity, permissions, requestCode); + } + + /** + * 申请权限组(Fragment 调用) + */ + public static void requestPermissions(@NonNull Fragment fragment, + @NonNull String[] permissions, + int requestCode) { + fragment.requestPermissions(permissions, requestCode); + } + + /** + * 检查悬浮窗权限 + */ + public static boolean isOverlayPermissionGranted(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= ANDROID_6_API) { + return Settings.canDrawOverlays(context); + } + return true; + } + + /** + * 申请悬浮窗权限 + */ + 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); + } + } + } + + /** + * 检查修改系统设置权限 + */ + public static boolean isWriteSettingsPermissionGranted(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= ANDROID_6_API) { + return Settings.System.canWrite(context); + } + return true; + } + + /** + * 申请修改系统设置权限 + */ + 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; + // 反射调用 API 33+ 新增方法,低版本异常兜底 + try { + Method method = TelecomManager.class.getMethod("getDefaultCallScreeningAppPackage"); + defaultPackage = (String) method.invoke(telecomManager); + } catch (Exception e) { + // API 30-32 无此方法,直接返回未授权状态 + return false; + } + return defaultPackage != null && defaultPackage.equals(context.getPackageName()); + } + return true; + } + + /** + * 申请通话筛选权限(完全适配 API 30:硬编码常量 + 版本分级处理) + */ + 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); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + if (context instanceof FragmentActivity) { + ((FragmentActivity) context).startActivityForResult(intent, requestCode); + } + } else { + // API 30-32 无系统授权页,引导用户手动到应用详情页找相关权限 + goAppDetailsSettings(context); + } + } + } + + /** + * 跳转应用详情页(权限兜底引导) + */ + 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 字符串操作) + */ + public static String getDeniedPermissions(@NonNull Context context, @NonNull String[] permissions) { + StringBuilder deniedPerms = new StringBuilder(); + 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("、"); + } + } + if (deniedPerms.length() > 0) { + deniedPerms.deleteCharAt(deniedPerms.length() - 1); + } + return deniedPerms.toString(); + } +} +