Android蓝牙音乐
项目背景: 展示蓝牙音乐信息(歌曲名称、播放进度、歌手、歌词等等)和控制操作蓝牙音乐
1.监听蓝牙设备状态
注意点 :需要动态注册广播,和动态申请权限,不然会接收不到广播
class BluetoothBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BluetoothMusicTool"
}
override fun onReceive(context: Context, intent: Intent) {
log("intent.action :${intent.action}")
when (intent.action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
BluetoothAdapter.STATE_ON -> {
log("open bluetooth")
}
BluetoothAdapter.STATE_OFF -> {
log("close bluetooth")
}
}
}
BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, 0)) {
BluetoothAdapter.STATE_CONNECTED -> {
PreferenceCompanion.setBt()
log("bluetooth connect device===${PreferenceCompanion.getBt()}")
}
BluetoothAdapter.STATE_DISCONNECTED -> {
PreferenceCompanion.setBt(IBluetoothMusicTool.STATE_DISCONNECT)
log("bluetooth disconnect device===${PreferenceCompanion.getBt()}")
}
}
}
}
}
}
申请权限
fun initPermission(activity: Activity?) {
activity?.parent?.let {
ActivityCompat.requestPermissions(
it, arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.BLUETOOTH_PRIVILEGED
), BLUETOOTH_REQUEST_CODE
)
}
}
2.媒体会话连接管理工具类
class MediaServiceConnection(context: Context, serviceComponent: ComponentName) {
val isConnected = MutableLiveData<Boolean>()
.apply { postValue(false) }
val rootMediaId: String get() = mediaBrowser.root
val playbackState = MutableLiveData<PlaybackStateCompat>()
.apply { postValue(EMPTY_PLAYBACK_STATE) }
val nowPlaying = MutableLiveData<MediaMetadataCompat>()
.apply { postValue(NOTHING_PLAYING) }
val transportControls: MediaControllerCompat.TransportControls
get() = mediaController.transportControls
private val mediaBrowserConnectionCallback = MediaBrowserConnectionCallback(context)
private val mediaBrowser = MediaBrowserCompat(
context,
serviceComponent,
mediaBrowserConnectionCallback, null
).apply { connect() }
lateinit var mediaController: MediaControllerCompat
fun subscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {
mediaBrowser.subscribe(parentId, callback)
}
fun unsubscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {
mediaBrowser.unsubscribe(parentId, callback)
}
fun sendCommand(command: String, parameters: Bundle?) =
sendCommand(command, parameters) { _, _ -> }
fun sendCommand(
command: String,
parameters: Bundle?,
resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
resultCallback(resultCode, resultData)
}
})
true
} else {
false
}
private inner class MediaBrowserConnectionCallback(private val context: Context) :
MediaBrowserCompat.ConnectionCallback() {
/**
* Invoked after [MediaBrowserCompat.connect] when the request has successfully
* completed.
*/
override fun onConnected() {
log("onConnect")
mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken).apply {
registerCallback(MediaControllerCallback())
}
mediaController.playbackState
isConnected.postValue(true)
}
/**
* Invoked when the client is disconnected from the media browser.
*/
override fun onConnectionSuspended() {
isConnected.postValue(false)
}
/**
* Invoked when the connection to the media browser failed.
*/
override fun onConnectionFailed() {
isConnected.postValue(false)
}
}
private inner class MediaControllerCallback : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
//PlaybackState {state=3, position=53956, buffered position=0, speed=1.0, updated=2876951, actions=55,
// error code=0, error message=null, custom actions=[], active item id=-1}
log("state===$state")
playbackState.postValue(state ?: EMPTY_PLAYBACK_STATE)
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
nowPlaying.postValue(
if (metadata?.id == null) {
NOTHING_PLAYING
} else {
metadata
}
)
}
override fun onQueueChanged(queue: MutableList<MediaSessionCompat.QueueItem>?) {
}
override fun onSessionEvent(event: String?, extras: Bundle?) {
super.onSessionEvent(event, extras)
}
/**
* Normally if a [MediaBrowserServiceCompat] drops its connection the callback comes via
* [MediaControllerCompat.Callback] (here). But since other connection status events
* are sent to [MediaBrowserCompat.ConnectionCallback], we catch the disconnect here and
* send it on to the other callback.
*/
override fun onSessionDestroyed() {
mediaBrowserConnectionCallback.onConnectionSuspended()
}
}
companion object {
@Volatile
private var instance: MediaServiceConnection? = null
fun getInstance(context: Context, serviceComponent: ComponentName) =
instance ?: synchronized(this) {
instance ?: MediaServiceConnection(context, serviceComponent)
.also { instance = it }
}
}
}
@Suppress("PropertyName")
val EMPTY_PLAYBACK_STATE: PlaybackStateCompat = PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE, 0, 0f)
.build()
@Suppress("PropertyName")
val NOTHING_PLAYING: MediaMetadataCompat = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "")
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 0)
.build()
3.连接蓝牙音乐监听状态变化
注意点 获取歌词需要在手机端App上面 开启外界设备蓝牙歌词
interface IBluetoothMusicTool {
fun init(app: Application)
}
object BluetoothMusicTool : IBluetoothMusicTool {
private const val TAG = "BluetoothMusicTool"
//蓝牙音乐service
private const val MEDIA_BROWSER_SERVICE_PACKAGE = "com.android.bluetooth"
private const val MEDIA_BROWSER_SERVICE =
"com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService"
private lateinit var app: Application
private lateinit var mediaBrowser: MediaBrowserCompat
private lateinit var mediaControllerCompat: MediaControllerCompat
private const val BLUETOOTH_REQUEST_CODE = 100
private var isPlaying = false
private var mediaMetadataCompat = MutableLiveData<MediaMetadataCompat>()
private var playbackStateCompat = MutableLiveData<PlaybackStateCompat>()
private var isPlayAudioFocus = false
private const val UNKNOWN = "未知"
//在Application中初始化蓝牙连接操作
override fun init(app: Application) {
BluetoothMusicTool.app = app
val componentName = ComponentName(MEDIA_BROWSER_SERVICE_PACKAGE, MEDIA_BROWSER_SERVICE)
mediaBrowser = MediaBrowserCompat(
app,
componentName,
connectionCallbacks,
null
)
mediaBrowser.connect()
//注册蓝牙状态监听广播
registerBluetoothBroadcast()
}
private fun registerBluetoothBroadcast() {
val broadcastReceiver = BluetoothBroadcastReceiver()
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
App.get().registerReceiver(broadcastReceiver, intentFilter)
}
private val connectionCallbacks: MediaBrowserCompat.ConnectionCallback =
object : MediaBrowserCompat.ConnectionCallback() {
override fun onConnected() {
// Android 7-9的service com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService
//蓝牙音乐服务连接成功
log("bluetooth music connect success")
mediaControllerCompat = MediaControllerCompat(app, mediaBrowser.sessionToken)
//注册蓝牙音乐信息状态监听
if (mediaControllerCompat.metadata != null) {
mediaMetadataCompat.value = mediaControllerCompat.metadata
} mediaControllerCompat.registerCallback(controllerCallback)
}
override fun onConnectionSuspended() {
log("bluetooth music connect suspend")
}
override fun onConnectionFailed() {
log("bluetooth music connect failed")
}
}
private val controllerCallback: MediaControllerCompat.Callback =
object : MediaControllerCompat.Callback() {
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
//蓝牙音乐信息变化之后在这里进行回调
if (playbackStateCompat.value?.state == PlaybackStateCompat.STATE_PLAYING) {
//捕获异常的工具类
catchException {
metadata?.apply {
if (album == currentAlbum) {
//音乐信息相同直接返回
return@apply
}
mediaMetadataCompat.value = this
//歌词信息 title是kotlin扩展函数
//getString(MediaMetadataCompat.METADATA_KEY_TITLE)
val title = title
//歌曲名称
//getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
val songName = (artist?.substringBefore("-") ?: artist) ?: UNKNOWN
//getString(MediaMetadataCompat.METADATA_KEY_ALBUM)
val album = album
//歌曲播放总时长
//getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
val duration = duration
//歌手
val artist = (artist?.substringAfter("-") ?: artist) ?: UNKNOWN
//音频封面
val albumArtUri = albumArtUri ?: UNKNOWN
var lyric = "暂无歌词"
if (title != album) {
//有歌词
title?.apply {
lyric = this
}
}
}
}
}
}
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
state?.let { playInfo ->
playbackStateCompat.value = playInfo
//更新播放状态
// 播放状态和进度
}
}
}
fun unregisterCallback() {
mediaControllerCompat.unregisterCallback(controllerCallback)
}
fun initPermission(activity: Activity?) {
activity?.parent?.let {
ActivityCompat.requestPermissions(
it, arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.BLUETOOTH_PRIVILEGED
), BLUETOOTH_REQUEST_CODE
)
}
}
fun playOrPause() {
if (PlayControl.get().playbackState.value?.state == PlaybackStateCompat.STATE_PAUSED) {
play()
} else {
pause()
}
}
}
4.控制蓝牙音乐
fun skipToPrevious() {
catchException {
mediaControllerCompat.transportControls.skipToPrevious()
}
}
fun skipToNext() {
catchException {
mediaControllerCompat.transportControls.skipToNext()
}
}
fun play() {
catchException {
mediaControllerCompat.transportControls.play()
}
}
fun pause() {
catchException {
mediaControllerCompat.transportControls.pause()
}
}
fun seekTo(@FloatRange(from = 0.0, to = 1.0) pos: Float) {
catchException {
val duration = mediaMetadataCompat.value?.duration
duration?.apply {
mediaControllerCompat.transportControls.seekTo((this * pos).toLong())
}
}
}
private fun catchException(command: (() -> Unit)) {
try {
command()
} catch (e: Exception) {
log("catchException :$e")
}
}