目录
步骤一:禁止系统拉起来去电页面 InCallActivity
步骤一:注册 BluetoothHeadsetClient.ACTION_CALL_CHANGED 广播 Action 监听来电/接通/挂断状态
📂 前言
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.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. ✅ 小结
对于蓝牙通话,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。