Android蓝牙线控切歌、连接状态监听(无线耳机也适用)
阅读原文时间:2023年07月08日阅读:1

1. 监听蓝牙设备(音频)连接状态

所有代码已测试在Android11也能正常使用 (Android SDK 30)

首先新建一个广播类 BluetoothStateReceiver

/**
 * @author komine
 * 监听蓝牙音频设备的连接状态
 */
class BluetoothStateReceiver : BroadcastReceiver(){

    override fun onReceive(context: Context?, intent: Intent?) {
        when(intent?.action){

            //蓝牙已连接,如果不需要判断是否为音频设备,下面代码块的判断可以不要
            BluetoothDevice.ACTION_ACL_CONNECTED -> {
                val device =
                    intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
                        ?: return
                //如果在Android32 SDK还需要声明 android.permission.BLUETOOTH_CONNECT权限
                if (isHeadPhone(device.bluetoothClass)) {
                    Log.d("BluetoothStateReceiver", "蓝牙音频设备已连接")
                }
            }

            //蓝牙状态发生改变,断开,正在扫描,正在连接
            BluetoothAdapter.ACTION_STATE_CHANGED -> {
                val blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)
                if (blueState == BluetoothAdapter.STATE_OFF) {
                    Log.d("BluetoothStateReceiver", "蓝牙设备已断开")
                }
            }
        }
    }

    companion object{
        private const val PROFILE_HEADSET = 0

        private const val PROFILE_A2DP = 1

        private const val PROFILE_OPP = 2

        private const val PROFILE_HID = 3

        private const val PROFILE_PANU = 4

        private const val PROFILE_NAP = 5

        private const val PROFILE_A2DP_SINK = 6
    }

    private fun isHeadPhone(bluetoothClass: BluetoothClass?): Boolean {
        if (bluetoothClass == null) {
            return false
        }
        return if (doesClassMatch(bluetoothClass, PROFILE_HEADSET)) true else doesClassMatch(bluetoothClass, PROFILE_A2DP)
    }

    //系统源码拷贝
    //可以参考 http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/bluetooth/BluetoothClass.java
    private fun doesClassMatch(bluetoothClass: BluetoothClass, profile: Int): Boolean {
        return if (profile == PROFILE_A2DP) {
            if (bluetoothClass.hasService(BluetoothClass.Service.RENDER)) {
                return true
            }
            when (bluetoothClass.deviceClass) {
                BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO, BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES, BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER, BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO -> true
                else -> false
            }
        } else if (profile == PROFILE_A2DP_SINK) {
            if (bluetoothClass.hasService(BluetoothClass.Service.CAPTURE)) {
                return true
            }
            when (bluetoothClass.deviceClass) {
                BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO, BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX, BluetoothClass.Device.AUDIO_VIDEO_VCR -> true
                else -> false
            }
        } else if (profile == PROFILE_HEADSET) {
            // The render service class is required by the spec for HFP, so is a
            // pretty good signal
            if (bluetoothClass.hasService(BluetoothClass.Service.RENDER)) {
                return true
            }
            when (bluetoothClass.deviceClass) {
                BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE, BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET, BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO -> true
                else -> false
            }
        } else if (profile == PROFILE_OPP) {
            if (bluetoothClass.hasService(BluetoothClass.Service.OBJECT_TRANSFER)) {
                return true
            }
            when (bluetoothClass.deviceClass) {
                BluetoothClass.Device.COMPUTER_UNCATEGORIZED, BluetoothClass.Device.COMPUTER_DESKTOP, BluetoothClass.Device.COMPUTER_SERVER, BluetoothClass.Device.COMPUTER_LAPTOP, BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA, BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA, BluetoothClass.Device.COMPUTER_WEARABLE, BluetoothClass.Device.PHONE_UNCATEGORIZED, BluetoothClass.Device.PHONE_CELLULAR, BluetoothClass.Device.PHONE_CORDLESS, BluetoothClass.Device.PHONE_SMART, BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY, BluetoothClass.Device.PHONE_ISDN -> true
                else -> false
            }
        } else if (profile == PROFILE_HID) {
            bluetoothClass.deviceClass and BluetoothClass.Device.Major.PERIPHERAL == BluetoothClass.Device.Major.PERIPHERAL
        } else if (profile == PROFILE_PANU || profile == PROFILE_NAP) {
            // No good way to distinguish between the two, based on class bits.
            if (bluetoothClass.hasService(BluetoothClass.Service.NETWORKING)) {
                true
            } else bluetoothClass.deviceClass and BluetoothClass.Device.Major.NETWORKING == BluetoothClass.Device.Major.NETWORKING
        } else {
            false
        }
    }
}

