Android 自定义来电秀实现总结

目录
  • 前言
  • 实现思想
  • 申请权限
    • 静态权限
    • 动态权限
  • 监听电话
  • BroadcastReceiver +悬浮窗显示实现
  • InCallService + Activity实现

前言

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

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

效果图

  • 添加包活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()

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

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

# 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是否保持等一些操作。

以上就是Android 自定义来电秀实现总结的详细内容,更多关于Android 自定义来电秀的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android如何帮助用户自动接听或者挂断来电

    这篇文章教你如何帮助用户自动接听或者挂断来电.当然并不是我原创的代码,我只不过是把stackoverflow上的一些代码整合了一下,做个代码的二传手. 源码 AcceptOrRejectCallDemo 源码中用了MVP的模式,只是最简单的使用,如果不熟悉的话刚好可以学学,逻辑部分在IncomingPresenter类中. 首先需要监听来电的广播 在AndroidManifest文件中添加: <receiver android:name=".incomingcall.PhoneListen

  • Android8.1 通过黑名单屏蔽系统短信和来电功能

    前言 同样的最近有个新需求,需要将8.1 设备的来电功能和短信功能都屏蔽掉,特殊产品就是特殊定制,那就开始吧. 屏蔽短信功能 还可沿用之前的6.0 处理方法, 在 SmsReceiverService.java 中 handleSmsReceived()中进行拦截分发 但是想了想这次准备搞点不一样的,我发现通讯录和之前6.0不太一样,8.1有个添加屏蔽联系人的功能,能够屏蔽指定电话和短信,也就是黑名单功能.所以此次就通过黑名单的方式进行拦截,通过下面几位老哥的详尽分析和打印日志找到修改地方 源码

  • Android来电监听和去电监听实现代码

    我觉得写文章就得写得有用一些的,必须要有自己的思想,关于来电去电监听将按照下面三个问题展开 1.监听来电去电有什么用? 2.怎么监听,来电去电监听方式一样吗? 3.实战,有什么需要特别注意地方? 监听来电去电能干什么 1.能够对监听到的电话做个标识,告诉用户这个电话是诈骗.推销.广告什么的 2.能够针对那些特殊的电话进行自动挂断,避免打扰到用户 来电去电的监听方式(不一样的方式) 1.来电监听(PhoneStateListener) 来电监听是使用PhoneStateListener类,使用方式

  • Android之来电秀实战示例

    目录 正文 第一:监听来电状态 第二:通过手机状态来做不同的处理 第三:来电秀界面的实现了 第四:开机自动启动 第五:注册这些四大组件和申请用到的一些权限 正文 简单的说下实现来电秀的大概原理流程:首先通过监听来电状态,通过拦截来电然后在窗口弹出一层系统级别的弹窗,这层弹窗即是来电秀. 先来两张效果图: 下面来说下实现,因为商用的原因,不能直接贴代码,所以在这里,会贴一些比较核心的代码,大概分为五个步骤: 第一:监听来电状态 /** * 电话状态监听(来电或去电) * * @author Jen

  • Android仿通话来电界面效果

    Android仿通话来电界面,供大家参考,具体内容如下 简介:开发中需要模拟来电时的通话界面,仿照来电界面实现来电时播放铃声,界面通过动画模拟来电动效. 效果图: 自定义图片背景,图片由小变大的动态效果. shap_circle.xml <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/androi

  • Android来电拦截的实现方法

    本文实例为大家分享了Android来电拦截的方法,供大家参考,具体内容如下 权限 <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android

  • Android监听系统来电并弹出提示窗口

    1.问题 项目中有自己企业的通讯录,但是在应用中拨打公司通讯录的联系人,由于手机通讯录中没有相应的信息,只显示一串电话号 2 .目的 监听系统来电,获取到电话号码,通过调用接口,查询出来相应电话号码的详细信息,并弹出系统悬浮框,给用户提示. 3.实现 首先 注册广播监听系统来电.监听系统来电需要.注册相应的权限 代码地址:https://github.com/sdsjk/phone_alert.git <uses-permission android:name="android.permi

  • Android自定义密码样式 黑点转换成特殊字符

    本文为大家分享了Android自定义密码样式的制作代码,黑点换成¥.%等特殊字符,供大家参考,具体内容如下 复制下面代码即可: 布局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_par

  • Android 自定义九宫格手势锁

    预览效果图如下: 主要的方法是重写View.onTouchEvent( MotionEvent event ) , 常用的三个操作:ACTION_DOWN 手指触摸屏幕 ; ACTION_UP 手指离开屏幕; ACTION_MOVE手指在屏幕滑动. 如果该方法返回true ,表示该事件已经被View处理,不再向上层的View或Activity传递 : 如果返回false, 表示事件未处理,继续传递. 具体代码如下: package com.ninegrid; import android.con

  • Android自定义view实现阻尼效果的加载动画

    效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动.衰减振动.[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来.这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运

  • Android 自定义View 密码框实例代码

    暴露您view中所有影响可见外观的属性或者行为. •通过XML添加和设置样式 •通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 效果图展示: 支持的样式 可以通过XML定义影响外边和行为的属性如下 边框圆角值,边框颜色,分割线颜色,边框宽度,密码长度,密码大小,密码颜色 <declare-styleable name="PasswordInputView"> <attr name="borde

  • Android自定义ViewGroup之FlowLayout(三)

    本篇继续来讲自定义ViewGroup,给大家带来一个实例:FlowLayout.何为FlowLayout,就是控件根据ViewGroup的宽,自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行,所以也叫流式布局.Android并没有提供流式布局,但是某些场合中,流式布局还是非常适合使用的,比如关键字标签,搜索热词列表等,比如下图: 定义FlowLayout LayoutParams,onLayout的写法都和上一篇讲WaterfallLayout一模一样,在此不再赘述了,没看过的可以参照

  • Android自定义View详解

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章.先总结下自定义View的步骤: 1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 [ 3.重写onMesure ] 4.重写onDraw 我把3用[]标出了,所以说3不一

  • Android自定义View中attrs.xml的实例详解

    Android自定义View中attrs.xml的实例详解 我们在自定义View的时候通常需要先完成attrs.xml文件 在values中定义一个attrs.xml 然后添加相关属性 这一篇先详细介绍一下attrs.xml的属性. <?xml version="1.0" encoding="utf-8"?> <resources> //自定义属性名,定义公共属性 <attr name="titleText" for

  • Android自定义Button并设置不同背景图片的方法

    本文实例讲述了Android自定义Button并设置不同背景图片的方法.分享给大家供大家参考,具体如下: 1.自定义MyButton类 public class MyButton extends Button { //This constructormust be public MyButton(Context context, AttributeSet attrs) { super(context, attrs); } public MyButton(Context context) { su

  • Android自定义View 仿QQ侧滑菜单的实现代码

    先看看QQ的侧滑效果 分析一下 先上原理图(不知道能否表达的清楚 ==) -首先这里使用了 Android 的HorizontalScrollView 水平滑动布局作为容器,当然我们需要继承它自定义一个侧滑视图 - 这个容器里面有一个父布局(一般用LinerLayout,本demo用的是),这个父布局里面有且只有两个子控件(布局),初始状态菜单页的位置在Y轴上存在偏移这样可以就可以形成主页叠在菜单页的上方的视觉效果:然后在滑动的过程程中 逐渐修正偏移,最后菜单页和主页并排排列.原理搞清了实现起来

随机推荐