Android 来电秀总结

Android 来电秀总结

前言

效果图

TODO

参考文章

实现思想

申请权限

静态权限

动态权限

监听电话

BroadcastReceiver +悬浮窗显示实现

InCallService + Activity实现

最后

上目录仅用于展示文章结构,简书平台点击不可跳转到指定锚点

该文章为对工作中部分业务实现的总结,阅读时间:20分钟,版本:Android 6.0 - 9.0

update time 2021年02月03日11:48:55

文章可能存在不足之处,还望评论批评,一起学习进步。

前言

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

效果图

WechatIMG39.jpeg

TODO

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

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

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

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

参考文章

来电秀实现

实现思想

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

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

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

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

申请权限

静态权限

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

动态权限

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

// 监听电话状态广播 注册

# 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

android:name=".phone.service.CallListenerService"

android:enabled="true"

android:exported="false">

android:name=".phone.service.NotificationService"

android:label="@string/app_name"

android:permission="android.permission.BIND_NOTIFICATION_LISTENER_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

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

android:name=".phone.service.PhoneCallService"

android:permission="android.permission.BIND_INCALL_SERVICE">

android:name="android.telecom.IN_CALL_SERVICE_UI"

android:value="true" />

# 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,如果有哪里侵权的地方,私信沟通,我会进行修改。感谢大家能够观看我的 开发笔记总结。

相关推荐

快递已揽收是什么意思?物流状态查询与签收规则
365bet足球比分直播

快递已揽收是什么意思?物流状态查询与签收规则

📅 09-23 👁️ 4977
不要采、不要吃!余杭已大量出现!
365bet网址是多少

不要采、不要吃!余杭已大量出现!

📅 08-23 👁️ 6482
十大赚钱的app哪个靠谱赚钱还快,感觉只有这几个靠谱
全球最大体育平台365

十大赚钱的app哪个靠谱赚钱还快,感觉只有这几个靠谱

📅 07-03 👁️ 1167