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:
parent
c9f789e388
commit
317b12579c
11 changed files with 107 additions and 82 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
||||||
local.properties
|
local.properties
|
||||||
build/
|
build/
|
||||||
release/
|
release/
|
||||||
deps/
|
srclibs/
|
||||||
|
|
||||||
# Studio
|
# Studio
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
Loading…
Reference in a new issue