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
|
||||
build/
|
||||
release/
|
||||
deps/
|
||||
srclibs/
|
||||
|
||||
# Studio
|
||||
.idea/
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.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"
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
Loading…
Reference in a new issue