然后在 MainActivity中动态注册广播

class MainActivity : AppCompatActivity() {
    private val mBluetoothStateReceiver:BluetoothStateReceiver = BluetoothStateReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        registerBluetoothReceiver()
    }

    private fun registerBluetoothReceiver(){
        val intentFilter = IntentFilter()
        intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
        intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
        intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
        intentFilter.addAction("android.bluetooth.BluetoothAdapter.STATE_OFF")
        intentFilter.addAction("android.bluetooth.BluetoothAdapter.STATE_ON")

        registerReceiver(mBluetoothStateReceiver,intentFilter)
    }
}

最后在 AndroidManifest.xml声明权限

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

记得取消注册广播,不然打开其他音乐应用的时候,程序可能会crash

2. 监听蓝牙设备键按下

####################################2022-04-28更新############################

1.疑似Bug,需要在初始化MediaPlayer之后再注册才会生效,MediaButtonReceiver(applicationContext,this)放在MeidePlayer初始化之后代码

2.MediaSession只能有一个实例,如果在程序中创建了多个实例也会导致接收不到,其实看它命名也应该知道了…

####################################2022-04-28更新############################

要监听蓝牙设备的按键按下,你的应用需要处于音乐播放的状态,比如应用正在使用 MediaPlayer播放音频,

如果未处于音频播放状态是不会触发回调方法.所以在演示代码中添加了MediaPlayer来播放音频.

首先新建一个类MediaButtonReceiver类

class MediaButtonReceiver(mContent: Context, mKeyDownListener: IKeyDownListener) {

    //创建一个MediaSession的实例,参数2是一个字符串,可以随便填
    private val mMediaSession = MediaSession(mContent, javaClass.name)

    internal annotation class KeyActions {
        companion object {
        //好像还支持手柄按键...
        //所有keyCode参考:https://www.apiref.com/android-zh/android/view/KeyEvent.html
            var PLAY_ACTION: Int = 126
        var PAUSE_ACTION: Int = 127
        var PREV_ACTION: Int = 88
        var NEXT_ACTION: Int = 87
    }
    }

    init {
        mMediaSession.setCallback(object : MediaSession.Callback() {
            override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
                val keyEvent: KeyEvent =
                    mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
                        ?: return false

        mKeyDownListener.onKeyDown(keyEvent.keyCode)

        //返回值的作用跟事件分发的原理是一样的,返回true代表事件被消费,其他应用也就收不到了
                return true
        }
        })
        mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS)
        mMediaSession.isActive = true
    }

    interface IKeyDownListener {
        fun onKeyDown(@KeyActions keyAction: Int)
    }
}

然后在 MainActivity添加播放的代码

class MainActivity : AppCompatActivity(),MediaButtonReceiver.IKeyDownListener {
    private val tag = javaClass.simpleName
    private val mMediaPlay:MediaPlayer = MediaPlayer()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mMediaPlay.reset()
        val audioAttributesBuilder = AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        mMediaPlay.setAudioAttributes(audioAttributesBuilder.build())
        //注意文件读写权限
        mMediaPlay.setDataSource(Environment.getExternalStorageDirectory().toString() + File.separator + "1.ape")
        mMediaPlay.setOnPreparedListener{
        mMediaPlay.start()
            MediaButtonReceiver(applicationContext,this)
        }
    mMediaPlay.prepareAsync()
    }

    override fun onKeyDown(keyAction: Int) {
        when(keyAction){
            MediaButtonReceiver.KeyActions.PLAY_ACTION -> Log.d(tag,"播放...")
            MediaButtonReceiver.KeyActions.PAUSE_ACTION -> Log.d(tag,"暂停...")
            MediaButtonReceiver.KeyActions.PREV_ACTION -> Log.d(tag,"上一首...")
            MediaButtonReceiver.KeyActions.NEXT_ACTION -> Log.d(tag,"下一首...")
        }
    }
}

打印结果

参考:

Android 扫描蓝牙设备并获取设备类型

ボタンイベントの受信と処理

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章