#イヤホンの接続を検出したい
Androidで音楽アプリを作っているのですが(下記参照)、イヤホンの接続時に自動で音楽再生待機状態になって、Bluetoothイヤホンの場合なんか接続した瞬間にそのまま再生ボタンを押せば音楽再生できれば神じゃないかと。そう思いまして実装してみました。(イヤホンかイヤフォンか。どっちが正しいのかは知りません)
最初、この記事は「イヤホンの抜き差しを検出したい」ってタイトルだったんですけど、イヤホンの「抜け」はAudioManager.ACTION_AUDIO_BECOMING_NOISY
をキャッチするだけで簡単なんでやめました。
作っているのはこのアプリ。実際にどのような動作になるのかはアプリをインストールして確認してほしい。設定方法は下記の「解説」部分に書いてあるのでそちら参照。
#問題
単にイヤホンの接続といっても色々なパターンがある。例えば有線のイヤホン接続やワイヤレスのBluetooth接続なんかもある。それらのすべてに対応したい。
-> 有線とBluetoothで処理を分ける必要がある
#方針
今回はイヤホンの接続時に音楽再生待機状態にしたいので、アプリが起動していないときに接続される可能性も鑑みなければならない。故に、ForegrondServiceでの実装となる。
###有線イヤホンの場合
AudioManager.ACTION_HEADSET_PLUG
を拾えばいいらしい。その時にintentにくっついてきているstateを取り出して、接続状態を調べる。ここがちょっと不確かなのだが、以下のような感じらしい。
state | 状態 |
---|---|
0 | イヤホンが抜けている |
1 | イヤホンが刺さっている |
2 | マイク付きイヤホンが刺さっている |
ということでstateが0より大きいかで判断すればいいようだ。情報が不確かなので間違ていたらコメントよろしくお願いします。
###Bluetoothイヤホンの場合
Bluetoothの場合は一段とめんどくさい。とりあえずBluetoothへのアクセスを実行するためにandroid.permission.BLUETOOTH
を追加する。
そして、Bluetoothの接続・切断を検知するにはBluetoothProfile.ServiceListener
をgetProfileProxy
で登録すればいいらしい。接続時に呼ばれるリスナーの引数にあるBluetoothProfile
より、接続したのがBluetoothイヤホン(ヘッドセット)なのかを判定すればいい。
...と思っていた。実際途中までこの方針でコードを組んでいたのだが、ここであることに気づく。そう、BluetoothProfile.ServiceListener
が意味わからんタイミングで呼ばれるのである。接続した瞬間に切断したり、接続が2回呼ばれたりと。
これはたまらんと思い、BluetoothProfile.ServiceListener
は接続された(接続される予定の・接続されている)デバイスがイヤホンなのかを判定するためだけに使用することにした。では、接続されたと最終的に検知するにはどのようにしたらいいのか。
これはBluetoothDevice.ACTION_ACL_CONNECTED
をキャッチすればいい。先ほどのBluetoothProfile.ServiceListener
よりもわずかに遅く呼ばれるため、BluetoothProfile.ServiceListener
でBluetoothProfile
を保持しておけば、BluetoothDevice.ACTION_ACL_CONNECTED
をキャッチしたときに最終的な判定ができるという仕組みだ。
#実装
class MonitorHeadsetService : Service() {
private var currentBluetoothHeadset: BluetoothHeadset? = null
private var isBluetoothHeadsetConnected = false
private val monitorHeadsetFilter = IntentFilter().apply {
addAction(AudioManager.ACTION_HEADSET_PLUG)
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
private val monitorHeadsetReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
AudioManager.ACTION_HEADSET_PLUG -> {
if (intent.getIntExtra("state", -1) > 0) {
onInsertHeadset()
}
}
BluetoothDevice.ACTION_ACL_CONNECTED -> {
Thread.sleep(2000)
Log.d(TAG, "Broadcast: ACTION_ACL_CONNECTED")
if (currentBluetoothHeadset?.connectedDevices?.size ?: 0 > 0) {
isBluetoothHeadsetConnected = true
onInsertHeadset()
}
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
Log.d(TAG, "Broadcast: ACTION_ACL_DISCONNECTED")
isBluetoothHeadsetConnected = false
}
}
}
}
private val bluetoothPolicyListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
if (profile == BluetoothProfile.HEADSET) {
Log.d(TAG, "BluetoothProfile onServiceConnected")
currentBluetoothHeadset = proxy as BluetoothHeadset
isBluetoothHeadsetConnected = (currentBluetoothHeadset?.connectedDevices?.size ?: 0 > 0)
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
Log.d(TAG, "BluetoothProfile onServiceDisconnected")
currentBluetoothHeadset = null
}
}
}
override fun onBind(p0: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
Log.d(TAG, "MonitorHeadsetService onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(MONITOR_HEADSET_SERVICE_ID, createMonitorNotify())
}
}
override fun onDestroy() {
super.onDestroy()
try {
unregisterReceiver(monitorHeadsetReceiver)
}
catch (e: Exception){
e.printStackTrace()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
registerReceiver(monitorHeadsetReceiver, monitorHeadsetFilter)
bluetoothAdapter.getProfileProxy(this, bluetoothPolicyListener, BluetoothProfile.HEADSET)
return START_STICKY
}
private fun onInsertHeadset() {
Log.d(TAG, "Headset is inserting.")
//音楽再生待機状態へ
}
private fun createMonitorNotify(): Notification {
val notifyTitle = getString(R.string.mMonitorNotifyTitle)
val notifyText = getString(R.string.mMonitorNotifyText)
val notifyDescription = getString(R.string.mMonitorNotifyDescription)
val notifyChannelName = getString(R.string.mMonitorNotifyChannelName)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(MONITOR_HEADSET_NOTIFY_ID.toString()) == null) {
val channel = NotificationChannel(MONITOR_HEADSET_NOTIFY_ID.toString(), notifyChannelName, NotificationManager.IMPORTANCE_LOW).apply {
description = notifyDescription
}
notificationManager.createNotificationChannel(channel)
}
val stackBuilder = TaskStackBuilder.create(this).addNextIntent(Intent(this, MainActivity::class.java))
val pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
val notificationBuilder = NotificationCompat.Builder(this, MONITOR_HEADSET_NOTIFY_ID.toString()).apply {
setSmallIcon(R.drawable.ic_headset)
setContentTitle(notifyTitle)
setContentText(notifyText)
setAutoCancel(false)
setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
setContentIntent(pendingIntent)
}
return notificationBuilder.build()
}
companion object {
const val MONITOR_HEADSET_SERVICE_ID = 72
const val MONITOR_HEADSET_NOTIFY_ID = 69
}
}
#解説
...とはいっても「方針」の部分で大部分を話しているので省略。
が、ポイントを一つ。先程、BluetoothDevice.ACTION_ACL_CONNECTED
のほうが飛んでくるのが遅いと書いたけれど、実機で確認していると「遅い」というより「同時」で、BluetoothProfile
を保持するよりも早くBluetoothDevice.ACTION_ACL_CONNECTED
が飛んできてしまうという事態が何度かあった。
そのため、BluetoothDevice.ACTION_ACL_CONNECTED
の処理部分では2秒の待機処理を挟むことにした。本当はこのような書き方はよくないと思うのだけど、2秒も待機すればほぼ大丈夫だと思う。もっと良い方法がある方はコメントお願いします。
#感想
色々と腑に落ちない部分はあったけれど、以外にもあっさりと解決してしまった。宣伝ではないけど、最終的にどのような動作になったのか確認したい方は、下記のアプリで確認できる。設定->全般->ヘッドセットの状態を検出をONにしてから、イヤホンを抜き差ししてもらいたい。アプリがKillされていてもサービスが起動状態になっていれば音楽再生待機状態に移行すると思う(通知が来る)。
この記事が参考になったなら、ぜひアプリをインストールしてレビューしてほしい。