diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5afebd8f4..9f0767734 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
## dev
+#### What's New
+- Added Android 13 support [#129]
+ - Switch to new storage permissions
+ - Add monochrome icon
+ - Fix issue where widget covers would not load
+
#### What's Improved
- Playback bar now has a marquee effect
- Added a way to access the system equalizer from the playback menu.
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
index e0610387b..d69f02ae8 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.support.v4.media.MediaDescriptionCompat
@@ -40,15 +41,20 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
/**
- * The component managing the [MediaSessionCompat] instance.
+ * The component managing the [MediaSessionCompat] instance, alongside the [NotificationComponent]
*
- * Media3 is a joke. It tries so hard to be "hElpfUl" and implement so many fundamental behaviors
- * into a one-size-fits-all package that it only ends up causing unending bugs and frustration. The
- * queue system is horribly designed, the notification code is outdated, and the overstretched
- * abstractions result in terrible performance bottlenecks and insane state bugs..
+ * MediaSession is easily one of the most poorly thought out APIs in Android. It tries to hard to be
+ * hElpfUl and implement so many fundamental behaviors into a one-size-fits-all package that it only
+ * ends up causing unending bugs and frustrating. The queue system is horribly designed, the
+ * playback state system has unending coherency bugs, and the overstretched abstractions result in
+ * god-awful performance bottlenecks and insane state bugs.
*
- * Show me a way to adapt my internal queue into the new system and I will change my mind, but
- * otherwise, I will stick with my normal system that works correctly.
+ * The sheer absurdity of the hoops we have jump through to get this working in an okay manner is
+ * the reason why Auxio only mirrors a saner playback state to the media session instead of relying
+ * on it. I thought that Android 13 would at least try to make the state more coherent, but NOPE.
+ * You still have to do a delicate dance of posting notifications and updating the session state
+ * while also keeping in mind the absurd rate limiting system in place just to have a sort-of
+ * coherent state. And even then it will break if you skip too much.
*
* @author OxygenCobalt
*
@@ -64,7 +70,13 @@ class MediaSessionComponent(
PlaybackStateManager.Callback,
Settings.Callback {
interface Callback {
- fun onPostNotification(notification: NotificationComponent?, reason: String)
+ fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
+ }
+
+ enum class PostingReason {
+ METADATA,
+ ACTIONS,
+ POSITION
}
private val mediaSession =
@@ -126,7 +138,7 @@ class MediaSessionComponent(
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
if (song == null) {
mediaSession.setMetadata(emptyMetadata)
- callback.onPostNotification(null, "song update")
+ callback.onPostNotification(null, PostingReason.METADATA)
return
}
@@ -184,7 +196,7 @@ class MediaSessionComponent(
val metadata = builder.build()
mediaSession.setMetadata(metadata)
notification.updateMetadata(metadata)
- callback.onPostNotification(notification, "song update")
+ callback.onPostNotification(notification, PostingReason.METADATA)
}
})
}
@@ -211,7 +223,7 @@ class MediaSessionComponent(
override fun onPlayingChanged(isPlaying: Boolean) {
invalidateSessionState()
- invalidateNotificationActions()
+ invalidateActions()
}
override fun onRepeatChanged(repeatMode: RepeatMode) {
@@ -222,7 +234,7 @@ class MediaSessionComponent(
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
})
- invalidateNotificationActions()
+ invalidateActions()
}
override fun onShuffledChanged(isShuffled: Boolean) {
@@ -233,7 +245,7 @@ class MediaSessionComponent(
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
- invalidateNotificationActions()
+ invalidateActions()
}
// --- SETTINGSMANAGER CALLBACKS ---
@@ -243,7 +255,7 @@ class MediaSessionComponent(
context.getString(R.string.set_key_show_covers),
context.getString(R.string.set_key_quality_covers) ->
updateMediaMetadata(playbackManager.song, playbackManager.parent)
- context.getString(R.string.set_key_alt_notif_action) -> invalidateNotificationActions()
+ context.getString(R.string.set_key_alt_notif_action) -> invalidateActions()
}
}
@@ -262,7 +274,7 @@ class MediaSessionComponent(
// when we invalidate the state (that will cause us to be rate-limited), and also not
// always when we seek (that will also cause us to be rate-limited). Someone looked at
// this system and said it was well-designed.
- callback.onPostNotification(notification, "position discontinuity")
+ callback.onPostNotification(notification, PostingReason.POSITION)
}
}
@@ -280,7 +292,7 @@ class MediaSessionComponent(
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
super.onPlayFromSearch(query, extras)
- // STUB: Unimplemented
+ // STUB: Unimplemented, no media browser
}
override fun onAddQueueItem(description: MediaDescriptionCompat?) {
@@ -345,6 +357,14 @@ class MediaSessionComponent(
}
}
+ override fun onCustomAction(action: String?, extras: Bundle?) {
+ super.onCustomAction(action, extras)
+
+ // Service already handles intents from the old notification actions, easier to
+ // plug into that system.
+ context.sendBroadcast(Intent(action))
+ }
+
override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT))
@@ -360,7 +380,6 @@ class MediaSessionComponent(
// aggressively batch notification updates to prevent rate-limiting.
// Android 13 seems to resolve these, but I'm still stuck with these issues below that
// version.
- // TODO: Add the custom actions for Android 13
val state =
PlaybackStateCompat.Builder()
.setActions(ACTIONS)
@@ -376,20 +395,56 @@ class MediaSessionComponent(
state.setState(playerState, player.currentPosition, 1.0f, SystemClock.elapsedRealtime())
+ // Android 13+ leverages custom actions.
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val extraAction =
+ if (settings.useAltNotifAction) {
+ PlaybackStateCompat.CustomAction.Builder(
+ PlaybackService.ACTION_INVERT_SHUFFLE,
+ context.getString(R.string.desc_change_repeat),
+ if (playbackManager.isShuffled) {
+ R.drawable.ic_shuffle_on_24
+ } else {
+ R.drawable.ic_shuffle_off_24
+ })
+ } else {
+ PlaybackStateCompat.CustomAction.Builder(
+ PlaybackService.ACTION_INC_REPEAT_MODE,
+ context.getString(R.string.desc_change_repeat),
+ playbackManager.repeatMode.icon)
+ }
+
+ val exitAction =
+ PlaybackStateCompat.CustomAction.Builder(
+ PlaybackService.ACTION_EXIT,
+ context.getString(R.string.desc_exit),
+ R.drawable.ic_close_24)
+ .build()
+
+ state.addCustomAction(extraAction.build())
+ state.addCustomAction(exitAction)
+ }
+
mediaSession.setPlaybackState(state.build())
}
- private fun invalidateNotificationActions() {
- notification.updatePlaying(playbackManager.isPlaying)
+ private fun invalidateActions() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ notification.updatePlaying(playbackManager.isPlaying)
- if (settings.useAltNotifAction) {
- notification.updateShuffled(playbackManager.isShuffled)
+ if (settings.useAltNotifAction) {
+ notification.updateShuffled(playbackManager.isShuffled)
+ } else {
+ notification.updateRepeatMode(playbackManager.repeatMode)
+ }
} else {
- notification.updateRepeatMode(playbackManager.repeatMode)
+ // Update custom actions instead of the notification
+ invalidateSessionState()
}
if (!provider.isBusy) {
- callback.onPostNotification(notification, "new notification actions")
+ callback.onPostNotification(notification, PostingReason.ACTIONS)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index 251385352..bd4ee573e 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -106,15 +106,40 @@ class PlaybackService :
override fun onCreate() {
super.onCreate()
- foregroundManager = ForegroundManager(this)
-
- // --- PLAYER SETUP ---
-
replayGainProcessor = ReplayGainAudioProcessor(this)
- player = newPlayer()
+ // Since Auxio is a music player, only specify an audio renderer to save
+ // battery/apk size/cache size
+ val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
+ arrayOf(
+ MediaCodecAudioRenderer(
+ this,
+ MediaCodecSelector.DEFAULT,
+ handler,
+ audioListener,
+ AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
+ replayGainProcessor),
+ LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
+ }
+
+ // Enable constant bitrate seeking so that certain MP3s/AACs are seekable
+ val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
+
+ player =
+ ExoPlayer.Builder(this, audioRenderer)
+ .setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
+ .setWakeMode(C.WAKE_MODE_LOCAL)
+ .setAudioAttributes(
+ AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
+ .build(),
+ true)
+ .build()
+
player.addListener(this)
+ playbackManager.registerController(this)
positionScope.launch {
while (true) {
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
@@ -122,7 +147,8 @@ class PlaybackService :
}
}
- // --- SYSTEM SETUP ---
+ settings = Settings(this, this)
+ foregroundManager = ForegroundManager(this)
widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, player, this)
@@ -144,9 +170,6 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP ---
- settings = Settings(this, this)
- playbackManager.registerController(this)
-
logD("Service created")
}
@@ -281,6 +304,26 @@ class PlaybackService :
}
}
+ private fun broadcastAudioEffectAction(event: String) {
+ sendBroadcast(
+ Intent(event)
+ .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
+ .putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
+ .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
+ }
+
+ /** Stop the foreground state and hide the notification */
+ private fun stopAndSave() {
+ hasPlayed = false
+
+ if (foregroundManager.tryStopForeground()) {
+ logD("Saving playback state")
+ saveScope.launch {
+ playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
+ }
+ }
+ }
+
override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs)
@@ -293,7 +336,10 @@ class PlaybackService :
player.playWhenReady = isPlaying
}
- override fun onPostNotification(notification: NotificationComponent?, reason: String) {
+ override fun onPostNotification(
+ notification: NotificationComponent?,
+ reason: MediaSessionComponent.PostingReason
+ ) {
if (notification == null) {
// This case is only here if I ever need to move foreground stopping from
// the player code to the notification code.
@@ -321,55 +367,6 @@ class PlaybackService :
// --- OTHER FUNCTIONS ---
- /** Create the [ExoPlayer] instance. */
- private fun newPlayer(): ExoPlayer {
- // Since Auxio is a music player, only specify an audio renderer to save
- // battery/apk size/cache size
- val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
- arrayOf(
- MediaCodecAudioRenderer(
- this,
- MediaCodecSelector.DEFAULT,
- handler,
- audioListener,
- AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
- replayGainProcessor),
- LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
- }
-
- // Enable constant bitrate seeking so that certain MP3s/AACs are seekable
- val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
-
- return ExoPlayer.Builder(this, audioRenderer)
- .setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
- .setWakeMode(C.WAKE_MODE_LOCAL)
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
- .build(),
- true)
- .build()
- }
-
- private fun broadcastAudioEffectAction(event: String) {
- sendBroadcast(
- Intent(event)
- .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
- .putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
- .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
- }
-
- /** Stop the foreground state and hide the notification */
- private fun stopAndSave() {
- if (foregroundManager.tryStopForeground()) {
- logD("Saving playback state")
- saveScope.launch {
- playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
- }
- }
- }
-
/** A [BroadcastReceiver] for receiving general playback events from the system. */
private inner class PlaybackReceiver : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
index f07f54bdd..d9aa4fe6e 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
@@ -111,7 +111,6 @@ class WidgetComponent(private val context: Context) :
RoundedCornersTransformation(cornerRadius.toFloat()))
} else {
// Divide by two to really make sure we aren't hitting the memory limit.
-
builder.size(computeSize(sw, sh, 2f))
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 49f738535..bd1cc44a8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -250,6 +250,7 @@
Change repeat mode
Turn shuffle on or off
Shuffle all songs
+ Stop playback
Remove this queue song
Move this queue song