playback: add shuffle shortcut

Add a shortcut to shuffle all songs.

This is likely the only static shortcut Auxio will have. Top tracks
and recently added are completely useless for me, so I will never
add them. I may add more dynamic shortcuts for recently played items,
however.

Note that we use a basic black shuffle icon here. I will not add icon
customization to these shortcuts.
This commit is contained in:
OxygenCobalt 2022-06-18 11:17:40 -06:00
parent 20e7b25808
commit 29fe849565
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 73 additions and 22 deletions

View file

@ -2,6 +2,9 @@
## dev ## dev
#### What's New
- Added a shuffle shortcut
#### What's Fixed #### What's Fixed
- Fixed broken tablet layouts - Fixed broken tablet layouts
- Fixed seam that would appear on some album covers - Fixed seam that would appear on some album covers

View file

@ -14,8 +14,6 @@
<queries /> <queries />
<!-- TODO: A "shuffle" shortcut -->
<!-- <!--
Note: We have to simultaneously define the fullBackupContent and dataExtractionRules Note: We have to simultaneously define the fullBackupContent and dataExtractionRules
fields, as there is no way to make version-specific manifests. This should be okay, fields, as there is no way to make version-specific manifests. This should be okay,

View file

@ -18,6 +18,10 @@
package org.oxycblt.auxio package org.oxycblt.auxio
import android.app.Application import android.app.Application
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.request.CachePolicy import coil.request.CachePolicy
@ -27,12 +31,30 @@ import org.oxycblt.auxio.image.CrossfadeTransitionFactory
import org.oxycblt.auxio.image.GenreImageFetcher import org.oxycblt.auxio.image.GenreImageFetcher
import org.oxycblt.auxio.image.MusicKeyer import org.oxycblt.auxio.image.MusicKeyer
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD
@Suppress("UNUSED")
class AuxioApp : Application(), ImageLoaderFactory { class AuxioApp : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
logD(BuildConfig.APPLICATION_ID + ".MainActivity")
// Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release
// Auxio instances.
ShortcutManagerCompat.addDynamicShortcuts(
this,
listOf(
ShortcutInfoCompat.Builder(this, SHORTCUT_SHUFFLE_ID)
.setShortLabel(getString(R.string.lbl_shuffle_shortcut_short))
.setLongLabel(getString(R.string.lbl_shuffle_shortcut_long))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_shuffle_shortcut))
.setIntent(
Intent(this, MainActivity::class.java).apply {
action = INTENT_KEY_SHORTCUT_SHUFFLE
})
.build()))
// Init SettingsManager here so that there aren't any race conditions // Init SettingsManager here so that there aren't any race conditions
// [e.g PlaybackService gets SettingsManager before activity can init SettingsManager] // [e.g PlaybackService gets SettingsManager before activity can init SettingsManager]
SettingsManager.init(applicationContext) SettingsManager.init(applicationContext)
@ -51,4 +73,9 @@ class AuxioApp : Application(), ImageLoaderFactory {
.diskCachePolicy(CachePolicy.DISABLED) // Not downloading anything, so no disk-caching .diskCachePolicy(CachePolicy.DISABLED) // Not downloading anything, so no disk-caching
.build() .build()
} }
companion object {
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
}
} }

View file

@ -71,21 +71,31 @@ class MainActivity : AppCompatActivity() {
startService(Intent(this, IndexerService::class.java)) startService(Intent(this, IndexerService::class.java))
startService(Intent(this, PlaybackService::class.java)) startService(Intent(this, PlaybackService::class.java))
// If we have a file URI already, open it. Otherwise, restore the playback state. // If we have a valid intent, use that. Otherwise, restore the playback state.
val action = val action = intentToDelayedAction(intent) ?: PlaybackViewModel.DelayedAction.RestoreState
retrieveViewUri(intent)?.let { PlaybackViewModel.DelayedAction.Open(it) }
?: PlaybackViewModel.DelayedAction.RestoreState
playbackModel.performAction(this, action) playbackModel.startDelayedAction(this, action)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
// See if the new intent is a file intent. If so, open it. intentToDelayedAction(intent)?.let { action ->
val uri = retrieveViewUri(intent) playbackModel.startDelayedAction(this, action)
if (uri != null) { }
playbackModel.performAction(this, PlaybackViewModel.DelayedAction.Open(uri)) }
private fun intentToDelayedAction(intent: Intent?): PlaybackViewModel.DelayedAction? {
if (intent == null || intent.getBooleanExtra(KEY_INTENT_USED, false)) {
return null
}
intent.putExtra(KEY_INTENT_USED, true)
return when (intent.action) {
Intent.ACTION_VIEW -> intent.data?.let { PlaybackViewModel.DelayedAction.Open(it) }
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> PlaybackViewModel.DelayedAction.ShuffleAll
else -> null
} }
} }

