服务启动应用配置问题修复。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -100,10 +100,10 @@
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- 辅助服务:dataSync + microphone 类型(适配录音业务) -->
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync|microphone"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
@@ -135,9 +135,10 @@
|
||||
</service>
|
||||
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:foregroundServiceType="phoneCall"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主服务连接回调
|
||||
|
||||
@@ -34,30 +34,24 @@ import java.util.TimerTask;
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @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);
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,24 +18,25 @@ import java.util.List;
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @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<String> permissions = new ArrayList<String>();
|
||||
// Java 7 传统循环遍历数组
|
||||
for (int i = 0; i < BASE_PERMISSIONS.length; i++) {
|
||||
permissions.add(BASE_PERMISSIONS[i]);
|
||||
}
|
||||
// 显式创建数组并转换,避免 Java 7 泛型转换警告
|
||||
String[] permissionArray = new String[permissions.size()];
|
||||
return permissions.toArray(permissionArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个权限是否授予
|
||||
* 检查单个权限是否授予(使用 PackageManager 标准常量)
|
||||
*/
|
||||
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
|
||||
return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限组是否全部授予
|
||||
* 检查权限组是否全部授予(Java 7 传统循环)
|
||||
*/
|
||||
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
// Java 7 遍历数组,避免增强 for 循环的语法糖问题
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
if (!checkPermission(context, permission)) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user