20251212_171425_295应用权限申请部分改造,版本可能会回退。。。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -9,36 +9,40 @@
|
||||
<!-- 拨打电话 -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||
|
||||
<!-- 读取手机状态和身份 -->
|
||||
<!-- 读取手机状态和身份(API 30+ 需细化权限) -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
||||
|
||||
<!-- 修改系统设置 -->
|
||||
<!-- 修改系统设置(移除无效的 protectionLevel 声明,该属性由系统定义) -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
|
||||
<!-- 重新设置外拨电话的路径 -->
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
|
||||
|
||||
<!-- 读取联系人 -->
|
||||
<!-- 联系人权限(适配 Android 13+ 细分权限) -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
|
||||
<!-- 修改您的通讯录 -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.GET_CONTACTS"/>
|
||||
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<!-- 悬浮窗权限(需动态申请) -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 更改您的音频设置 -->
|
||||
<!-- 更改音频设置 -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
|
||||
<!-- 读取通话记录 -->
|
||||
<!-- 通话记录权限(适配 Android 13+ 细分权限) -->
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
|
||||
|
||||
<!-- 录音 -->
|
||||
<!-- 录音权限 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
|
||||
<!-- 前台服务权限 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!-- 前台服务权限(按业务类型声明) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||
|
||||
<!-- API 30+ 通话筛选服务权限(替代 PROCESS_OUTGOING_CALLS) -->
|
||||
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -55,11 +59,8 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
@@ -69,7 +70,6 @@
|
||||
android:label="CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -78,41 +78,35 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="tel"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
|
||||
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync|phoneCall"
|
||||
android:exported="false" />
|
||||
<!-- 主服务:仅 dataSync 类型(与代码中 0x00000008 匹配) -->
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
<!-- 辅助服务:dataSync + microphone 类型(适配录音业务) -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync|microphone"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
@@ -123,34 +117,37 @@
|
||||
android:value="true"/>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.telecom.InCallService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- 通话监听服务:phoneCall 类型(与代码中 0x00000020 匹配) -->
|
||||
<service
|
||||
android:name=".listenphonecall.CallListenerService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name=".service.CallShowService"/>
|
||||
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
@@ -158,13 +155,9 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
@@ -174,13 +167,9 @@
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
@@ -202,3 +191,4 @@
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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<Fragment> fragmentList;
|
||||
private final List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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, "通话筛选服务已销毁");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/12 16:28
|
||||
* @Describe 敏感权限申请工具类(完全适配 Android API 30 + Java 7)
|
||||
* 修复 ACTION_CHANGE_DEFAULT_CALL_SCREENING_APP / EXTRA_PACKAGE_NAME 未定义问题
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static final String TAG = "PermissionUtils";
|
||||
|
||||
// API 版本硬编码常量
|
||||
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<String> permissions = new ArrayList<String>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user