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 local.properties
build/ build/
release/ release/
deps/ srclibs/
# Studio # Studio
.idea/ .idea/

View file

@ -50,8 +50,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!--
Workaround to get apps that blindly query for apps handling media buttons working
-->
<receiver <receiver
android:name="androidx.media.session.MediaButtonReceiver" android:name=".playback.system.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" />
@ -62,16 +65,8 @@
android:name=".playback.system.PlaybackService" android:name=".playback.system.PlaybackService"
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:exported="true" android:exported="false"
android:roundIcon="@mipmap/ic_launcher"> 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>
<receiver <receiver
android:name=".widgets.WidgetProvider" android:name=".widgets.WidgetProvider"

View file

@ -144,6 +144,8 @@ class AudioReactor(
// Final adjustment along the volume curve. // Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume. // 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) 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. // Case 2: R128 ReplayGain, most commonly found on FLAC files.
// While technically there is the R128 base gain in Opus files, ExoPlayer doesn't // While technically there is the R128 base gain in Opus files, that is automatically
// have metadata parsing functionality for those, so we just ignore it. // 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 -> tags.findLast { tag -> tag.key == R128_TRACK }?.let { tag ->
trackGain += tag.value / 256f trackGain += tag.value / 256f
found = true 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.NotificationManager
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -88,7 +87,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// System backend components // System backend components
private lateinit var audioReactor: AudioReactor private lateinit var audioReactor: AudioReactor
private lateinit var widgets: WidgetController private lateinit var widgets: WidgetController
private val systemReceiver = SystemEventReceiver() private val systemReceiver = PlaybackReceiver()
// Managers // Managers
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
@ -102,10 +101,13 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// --- SERVICE OVERRIDES --- // --- SERVICE OVERRIDES ---
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Since this service exposes a media button intent, pass it off to the if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
// MediaSession if the intent really is an instance of one. // Workaround to get GadgetBridge and other apps that blindly query for
MediaButtonReceiver.handleIntent(mediaSession, intent) // ACTION_MEDIA_BUTTON working.
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
return START_NOT_STICKY return START_NOT_STICKY
} }
@ -146,18 +148,16 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// Then the notif/headset callbacks // Then the notif/headset callbacks
IntentFilter().apply { 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_LOOP)
addAction(ACTION_SHUFFLE) addAction(ACTION_SHUFFLE)
addAction(ACTION_SKIP_PREV) addAction(ACTION_SKIP_PREV)
addAction(ACTION_PLAY_PAUSE) addAction(ACTION_PLAY_PAUSE)
addAction(ACTION_SKIP_NEXT) addAction(ACTION_SKIP_NEXT)
addAction(ACTION_EXIT) 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) addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
registerReceiver(systemReceiver, this) registerReceiver(systemReceiver, this)
@ -449,15 +449,29 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
} }
/** /**
* A [BroadcastReceiver] for receiving system events from notifications, widgets, or * A [BroadcastReceiver] for receiving general playback events from the system.
* headset plug events.
*/ */
private inner class SystemEventReceiver : BroadcastReceiver() { private inner class PlaybackReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.action) { 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( ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
!playbackManager.isPlaying !playbackManager.isPlaying
) )
@ -478,57 +492,41 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
stopForegroundAndNotification() 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() WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
}
}
/** else -> handleSystemIntent(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)
} }
} }
} }
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 { companion object {
private const val DISCONNECTED = 0
private const val CONNECTED = 1
private const val POS_POLL_INTERVAL = 500L private const val POS_POLL_INTERVAL = 500L
const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP" const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP"

View file

@ -71,7 +71,6 @@ class SearchViewModel : ViewModel() {
if (query.isEmpty() || musicStore == null) { if (query.isEmpty() || musicStore == null) {
mSearchResults.value = listOf() mSearchResults.value = listOf()
return return
} }
@ -80,7 +79,7 @@ class SearchViewModel : ViewModel() {
val sort = Sort.ByName(true) val sort = Sort.ByName(true)
val results = mutableListOf<BaseModel>() 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) { if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
musicStore.artists.filterByOrNull(query)?.let { 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_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">Kopfhörerfokus</string>
<string name="set_plug_mgt_desc">Abspielen/Pausieren wenn sich die Kopfhörerverbindung ändert</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_off">Aus</string>
<string name="set_replay_gain_track">Titel bevorzugen</string> <string name="set_replay_gain_track">Titel bevorzugen</string>
<string name="set_replay_gain_album">Album 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_focus_desc">Pause when other audio plays (ex. Calls)</string>
<string name="set_plug_mgt">Headset focus</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_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_off">Off</string>
<string name="set_replay_gain_track">Prefer track</string> <string name="set_replay_gain_track">Prefer track</string>
<string name="set_replay_gain_album">Prefer album</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. 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 - Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
an unrecognized name. 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? #### 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. gain depending on if the current playback is from an album or not.
#### Why are accents lighter/less saturated in dark mode? #### 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)) print(FATAL + "fatal:" + NC + " command failed with exit code " + str(code))
sys.exit(1) 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): if os.path.exists(exoplayer_path):
reinstall = input(INFO + "info:" + NC + " exoplayer is already installed. would you like to reinstall it? [y/n] ") 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) system.exit(1)
# Now try to install ExoPlayer. # Now try to install ExoPlayer.
sh("rm -rf deps") sh("rm -rf srclibs")
print(INFO + "info:" + NC + " cloning exoplayer...") print(INFO + "info:" + NC + " cloning exoplayer...")
sh("git clone https://github.com/oxygencobalt/ExoPlayer.git " + exoplayer_path) sh("git clone https://github.com/oxygencobalt/ExoPlayer.git " + exoplayer_path)

View file

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