Use MediaButtonReceiver from androidx.media to handle headset button actions
This commit is contained in:
parent
b4dc50306f
commit
ad51d09672
5 changed files with 11 additions and 222 deletions
|
@ -172,4 +172,5 @@ dependencies {
|
||||||
implementation 'cat.ereza:customactivityoncrash:2.3.0'
|
implementation 'cat.ereza:customactivityoncrash:2.3.0'
|
||||||
implementation 'me.tankery.lib:circularSeekBar:1.3.2'
|
implementation 'me.tankery.lib:circularSeekBar:1.3.2'
|
||||||
debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6'
|
debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6'
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||||
}
|
}
|
|
@ -38,8 +38,8 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/SplashTheme"
|
android:launchMode="singleTop"
|
||||||
android:launchMode="singleTop">
|
android:theme="@style/SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.MUSIC_PLAYER" />
|
<action android:name="android.intent.action.MUSIC_PLAYER" />
|
||||||
|
@ -212,7 +212,7 @@
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".service.MediaButtonIntentReceiver"
|
android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
@ -323,6 +323,9 @@
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 Hemanth Savarala.
|
|
||||||
*
|
|
||||||
* Licensed under the GNU General Public License v3
|
|
||||||
*
|
|
||||||
* This is free software: you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
||||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the GNU General Public License for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package code.name.monkey.retromusic.service
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Message
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.os.PowerManager.WakeLock
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import code.name.monkey.retromusic.BuildConfig
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PAUSE
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PLAY
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_STOP
|
|
||||||
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to control headset playback.
|
|
||||||
* Single press: pause/resume
|
|
||||||
* Double press: actionNext track
|
|
||||||
* Triple press: previous track
|
|
||||||
*/
|
|
||||||
class MediaButtonIntentReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (DEBUG) Log.v(TAG, "Received intent: $intent")
|
|
||||||
if (handleIntent(context, intent) && isOrderedBroadcast) {
|
|
||||||
abortBroadcast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val TAG: String = MediaButtonIntentReceiver::class.java.simpleName
|
|
||||||
private val DEBUG = BuildConfig.DEBUG
|
|
||||||
private const val MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2
|
|
||||||
|
|
||||||
private const val DOUBLE_CLICK = 400
|
|
||||||
|
|
||||||
private var wakeLock: WakeLock? = null
|
|
||||||
private var mClickCounter = 0
|
|
||||||
private var mLastClickTime: Long = 0
|
|
||||||
|
|
||||||
@SuppressLint("HandlerLeak") // false alarm, handler is already static
|
|
||||||
private val mHandler = object : Handler() {
|
|
||||||
|
|
||||||
override fun handleMessage(msg: Message) {
|
|
||||||
when (msg.what) {
|
|
||||||
MSG_HEADSET_DOUBLE_CLICK_TIMEOUT -> {
|
|
||||||
val clickCount = msg.arg1
|
|
||||||
|
|
||||||
if (DEBUG) Log.v(TAG, "Handling headset click, count = $clickCount")
|
|
||||||
val command = when (clickCount) {
|
|
||||||
1 -> ACTION_TOGGLE_PAUSE
|
|
||||||
2 -> ACTION_SKIP
|
|
||||||
3 -> ACTION_REWIND
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command != null) {
|
|
||||||
val context = msg.obj as Context
|
|
||||||
startService(context, command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
releaseWakeLockIfHandlerIdle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleIntent(context: Context, intent: Intent): Boolean {
|
|
||||||
val intentAction = intent.action
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON == intentAction) {
|
|
||||||
val event = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
|
||||||
?: return false
|
|
||||||
|
|
||||||
val keycode = event.keyCode
|
|
||||||
val action = event.action
|
|
||||||
val eventTime = if (event.eventTime != 0L)
|
|
||||||
event.eventTime
|
|
||||||
else
|
|
||||||
System.currentTimeMillis()
|
|
||||||
|
|
||||||
var command: String? = null
|
|
||||||
when (keycode) {
|
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> command = ACTION_STOP
|
|
||||||
KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> command =
|
|
||||||
ACTION_TOGGLE_PAUSE
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> command = ACTION_SKIP
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> command = ACTION_REWIND
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> command = ACTION_PAUSE
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> command = ACTION_PLAY
|
|
||||||
}
|
|
||||||
if (command != null) {
|
|
||||||
if (action == KeyEvent.ACTION_DOWN) {
|
|
||||||
if (event.repeatCount == 0) {
|
|
||||||
// Only consider the first event in a sequence, not the repeat events,
|
|
||||||
// so that we don't trigger in cases where the first event went to
|
|
||||||
// a different app (e.g. when the user ends a phone call by
|
|
||||||
// long pressing the headset button)
|
|
||||||
|
|
||||||
// The service may or may not be running, but we need to send it
|
|
||||||
// a command.
|
|
||||||
if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
|
||||||
if (eventTime - mLastClickTime >= DOUBLE_CLICK) {
|
|
||||||
mClickCounter = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
mClickCounter++
|
|
||||||
if (DEBUG) Log.v(TAG, "Got headset click, count = $mClickCounter")
|
|
||||||
mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)
|
|
||||||
|
|
||||||
val msg = mHandler.obtainMessage(
|
|
||||||
MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context
|
|
||||||
)
|
|
||||||
|
|
||||||
val delay = (if (mClickCounter < 3) DOUBLE_CLICK else 0).toLong()
|
|
||||||
if (mClickCounter >= 3) {
|
|
||||||
mClickCounter = 0
|
|
||||||
}
|
|
||||||
mLastClickTime = eventTime
|
|
||||||
acquireWakeLockAndSendMessage(context, msg, delay)
|
|
||||||
} else {
|
|
||||||
startService(context, command)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startService(context: Context, command: String?) {
|
|
||||||
val intent = Intent(context, MusicService::class.java)
|
|
||||||
intent.action = command
|
|
||||||
try {
|
|
||||||
// IMPORTANT NOTE: (kind of a hack)
|
|
||||||
// on Android O and above the following crashes when the app is not running
|
|
||||||
// there is no good way to check whether the app is running so we catch the exception
|
|
||||||
// we do not always want to use startForegroundService() because then one gets an ANR
|
|
||||||
// if no notification is displayed via startForeground()
|
|
||||||
// according to Play analytics this happens a lot, I suppose for example if command = PAUSE
|
|
||||||
context.startService(intent)
|
|
||||||
} catch (ignored: IllegalStateException) {
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun acquireWakeLockAndSendMessage(context: Context, msg: Message, delay: Long) {
|
|
||||||
if (wakeLock == null) {
|
|
||||||
val appContext = context.applicationContext
|
|
||||||
val pm = appContext.getSystemService<PowerManager>()
|
|
||||||
wakeLock = pm?.newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK,
|
|
||||||
"RetroMusicApp:Wakelock headset button"
|
|
||||||
)
|
|
||||||
wakeLock!!.setReferenceCounted(false)
|
|
||||||
}
|
|
||||||
if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what)
|
|
||||||
// Make sure we don't indefinitely hold the wake lock under any circumstances
|
|
||||||
wakeLock!!.acquire(10000)
|
|
||||||
|
|
||||||
mHandler.sendMessageDelayed(msg, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseWakeLockIfHandlerIdle() {
|
|
||||||
if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
|
|
||||||
if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wakeLock != null) {
|
|
||||||
if (DEBUG) Log.v(TAG, "Releasing wake lock")
|
|
||||||
wakeLock!!.release()
|
|
||||||
wakeLock = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@
|
||||||
package code.name.monkey.retromusic.service
|
package code.name.monkey.retromusic.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
@ -179,10 +178,6 @@ class MediaSessionCallback(
|
||||||
musicService.seek(pos.toInt())
|
musicService.seek(pos.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
|
|
||||||
return MediaButtonIntentReceiver.handleIntent(context, mediaButtonIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCustomAction(action: String, extras: Bundle?) {
|
override fun onCustomAction(action: String, extras: Bundle?) {
|
||||||
when (action) {
|
when (action) {
|
||||||
CYCLE_REPEAT -> {
|
CYCLE_REPEAT -> {
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
package code.name.monkey.retromusic.service
|
package code.name.monkey.retromusic.service
|
||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
@ -47,8 +46,8 @@ import androidx.media.AudioAttributesCompat.CONTENT_TYPE_MUSIC
|
||||||
import androidx.media.AudioFocusRequestCompat
|
import androidx.media.AudioFocusRequestCompat
|
||||||
import androidx.media.AudioManagerCompat
|
import androidx.media.AudioManagerCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
import androidx.media.session.MediaButtonReceiver.handleIntent
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import code.name.monkey.appthemehelper.util.VersionUtils.hasMarshmallow
|
|
||||||
import code.name.monkey.appthemehelper.util.VersionUtils.hasQ
|
import code.name.monkey.appthemehelper.util.VersionUtils.hasQ
|
||||||
import code.name.monkey.retromusic.*
|
import code.name.monkey.retromusic.*
|
||||||
import code.name.monkey.retromusic.activities.LockScreenActivity
|
import code.name.monkey.retromusic.activities.LockScreenActivity
|
||||||
|
@ -94,6 +93,7 @@ import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener
|
||||||
import com.bumptech.glide.RequestBuilder
|
import com.bumptech.glide.RequestBuilder
|
||||||
import com.bumptech.glide.request.target.SimpleTarget
|
import com.bumptech.glide.request.target.SimpleTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.google.android.gms.cast.framework.media.MediaIntentReceiver
|
||||||
import org.koin.java.KoinJavaComponent.get
|
import org.koin.java.KoinJavaComponent.get
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
@ -753,6 +753,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent != null && intent.action != null) {
|
if (intent != null && intent.action != null) {
|
||||||
|
handleIntent(mediaSession, intent)
|
||||||
restoreQueuesAndPositionIfNecessary()
|
restoreQueuesAndPositionIfNecessary()
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_TOGGLE_PAUSE -> if (isPlaying) {
|
ACTION_TOGGLE_PAUSE -> if (isPlaying) {
|
||||||
|
@ -1445,24 +1446,13 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupMediaSession() {
|
private fun setupMediaSession() {
|
||||||
val mediaButtonReceiverComponentName =
|
|
||||||
ComponentName(applicationContext, MediaButtonIntentReceiver::class.java)
|
|
||||||
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
|
||||||
mediaButtonIntent.component = mediaButtonReceiverComponentName
|
|
||||||
val mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(
|
|
||||||
applicationContext, 0, mediaButtonIntent,
|
|
||||||
if (hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE else 0
|
|
||||||
)
|
|
||||||
mediaSession = MediaSessionCompat(
|
mediaSession = MediaSessionCompat(
|
||||||
this,
|
this,
|
||||||
"RetroMusicPlayer",
|
"RetroMusicPlayer"
|
||||||
mediaButtonReceiverComponentName,
|
|
||||||
mediaButtonReceiverPendingIntent
|
|
||||||
)
|
)
|
||||||
val mediasessionCallback = MediaSessionCallback(applicationContext, this)
|
val mediasessionCallback = MediaSessionCallback(applicationContext, this)
|
||||||
mediaSession?.setCallback(mediasessionCallback)
|
mediaSession?.setCallback(mediasessionCallback)
|
||||||
mediaSession?.isActive = true
|
mediaSession?.isActive = true
|
||||||
mediaSession?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MusicBinder : Binder() {
|
inner class MusicBinder : Binder() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue