目录
📂 前言
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 实现方案
步骤一:屏蔽原生蓝牙电话相关功能
-
禁止系统拉起来去电页面 InCallActivity;
-
屏蔽来电消息 Notification 显示;
-
替换来电铃声。
步骤二:自定义蓝牙电话实现
-
注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 Action 监听来电/接通/挂断状态;
-
开发来电弹窗、来电界面,并处理相关业务逻辑;
-
通过 BluetoothAdapter 获取并且初始化 BluetoothHeadsetClient 对象,然后就可以调用 api:dial()/acceptCall()/rejectCall()/terminateCall() 方法进行拨号/接通/拒接的操作。
2. 💠 屏蔽原生蓝牙电话相关功能
-
系统来去电页面处理类:w517\packages\apps\Dialer\java\com\android\incallui\InCallPresenter.java
-
系统来电消息通知类:w517\packages\apps\Dialer\java\com\android\incallui\StatusBarNotifier.java
-
系统来电铃声类:w517\packages\services\Telecomm\src\com\android\server\telecom\Ringer.java
-
系统来电铃声文件路径: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.Contextprivate var headsetClient: BluetoothHeadsetClient? = nullfun getHeadsetClient(context: Context): BluetoothHeadsetClient? {if (headsetClient != null) return headsetClientBluetoothAdapter.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 = truefun getHeadsetClientCall() = headsetClientCallprivate fun handleCallState(context: Context, call: BluetoothHeadsetClientCall) {headsetClientCall = callwhen (call.state) {BluetoothHeadsetClientCall.CALL_STATE_ACTIVE -> {Log.i(TAG, "Call is active:mNumber = ${call.number}")// 佩戴检测逻辑if (headsetClient != null) {val isAudioConnected = headsetClient!!.getAudioState(call.device) == 2Log.i(TAG, "isAudioConnected = $isAudioConnected,isWearing = $isWearing")if (isWearing) {if (!isAudioConnected) {headsetClient!!.connectAudio(call.device)}} else {if (isAudioConnected) {headsetClient!!.disconnectAudio(call.device)}}}if (isInComing) {isInComing = falsePhoneTalkingDialogHelper.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 = truePhoneTalkingDialogHelper.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 = falsePhoneTalkingDialogHelper.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.simpleNameprivate var mInCallDialog: View? = nullprivate var mWindowManager: WindowManager? = nullprivate var mLayoutParams: WindowManager.LayoutParams? = nullprivate 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 WindowManagermLayoutParams = WindowManager.LayoutParams().apply {type = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANELgravity = Gravity.CENTERwidth = (354 * context.resources.displayMetrics.density + 0.5f).toInt()height = WindowManager.LayoutParams.WRAP_CONTENTflags =(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 = falseremoveDialog()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.simpleNameprivate var mTalkingDialog: View? = nullprivate var mContentView: TextView? = nullprivate var mTerminateView: ImageView? = nullprivate var mWindowManager: WindowManager? = nullprivate var mLayoutParams: WindowManager.LayoutParams? = nullprivate var mTalkingTimer = Timer()private var mCurrentTalkingTime = 0fun 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 = 0mTalkingTimer.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 = nullmTalkingTimer.cancel()}}}}fun terminatedCall(context: Context, delayMillis: Long) {Log.i(TAG, "terminatedCall: delayMillis = $delayMillis")mTalkingTimer.cancel()mTerminateView?.isEnabled = falsemContentView?.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 WindowManagermLayoutParams = WindowManager.LayoutParams().apply {type = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANELgravity = Gravity.CENTERwidth = (354 * context.resources.displayMetrics.density + 0.5f).toInt()height = WindowManager.LayoutParams.WRAP_CONTENTflags =(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:
<activityandroid: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? = nullprivate var headsetClient: BluetoothHeadsetClient? = nullprivate lateinit var binding: NotificationActivityPhoneTalkingBindingprivate var mCurrentTalkingTime = 0private var mIsMute = falseprivate var mInitIsMute = falseprivate var mAudioManager: AudioManager? = nullprivate var mTalkingTimer = Timer()companion object {private val TAG = PhoneTalkingActivity::class.java.simpleNameprivate 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.packageNameintent.flags = Intent.FLAG_ACTIVITY_NEW_TASKintent.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 = mInitIsMuteLog.i(TAG, "onDestroy: mInitIsMute = $mInitIsMute,mIsMute = $mIsMute")}private fun initAudio() {mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManagermInitIsMute = mAudioManager?.isMicrophoneMute == truemIsMute = mInitIsMuteLog.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.GONEbinding.hangup.visibility = View.GONE// 还未接入指环,先不显示指环动画
// ringAnimationLayout.visibility = View.GONEbinding.hangupBig.visibility = View.VISIBLEbinding.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.VISIBLEbinding.mute.setOnClickListener {if (mIsMute) {mIsMute = falsebinding.mute.setImageResource(R.drawable.notification_mute_close)} else {mIsMute = truebinding.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.VISIBLEstartRecordTalkingTime()}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 = 1const val MUSIC = 2const val NOTIFICATION = 3@IntDef(RING, MUSIC, NOTIFICATION)@Retention(AnnotationRetention.SOURCE)private annotation class Typeprivate val TAG = SoundPoolTools::class.java.simpleNamefun play(context: Context, @Type type: Int, resId: Int?) {// 若是静音不播放val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManagerif (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_RINGMUSIC -> AudioManager.STREAM_MUSICNOTIFICATION -> AudioManager.STREAM_NOTIFICATIONelse -> AudioManager.STREAM_MUSIC}// 获取音效资源val rawId = resId ?: when (type) {RING -> R.raw.notification_messageMUSIC -> R.raw.notification_messageNOTIFICATION -> R.raw.notification_messageelse -> 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? = nullfun 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. ✅ 小结
对于蓝牙通话,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。