Android 自定义来电秀实现总结

作者:小肥羊冲冲冲Android 时间:2023-12-17 07:52:32 

前言

该文章为对工作中部分业务实现的总结,阅读时间:20分钟,版本:Android 6.0 - 9.0 update time 2021年02月03日11:48:55 文章可能存在不足之处,还望评论批评,一起学习进步。

要想实现自定义 来电秀,首先我们先这样 再这样,然后你这样,最后你再这样一下,就可以了,很好实现的,听懂了么?-,-

效果图

Android 自定义来电秀实现总结

  • 添加包活lib,提高App在设置成功后 退居后台,成功拉起的概率

  • 项目中已经包含lib_ijk的代码,我们可以添加视频来电展示,添加美女或者豪车等全屏视频,效果更佳。

  • 由于反编译能力有限,对于多种机型权限的跳转(后续可以开起 无障碍服务,直接一步搞定多种需要用户手动设置操作)

  • 该Demo中有一部分不完善的Rom 权限跳转机制,后续还需要时间来完善。

参考文章 来电秀实现

实现思想

  • 通过监听手机Service 分辨来电状态,然后弹出我们自定义的来电页面,覆盖系统来电页面。

  • 通过相关API (主要两种:读取来电系统的Notification信息 和 模拟耳机线控的方式进行挂断/接听)实现接听和挂断功能。我这里会使用两种(低版本 使用电话状态广播监听,高版本使用InCallService) 监听电话状态的Service 及两种界面展示 来呈现来电信息,多个界面和多个Service的监听 能够增加高版本的容错率兼容性。

  • 实现自定义的拨号界面 或者 直接使用系统的拨号界面。

注意:因为篇幅问题,博客只会截取部分代码,太长读者很难读下去,Demo已经调试通过,如果有想看源码的可以移步到我的 GitHub项目地址

申请权限

静态权限

  电话应用,会用到很多权限,我这里尽可能多的静态注册了一些权限,如果引入项目中,需要甄别下,代码如下:

<uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
   <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
   <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
   <uses-permission android:name="android.permission.READ_PHONE_STATE" />
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
   <uses-permission android:name="android.permission.WRITE_SETTINGS" />
   <!-- 读取联系人权限 -->
   <uses-permission android:name="android.permission.READ_CONTACTS" />
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
   <uses-permission android:name="android.permission.WAKE_LOCK" />
   <uses-permission android:name="android.permission.DEVICE_POWER" />
   <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
   <uses-permission android:name="android.permission.CALL_PHONE" />
   <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
   <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
   <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
   <!-- 读写 联系信息 显示联系人名称 -->
   <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
   <uses-permission android:name="android.permission.READ_CALL_LOG" />
   <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
   <uses-permission android:name="android.permission.READ_LOGS" />
   <uses-permission android:name="android.permission.RECORD_AUDIO" />
   <uses-permission android:name="android.permission.VIBRATE" />
   <uses-permission android:name="android.permission.GET_TASKS" />
   <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
   <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
   <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
   <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
   <uses-permission android:name="android.permission.BROADCAST_PACKAGE_ADDED" />
   <uses-permission android:name="android.permission.BROADCAST_PACKAGE_CHANGED" />
   <uses-permission android:name="android.permission.BROADCAST_PACKAGE_INSTALL" />
   <uses-permission android:name="android.permission.BROADCAST_PACKAGE_REPLACED" />
   <uses-permission android:name="android.permission.RESTART_PACKAGES" />
   <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
   <!--android 9.0上使用前台服务,需要添加权限-->
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
   <permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />

动态权限

AndPermission.with(this)
               .runtime()
               .permission(
                       Permission.Group.PHONE,
                       Permission.Group.LOCATION,
                       Permission.Group.CALL_LOG
               )
               .onGranted {
                   Toast.makeText(applicationContext, "权限同意", Toast.LENGTH_SHORT).show()
               }.onDenied {
                   Toast.makeText(applicationContext, "权限拒绝", Toast.LENGTH_SHORT).show()
               }.start()

&emsp;&emsp;上述代码,为自己测试使用的Demo,所以请求权限直接请求分组中的全部权限了,项目中根据需要动态申请部分权限

&emsp;&emsp;虽然我们已经申请了这么多权限,但是为了能够替换系统电话界面成功,还有一部分权限是需要通过弹框来引导用户去 设置中开启的。

# CallerShowPermissionManager.kt
/**
    * 判断是否有 锁屏弹出、 后台弹出悬浮窗 、允许系统修改、读取通知栏等权限(必须同意)
    */
   fun setRingPermission(context: Context): Boolean {
       perArray.clear()
       if (!OpPermissionUtils.checkPermission(context)) {
           //跳转到悬浮窗设置
           toRequestFloatWindPermission(context)
       }
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) {
           //准许系统修改
           opWriteSetting(context)
       }
       if (!isAllowed(context)) {
           //后台弹出权限
           openSettings(context)
       }
       if (!notificationListenerEnable(context)) {
           //通知使用权
           gotoNotificationAccessSetting()
       }
       if (perArray.size != 0) {
           context.startActivities(perArray.toTypedArray())
           return false
       } else {
           LogUtils.e("铃声 高级权限全部同意")
           return true
       }
   }
/**
    * 点击授权按钮,编辑好需要申请的权限后,统一跳转,oppo/小米 的后台弹出权限 锁屏显示权限,
    * 需要用户去设置中手动开始,在项目中 可以使用 蒙层引导用户点击
    */
   fun setRingPermission(context: Context): Boolean {
       perArray.clear()
       if (!OpPermissionUtils.checkPermission(context)) {
           //跳转到悬浮窗设置
           toRequestFloatWindPermission(context)
       }
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) {
           //准许系统修改
           opWriteSetting(context)
       }
       if (!isAllowed(context)) {
           //后台弹出权限
           openSettings(context)
       }
       if (!notificationListenerEnable(context)) {
           //通知使用权
           gotoNotificationAccessSetting()
       }
       if (perArray.size != 0) {
           context.startActivities(perArray.toTypedArray())
           return false
       } else {
           LogUtils.e("铃声 高级权限全部同意")
           return true
       }
   }
/**
    * 申请悬浮窗权限
    */
   private fun toRequestFloatWindPermission(context: Context) {
       try {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
               val clazz: Class<*> = Settings::class.java
               val field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION")
               val intent = Intent(field[null].toString())
               intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
               intent.data = Uri.parse("package:" + context.packageName)
               perArray.add(intent)
               return
           }
           val intent2 = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
           context.startActivity(intent2)
           return
       } catch (e: Exception) {
           if (RomUtils.checkIsMeizuRom()) {
               try {
                   val intent = Intent("com.meizu.safe.security.SHOW_APPSEC")
                   intent.putExtra("packageName", context.packageName)
                   intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                   context.startActivity(intent)
               } catch (e: java.lang.Exception) {
                   LogUtils.e("请在权限管理中打开悬浮窗管理权限")
               }
           }
           LogUtils.e("请在权限管理中打开悬浮窗管理权限")
           return
       }
   }
   /**
    * 判断锁屏显示
    */
   private fun isLock(context: Context): Boolean {
       if (RomUtils.checkIsMiuiRom()) {
           return MiuiUtils.canShowLockView(context)
       } else if (RomUtils.checkIsVivoRom()) {
           return VivoUtils.getVivoLockStatus(context)
       }
       return true
   }
   /**
    * 判断锁屏显示
    */
   private fun isAllowed(context: Context): Boolean {
       if (RomUtils.checkIsMiuiRom()) {
           return MiuiUtils.isAllowed(context)
       } else if (RomUtils.checkIsVivoRom()) {
           return VivoUtils.getvivoBgStartActivityPermissionStatus(context)
       }
       return true
   }
   /**
    * 打开设置(后台弹出 锁屏显示)
    */
   private fun openSettings(context: Context) {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           try {
               val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
               intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
               intent.data = Uri.parse("package:${context.packageName}")
               perArray.add(intent)
           } catch (e: java.lang.Exception) {
               LogUtils.e("请在权限管理中打开后台弹出权限")
           }
       } else {
           LogUtils.e("android 6.0以下")
       }
   }
   /**
    * 系统修改
    */
   private fun opWriteSetting(context: Context) {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           if (!Settings.System.canWrite(context)) {
               val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
               intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
               intent.data = Uri.parse("package:${context.packageName}")
               perArray.add(intent)
           }
       }
   }
   /**
    * 读取系统通知
    */
   private fun gotoNotificationAccessSetting() {
       try {
           val intent = Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
           intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
           perArray.add(intent)
       } catch (e: ActivityNotFoundException) {
           try {
               val intent = Intent()
               intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
               val cn = ComponentName("com.android.settings", "com.android.settings.Settings\$NotificationAccessSettingsActivity");
               intent.component = cn
               intent.putExtra(":settings:show_fragment", "NotificationAccessSettings")
               perArray.add(intent)
           } catch (ex: Exception) {
               LogUtils.e("获取系统通知失败 e : $ex")
           }
       }
   }