View file

@ -53,7 +53,7 @@ class IndexerService : Service(), Indexer.Callback {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val serviceJob = Job() private val serviceJob = Job()
private val indexScope = CoroutineScope(serviceJob + Dispatchers.Default) private val indexScope = CoroutineScope(serviceJob + Dispatchers.Main)
private var isForeground = false private var isForeground = false
private lateinit var notification: IndexerNotification private lateinit var notification: IndexerNotification

View file

@ -189,7 +189,7 @@ class PlaybackPanelFragment :
} }
private fun updatePlaying(isPlaying: Boolean) { private fun updatePlaying(isPlaying: Boolean) {
requireBinding().playbackPlayPause.isActivated = isPlaying requireBinding().playbackPlayPause.apply { isActivated = isPlaying }
} }
private fun updateShuffled(isShuffled: Boolean) { private fun updateShuffled(isShuffled: Boolean) {

View file

@ -144,12 +144,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
* usually alongside a context too. Examples include: * usually alongside a context too. Examples include:
* - Opening files * - Opening files
* - Restoring the playback state * - Restoring the playback state
* - (Future) app shortcuts * - App shortcuts
* *
* We would normally want to put this kind of functionality into PlaybackService, but it's * We would normally want to put this kind of functionality into PlaybackService, but it's
* lifecycle makes that more or less impossible. * lifecycle makes that more or less impossible.
*/ */
fun performAction(context: Context, action: DelayedAction) { fun startDelayedAction(context: Context, action: DelayedAction) {
val library = musicStore.library val library = musicStore.library
val actionImpl = DelayedActionImpl(context.applicationContext, action) val actionImpl = DelayedActionImpl(context.applicationContext, action)
if (library != null) { if (library != null) {
@ -166,6 +166,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
viewModelScope.launch { playbackManager.restoreState(action.context) } viewModelScope.launch { playbackManager.restoreState(action.context) }
} }
} }
is DelayedAction.ShuffleAll -> shuffleAll()
is DelayedAction.Open -> { is DelayedAction.Open -> {
library.findSongForUri(action.context, action.inner.uri)?.let(::play) library.findSongForUri(action.context, action.inner.uri)?.let(::play)
} }
@ -284,6 +285,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
/** An action delayed until the complete load of the music library. */ /** An action delayed until the complete load of the music library. */
sealed class DelayedAction { sealed class DelayedAction {
object RestoreState : DelayedAction() object RestoreState : DelayedAction()
object ShuffleAll : DelayedAction()
data class Open(val uri: Uri) : DelayedAction() data class Open(val uri: Uri) : DelayedAction()
} }

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
</vector>

View file

@ -8,4 +8,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_artist" /> tools:listitem="@layout/item_parent" />

View file

@ -17,11 +17,7 @@
app:popExitAnim="@anim/nav_slide_pop_exit_anim" /> app:popExitAnim="@anim/nav_slide_pop_exit_anim" />
<action <action
android:id="@+id/action_show_settings" android:id="@+id/action_show_settings"
app:destination="@id/settings_fragment" app:destination="@id/settings_fragment" />
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action <action
android:id="@+id/action_show_about" android:id="@+id/action_show_about"
app:destination="@id/about_fragment" app:destination="@id/about_fragment"

View file

@ -59,6 +59,11 @@
<string name="lbl_state_saved">State saved</string> <string name="lbl_state_saved">State saved</string>
<!-- Limit to 10 characters -->
<string name="lbl_shuffle_shortcut_short">Shuffle</string>
<!-- Limit to 25 characters -->
<string name="lbl_shuffle_shortcut_long">Shuffle All</string>
<!-- Actual string: Ok --> <!-- Actual string: Ok -->
<string name="lbl_ok">@android:string/ok</string> <string name="lbl_ok">@android:string/ok</string>
<!-- Actual string: Cancel --> <!-- Actual string: Cancel -->