AR 眼镜之-蓝牙电话-实现方案

avatar
作者
筋斗云
阅读量:0

目录

📂 前言

AR 眼镜系统版本

蓝牙电话

来电铃声

1. 🔱 技术方案

1.1 结构框图

1.2 方案介绍

1.3 实现方案

步骤一:屏蔽原生蓝牙电话相关功能

步骤二:自定义蓝牙电话实现

2. 💠 屏蔽原生蓝牙电话相关功能

2.1 蓝牙电话核心时序图

2.2 实现细节

步骤一:禁止系统拉起来去电页面 InCallActivity

步骤二:屏蔽来电消息 Notification 显示

步骤三:替换来电铃声

3. ⚛️ 自定义蓝牙电话实现

3.1 自定义蓝牙电话时序图

3.2 实现细节

步骤一:注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 Action 监听来电/接通/挂断状态

步骤二:开发来电弹窗、来电界面,并处理相关业务逻辑

步骤三:调用拨号/接通/拒接等操作

4. ✅ 小结


📂 前言

AR 眼镜系统版本

        W517 Android9。

蓝牙电话

        主要实现 HFP 协议,主要实现拨打、接听、挂断电话(AG 侧、HF 侧)、切换声道等功能。

  • HFP(Hands-Free Profile)协议——一种蓝牙通信协议,实现 AR 眼镜与手机之间的通信;

  • AG(Audio Gate)音频网关——音频设备输入输出网关 ;

  • HF(Hands Free)免提——该设备作为音频网关的远程音频输入/输出机制,并可提供若干遥控功能。

        在 AR 眼镜蓝牙中,手机侧是 AG,AR 眼镜蓝牙侧是 HF,在 Android 源代码中,将 AG 侧称为 HFP/AG,将 HF 侧称为 HFPClient/HF。

来电铃声

        Andriod 来电的铃声默认保存在 system/media/audio/ 下面,有四个文件夹,分别是 alarms(闹钟)、notifications(通知)、ringtones(铃声)、ui(UI音效),源码中这些文件保存在 frameworks\base\data\sounds 目录下面。

1. 🔱 技术方案

1.1 结构框图

1.2 方案介绍

        技术方案概述:由于定制化程度较高,包括 3dof/6dof 渲染效果、佩戴检测功能等,所以采取屏蔽原生蓝牙电话相关功能,使用完全自定义的蓝牙电话实现方案。

1.3 实现方案

步骤一:屏蔽原生蓝牙电话相关功能
  1. 禁止系统拉起来去电页面 InCallActivity;

  2. 屏蔽来电消息 Notification 显示;

  3. 替换来电铃声。

步骤二:自定义蓝牙电话实现
  1. 注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 Action 监听来电/接通/挂断状态;

  2. 开发来电弹窗、来电界面,并处理相关业务逻辑;

  3. 通过 BluetoothAdapter 获取并且初始化 BluetoothHeadsetClient 对象,然后就可以调用 api:dial()/acceptCall()/rejectCall()/terminateCall() 方法进行拨号/接通/拒接的操作。

2. 💠 屏蔽原生蓝牙电话相关功能

  1. 系统来去电页面处理类:w517\packages\apps\Dialer\java\com\android\incallui\InCallPresenter.java

  2. 系统来电消息通知类:w517\packages\apps\Dialer\java\com\android\incallui\StatusBarNotifier.java

  3. 系统来电铃声类:w517\packages\services\Telecomm\src\com\android\server\telecom\Ringer.java

  4. 系统来电铃声文件路径:w517\frameworks\base\data\sounds\Ring_Synth_04.ogg

2.1 蓝牙电话核心时序图

2.2 实现细节

步骤一:禁止系统拉起来去电页面 InCallActivity

步骤二:屏蔽来电消息 Notification 显示

步骤三:替换来电铃声

        制作一个来电铃声的 Ring_Synth_04.ogg 文件,替换即可。

3. ⚛️ 自定义蓝牙电话实现

3.1 自定义蓝牙电话时序图

3.2 实现细节

步骤一:注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 Action 监听来电/接通/挂断状态

1、获取 BluetoothHeadsetClient 实例:

import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothHeadsetClient import android.bluetooth.BluetoothProfile import android.content.Context  private var headsetClient: BluetoothHeadsetClient? = null  fun getHeadsetClient(context: Context): BluetoothHeadsetClient? {         if (headsetClient != null) return headsetClient         BluetoothAdapter.getDefaultAdapter().apply {             getProfileProxy(                 context, object : ServiceListener {                     override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {                         headsetClient = proxy as BluetoothHeadsetClient                     }                      override fun onServiceDisconnected(profile: Int) {}                 }, BluetoothProfile.HEADSET_CLIENT             )         }         return headsetClient     }

2、注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 :

context.registerReceiver(     object : BroadcastReceiver() {         override fun onReceive(context: Context, intent: Intent) {             if (BluetoothHeadsetClient.ACTION_CALL_CHANGED == intent.action) {                 intent.getParcelableExtra<BluetoothHeadsetClientCall>(BluetoothHeadsetClient.EXTRA_CALL)                     ?.let { handleCallState(context, it) }             }         }     }, IntentFilter(BluetoothHeadsetClient.ACTION_CALL_CHANGED) )

3、处理广播回调的蓝牙状态:

var isInComing = false private var headsetClientCall: BluetoothHeadsetClientCall? = null private var mainHandler: Handler = Handler(Looper.getMainLooper()) private var isWearing = true  fun getHeadsetClientCall() = headsetClientCall  private fun handleCallState(context: Context, call: BluetoothHeadsetClientCall) {         headsetClientCall = call         when (call.state) {             BluetoothHeadsetClientCall.CALL_STATE_ACTIVE -> {                 Log.i(TAG, "Call is active:mNumber = ${call.number}")                 // 佩戴检测逻辑                 if (headsetClient != null) {                     val isAudioConnected = headsetClient!!.getAudioState(call.device) == 2                     Log.i(TAG, "isAudioConnected = $isAudioConnected,isWearing = $isWearing")                     if (isWearing) {                         if (!isAudioConnected) {                             headsetClient!!.connectAudio(call.device)                         }                     } else {                         if (isAudioConnected) {                             headsetClient!!.disconnectAudio(call.device)                         }                     }                 }                  if (isInComing) {                     isInComing = false                     PhoneTalkingDialogHelper.removeDialog()                     PhoneInCallDialogHelper.removeDialog()                     PhoneTalkingActivity.start(context)                 }             }              BluetoothHeadsetClientCall.CALL_STATE_HELD -> Log.d(TAG, "Call is held")             BluetoothHeadsetClientCall.CALL_STATE_DIALING -> Log.d(TAG, "Call is dialing")             BluetoothHeadsetClientCall.CALL_STATE_ALERTING -> Log.d(TAG, "Call is alerting")             BluetoothHeadsetClientCall.CALL_STATE_INCOMING -> {                 Log.i(TAG, "Incoming call:mNumber = ${call.number}")                 if (!isInComing) {                     isInComing = true                     PhoneTalkingDialogHelper.removeDialog()                     PhoneInCallDialogHelper.removeDialog()                      headsetClient?.let {                         PhoneInCallDialogHelper.addDialog(context, call, it)                     } ?: let {                         getHeadsetClient(context)                         mainHandler.post {                             headsetClient?.let {                                 PhoneInCallDialogHelper.addDialog(context, call, it)                             } ?: let {                                 Log.e(TAG, "Incoming call:headsetClient=null!!!")                             }                         }                     }                 }             }              BluetoothHeadsetClientCall.CALL_STATE_WAITING -> Log.d(TAG, "Call is waiting")             BluetoothHeadsetClientCall.CALL_STATE_TERMINATED -> {                 Log.i(TAG, "Call is terminated")                 isInComing = false                 PhoneTalkingDialogHelper.terminatedCall(context, PHONE_TALKING_UI_DISMISS)                 PhoneInCallDialogHelper.removeDialog(PHONE_TALKING_TIME_UPDATE)                 LiveEventBus.get<Boolean>(NOTIFICATION_CALL_STATE_TERMINATED).post(true)             }              else -> Log.d(TAG, "Unknown call state: ${call.state}")         }     }

        通过 BluetoothHeadsetClientCall.CALL_STATE_INCOMING 事件,触发来电弹窗 PhoneInCallDialogHelper.addDialog()。

步骤二:开发来电弹窗、来电界面,并处理相关业务逻辑

1、addDialog 显示来电弹窗:

object PhoneInCallDialogHelper {      private val TAG = PhoneInCallDialogHelper::class.java.simpleName     private var mInCallDialog: View? = null     private var mWindowManager: WindowManager? = null     private var mLayoutParams: WindowManager.LayoutParams? = null     private val mTimeOut: CountDownTimer = object : CountDownTimer(60000L, 1000) {         override fun onTick(millisUntilFinished: Long) {}          override fun onFinish() {             removeDialog()         }     }.start()      fun addDialog(         context: Context,         call: BluetoothHeadsetClientCall,         headsetClient: BluetoothHeadsetClient,     ) {         ThemeUtils.setTheme(context)         removeDialog()         mInCallDialog = (LayoutInflater.from(context)             .inflate(R.layout.notification_incall_layout, null) as View).apply {              // 还未接入指环,先不显示指环动画 //            val ringAnimation = findViewById<ImageView>(R.id.ringAnimation) //            ringAnimation.setImageResource(R.drawable.notification_ring_animation) //            (ringAnimation.drawable as AnimationDrawable).start()              findViewById<TextView>(R.id.title).text =                 getContactNameFromPhoneBook(context, call.number)             findViewById<TextView>(R.id.content).text = call.number         }          initLayoutParams(context)         mWindowManager?.addView(mInCallDialog, mLayoutParams)         mTimeOut.cancel()         mTimeOut.start()     }      fun removeDialog(delayMillis: Long = 0) {         kotlin.runCatching {             mTimeOut.cancel()             mInCallDialog?.let {                 if (it.isAttachedToWindow) {                     it.postDelayed({                         mWindowManager?.removeView(it)                         mInCallDialog = null                     }, delayMillis)                 }             }         }     }      private fun initLayoutParams(context: Context) {         mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager         mLayoutParams = WindowManager.LayoutParams().apply {             type = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL             gravity = Gravity.CENTER             width = (354 * context.resources.displayMetrics.density + 0.5f).toInt()             height = WindowManager.LayoutParams.WRAP_CONTENT             flags =                 (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)             format = PixelFormat.RGBA_8888 // 去除默认时有的黑色背景,设置为全透明              dofIndex = 1 //  默认为1。 为0,则表示窗口为0DOF模式;为1,则表示窗口为3DOF模式;为2,则表示窗口为6DOF模式。             setTranslationZ(TRANSLATION_Z_150CM)              setRotationXAroundOrigin(-XrEnvironment.getInstance().headPose.roll)             setRotationYAroundOrigin(-XrEnvironment.getInstance().headPose.yaw)             setRotationZAroundOrigin(-XrEnvironment.getInstance().headPose.pitch)             title = AGG_SYSUI_INCOMING         }     }  }

2、用户点击来电弹窗窗口、拒接或接听:

findViewById<ConstraintLayout>(R.id.inCallLayout).setOnClickListener {     Log.i(TAG, "addDialog: 进入activity页面")     removeDialog()     XrEnvironment.getInstance().imuReset()     PhoneTalkingActivity.start(context) } findViewById<ImageView>(R.id.reject).setOnClickListener {     Log.i(TAG, "addDialog: 拒接 ${call.number}")     headsetClient.rejectCall(call.device)     SoundPoolTools.play(         context,         SoundPoolTools.RING,         com.agg.launcher.middleware.R.raw.phone_hang_up     )     removeDialog(Constants.PHONE_TALKING_TIME_UPDATE) } findViewById<ImageView>(R.id.answer).setOnClickListener {     Log.i(TAG, "addDialog: 接听 ${call.number}")     headsetClient.acceptCall(call.device, BluetoothHeadsetClient.CALL_ACCEPT_NONE)     PhoneNotificationHelper.isInComing = false     removeDialog()     PhoneTalkingDialogHelper.addDialog(context, call, headsetClient)     SoundPoolTools.play(         context,         SoundPoolTools.RING,         com.agg.launcher.middleware.R.raw.phone_answer     ) }

3、跳转通话中弹窗:

object PhoneTalkingDialogHelper {      private val TAG = PhoneTalkingDialogHelper::class.java.simpleName     private var mTalkingDialog: View? = null     private var mContentView: TextView? = null     private var mTerminateView: ImageView? = null     private var mWindowManager: WindowManager? = null     private var mLayoutParams: WindowManager.LayoutParams? = null     private var mTalkingTimer = Timer()     private var mCurrentTalkingTime = 0      fun addDialog(         context: Context, call: BluetoothHeadsetClientCall, headsetClient: BluetoothHeadsetClient     ) {         ThemeUtils.setTheme(context)         removeDialog()         mTalkingDialog = (LayoutInflater.from(context)             .inflate(R.layout.notification_talking_layout, null) as View).apply {             findViewById<ConstraintLayout>(R.id.talkingLayout).setOnClickListener {                 Log.i(TAG, "addDialog: 进入activity页面")                 removeDialog()                 XrEnvironment.getInstance().imuReset()                 PhoneTalkingActivity.start(context, mCurrentTalkingTime)             }             findViewById<TextView>(R.id.title).text =                 AppUtils.getContactNameFromPhoneBook(context, call.number)             mContentView = findViewById(R.id.content)             mTerminateView = findViewById<ImageView>(R.id.terminate).apply {                 setOnClickListener {                     Log.i(TAG, "addDialog: 挂断 ${call.number}")                     headsetClient.terminateCall(call.device, call)                     terminatedCall(context, PHONE_TALKING_TIME_UPDATE)                     SoundPoolTools.play(                         context,                         SoundPoolTools.RING,                         com.agg.launcher.middleware.R.raw.phone_hang_up                     )                 }             }         }          initLayoutParams(context)         mWindowManager?.addView(mTalkingDialog, mLayoutParams)         mTalkingTimer = Timer()         mCurrentTalkingTime = 0         mTalkingTimer.scheduleAtFixedRate(object : TimerTask() {             override fun run() {                 mContentView?.text = DateUtils.getTalkingTimeString(mCurrentTalkingTime++)             }         }, PHONE_TALKING_TIME_UPDATE, PHONE_TALKING_TIME_UPDATE)     }      fun removeDialog() {         kotlin.runCatching {             mTalkingDialog?.let {                 if (it.isAttachedToWindow) {                     mWindowManager?.removeView(it)                     mTalkingDialog = null                     mTalkingTimer.cancel()                 }             }         }     }      fun terminatedCall(context: Context, delayMillis: Long) {         Log.i(TAG, "terminatedCall: delayMillis = $delayMillis")         mTalkingTimer.cancel()         mTerminateView?.isEnabled = false         mContentView?.text = context.getString(R.string.agg_notification_phone_finish)         mContentView?.postDelayed({ removeDialog() }, delayMillis)     }      private fun initLayoutParams(context: Context) {         mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager         mLayoutParams = WindowManager.LayoutParams().apply {             type = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL             gravity = Gravity.CENTER             width = (354 * context.resources.displayMetrics.density + 0.5f).toInt()             height = WindowManager.LayoutParams.WRAP_CONTENT             flags =                 (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)             format = PixelFormat.RGBA_8888 // 去除默认时有的黑色背景,设置为全透明              dofIndex = 1 //  默认为1。 为0,则表示窗口为0DOF模式;为1,则表示窗口为3DOF模式;为2,则表示窗口为6DOF模式。             setTranslationZ(TRANSLATION_Z_150CM)             setRotationXAroundOrigin(-XrEnvironment.getInstance().headPose.roll)             setRotationYAroundOrigin(-XrEnvironment.getInstance().headPose.yaw)             setRotationZAroundOrigin(-XrEnvironment.getInstance().headPose.pitch)             title = AGG_SYSUI_TALKING         }     }  }

4、进入通话中 Activity:

<activity     android:name=".phonenotification.activity.PhoneTalkingActivity"     android:exported="false"     android:launchMode="singleTask">     <intent-filter>         <action android:name="com.agg.launcher.action.PHONE_TALKING" />         <category android:name="android.intent.category.DEFAULT" />     </intent-filter> </activity>
class PhoneTalkingActivity : Activity() {      private var call: BluetoothHeadsetClientCall? = null     private var headsetClient: BluetoothHeadsetClient? = null     private lateinit var binding: NotificationActivityPhoneTalkingBinding     private var mCurrentTalkingTime = 0     private var mIsMute = false     private var mInitIsMute = false     private var mAudioManager: AudioManager? = null     private var mTalkingTimer = Timer()      companion object {         private val TAG = PhoneTalkingActivity::class.java.simpleName         private val EXTRA_CALL_TIME = "EXTRA_CALL_TIME"          fun start(context: Context, time: Int = 0) {             try {                 val intent = Intent("com.agg.launcher.action.PHONE_TALKING")                 intent.`package` = context.packageName                 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK                 intent.putExtra(EXTRA_CALL_TIME, time)                 context.startActivity(intent)             } catch (e: Exception) {                 e.printStackTrace()             }         }     }      override fun onCreate(savedInstanceState: Bundle?) {         Log.i(TAG, "onCreate: ")         super.onCreate(savedInstanceState)         ThemeUtils.setTheme(this)         binding = NotificationActivityPhoneTalkingBinding.inflate(layoutInflater)         setContentView(binding.root)         initAudio()         initPhoneData()         initView()         initInfo()     }      override fun onNewIntent(intent: Intent) {         super.onNewIntent(intent)         Log.i(TAG, "onNewIntent: ")         call = PhoneNotificationHelper.getHeadsetClientCall()         if (mCurrentTalkingTime <= 0) {             mCurrentTalkingTime = intent.getIntExtra(EXTRA_CALL_TIME, 0)         }     }      override fun onResume() {         super.onResume()         if (call != null) {             Log.i(TAG, "onResume: CALL_STATE_ACTIVE = ${call!!.state == CALL_STATE_ACTIVE}")             if (call!!.state == CALL_STATE_ACTIVE) {                 initAnswerView()             }         }     }      override fun onDestroy() {         super.onDestroy()         mAudioManager?.isMicrophoneMute = mInitIsMute         Log.i(TAG, "onDestroy: mInitIsMute = $mInitIsMute,mIsMute = $mIsMute")     }      private fun initAudio() {         mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager         mInitIsMute = mAudioManager?.isMicrophoneMute == true         mIsMute = mInitIsMute         Log.i(TAG, "initAudio: mInitIsMute = $mInitIsMute,mIsMute = $mIsMute")     }      private fun initPhoneData() {         headsetClient = PhoneNotificationHelper.getHeadsetClient(this)         if (headsetClient == null) {             Log.i(TAG, "initBluetoothHeadsetClient: headsetClient = null")             binding.root.post {                 headsetClient = PhoneNotificationHelper.getHeadsetClient(this)                 Log.i(TAG, "initBluetoothHeadsetClient: ${headsetClient == null}")             }         }         call = PhoneNotificationHelper.getHeadsetClientCall()         mCurrentTalkingTime = intent.getIntExtra(EXTRA_CALL_TIME, 0)     }      private fun initView() {         // 还未接入指环,先不显示指环动画 //        val ringAnimationLayout = findViewById<FrameLayout>(R.id.ringAnimationLayout) //        val ringAnimation = findViewById<ImageView>(R.id.ringAnimation) //        ringAnimation.setImageResource(R.drawable.notification_ring_animation) //        (ringAnimation.drawable as AnimationDrawable).start()          binding.hangup.setOnClickListener {             // 拒接             if (call != null) {                 headsetClient?.rejectCall(call!!.device)             }             SoundPoolTools.play(                 this, SoundPoolTools.RING, com.agg.launcher.middleware.R.raw.phone_hang_up             )             terminatedCall(PHONE_TALKING_TIME_UPDATE)         }         binding.answer.setOnClickListener {             // 接听             if (call != null) {                 headsetClient?.acceptCall(call!!.device, BluetoothHeadsetClient.CALL_ACCEPT_NONE)             }             initAnswerView()             SoundPoolTools.play(                 this, SoundPoolTools.RING, com.agg.launcher.middleware.R.raw.phone_answer             )         }         findViewById<ImageView>(R.id.more).setOnClickListener {             AGGDialog.Builder(this)                 .setIcon(resources.getDrawable(R.drawable.notification_ic_phone_subtitles))                 .setContent(resources.getString(R.string.agg_notification_phone_subtitles))                 .setLeftButton(resources.getString(R.string.agg_notification_cancel),                     object : AGGDialog.OnClickListener {                         override fun onClick(dialog: Dialog) {                             dialog.dismiss()                         }                     }).show()             AGGToast(                 this, Toast.LENGTH_SHORT, resources.getString(R.string.agg_notification_not_open_yet)             ).show()         }         LiveEventBus.get(LiveEventBusKey.NOTIFICATION_CALL_STATE_TERMINATED, Boolean::class.java)             .observeForever { terminatedCall(PHONE_TALKING_UI_DISMISS) }     }      private fun initInfo() {         call?.let {             findViewById<TextView>(R.id.title).text =                 AppUtils.getContactNameFromPhoneBook(this, it.number)             findViewById<TextView>(R.id.content).text = it.number         }     }      private fun initAnswerView() {         binding.answer.visibility = View.GONE         binding.hangup.visibility = View.GONE          // 还未接入指环,先不显示指环动画 //            ringAnimationLayout.visibility = View.GONE         binding.hangupBig.visibility = View.VISIBLE         binding.hangupBig.setOnClickListener {             // 挂断             if (call != null) {                 headsetClient?.terminateCall(call!!.device, call)             }             SoundPoolTools.play(                 this, SoundPoolTools.RING, com.agg.launcher.middleware.R.raw.phone_hang_up             )             terminatedCall(PHONE_TALKING_TIME_UPDATE)         }          binding.mute.visibility = View.VISIBLE         binding.mute.setOnClickListener {             if (mIsMute) {                 mIsMute = false                 binding.mute.setImageResource(R.drawable.notification_mute_close)             } else {                 mIsMute = true                 binding.mute.setImageResource(R.drawable.notification_mute_open)                 AGGToast(                     this@PhoneTalkingActivity,                     Toast.LENGTH_SHORT,                     resources.getString(R.string.agg_notification_mute)                 ).show()             }             // 开启/关闭静音             Log.i(TAG, "initView: mIsMute=$mIsMute")             mAudioManager?.isMicrophoneMute = mIsMute         }          binding.talkingTime.visibility = View.VISIBLE         startRecordTalkingTime()     }      private fun startRecordTalkingTime() {         Log.i(TAG, "startRecordTalkingTime: mCurrentTalkingTime = $mCurrentTalkingTime")         mTalkingTimer.cancel()         mTalkingTimer = Timer()         mTalkingTimer.scheduleAtFixedRate(object : TimerTask() {             override fun run() {                 binding.talkingTime.post {                     binding.talkingTime.text = DateUtils.getTalkingTimeString(mCurrentTalkingTime++)                 }             }         }, 0, PHONE_TALKING_TIME_UPDATE)     }      private fun terminatedCall(delayMillis: Long) {         Log.i(TAG, "terminatedCall: delayMillis = $delayMillis")         mTalkingTimer.cancel()         binding.talkingTime.text = getString(R.string.agg_notification_phone_finish)         binding.talkingTime.postDelayed({ finish() }, delayMillis)     }  }

5、通话时长相关:

/**  * 通话时长更新。单位:ms  */ const val PHONE_TALKING_TIME_UPDATE = 1000L /**  * 通话结束UI停留时长。单位:ms  */ const val PHONE_TALKING_UI_DISMISS = 2000L  /**  * 获取来电,通话时长字符串  */ fun getTalkingTimeString(seconds: Int): String {     return if (seconds <= 0) {         "00:00:00"     } else if (seconds < 60) {         String.format(Locale.getDefault(), "00:00:%02d", seconds % 60)     } else if (seconds < 3600) {         String.format(Locale.getDefault(), "00:%02d:%02d", seconds / 60, seconds % 60)     } else {         String.format(             Locale.getDefault(),             "%02d:%02d:%02d",             seconds / 3600,             seconds % 3600 / 60,             seconds % 60         )     } }  private fun startRecordTalkingTime() {     Log.i(TAG, "startRecordTalkingTime: mCurrentTalkingTime = $mCurrentTalkingTime")     mTalkingTimer.cancel()     mTalkingTimer = Timer()     mTalkingTimer.scheduleAtFixedRate(object : TimerTask() {         override fun run() {             binding.talkingTime.post {                 binding.talkingTime.text = DateUtils.getTalkingTimeString(mCurrentTalkingTime++)             }         }     }, 0, PHONE_TALKING_TIME_UPDATE) }

6、音效播放相关:

object SoundPoolTools {      const val RING = 1     const val MUSIC = 2     const val NOTIFICATION = 3      @IntDef(RING, MUSIC, NOTIFICATION)     @Retention(AnnotationRetention.SOURCE)     private annotation class Type      private val TAG = SoundPoolTools::class.java.simpleName      fun play(context: Context, @Type type: Int, resId: Int?) {         // 若是静音不播放         val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager         if (audioManager.ringerMode == AudioManager.RINGER_MODE_SILENT) {             Log.i(TAG, "play: RINGER_MODE_SILENT")             return         }          // 获取音效默认音量         val sSoundEffectVolumeDb =             context.resources.getInteger(com.android.internal.R.integer.config_soundEffectVolumeDb)         val volFloat: Float = 10.0.pow((sSoundEffectVolumeDb.toFloat() / 20).toDouble()).toFloat()         // 获取音效类型         val streamType = when (type) {             RING -> AudioManager.STREAM_RING             MUSIC -> AudioManager.STREAM_MUSIC             NOTIFICATION -> AudioManager.STREAM_NOTIFICATION             else -> AudioManager.STREAM_MUSIC         }         // 获取音效资源         val rawId = resId ?: when (type) {             RING -> R.raw.notification_message             MUSIC -> R.raw.notification_message             NOTIFICATION -> R.raw.notification_message             else -> R.raw.notification_message         }          SoundPool(1, streamType, 0).apply {             // 1. 加载音效             val soundId = load(context, rawId, 1)             setOnLoadCompleteListener { _, _, _ ->                 // 2. 播放音效                 // soundId:加载的音频资源的 ID。                 // leftVolume和rightVolume:左右声道的音量,范围为 0.0(静音)到 1.0(最大音量)。                 // priority:播放优先级,一般设为 1。                 // loop:是否循环播放,0 表示不循环,-1 表示无限循环。                 // rate:播放速率,1.0 表示正常速率,更大的值表示更快的播放速率,0.5 表示慢速播放。                 play(soundId, volFloat, volFloat, 1, 0, 1.0f)             }         }     }  }

7、获取联系人名字:

fun getContactNameFromPhoneBook(context: Context, phoneNum: String): String {     var contactName = ""     try {         context.contentResolver.query(             ContactsContract.CommonDataKinds.Phone.CONTENT_URI,             null,             ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?",             arrayOf(phoneNum),             null         )?.let {             if (it.moveToFirst()) {                 contactName = it.getString(                     it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)                 )                 it.close()             }         }     } catch (e: Exception) {         e.printStackTrace()     }     return contactName }
步骤三:调用拨号/接通/拒接等操作
private var headsetClient: BluetoothHeadsetClient? = null private var call: BluetoothHeadsetClientCall? = null private var mAudioManager: AudioManager? = null  fun t(){     // 拒接     headsetClient?.rejectCall(call?.device)     // 接听     headsetClient?.acceptCall(call?.device, BluetoothHeadsetClient.CALL_ACCEPT_NONE)     // 挂断     headsetClient?.terminateCall(call?.device, call)     // 拨打     headsetClient?.dial(call?.device, number)     // 打开蓝牙音频通道——通话对方声音从眼镜端输出     headsetClient!!.connectAudio(call?.device)     // 关闭蓝牙音频通话——通话对方声音从手机端输出     headsetClient!!.disconnectAudio(call?.device)     // 打开/关闭通话己方声音     mAudioManager = context.getSystemService(Context.AUDIO_SERVICE)     mAudioManager?.isMicrophoneMute = mIsMute }

4. ✅ 小结

        对于蓝牙通话,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。

        另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。


广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!