// 暂时把重要代码cv出来了一部分,建议下载Demo源码 ,结合博客一起观看

上述代码 主要罗列了需要引导用户开启部分设置权限的核心代码和方法。

监听电话

对于监听电话这块,会有很多兼容性的问题,我们这里先使用广播监听 action = android.intent.action.PHONE_STATE 的广播,然后根据状态调用起来悬浮窗。但是测试Android高版本手机 发现 InCallService 会更好的获取到电话状态,所以我这里的处理方案是 两个方案都保存在了代码中,最后通过调用不同的界面来区分。

BroadcastReceiver +悬浮窗显示实现

# AndroidManifest.xml
// 监听电话状态广播 注册
<receiver android:name=".phone.receiver.PhoneStateReceiver">
           <intent-filter android:priority="2147483647">
               <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
               <category android:name="android.intent.category.DEFAULT" />
           </intent-filter>
           <intent-filter android:priority="2147483647">
               <action android:name="android.intent.action.PHONE_STATE" />
           </intent-filter>
           <intent-filter android:priority="2147483647">
               <action android:name="android.intent.action.DUAL_PHONE_STATE" />
           </intent-filter>
           <intent-filter android:priority="2147483647">
               <action android:name="android.intent.action.PHONE_STATE_2" />
           </intent-filter>
           <intent-filter android:priority="2147483647">
               <action android:name="com.cootek.smartdialer.action.PHONE_STATE" />
           </intent-filter>
           <intent-filter android:priority="2147483647">
               <action android:name="com.cootek.smartdialer.action.INCOMING_CALL" />
           </intent-filter>
       </receiver>
# PhoneStateReceiver.kt
class PhoneStateReceiver : BroadcastReceiver() {
   override fun onReceive(context: Context?, intent: Intent?) {
       context?.let {
           val action = intent?.action
           if (Intent.ACTION_NEW_OUTGOING_CALL == action || TelephonyManager.ACTION_PHONE_STATE_CHANGED == action) {
               try {
                   val manager = it.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
                   var state = manager.callState
                   val phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)
                   if (Intent.ACTION_NEW_OUTGOING_CALL.equals(action, true)) {
                       state = 1000
                   }
                   dealWithCallAction(state, phoneNumber)
               } catch (e: Exception) {
               }
           }
       }
   }
   //来去电的几个状态
   private fun dealWithCallAction(state: Int?, phoneNumber: String?) {
       when (state) {
           // 来电状态 - 显示悬浮窗
           TelephonyManager.CALL_STATE_RINGING -> {
               PhoneStateActionImpl.instance.onRinging(phoneNumber)
           }
           // 空闲状态(挂断) - 关闭悬浮窗
           TelephonyManager.CALL_STATE_IDLE -> {
               PhoneStateActionImpl.instance.onHandUp()
           }
           // 摘机状态(接听) - 保持不作操作
           TelephonyManager.CALL_STATE_OFFHOOK -> {
               PhoneStateActionImpl.instance.onPickUp(phoneNumber)
           }
           1000 -> {   //拨打电话广播状态  - 显示悬浮窗
               PhoneStateActionImpl.instance.onCallOut(phoneNumber)
           }
       }
   }
}

获取到广播的信息后 我们就可以着手 悬浮窗的绘制和 初始化工作

# FloatingWindow.kt
private fun initView() {
       windowManager = mContext?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
       params = WindowManager.LayoutParams()
       //高版本适配 全面/刘海屏
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
           params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
       }
       params.gravity = Gravity.CENTER
       params.width = WindowManager.LayoutParams.MATCH_PARENT
       params.height = WindowManager.LayoutParams.MATCH_PARENT
       params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
       params.format = PixelFormat.TRANSLUCENT
       // 设置 Window flag 为系统级弹框 | 覆盖表层
       params.type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
           WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
       else
           WindowManager.LayoutParams.TYPE_PHONE
       // 去掉FLAG_NOT_FOCUSABLE隐藏输入 全面屏隐藏虚拟物理按钮办法
       params.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN or
               WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or
               WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION or
               WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
       params.systemUiVisibility =
               View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
                       View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
                       View.SYSTEM_UI_FLAG_FULLSCREEN
       val interceptorLayout: FrameLayout = object : FrameLayout(mContext!!) {
           override fun dispatchKeyEvent(event: KeyEvent): Boolean {
               if (event.action == KeyEvent.ACTION_DOWN) {
                   if (event.keyCode == KeyEvent.KEYCODE_BACK) {
                       return true
                   }
               }
               return super.dispatchKeyEvent(event)
           }
       }
       phoneCallView = LayoutInflater.from(mContext).inflate(R.layout.view_phone_call, interceptorLayout)
       tvCallNumber = phoneCallView.findViewById(R.id.tv_call_number)
       tvPhoneHangUp = phoneCallView.findViewById(R.id.tv_phone_hang_up)
       tvPhonePickUp = phoneCallView.findViewById(R.id.tv_phone_pick_up)
       tvCallingTime = phoneCallView.findViewById(R.id.tv_phone_calling_time)
       tvCallRemark = phoneCallView.findViewById(R.id.tv_call_remark)
   }
...
// 部分代码省略

悬浮窗展示完成后,就要设置电话接通和挂断的操作(注意:这里很多低版本手机存在兼容问题,所以会有一些代码比较奇怪)

# IPhoneCallListenerImpl.kt
override fun onAnswer() {
       val mContext = App.context
       try {
           val intent = Intent(mContext, ForegroundActivity::class.java)
           intent.action = CallListenerService.ACTION_PHONE_CALL
           intent.putExtra(CallListenerService.PHONE_CALL_ANSWER, "0")
           intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
           mContext.startActivity(intent)
       } catch (e: Exception) {
           Log.e("ymc","startForegroundActivity exception>>$e")
           PhoneCallUtil.answer()
       }
   }
   override fun onOpenSpeaker() {
       PhoneCallUtil.openSpeaker()
   }
   override fun onDisconnect() {
       Log.e("ymc"," onDisconnect")
       val mContext = App.context
       try {
           val intent = Intent(mContext, ForegroundActivity::class.java)
           intent.action = CallListenerService.ACTION_PHONE_CALL
           intent.putExtra(CallListenerService.PHONE_CALL_DISCONNECT, "0")
           intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
           mContext.startActivity(intent)
       } catch (e: Exception) {
           Log.e("ymc","startForegroundActivity exception>>$e")
           PhoneCallUtil.disconnect()
       }
   }

