Refactor file intent system
Finally find a way to handle file intents without having to coordinate it across three different fragments in onResume.
This commit is contained in:
parent
f04ffdb59b
commit
7f0042fd2f
8 changed files with 96 additions and 110 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -598,6 +598,13 @@ class PlaybackStateManager private constructor() {
|
|||
mHasPlayed = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this instance as restored.
|
||||
*/
|
||||
fun setRestored() {
|
||||
mIsRestored = true
|
||||
}
|
||||
|
||||
// --- PERSISTENCE FUNCTIONS ---
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue