From 7f0042fd2ff702c652e145c181f8eedff11afeb4 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Tue, 23 Feb 2021 14:04:45 -0700 Subject: [PATCH] Refactor file intent system Finally find a way to handle file intents without having to coordinate it across three different fragments in onResume. --- .../java/org/oxycblt/auxio/MainActivity.kt | 71 ++++++++++++------- .../java/org/oxycblt/auxio/MainFragment.kt | 15 +--- .../auxio/playback/CompactPlaybackFragment.kt | 11 +-- .../auxio/playback/PlaybackFragment.kt | 14 ++-- .../oxycblt/auxio/playback/PlaybackUtils.kt | 31 -------- .../auxio/playback/PlaybackViewModel.kt | 46 ++++++++---- .../auxio/playback/queue/QueueFragment.kt | 11 --- .../playback/state/PlaybackStateManager.kt | 7 ++ 8 files changed, 96 insertions(+), 110 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtils.kt diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 360a7d733..789607a9a 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -5,10 +5,12 @@ import android.os.Build import android.os.Bundle import android.view.View import android.view.WindowInsets +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.databinding.DataBindingUtil import org.oxycblt.auxio.databinding.ActivityMainBinding +import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.Accent @@ -18,6 +20,8 @@ import org.oxycblt.auxio.ui.isEdgeOn * The single [AppCompatActivity] for Auxio. */ class MainActivity : AppCompatActivity() { + private val playbackModel: PlaybackViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,6 +38,9 @@ class MainActivity : AppCompatActivity() { // Apply the theme setTheme(accent.theme) + // onNewIntent doesnt automatically call on startup, so call it here. + onNewIntent(intent) + if (isEdgeOn()) { doEdgeToEdgeSetup(binding) } @@ -49,36 +56,48 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - // Since the activity is set to singleInstance [Given that there's only MainActivity] - // We have to manually push the intent whenever we get one so that the fragments - // can catch any file intents - setIntent(intent) - } + if (intent != null) { + val action = intent.action + val isConsumed = intent.getBooleanExtra(KEY_INTENT_CONSUMED, false) - @Suppress("DEPRECATION") - private fun doEdgeToEdgeSetup(binding: ActivityMainBinding) { - window?.apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Do modern edge to edge [Which is really a shot in the dark tbh] - this@MainActivity.logD("Doing R+ edge-to-edge.") + if (action == Intent.ACTION_VIEW && !isConsumed) { + // Mark the intent as used so this does not fire again + intent.putExtra(KEY_INTENT_CONSUMED, true) - setDecorFitsSystemWindows(false) - - binding.root.setOnApplyWindowInsetsListener { _, insets -> - WindowInsets.Builder() - .setInsets( - WindowInsets.Type.systemBars(), - insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) - ) - .build() + intent.data?.let { fileUri -> + playbackModel.playWithUri(fileUri, this) } - } else { - // Do old edge-to-edge otherwise - this@MainActivity.logD("Doing legacy edge-to-edge.") - - binding.root.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE } } } + + private fun doEdgeToEdgeSetup(binding: ActivityMainBinding) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Do modern edge to edge, which happens to be around twice the size of the + // old way of doing things. Thanks android, very cool! + logD("Doing R+ edge-to-edge.") + + window?.setDecorFitsSystemWindows(false) + + binding.root.setOnApplyWindowInsetsListener { _, insets -> + WindowInsets.Builder() + .setInsets( + WindowInsets.Type.systemBars(), + insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) + ) + .build() + } + } else { + // Do old edge-to-edge otherwise. + logD("Doing legacy edge-to-edge.") + + @Suppress("DEPRECATION") + binding.root.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + } + } + + companion object { + private const val KEY_INTENT_CONSUMED = "KEY_FILE_INTENT_USED" + } } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 04a08d5ad..e1b80594d 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -19,8 +19,6 @@ import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.playback.handleFileIntent -import org.oxycblt.auxio.playback.shouldHandleFileIntent import org.oxycblt.auxio.ui.Accent import org.oxycblt.auxio.ui.fixAnimInfoLeak import org.oxycblt.auxio.ui.isLandscape @@ -112,22 +110,13 @@ class MainFragment : Fragment() { } } + playbackModel.setupPlayback(requireContext()) + logD("Fragment Created.") return binding.root } - override fun onResume() { - super.onResume() - - if (shouldHandleFileIntent()) { - handleFileIntent(playbackModel) - } else { - // If there is no file intent restore playback as usual - playbackModel.restorePlaybackIfNeeded(requireContext()) - } - } - override fun onDestroyView() { super.onDestroyView() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index 7469d6f69..9b6b0ecf4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -38,6 +38,7 @@ class CompactPlaybackFragment : Fragment() { // Put a placeholder song in the binding & hide the playback fragment initially. binding.song = MusicStore.getInstance().songs[0] binding.playbackModel = playbackModel + binding.executePendingBindings() binding.root.apply { setOnClickListener { @@ -54,12 +55,12 @@ class CompactPlaybackFragment : Fragment() { // --- VIEWMODEL SETUP --- - playbackModel.song.observe(viewLifecycleOwner) { - if (it != null) { - logD("Updating song display to ${it.name}") + playbackModel.song.observe(viewLifecycleOwner) { song -> + if (song != null) { + logD("Updating song display to ${song.name}") - binding.song = it - binding.playbackProgress.max = it.seconds.toInt() + binding.song = song + binding.playbackProgress.max = song.seconds.toInt() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index b6f315157..2498080b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -82,12 +82,12 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // --- VIEWMODEL SETUP -- - playbackModel.song.observe(viewLifecycleOwner) { - if (it != null) { - logD("Updating song display to ${it.name}.") + playbackModel.song.observe(viewLifecycleOwner) { song -> + if (song != null) { + logD("Updating song display to ${song.name}.") - binding.song = it - binding.playbackSeekBar.max = it.seconds.toInt() + binding.song = song + binding.playbackSeekBar.max = song.seconds.toInt() } else { logD("No song is being played, leaving.") @@ -182,10 +182,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { playbackModel.enableAnimation() } - - if (shouldHandleFileIntent()) { - handleFileIntent(playbackModel) - } } // --- SEEK CALLBACKS --- diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtils.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtils.kt deleted file mode 100644 index 214210f84..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtils.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.oxycblt.auxio.playback - -import android.content.Intent -import androidx.fragment.app.Fragment - -object PlaybackUtils { - const val KEY_INTENT_FIRED = "KEY_FILE_INTENT_FIRED" -} - -/** - * Check if the current activity intent is a file intent and needs to be dealt with. - */ -fun Fragment.shouldHandleFileIntent(): Boolean { - val intent = requireActivity().intent - - return intent != null && - intent.action == Intent.ACTION_VIEW && - !intent.getBooleanExtra(PlaybackUtils.KEY_INTENT_FIRED, false) -} - -/** - * Actually use the intent and push it to [playbackModel] - */ -fun Fragment.handleFileIntent(playbackModel: PlaybackViewModel) { - val intent = requireActivity().intent - - // Ensure that this wont fire again by putting a boolean extra - intent.putExtra(PlaybackUtils.KEY_INTENT_FIRED, true) - - playbackModel.playWithIntent(intent, requireContext()) -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 7fb3ddf55..86a1b709d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -1,7 +1,7 @@ package org.oxycblt.auxio.playback import android.content.Context -import android.content.Intent +import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations @@ -53,6 +53,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // Other private val mIsSeeking = MutableLiveData(false) + private var mIntentUri: Uri? = null private var mCanAnimate = false /** The current song. */ @@ -113,9 +114,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- PLAYING FUNCTIONS --- /** - * Play a song. - * @param song The song to be played - * @param mode The [PlaybackMode] for it to be played in. Defaults to the preferred song playback mode of the user if not specified. + * Play a [song] with the [mode] specified. [mode] will default to the preferred song + * playback mode of the user if not specified. */ fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) { playbackManager.playSong(song, mode) @@ -154,15 +154,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.playParent(genre, shuffled) } - /** Shuffle all songs */ - fun shuffleAll() { - playbackManager.shuffleAll() + /** Play using a file URI. This will not play instantly during the initial startup sequence.*/ + fun playWithUri(uri: Uri, context: Context) { + // Check if everything is already running to run the URI play + if (playbackManager.isRestored && musicStore.loaded) { + playWithUriInternal(uri, context) + } else { + mIntentUri = uri + } } - /** Play a song using an intent */ - fun playWithIntent(intent: Intent, context: Context) { - val uri = intent.data ?: return - + /** Actually play with a given URI internally. The frontend doesn't play instantly. */ + private fun playWithUriInternal(uri: Uri, context: Context) { viewModelScope.launch { musicStore.getSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) @@ -170,6 +173,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } } + /** Shuffle all songs */ + fun shuffleAll() { + playbackManager.shuffleAll() + } + // --- POSITION FUNCTIONS --- /** Update the position and push it to [PlaybackStateManager] */ @@ -326,11 +334,19 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } /** - * Get [PlaybackStateManager] to restore its state from the database, if needed. Called by MainFragment. - * @param context [Context] required. + * Handle the file last-saved file intent, or restore playback, depending on the situation. */ - fun restorePlaybackIfNeeded(context: Context) { - if (!playbackManager.isRestored && playbackManager.song == null) { + fun setupPlayback(context: Context) { + val intentUri = mIntentUri + + if (intentUri != null) { + // Were not going to be restoring playbackManager after this, so mark it as such. + playbackManager.setRestored() + playWithUriInternal(intentUri, context) + + // Remove the uri after finishing the calls so that this does not fire again. + mIntentUri = null + } else if (!playbackManager.isRestored) { viewModelScope.launch { playbackManager.getStateFromDatabase(context) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index e14a003a6..5479c8fc4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -16,7 +16,6 @@ import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.playback.shouldHandleFileIntent import org.oxycblt.auxio.ui.isEdgeOn import org.oxycblt.auxio.ui.isIrregularLandscape @@ -106,16 +105,6 @@ class QueueFragment : Fragment() { return binding.root } - override fun onResume() { - super.onResume() - - // QueueFragment shouldn't be handling file intents as it will cause the queue recycler - // to flip out - if (shouldHandleFileIntent()) { - findNavController().navigateUp() - } - } - /** * Create the queue data that should be displayed * @return The list of headers/songs that should be displayed. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index ca36d9316..a54249f89 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -598,6 +598,13 @@ class PlaybackStateManager private constructor() { mHasPlayed = false } + /** + * Mark this instance as restored. + */ + fun setRestored() { + mIsRestored = true + } + // --- PERSISTENCE FUNCTIONS --- /**