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 'me.tankery.lib:circularSeekBar:1.3.2'
|
||||
debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||
}
|
|
@ -38,8 +38,8 @@
|
|||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.MUSIC_PLAYER" />
|
||||
|
@ -212,7 +212,7 @@
|
|||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".service.MediaButtonIntentReceiver"
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
|
@ -323,6 +323,9 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
|
@ -179,10 +178,6 @@ class MediaSessionCallback(
|
|||
musicService.seek(pos.toInt())
|
||||
}
|
||||
|
||||
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
|
||||
return MediaButtonIntentReceiver.handleIntent(context, mediaButtonIntent)
|
||||
}
|
||||
|
||||
override fun onCustomAction(action: String, extras: Bundle?) {
|
||||
when (action) {
|
||||
CYCLE_REPEAT -> {
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
package code.name.monkey.retromusic.service
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.*
|
||||
|
@ -47,8 +46,8 @@ import androidx.media.AudioAttributesCompat.CONTENT_TYPE_MUSIC
|
|||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.session.MediaButtonReceiver.handleIntent
|
||||
import androidx.preference.PreferenceManager
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils.hasMarshmallow
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils.hasQ
|
||||
import code.name.monkey.retromusic.*
|
||||
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.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.gms.cast.framework.media.MediaIntentReceiver
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
@ -753,6 +753,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null && intent.action != null) {
|
||||
handleIntent(mediaSession, intent)
|
||||
restoreQueuesAndPositionIfNecessary()
|
||||
when (intent.action) {
|
||||
ACTION_TOGGLE_PAUSE -> if (isPlaying) {
|
||||
|
@ -1445,24 +1446,13 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
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(
|
||||
this,
|
||||
"RetroMusicPlayer",
|
||||
mediaButtonReceiverComponentName,
|
||||
mediaButtonReceiverPendingIntent
|
||||
"RetroMusicPlayer"
|
||||
)
|
||||
val mediasessionCallback = MediaSessionCallback(applicationContext, this)
|
||||
mediaSession?.setCallback(mediasessionCallback)
|
||||
mediaSession?.isActive = true
|
||||
mediaSession?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent)
|
||||
}
|
||||
|
||||
inner class MusicBinder : Binder() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue