自定義來電秀怎麼實現?Android 來電秀原始碼分析
前言
要想實現自定義 來電秀,首先我們先這樣 再這樣,然後你這樣,最後你再這樣一下,就可以了,很好實現的,聽懂了麼?
效果圖
TODO
- 新增包活lib,提高App在設定成功後 退居後臺,成功拉起的機率
- 專案中已經包含lib_ijk的程式碼,我們可以新增影片來電展示,新增美女或者豪車等全屏影片,效果更佳。
- 由於反編譯能力有限,對於多種機型許可權的跳轉(後續可以開起 無障礙服務,直接一步搞定多種需要使用者手動設定操作)
- 該Demo中有一部分不完善的Rom 許可權跳轉機制,後續還需要時間來完善。
實現思想
- 透過監聽手機Service 分辨來電狀態,然後彈出我們自定義的來電頁面,覆蓋系統來電頁面。
- 透過相關API (主要兩種: 讀取來電系統的Notification資訊 和 模擬耳機線控的方式進行結束通話/接聽)實現接聽和結束通話功能。我這裡會使用兩種(低版本 使用電話狀態廣播監聽,高版本使用InCallService) 監聽電話狀態的Service 及兩種介面展示 來呈現來電資訊,多個介面和多個Service的監聽 能夠增加高版本的容錯率相容性。
- 實現自定義的撥號介面 或者 直接使用系統的撥號介面。
申請許可權
靜態許可權
電話應用,會用到很多許可權,我這裡儘可能多的靜態註冊了一些許可權,如果引入專案中,需要甄別下,程式碼如下:
<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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 華為手機怎麼設定來電影片?華為EMUI來電影片秀的設定教程UI
- 華為手機怎麼設定來電視訊?華為EMUI來電視訊秀的設定教程UI
- 直播平臺原始碼,Android自定義View實現呼吸燈效果原始碼AndroidView
- Mybatis原始碼分析(七)自定義快取、分頁的實現MyBatis原始碼快取
- Android自定義拍照實現Android
- 背景透明的實現,直播電商原始碼是怎麼做的原始碼
- JAVA Comparator 自定義排序 原始碼分析Java排序原始碼
- 閱讀原始碼後,來講講React Hooks是怎麼實現的原始碼ReactHook
- 美圖秀秀怎麼給圖片新增背景?美圖秀秀給圖片新增背景的教程
- 怎麼利用AbstractQueuedSynchronizer實現自定義同步元件?元件
- 直播帶貨原始碼,Android Studio實現電商引導頁原始碼Android
- 節電模式黑屏怎麼解除快捷鍵 電腦節電模式黑屏怎麼調回來模式
- 美圖秀秀怎麼摳圖?美圖秀秀對圖片進行圓形摳圖的教程
- win10 我的電腦怎麼弄出來_win10怎麼把我的電腦弄出來Win10
- 曾經優秀的人,怎麼就突然不優秀了。
- Android Activity Deeplink啟動來源獲取原始碼分析Android原始碼
- mybaits原始碼分析--自定義外掛(七)AI原始碼
- 梯度提升二三事:怎麼來自定義損失函式?梯度函式
- android 自定義狀態列和導航欄分析與實現Android
- 講師招募 | Apache SeaTunnel Meetup等你來秀!Apache
- Netty原始碼分析之自定義編解碼器Netty原始碼
- android招聘啦,美圖秀秀歡迎你加入!Android
- android 優秀框架整理Android框架
- 【原始碼】btfilm專業電影搜尋引擎,海量電影等你來搜原始碼
- 解析度300dpi怎麼設定 美圖秀秀300dpi怎麼調
- 直播系統程式碼,Android自定義View實現呼吸燈效果AndroidView
- 怎麼能接聽來電?獲取不到不到元素
- 誤刪了電腦資料怎麼找回來?
- 此電腦圖示怎麼弄出來 win10此電腦圖示弄出來的教程Win10
- 電腦工作列隱藏了怎麼顯示出來 win10電腦工作列隱藏怎麼調出來Win10
- 極氪釋出浩瀚-M架構並全球首秀極氪MIX,未來電動車架構打造未來的家架構
- 小米手環3來電提醒開啟方法教程 小米手環3怎麼開啟來電提醒?
- 優秀開源庫SDWebImage原始碼淺析Web原始碼
- _下劃線這個符號電腦怎麼打 _符號電腦上怎麼打出來符號
- 來談談限流-RateLimiter原始碼分析MIT原始碼
- Android自定義view之實現帶checkbox的SnackbarAndroidView
- Android自定義View:快遞時間軸實現AndroidView
- Android 自定義 View 實現橫行時間軸AndroidView