当前位置: 首页 > news >正文

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

目录

📂 前言

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.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. ✅ 小结

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

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


相关文章:

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

目录 &#x1f4c2; 前言 AR 眼镜系统版本 蓝牙电话 来电铃声 1. &#x1f531; 技术方案 1.1 结构框图 1.2 方案介绍 1.3 实现方案 步骤一&#xff1a;屏蔽原生蓝牙电话相关功能 步骤二&#xff1a;自定义蓝牙电话实现 2. &#x1f4a0; 屏蔽原生蓝牙电话相关功能 …...

stl-set

目录 目录 内部自动有序、不含重复元素 关于能不能自己造一个cmp&#xff0c;还挺复杂。 访问&#xff1a;只能用迭代器且受限 添加元素&#xff1a;没有pushback&#xff0c;用insert 复杂度&#xff1a;ologn ​编辑 查找元素find&#xff08;&#xff09;&#xff1…...

【Stable Diffusion】(基础篇五)—— 使用SD提升分辨率

使用SD提升分辨率 本系列博客笔记主要参考B站nenly同学的视频教程&#xff0c;传送门&#xff1a;B站第一套系统的AI绘画课&#xff01;零基础学会Stable Diffusion&#xff0c;这绝对是你看过的最容易上手的AI绘画教程 | SD WebUI 保姆级攻略_哔哩哔哩_bilibili 在前期作画的…...

5.CSS学习(浮动)

浮动&#xff08;float&#xff09; 是一种传统的网页布局方式&#xff0c;通过浮动&#xff0c;可以使元素脱离文档流的控制&#xff0c;使其横向排列。 其编写在CSS样式中。 float:none(默认值) 元素不浮动。 float:left 设置的元素在其包含…...

Spring Cloud微服务项目统一封装数据响应体

在微服务架构下&#xff0c;处理服务之间的通信和数据一致性是一个重要的挑战。为了提高开发效率、保证数据的一致性及简化前端开发&#xff0c;统一封装数据响应体是一种非常有效的实践。本文博主将介绍如何在 Spring Cloud 微服务项目中统一封装数据响应体&#xff0c;并分享…...

java算法day20

java算法day20 701.二叉搜索树中的插入操作450.删除二叉搜索树中的节点108 将有序数组转换为二叉搜索树 本次的题目都是用递归函数的返回值来完成&#xff0c;多熟悉这样的用法&#xff0c;很方便。 其实我感觉&#xff0c;涉及构造二叉树的题目&#xff0c;用递归函数的返回值…...

web自动化测试-python+selenium+unitest

文章目录 Web自动化测试工具1. 主流的Web自动化测试工具2. Selenium家族史 Web自动化测试环境搭建基于Python环境搭建示例&#xff1a;通过程序启动浏览器&#xff0c;并打开百度首页&#xff0c;暂停3秒&#xff0c;关闭浏览器 页面元素定位1. 如何进行元素定位&#xff1f;2.…...

LeetCode题练习与总结:组合两个表--175

一、题目描述 SQL Schema > Pandas Schema > 表: Person ---------------------- | 列名 | 类型 | ---------------------- | PersonId | int | | FirstName | varchar | | LastName | varchar | ---------------------- personId 是该表的主…...

数据结构:二叉搜索树(简单C++代码实现)

目录 前言 1. 二叉搜索树的概念 2. 二叉搜索树的实现 2.1 二叉树的结构 2.2 二叉树查找 2.3 二叉树的插入和中序遍历 2.4 二叉树的删除 3. 二叉搜索树的应用 3.1 KV模型实现 3.2 应用 4. 二叉搜索树分析 总结 前言 本文将深入探讨二叉搜索树这一重要的数据结构。二…...

深入理解Prompt工程

前言&#xff1a;因为大模型的流行&#xff0c;衍生出了一个小领域“Prompt工程”&#xff0c;不知道大家会不会跟小编一样&#xff0c;不就是写提示吗&#xff0c;这有什么难的&#xff0c;不过大家还是不要小瞧了Prompt工程&#xff0c;现在很多大模型把会“Prompt工程”作为…...

代码随想录算法训练营day6 | 242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1.两数之和

文章目录 哈希表键值 哈希函数哈希冲突拉链法线性探测法 常见的三种哈希结构集合映射C实现std::unordered_setstd::map 小结242.有效的字母异位词思路复习 349. 两个数组的交集使用数组实现哈希表的情况思路使用set实现哈希表的情况 202. 快乐数思路 1.两数之和思路 总结 今天是…...

vue3 vxe-table 点击行,不显示选中状态,加上设置isCurrent: true就可以设置选中行的状态。