以上代码为接口实现类,我们这里会跳转到 一个前台Activity(一定程度上可以将App拉活),主要逻辑我们放在自己的前台Service中操作。

# CallListenerService.kt
// Andorid新版本 启动服务的方式
fun forceForeground(intent: Intent) {
       try {
           ContextCompat.startForegroundService(App.context, intent)
           notification = CustomNotifyManager.instance?.getNotifyNotification(App.context)
           if (notification != null) {
               startForeground(CustomNotifyManager.STEP_COUNT_NOTIFY_ID, notification)
           } else {
               startForeground(CustomNotifyManager.STEP_COUNT_NOTIFY_ID,
                       CustomNotifyManager.instance?.getDefaultNotification(NotificationCompat.Builder(App.context)))
           }
       } catch (e: Exception) {
       }
   }
   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
       if (intent == null) {
           return START_STICKY
       }
       val action = intent.action ?: return START_STICKY
       when (action) {
           ACTION_PHONE_CALL -> {
               dispatchAction(intent)
           }
       }
       return START_STICKY
   }
private fun dispatchAction(intent: Intent) {
       if (intent.hasExtra(PHONE_CALL_DISCONNECT)) {
           PhoneCallUtil.disconnect()
           return
       }
       if (intent.hasExtra(PHONE_CALL_ANSWER)) {
           PhoneCallUtil.answer()
       }
   }

为保证我们的服务能够正常吊起来,吊起前台服务,并设置Service等级,代码如下:

# AndroidManifest.xml
<!-- 电话状态接收广播 -->
       <service
           android:name=".phone.service.CallListenerService"
           android:enabled="true"
           android:exported="false">
           <intent-filter android:priority="1000">
               <action android:name="com.maiya.call.phone.service.CallListenerService" />
           </intent-filter>
       </service>
<!-- 监听通知栏权限 必备 -->
<service
           android:name=".phone.service.NotificationService"
           android:label="@string/app_name"
           android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
           <intent-filter>
               <action android:name="android.service.notification.NotificationListenerService" />
           </intent-filter>
       </service>

低版本的接通和挂断电话,因为需要兼容部分机型,所以我们会有比较多的判断,代码如下:

# PhoneCallUtil.kt
/**
    * 接听电话
    */
   fun answer() {
       when {
           Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
               val telecomManager = App.context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
               if (ActivityCompat.checkSelfPermission(App.context, Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
                   return
               }
               telecomManager.acceptRingingCall()
           }
           Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> {
               finalAnswer()
           }
           else -> {
               try {
                   val method: Method = Class.forName("android.os.ServiceManager")
                           .getMethod("getService", String::class.java)
                   val binder = method.invoke(null, Context.TELEPHONY_SERVICE) as IBinder
                   val telephony = ITelephony.Stub.asInterface(binder)
                   telephony.answerRingingCall()
               } catch (e: Exception) {
                   finalAnswer()
               }
           }
       }
   }
   private fun finalAnswer() {
       try {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
               val mediaSessionManager = App.context.getSystemService("media_session") as MediaSessionManager
               val activeSessions = mediaSessionManager.getActiveSessions(ComponentName(App.context, NotificationService::class.java)) as List<MediaController>
               if (activeSessions.isNotEmpty()) {
                   for (mediaController in activeSessions) {
                       if ("com.android.server.telecom" == mediaController.packageName) {
                           mediaController.dispatchMediaButtonEvent(KeyEvent(0, 79))
                           mediaController.dispatchMediaButtonEvent(KeyEvent(1, 79))
                           break
                       }
                   }
               }
           }
       } catch (e: Exception) {
           e.printStackTrace()
           answerPhoneAidl()
       }
   }
   private fun answerPhoneAidl() {
       try {
           val keyEvent = KeyEvent(0, 79)
           val keyEvent2 = KeyEvent(1, 79)
           if (Build.VERSION.SDK_INT >= 19) {
               @SuppressLint("WrongConstant") val audioManager = App.context.getSystemService("audio") as AudioManager
               audioManager.dispatchMediaKeyEvent(keyEvent)
               audioManager.dispatchMediaKeyEvent(keyEvent2)
           }
       } catch (ex: java.lang.Exception) {
           val intent = Intent("android.intent.action.MEDIA_BUTTON")
           intent.putExtra("android.intent.extra.KEY_EVENT", KeyEvent(0, 79) as Parcelable)
           App.context.sendOrderedBroadcast(intent, "android.permission.CALL_PRIVILEGED")
           val intent2 = Intent("android.intent.action.MEDIA_BUTTON")
           intent2.putExtra("android.intent.extra.KEY_EVENT", KeyEvent(1, 79) as Parcelable)
           App.context.sendOrderedBroadcast(intent2, "android.permission.CALL_PRIVILEGED")
       }
   }
   /**
    * 断开电话,包括来电时的拒接以及接听后的挂断
    */
   fun disconnect() {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           with(PhoneCallManager.instance) {
               if (!hasDefaultCall()) {
                   return@with
               }
               mainCallId?.let {
                   val result = disconnect(it)
                   if (result) {
                       return
                   }
               }
           }
       }
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
           val telecomManager = App.context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
           if (ActivityCompat.checkSelfPermission(App.context, Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
               return
           }
           telecomManager.endCall()
       } else {
           try {
               val method: Method = Class.forName("android.os.ServiceManager")
                       .getMethod("getService", String::class.java)
               val binder = method.invoke(null, Context.TELEPHONY_SERVICE) as IBinder
               val telephony = ITelephony.Stub.asInterface(binder)
               telephony.endCall()
           } catch (e: Exception) {
               e.printStackTrace()
           }
       }
   }

到这里中低版本的电话接通和挂断,基本已经完毕。下一步 我们主要写,用户在同意设置应用为默认电话应用后的 更加简单方便的实现方式。

InCallService + Activity实现

在使用 InCallService 服务的同时,需要设置该应用为默认拨号应用 (这里只说明技术的可能性,不对用户行为分析)。

# AndroidManifest.xml
<!-- 电话service -->
       <service
           android:name=".phone.service.PhoneCallService"
           android:permission="android.permission.BIND_INCALL_SERVICE">
           <!-- name为自己的Service名字,per和 filter中的name为固定值 -->
           <intent-filter>
               <action android:name="android.telecom.InCallService" />
           </intent-filter>
           <meta-data
               android:name="android.telecom.IN_CALL_SERVICE_UI"
               android:value="true" />
       </service>
# PhoneCallService.kt
@RequiresApi(Build.VERSION_CODES.M)
class PhoneCallService : InCallService() {
   companion object {
       const val ACTION_SPEAKER_ON = "action_speaker_on"
       const val ACTION_SPEAKER_OFF = "action_speaker_off"
       const val ACTION_MUTE_ON = "action_mute_on"
       const val ACTION_MUTE_OFF = "action_mute_off"
       fun startService(action: String?) {
           val intent = Intent(App.context, PhoneCallService::class.java).apply {
               this.action = action
           }
           App.context.startService(intent)
       }
   }
   // Call 添加 (Call对象需要判断是否有多个呼入的情况)
   override fun onCallAdded(call: Call?) {
       super.onCallAdded(call)
       call?.let {
           it.registerCallback(callback)
           PhoneCallManager.instance.addCall(it)
       }
   }
   // Call 移除 (可以理解为某一个通话的结束)
   override fun onCallRemoved(call: Call?) {
       super.onCallRemoved(call)
       call?.let {
           it.unregisterCallback(callback)
           PhoneCallManager.instance.removeCall(it)
       }
   }
   override fun onCanAddCallChanged(canAddCall: Boolean) {
       super.onCanAddCallChanged(canAddCall)
       PhoneCallManager.instance.onCanAddCallChanged(canAddCall)
   }
   // 将Call CallBack放在PhoneCallManager类中统一处理
   private val callback: Call.Callback = object : Call.Callback() {
       override fun onStateChanged(call: Call?, state: Int) {
           super.onStateChanged(call, state)
           PhoneCallManager.instance.onCallStateChanged(call, state)
       }
       override fun onCallDestroyed(call: Call) {
           call.hold()
           super.onCallDestroyed(call)
       }
   }
   // 设置扬声器
   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
       when (intent?.action) {
           ACTION_SPEAKER_ON -> setAudioRoute(CallAudioState.ROUTE_SPEAKER)
           ACTION_SPEAKER_OFF -> setAudioRoute(CallAudioState.ROUTE_EARPIECE)
           ACTION_MUTE_ON -> setMuted(true)
           ACTION_MUTE_OFF -> setMuted(false)
           else -> {
           }
       }
       return super.onStartCommand(intent, flags, startId)
   }
}

