playback: expose media button receiver

Expose a custom MediaButtonReceiver that handles the media button
intent. This is not because I wanted to implement this. Some apps
like gadgetbridge just blindly query ACTION_MEDIA_BUTTON instead of
relying on the more modern MediaController API, which I expected
most apps would use instead.

Resolves #62.
This commit is contained in:
OxygenCobalt 2022-01-25 17:17:32 -07:00
parent c9f789e388
commit 317b12579c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 107 additions and 82 deletions

2
.gitignore vendored
View file

@ -3,7 +3,7 @@
local.properties
build/
release/
deps/
srclibs/
# Studio
.idea/

View file

@ -50,8 +50,11 @@
</intent-filter>
</activity>
<!--
Workaround to get apps that blindly query for apps handling media buttons working
-->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:name=".playback.system.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
@ -62,16 +65,8 @@
android:name=".playback.system.PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:icon="@mipmap/ic_launcher"
android:exported="true"
android:roundIcon="@mipmap/ic_launcher">
<!--
Workaround to get apps that search for media apps by checking for a BroadcastReceiver
to detect Auxio, as we let the media APIs handle this.
-->
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
android:exported="false"
android:roundIcon="@mipmap/ic_launcher" />
<receiver
android:name=".widgets.WidgetProvider"

View file

@ -144,6 +144,8 @@ class AudioReactor(
// Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
// TODO: Support positive ReplayGain values. They're more obscure but still exist.
// It will likely require moving functionality from this class to an AudioProcessor
volume = MathUtils.clamp((10f.pow((adjust / 20f))), 0f, 1f)
}
@ -191,8 +193,10 @@ class AudioReactor(
}
// Case 2: R128 ReplayGain, most commonly found on FLAC files.
// While technically there is the R128 base gain in Opus files, ExoPlayer doesn't
// have metadata parsing functionality for those, so we just ignore it.
// While technically there is the R128 base gain in Opus files, that is automatically
// applied by the media framework [which ExoPlayer relies on]. The only reason we would
// want to read it is to zero previous ReplayGain values for being invalid, however there
// is no demand to fix that edge case right now.
tags.findLast { tag -> tag.key == R128_TRACK }?.let { tag ->
trackGain += tag.value / 256f
found = true

View file

@ -0,0 +1,27 @@
package org.oxycblt.auxio.playback.system
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
/**
* Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON
* intent to determine the media apps on a system. *Auxio does not expose this.* Auxio exposes
* a MediaSession that an app should control instead through the much better MediaController API.
* But who cares about that, we need to make sure the 3% of barely functioning TouchWiz devices
* running KitKat don't break! To prevent Auxio from not showing up at all in these apps, we
* declare a BroadcastReceiver that deliberately handles this event. This also means that Auxio
* will start without warning if you use the media buttons while the app exists, because I guess
* we just have to deal with this.
* @author OxygenCobalt
*/
class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent)
}
}
}

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback.system
import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -88,7 +87,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// System backend components
private lateinit var audioReactor: AudioReactor
private lateinit var widgets: WidgetController
private val systemReceiver = SystemEventReceiver()
private val systemReceiver = PlaybackReceiver()
// Managers
private val playbackManager = PlaybackStateManager.getInstance()
@ -102,10 +101,13 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// --- SERVICE OVERRIDES ---
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Since this service exposes a media button intent, pass it off to the
// MediaSession if the intent really is an instance of one.
MediaButtonReceiver.handleIntent(mediaSession, intent)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
// Workaround to get GadgetBridge and other apps that blindly query for
// ACTION_MEDIA_BUTTON working.
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
return START_NOT_STICKY
}
@ -146,18 +148,16 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// Then the notif/headset callbacks
IntentFilter().apply {
addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG)
addAction(ACTION_LOOP)
addAction(ACTION_SHUFFLE)
addAction(ACTION_SKIP_PREV)
addAction(ACTION_PLAY_PAUSE)
addAction(ACTION_SKIP_NEXT)
addAction(ACTION_EXIT)
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(Intent.ACTION_HEADSET_PLUG)
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
registerReceiver(systemReceiver, this)
@ -449,15 +449,29 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
}
/**
* A [BroadcastReceiver] for receiving system events from notifications, widgets, or
* headset plug events.
* A [BroadcastReceiver] for receiving general playback events from the system.
*/
private inner class SystemEventReceiver : BroadcastReceiver() {
private inner class PlaybackReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// --- SYSTEM EVENTS ---
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
AudioManager.SCO_AUDIO_STATE_CONNECTED -> resumeFromPlug()
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pauseFromPlug()
}
}
// --- NOTIFICATION CASES ---
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
AudioManager.ACTION_HEADSET_PLUG -> {
when (intent.getIntExtra("state", -1)) {
0 -> resumeFromPlug()
1 -> pauseFromPlug()
}
}
// --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
!playbackManager.isPlaying
)
@ -478,57 +492,41 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
stopForegroundAndNotification()
}
// --- HEADSET CASES ---
BluetoothDevice.ACTION_ACL_CONNECTED -> resumeFromPlug()
BluetoothDevice.ACTION_ACL_DISCONNECTED -> pauseFromPlug()
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
AudioManager.SCO_AUDIO_STATE_CONNECTED -> resumeFromPlug()
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pauseFromPlug()
}
}
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
Intent.ACTION_HEADSET_PLUG -> {
when (intent.getIntExtra("state", -1)) {
CONNECTED -> resumeFromPlug()
DISCONNECTED -> pauseFromPlug()
}
}
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
}
}
/**
* Resume from a headset plug event, as long as its allowed.
*/
private fun resumeFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device connected, resuming...")
playbackManager.setPlaying(true)
}
}
/**
* Pause from a headset plug, as long as its allowed.
*/
private fun pauseFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device disconnected, pausing...")
playbackManager.setPlaying(false)
else -> handleSystemIntent(intent)
}
}
}
private fun handleSystemIntent(intent: Intent) {
when (intent.action) {
Intent.ACTION_MEDIA_BUTTON -> MediaButtonReceiver.handleIntent(mediaSession, intent)
}
}
/**
* Resume from a headset plug event, as long as its allowed.
*/
private fun resumeFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device connected, resuming...")
playbackManager.setPlaying(true)
}
}
/**
* Pause from a headset plug, as long as its allowed.
*/
private fun pauseFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device disconnected, pausing...")
playbackManager.setPlaying(false)
}
}
companion object {
private const val DISCONNECTED = 0
private const val CONNECTED = 1
private const val POS_POLL_INTERVAL = 500L
const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP"

View file

@ -71,7 +71,6 @@ class SearchViewModel : ViewModel() {
if (query.isEmpty() || musicStore == null) {
mSearchResults.value = listOf()
return
}
@ -80,7 +79,7 @@ class SearchViewModel : ViewModel() {
val sort = Sort.ByName(true)
val results = mutableListOf<BaseModel>()
// A filter mode of null means to not filter at all.
// Note: a filter mode of null means to not filter at all.
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
musicStore.artists.filterByOrNull(query)?.let { artists ->

View file

@ -73,7 +73,7 @@
<string name="set_focus_desc">Pausieren wenn andere Töne abspielt wird [Bsp. Anrufe]</string>
<string name="set_plug_mgt">Kopfhörerfokus</string>
<string name="set_plug_mgt_desc">Abspielen/Pausieren wenn sich die Kopfhörerverbindung ändert</string>
<string name="set_replay_gain">ReplayGain (Nur MP3/FLAC)</string>
<string name="set_replay_gain">ReplayGain (Experimentell)</string>
<string name="set_replay_gain_off">Aus</string>
<string name="set_replay_gain_track">Titel bevorzugen</string>
<string name="set_replay_gain_album">Album bevorzugen</string>

View file

@ -85,7 +85,7 @@
<string name="set_focus_desc">Pause when other audio plays (ex. Calls)</string>
<string name="set_plug_mgt">Headset focus</string>
<string name="set_plug_mgt_desc">Play/Pause when the headset connection changes</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_replay_gain">ReplayGain (Experimental)</string>
<string name="set_replay_gain_off">Off</string>
<string name="set_replay_gain_track">Prefer track</string>
<string name="set_replay_gain_album">Prefer album</string>

View file

@ -37,10 +37,12 @@ This is for a couple reason:
defined ReplayGain standard for those.
- Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
an unrecognized name.
- Your tags use a ReplayGain value higher than 0. Due to technical limitations, Auxio does not support this right now.
I do plan to add it eventually.
#### What is dynamic ReplayGain?
Dynamic ReplayGain is a quirk based off the FooBar2000 plugin that dynamically switches from track gain to album
Dynamic ReplayGain is a quirk setting based off the FooBar2000 plugin that dynamically switches from track gain to album
gain depending on if the current playback is from an album or not.
#### Why are accents lighter/less saturated in dark mode?

View file

@ -30,7 +30,7 @@ def sh(cmd):
print(FATAL + "fatal:" + NC + " command failed with exit code " + str(code))
sys.exit(1)
exoplayer_path = os.path.join(os.path.abspath(os.curdir), "deps", "exoplayer")
exoplayer_path = os.path.join(os.path.abspath(os.curdir), "srclibs", "exoplayer")
if os.path.exists(exoplayer_path):
reinstall = input(INFO + "info:" + NC + " exoplayer is already installed. would you like to reinstall it? [y/n] ")
@ -69,7 +69,7 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk_build")):
system.exit(1)
# Now try to install ExoPlayer.
sh("rm -rf deps")
sh("rm -rf srclibs")
print(INFO + "info:" + NC + " cloning exoplayer...")
sh("git clone https://github.com/oxygencobalt/ExoPlayer.git " + exoplayer_path)

View file

@ -1,4 +1,4 @@
include ':app'
rootProject.name = "Auxio"
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
apply from: file("deps/exoplayer/core_settings.gradle")
apply from: file("srclibs/exoplayer/core_settings.gradle")