1、上个图&#xff0c;要实现这样的&#xff1a; Vxe Table v4.6 官方文档 2、使用 row-config.isCurrent 显示高亮行&#xff0c;当前行是唯一的&#xff1b;用户操作点击选项时会触发事件 current-change <template><div><p><vxe-button click"sel…...

Linux没有telnet 如何测试对端的端口状态

前段时间有人问uos没有telnet&#xff0c;又找不到包。 追问了一下为什么非要安装telnet&#xff0c;答复是要测试对端的端口号。 这里简单介绍一下&#xff0c;测试端口号的方法有很多&#xff0c;telent只是在windows上经常使用&#xff0c;linux已很少安装并使用该命令&…...

花几千上万学习Java,真没必要!(二十九)

1、基本数据类型包装类&#xff1a; 测试代码1&#xff1a; package apitest.com; //使用Integer类的不同方法处理整数。 //将字符串转换为整数&#xff08;parseInt&#xff09;和Integer对象&#xff08;valueOf&#xff09;&#xff0c; //将整数转换回字符串&#xff08;…...

C#如何引用dll动态链接库文件的注释

1、dll动态库文件项目生成属性中要勾选“XML文档文件” 注意&#xff1a;XML文件的名字切勿修改。 2、添加引用时XML文件要与DLL文件在同一个目录下。 3、如果要是添加引用的时候XML不在相同目录下&#xff0c;之后又将XML文件复制到相同的目录下&#xff0c;需要删除引用&am…...

WordPress原创插件:自定义文章标题颜色

插件设置截图 文章编辑时&#xff0c;右边会出现一个标题颜色设置&#xff0c;可以设置为任何颜色 更新记录&#xff1a;从输入颜色css代码&#xff0c;改为颜色选择器&#xff0c;更方便&#xff01; 插件免费下载 https://download.csdn.net/download/huayula/89585192…...

Unity分享:继承自MonoBehaviour的脚步不要对引用类型的字段在声明时就初始化

如果某些字段在每个构造函数中都要进行初始化&#xff0c;很多人都喜欢在字段声明时就进行初始化&#xff0c;对于一个非继承自MonoBehaviour的脚步&#xff0c;这样做是没有问题的&#xff0c;然而继承自MonoBehaviour后就会造成内存的浪费&#xff0c;为什么呢&#xff1f;因…...

.NET Core中如何集成RabbitMQ

在.NET Core中集成RabbitMQ主要涉及到几个步骤&#xff0c;包括安装RabbitMQ的NuGet包、建立连接、定义队列、发送和接收消息等。下面是一个简单的指南来展示如何在.NET Core应用程序中集成RabbitMQ。 目录 1. 安装RabbitMQ.Client NuGet包 2. 建立连接 3. 定义队列 4. 发…...

嵌入式C++、STM32、MySQL、GPS、InfluxDB和MQTT协议数据可视化:智能物流管理系统设计思路流程(附代码示例)

目录 项目概述 系统设计 硬件设计 软件设计 系统架构图 代码实现 1. STM32微控制器与传感器代码 代码讲解 2. MQTT Broker设置 3. 数据接收与处理 代码讲解 4. 数据存储与分析 5. 数据分析与可视化 代码讲解 6. 数据可视化 项目总结 项目概述 随着电子商务的快…...

.net core docker部署教程和细节问题

在.NET Core中实现Docker一键部署&#xff0c;通常涉及以下几个步骤&#xff1a;编写Dockerfile以定义镜像构建过程、构建Docker镜像、运行Docker容器&#xff0c;以及&#xff08;可选地&#xff09;使用自动化工具如Docker Compose或CI/CD工具进行一键部署。以下是一个详细的…...

XCTF-web-easyupload

试了试php&#xff0c;php7&#xff0c;pht&#xff0c;phtml等&#xff0c;都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接&#xff0c;得到flag...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

【网络安全产品大调研系列】2. 体验漏洞扫描

前言 2023 年漏洞扫描服务市场规模预计为 3.06&#xff08;十亿美元&#xff09;。漏洞扫描服务市场行业预计将从 2024 年的 3.48&#xff08;十亿美元&#xff09;增长到 2032 年的 9.54&#xff08;十亿美元&#xff09;。预测期内漏洞扫描服务市场 CAGR&#xff08;增长率&…...

Objective-C常用命名规范总结

【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名&#xff08;Class Name)2.协议名&#xff08;Protocol Name)3.方法名&#xff08;Method Name)4.属性名&#xff08;Property Name&#xff09;5.局部变量/实例变量&#xff08;Local / Instance Variables&…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...