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 实现方案
步骤一:屏蔽原生蓝牙电话相关功能
-
禁止系统拉起来去电页面 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. ✅ 小结
对于蓝牙通话,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。
相关文章:
AR 眼镜之-蓝牙电话-实现方案
目录 📂 前言 AR 眼镜系统版本 蓝牙电话 来电铃声 1. 🔱 技术方案 1.1 结构框图 1.2 方案介绍 1.3 实现方案 步骤一:屏蔽原生蓝牙电话相关功能 步骤二:自定义蓝牙电话实现 2. 💠 屏蔽原生蓝牙电话相关功能 …...
stl-set
目录 目录 内部自动有序、不含重复元素 关于能不能自己造一个cmp,还挺复杂。 访问:只能用迭代器且受限 添加元素:没有pushback,用insert 复杂度:ologn 编辑 查找元素find()࿱…...
【Stable Diffusion】(基础篇五)—— 使用SD提升分辨率
使用SD提升分辨率 本系列博客笔记主要参考B站nenly同学的视频教程,传送门:B站第一套系统的AI绘画课!零基础学会Stable Diffusion,这绝对是你看过的最容易上手的AI绘画教程 | SD WebUI 保姆级攻略_哔哩哔哩_bilibili 在前期作画的…...
5.CSS学习(浮动)
浮动(float) 是一种传统的网页布局方式,通过浮动,可以使元素脱离文档流的控制,使其横向排列。 其编写在CSS样式中。 float:none(默认值) 元素不浮动。 float:left 设置的元素在其包含…...
Spring Cloud微服务项目统一封装数据响应体
在微服务架构下,处理服务之间的通信和数据一致性是一个重要的挑战。为了提高开发效率、保证数据的一致性及简化前端开发,统一封装数据响应体是一种非常有效的实践。本文博主将介绍如何在 Spring Cloud 微服务项目中统一封装数据响应体,并分享…...
java算法day20
java算法day20 701.二叉搜索树中的插入操作450.删除二叉搜索树中的节点108 将有序数组转换为二叉搜索树 本次的题目都是用递归函数的返回值来完成,多熟悉这样的用法,很方便。 其实我感觉,涉及构造二叉树的题目,用递归函数的返回值…...
web自动化测试-python+selenium+unitest
文章目录 Web自动化测试工具1. 主流的Web自动化测试工具2. Selenium家族史 Web自动化测试环境搭建基于Python环境搭建示例:通过程序启动浏览器,并打开百度首页,暂停3秒,关闭浏览器 页面元素定位1. 如何进行元素定位?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工程
前言:因为大模型的流行,衍生出了一个小领域“Prompt工程”,不知道大家会不会跟小编一样,不就是写提示吗,这有什么难的,不过大家还是不要小瞧了Prompt工程,现在很多大模型把会“Prompt工程”作为…...
代码随想录算法训练营day6 | 242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1.两数之和
文章目录 哈希表键值 哈希函数哈希冲突拉链法线性探测法 常见的三种哈希结构集合映射C实现std::unordered_setstd::map 小结242.有效的字母异位词思路复习 349. 两个数组的交集使用数组实现哈希表的情况思路使用set实现哈希表的情况 202. 快乐数思路 1.两数之和思路 总结 今天是…...
vue3 vxe-table 点击行,不显示选中状态,加上设置isCurrent: true就可以设置选中行的状态。
1、上个图,要实现这样的: Vxe Table v4.6 官方文档 2、使用 row-config.isCurrent 显示高亮行,当前行是唯一的;用户操作点击选项时会触发事件 current-change <template><div><p><vxe-button click"sel…...
Linux没有telnet 如何测试对端的端口状态
前段时间有人问uos没有telnet,又找不到包。 追问了一下为什么非要安装telnet,答复是要测试对端的端口号。 这里简单介绍一下,测试端口号的方法有很多,telent只是在windows上经常使用,linux已很少安装并使用该命令&…...
花几千上万学习Java,真没必要!(二十九)
1、基本数据类型包装类: 测试代码1: package apitest.com; //使用Integer类的不同方法处理整数。 //将字符串转换为整数(parseInt)和Integer对象(valueOf), //将整数转换回字符串(…...
C#如何引用dll动态链接库文件的注释
1、dll动态库文件项目生成属性中要勾选“XML文档文件” 注意:XML文件的名字切勿修改。 2、添加引用时XML文件要与DLL文件在同一个目录下。 3、如果要是添加引用的时候XML不在相同目录下,之后又将XML文件复制到相同的目录下,需要删除引用&am…...
WordPress原创插件:自定义文章标题颜色
插件设置截图 文章编辑时,右边会出现一个标题颜色设置,可以设置为任何颜色 更新记录:从输入颜色css代码,改为颜色选择器,更方便! 插件免费下载 https://download.csdn.net/download/huayula/89585192…...
Unity分享:继承自MonoBehaviour的脚步不要对引用类型的字段在声明时就初始化
如果某些字段在每个构造函数中都要进行初始化,很多人都喜欢在字段声明时就进行初始化,对于一个非继承自MonoBehaviour的脚步,这样做是没有问题的,然而继承自MonoBehaviour后就会造成内存的浪费,为什么呢?因…...
.NET Core中如何集成RabbitMQ
在.NET Core中集成RabbitMQ主要涉及到几个步骤,包括安装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一键部署,通常涉及以下几个步骤:编写Dockerfile以定义镜像构建过程、构建Docker镜像、运行Docker容器,以及(可选地)使用自动化工具如Docker Compose或CI/CD工具进行一键部署。以下是一个详细的…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...
pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)
目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
