自定義來電秀怎麼實現?Android 來電秀原始碼分析

南方吳彥祖_藍斯發表於2021-09-29

前言

要想實現自定義 來電秀,首先我們先這樣 再這樣,然後你這樣,最後你再這樣一下,就可以了,很好實現的,聽懂了麼?

效果圖

自定義來電秀怎麼實現?Android 來電秀原始碼分析

TODO

  1. 新增包活lib,提高App在設定成功後 退居後臺,成功拉起的機率
  2. 專案中已經包含lib_ijk的程式碼,我們可以新增影片來電展示,新增美女或者豪車等全屏影片,效果更佳。
  3. 由於反編譯能力有限,對於多種機型許可權的跳轉(後續可以開起  無障礙服務,直接一步搞定多種需要使用者手動設定操作)
  4. 該Demo中有一部分不完善的Rom 許可權跳轉機制,後續還需要時間來完善。

實現思想

  1. 透過監聽手機Service 分辨來電狀態,然後彈出我們自定義的來電頁面,覆蓋系統來電頁面。
  2. 透過相關API (主要兩種: 讀取來電系統的Notification資訊 和  模擬耳機線控的方式進行結束通話/接聽)實現接聽和結束通話功能。我這裡會使用兩種(低版本 使用電話狀態廣播監聽,高版本使用InCallService) 監聽電話狀態的Service 及兩種介面展示 來呈現來電資訊,多個介面和多個Service的監聽 能夠增加高版本的容錯率相容性。
  3. 實現自定義的撥號介面 或者 直接使用系統的撥號介面。

申請許可權

靜態許可權

電話應用,會用到很多許可權,我這裡儘可能多的靜態註冊了一些許可權,如果引入專案中,需要甄別下,程式碼如下:

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

最後

到這裡這篇文章基本已經寫得差不多了,在自己編寫Demo的時候也觀看了很多其他的自定義來電秀部落格,並且反編譯了一些市面上不錯的來電秀App,如果有哪裡侵權的地方,私信溝通,我會進行修改。感謝大家能夠觀看我的 開發筆記總結。

更多Android技術分享可以關注@我,也可以加入QQ群號:Android進階學習群:345659112,一起學習交流。

作者:小肥羊沖沖衝Android
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2794415/,如需轉載,請註明出處,否則將追究法律責任。

相關文章