以上为InCallService的代码。部分方法进行了说明。

# PhoneCallManager.kt
/**
    * 接听电话
    */
   @RequiresApi(Build.VERSION_CODES.M)
   fun answer(callId: String?) =
           getCallById(callId)?.let {
               it.answer(VideoProfile.STATE_AUDIO_ONLY)
               true
           } ?: false
   /**
    * 断开电话,包括来电时的拒接以及接听后的挂断
    */
   @RequiresApi(Build.VERSION_CODES.M)
   fun disconnect(callId: String?) =
           getCallById(callId)?.let {
               it.disconnect()
               true
           } ?: false

由于篇幅问题,PhoneCallManager中的代码不全部展示,需要的小伙伴请移步Github,该类中主要进行了一些默认拨号应用,呼叫Call是否保持等一些操作。

来源:https://juejin.cn/post/7012428946373361671

标签:Android,自定义,来电秀
0
投稿

猜你喜欢

  • C#有效防止同一账号多次登录(附三种方法)

    2023-05-23 10:32:45
  • C语言实现航空订票系统课程设计

    2023-11-15 10:50:20
  • Spring注解与P/C命名空间超详细解析

    2022-08-04 19:42:34
  • 详解Maven settings.xml配置(指定本地仓库、阿里云镜像设置)

    2022-04-09 23:45:14
  • C#中string用法实例详解

    2022-06-11 03:22:17
  • C#集合本质之队列的用法详解

    2023-03-17 06:42:38
  • 解决idea 项目编译后没有class文件的问题

    2023-11-04 07:55:06
  • java链式创建json对象的实现

    2023-11-12 12:36:51
  • SpringBoot使用自动配置xxxAutoConfiguration

    2022-11-20 09:05:56
  • Android进阶Hook拦截系统实例化View过程实现App换肤功能

    2023-12-01 21:22:12
  • Android优雅地处理按钮重复点击的几种方法

    2021-11-23 05:09:14
  • javafx tableview鼠标触发更新属性详解

    2022-01-24 23:47:32
  • C#使用日志组件log4net

    2022-11-05 03:48:15
  • Android webveiw 出现栈错误解决办法

    2023-11-29 01:54:26
  • C#中判断本地系统的网络连接状态的方法

    2023-07-02 15:39:41
  • Thymeleaf 3.0 自定义标签方言属性的实例讲解

    2023-03-24 20:40:23
  • Android开发之ListView的简单用法及定制ListView界面操作示例

    2021-10-17 13:26:15
  • springboot对接微信支付的完整流程(附前后端代码)

    2021-11-12 15:08:42
  • C#利用ZXing.Net生成条形码和二维码

    2023-11-04 00:36:02
  • C++函数指针和回调函数使用解析

    2022-11-16 21:01:15
  • asp之家 软件编程 m.aspxhome.com