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