Android?自定義來(lái)電秀實(shí)現(xiàn)總結(jié)
前言
該文章為對(duì)工作中部分業(yè)務(wù)實(shí)現(xiàn)的總結(jié),閱讀時(shí)間:20分鐘,版本:Android 6.0 - 9.0 update time 2021年02月03日11:48:55 文章可能存在不足之處,還望評(píng)論批評(píng),一起學(xué)習(xí)進(jìn)步。
要想實(shí)現(xiàn)自定義 來(lái)電秀,首先我們先這樣 再這樣,然后你這樣,最后你再這樣一下,就可以了,很好實(shí)現(xiàn)的,聽懂了么?-,-
效果圖

- 添加包活lib,提高App在設(shè)置成功后 退居后臺(tái),成功拉起的概率
- 項(xiàng)目中已經(jīng)包含lib_ijk的代碼,我們可以添加視頻來(lái)電展示,添加美女或者豪車等全屏視頻,效果更佳。
- 由于反編譯能力有限,對(duì)于多種機(jī)型權(quán)限的跳轉(zhuǎn)(后續(xù)可以開起 無(wú)障礙服務(wù),直接一步搞定多種需要用戶手動(dòng)設(shè)置操作)
- 該Demo中有一部分不完善的Rom 權(quán)限跳轉(zhuǎn)機(jī)制,后續(xù)還需要時(shí)間來(lái)完善。
實(shí)現(xiàn)思想
- 通過(guò)監(jiān)聽手機(jī)Service 分辨來(lái)電狀態(tài),然后彈出我們自定義的來(lái)電頁(yè)面,覆蓋系統(tǒng)來(lái)電頁(yè)面。
- 通過(guò)相關(guān)API (主要兩種:讀取來(lái)電系統(tǒng)的Notification信息 和 模擬耳機(jī)線控的方式進(jìn)行掛斷/接聽)實(shí)現(xiàn)接聽和掛斷功能。我這里會(huì)使用兩種(低版本 使用電話狀態(tài)廣播監(jiān)聽,高版本使用InCallService) 監(jiān)聽電話狀態(tài)的Service 及兩種界面展示 來(lái)呈現(xiàn)來(lái)電信息,多個(gè)界面和多個(gè)Service的監(jiān)聽 能夠增加高版本的容錯(cuò)率兼容性。
- 實(shí)現(xiàn)自定義的撥號(hào)界面 或者 直接使用系統(tǒng)的撥號(hào)界面。
注意:因?yàn)槠鶈?wèn)題,博客只會(huì)截取部分代碼,太長(zhǎng)讀者很難讀下去,Demo已經(jīng)調(diào)試通過(guò),如果有想看源碼的可以移步到我的 GitHub項(xiàng)目地址
申請(qǐng)權(quán)限
靜態(tài)權(quán)限
電話應(yīng)用,會(huì)用到很多權(quán)限,我這里盡可能多的靜態(tài)注冊(cè)了一些權(quán)限,如果引入項(xiàng)目中,需要甄別下,代碼如下:
<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" />
<!-- 讀取聯(lián)系人權(quán)限 -->
<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" />
<!-- 讀寫 聯(lián)系信息 顯示聯(lián)系人名稱 -->
<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上使用前臺(tái)服務(wù),需要添加權(quán)限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
動(dòng)態(tài)權(quán)限
AndPermission.with(this)
.runtime()
.permission(
Permission.Group.PHONE,
Permission.Group.LOCATION,
Permission.Group.CALL_LOG
)
.onGranted {
Toast.makeText(applicationContext, "權(quán)限同意", Toast.LENGTH_SHORT).show()
}.onDenied {
Toast.makeText(applicationContext, "權(quán)限拒絕", Toast.LENGTH_SHORT).show()
}.start()
上述代碼,為自己測(cè)試使用的Demo,所以請(qǐng)求權(quán)限直接請(qǐng)求分組中的全部權(quán)限了,項(xiàng)目中根據(jù)需要?jiǎng)討B(tài)申請(qǐng)部分權(quán)限
雖然我們已經(jīng)申請(qǐng)了這么多權(quán)限,但是為了能夠替換系統(tǒng)電話界面成功,還有一部分權(quán)限是需要通過(guò)彈框來(lái)引導(dǎo)用戶去 設(shè)置中開啟的。
# CallerShowPermissionManager.kt
/**
* 判斷是否有 鎖屏彈出、 后臺(tái)彈出懸浮窗 、允許系統(tǒng)修改、讀取通知欄等權(quán)限(必須同意)
*/
fun setRingPermission(context: Context): Boolean {
perArray.clear()
if (!OpPermissionUtils.checkPermission(context)) {
//跳轉(zhuǎn)到懸浮窗設(shè)置
toRequestFloatWindPermission(context)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) {
//準(zhǔn)許系統(tǒng)修改
opWriteSetting(context)
}
if (!isAllowed(context)) {
//后臺(tái)彈出權(quán)限
openSettings(context)
}
if (!notificationListenerEnable(context)) {
//通知使用權(quán)
gotoNotificationAccessSetting()
}
if (perArray.size != 0) {
context.startActivities(perArray.toTypedArray())
return false
} else {
LogUtils.e("鈴聲 高級(jí)權(quán)限全部同意")
return true
}
}
/**
* 點(diǎn)擊授權(quán)按鈕,編輯好需要申請(qǐng)的權(quán)限后,統(tǒng)一跳轉(zhuǎn),oppo/小米 的后臺(tái)彈出權(quán)限 鎖屏顯示權(quán)限,
* 需要用戶去設(shè)置中手動(dòng)開始,在項(xiàng)目中 可以使用 蒙層引導(dǎo)用戶點(diǎn)擊
*/
fun setRingPermission(context: Context): Boolean {
perArray.clear()
if (!OpPermissionUtils.checkPermission(context)) {
//跳轉(zhuǎn)到懸浮窗設(shè)置
toRequestFloatWindPermission(context)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) {
//準(zhǔn)許系統(tǒng)修改
opWriteSetting(context)
}
if (!isAllowed(context)) {
//后臺(tái)彈出權(quán)限
openSettings(context)
}
if (!notificationListenerEnable(context)) {
//通知使用權(quán)
gotoNotificationAccessSetting()
}
if (perArray.size != 0) {
context.startActivities(perArray.toTypedArray())
return false
} else {
LogUtils.e("鈴聲 高級(jí)權(quán)限全部同意")
return true
}
}
/**
* 申請(qǐng)懸浮窗權(quán)限
*/
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("請(qǐng)?jiān)跈?quán)限管理中打開懸浮窗管理權(quán)限")
}
}
LogUtils.e("請(qǐng)?jiān)跈?quán)限管理中打開懸浮窗管理權(quán)限")
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
}
/**
* 打開設(shè)置(后臺(tái)彈出 鎖屏顯示)
*/
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("請(qǐng)?jiān)跈?quán)限管理中打開后臺(tái)彈出權(quán)限")
}
} else {
LogUtils.e("android 6.0以下")
}
}
/**
* 系統(tǒng)修改
*/
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)
}
}
}
/**
* 讀取系統(tǒng)通知
*/
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("獲取系統(tǒng)通知失敗 e : $ex")
}
}
}
// 暫時(shí)把重要代碼cv出來(lái)了一部分,建議下載Demo源碼 ,結(jié)合博客一起觀看
上述代碼 主要羅列了需要引導(dǎo)用戶開啟部分設(shè)置權(quán)限的核心代碼和方法。
監(jiān)聽電話
對(duì)于監(jiān)聽電話這塊,會(huì)有很多兼容性的問(wèn)題,我們這里先使用廣播監(jiān)聽 action = android.intent.action.PHONE_STATE 的廣播,然后根據(jù)狀態(tài)調(diào)用起來(lái)懸浮窗。但是測(cè)試Android高版本手機(jī) 發(fā)現(xiàn) InCallService 會(huì)更好的獲取到電話狀態(tài),所以我這里的處理方案是 兩個(gè)方案都保存在了代碼中,最后通過(guò)調(diào)用不同的界面來(lái)區(qū)分。
BroadcastReceiver +懸浮窗顯示實(shí)現(xiàn)
# AndroidManifest.xml
// 監(jiān)聽電話狀態(tài)廣播 注冊(cè)
<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) {
}
}
}
}
//來(lái)去電的幾個(gè)狀態(tài)
private fun dealWithCallAction(state: Int?, phoneNumber: String?) {
when (state) {
// 來(lái)電狀態(tài) - 顯示懸浮窗
TelephonyManager.CALL_STATE_RINGING -> {
PhoneStateActionImpl.instance.onRinging(phoneNumber)
}
// 空閑狀態(tài)(掛斷) - 關(guān)閉懸浮窗
TelephonyManager.CALL_STATE_IDLE -> {
PhoneStateActionImpl.instance.onHandUp()
}
// 摘機(jī)狀態(tài)(接聽) - 保持不作操作
TelephonyManager.CALL_STATE_OFFHOOK -> {
PhoneStateActionImpl.instance.onPickUp(phoneNumber)
}
1000 -> { //撥打電話廣播狀態(tài) - 顯示懸浮窗
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
// 設(shè)置 Window flag 為系統(tǒng)級(jí)彈框 | 覆蓋表層
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)
}
...
// 部分代碼省略
懸浮窗展示完成后,就要設(shè)置電話接通和掛斷的操作(注意:這里很多低版本手機(jī)存在兼容問(wèn)題,所以會(huì)有一些代碼比較奇怪)
# 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()
}
}
以上代碼為接口實(shí)現(xiàn)類,我們這里會(huì)跳轉(zhuǎn)到 一個(gè)前臺(tái)Activity(一定程度上可以將App拉活),主要邏輯我們放在自己的前臺(tái)Service中操作。
# CallListenerService.kt
// Andorid新版本 啟動(dòng)服務(wù)的方式
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()
}
}
為保證我們的服務(wù)能夠正常吊起來(lái),吊起前臺(tái)服務(wù),并設(shè)置Service等級(jí),代碼如下:
# AndroidManifest.xml
<!-- 電話狀態(tài)接收廣播 -->
<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>
<!-- 監(jiān)聽通知欄權(quán)限 必備 -->
<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>
低版本的接通和掛斷電話,因?yàn)樾枰嫒莶糠謾C(jī)型,所以我們會(huì)有比較多的判斷,代碼如下:
# 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")
}
}
/**
* 斷開電話,包括來(lái)電時(shí)的拒接以及接聽后的掛斷
*/
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()
}
}
}
到這里中低版本的電話接通和掛斷,基本已經(jīng)完畢。下一步 我們主要寫,用戶在同意設(shè)置應(yīng)用為默認(rèn)電話應(yīng)用后的 更加簡(jiǎn)單方便的實(shí)現(xiàn)方式。
InCallService + Activity實(shí)現(xiàn)
在使用 InCallService 服務(wù)的同時(shí),需要設(shè)置該應(yīng)用為默認(rèn)撥號(hào)應(yīng)用 (這里只說(shuō)明技術(shù)的可能性,不對(duì)用戶行為分析)。
# 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對(duì)象需要判斷是否有多個(gè)呼入的情況)
override fun onCallAdded(call: Call?) {
super.onCallAdded(call)
call?.let {
it.registerCallback(callback)
PhoneCallManager.instance.addCall(it)
}
}
// Call 移除 (可以理解為某一個(gè)通話的結(jié)束)
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類中統(tǒng)一處理
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)
}
}
// 設(shè)置揚(yáng)聲器
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的代碼。部分方法進(jìn)行了說(shuō)明。
# PhoneCallManager.kt
/**
* 接聽電話
*/
@RequiresApi(Build.VERSION_CODES.M)
fun answer(callId: String?) =
getCallById(callId)?.let {
it.answer(VideoProfile.STATE_AUDIO_ONLY)
true
} ?: false
/**
* 斷開電話,包括來(lái)電時(shí)的拒接以及接聽后的掛斷
*/
@RequiresApi(Build.VERSION_CODES.M)
fun disconnect(callId: String?) =
getCallById(callId)?.let {
it.disconnect()
true
} ?: false
由于篇幅問(wèn)題,PhoneCallManager中的代碼不全部展示,需要的小伙伴請(qǐng)移步Github,該類中主要進(jìn)行了一些默認(rèn)撥號(hào)應(yīng)用,呼叫Call是否保持等一些操作。
以上就是Android 自定義來(lái)電秀實(shí)現(xiàn)總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Android 自定義來(lái)電秀的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)美團(tuán)下拉功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)美團(tuán)下拉功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
Android編程實(shí)現(xiàn)識(shí)別與掛載U盤的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)識(shí)別與掛載U盤的方法,對(duì)比分析了Android針對(duì)U盤的識(shí)別與掛載技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-02-02
Android編程之listView中checkbox用法實(shí)例分析
這篇文章主要介紹了Android編程之listView中checkbox用法,結(jié)合實(shí)例形式分析了Android中checkbox的頁(yè)面布局及功能實(shí)現(xiàn)相關(guān)技巧,需要的朋友可以參考下2016-01-01
android自定義按鈕示例(重寫imagebutton控件實(shí)現(xiàn)圖片按鈕)
由于項(xiàng)目這種類型的圖片按鈕比較多,所以重寫了ImageButton類,現(xiàn)在把代碼分享給大家,需要的朋友可以參考下2014-03-03
android imageview圖片居中技巧應(yīng)用
做UI布局,尤其是遇到比較復(fù)雜的多重LinearLayout嵌套,常常會(huì)被一些比較小的問(wèn)題困擾上半天,可是無(wú)論怎樣設(shè)置layout_gravity屬性,都無(wú)法達(dá)到效果2012-11-11

