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.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
|
@ -18,6 +20,8 @@ import org.oxycblt.auxio.ui.isEdgeOn
|
||||||
* The single [AppCompatActivity] for Auxio.
|
* The single [AppCompatActivity] for Auxio.
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private val playbackModel: PlaybackViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -34,6 +38,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
// Apply the theme
|
// Apply the theme
|
||||||
setTheme(accent.theme)
|
setTheme(accent.theme)
|
||||||
|
|
||||||
|
// onNewIntent doesnt automatically call on startup, so call it here.
|
||||||
|
onNewIntent(intent)
|
||||||
|
|
||||||
if (isEdgeOn()) {
|
if (isEdgeOn()) {
|
||||||
doEdgeToEdgeSetup(binding)
|
doEdgeToEdgeSetup(binding)
|
||||||
}
|
}
|
||||||
|
|
@ -49,20 +56,28 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
// Since the activity is set to singleInstance [Given that there's only MainActivity]
|
if (intent != null) {
|
||||||
// We have to manually push the intent whenever we get one so that the fragments
|
val action = intent.action
|
||||||
// can catch any file intents
|
val isConsumed = intent.getBooleanExtra(KEY_INTENT_CONSUMED, false)
|
||||||
setIntent(intent)
|
|
||||||
|
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
||||||
|
// Mark the intent as used so this does not fire again
|
||||||
|
intent.putExtra(KEY_INTENT_CONSUMED, true)
|
||||||
|
|
||||||
|
intent.data?.let { fileUri ->
|
||||||
|
playbackModel.playWithUri(fileUri, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun doEdgeToEdgeSetup(binding: ActivityMainBinding) {
|
private fun doEdgeToEdgeSetup(binding: ActivityMainBinding) {
|
||||||
window?.apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// Do modern edge to edge [Which is really a shot in the dark tbh]
|
// Do modern edge to edge, which happens to be around twice the size of the
|
||||||
this@MainActivity.logD("Doing R+ edge-to-edge.")
|
// old way of doing things. Thanks android, very cool!
|
||||||
|
logD("Doing R+ edge-to-edge.")
|
||||||
|
|
||||||
setDecorFitsSystemWindows(false)
|
window?.setDecorFitsSystemWindows(false)
|
||||||
|
|
||||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
WindowInsets.Builder()
|
WindowInsets.Builder()
|
||||||
|
|
@ -73,12 +88,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Do old edge-to-edge otherwise
|
// Do old edge-to-edge otherwise.
|
||||||
this@MainActivity.logD("Doing legacy edge-to-edge.")
|
logD("Doing legacy edge-to-edge.")
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
binding.root.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
binding.root.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
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.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
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.Accent
|
||||||
import org.oxycblt.auxio.ui.fixAnimInfoLeak
|
import org.oxycblt.auxio.ui.fixAnimInfoLeak
|
||||||
import org.oxycblt.auxio.ui.isLandscape
|
import org.oxycblt.auxio.ui.isLandscape
|
||||||
|
|
@ -112,22 +110,13 @@ class MainFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playbackModel.setupPlayback(requireContext())
|
||||||
|
|
||||||
logD("Fragment Created.")
|
logD("Fragment Created.")
|
||||||
|
|
||||||
return binding.root
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class CompactPlaybackFragment : Fragment() {
|
||||||
// Put a placeholder song in the binding & hide the playback fragment initially.
|
// Put a placeholder song in the binding & hide the playback fragment initially.
|
||||||
binding.song = MusicStore.getInstance().songs[0]
|
binding.song = MusicStore.getInstance().songs[0]
|
||||||
binding.playbackModel = playbackModel
|
binding.playbackModel = playbackModel
|
||||||
|
binding.executePendingBindings()
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
|
@ -54,12 +55,12 @@ class CompactPlaybackFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) {
|
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||||
if (it != null) {
|
if (song != null) {
|
||||||
logD("Updating song display to ${it.name}")
|
logD("Updating song display to ${song.name}")
|
||||||
|
|
||||||
binding.song = it
|
binding.song = song
|
||||||
binding.playbackProgress.max = it.seconds.toInt()
|
binding.playbackProgress.max = song.seconds.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,12 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) {
|
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||||
if (it != null) {
|
if (song != null) {
|
||||||
logD("Updating song display to ${it.name}.")
|
logD("Updating song display to ${song.name}.")
|
||||||
|
|
||||||
binding.song = it
|
binding.song = song
|
||||||
binding.playbackSeekBar.max = it.seconds.toInt()
|
binding.playbackSeekBar.max = song.seconds.toInt()
|
||||||
} else {
|
} else {
|
||||||
logD("No song is being played, leaving.")
|
logD("No song is being played, leaving.")
|
||||||
|
|
||||||
|
|
@ -182,10 +182,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
|
|
||||||
playbackModel.enableAnimation()
|
playbackModel.enableAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldHandleFileIntent()) {
|
|
||||||
handleFileIntent(playbackModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SEEK CALLBACKS ---
|
// --- 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
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
|
|
@ -53,6 +53,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
private val mIsSeeking = MutableLiveData(false)
|
private val mIsSeeking = MutableLiveData(false)
|
||||||
|
private var mIntentUri: Uri? = null
|
||||||
private var mCanAnimate = false
|
private var mCanAnimate = false
|
||||||
|
|
||||||
/** The current song. */
|
/** The current song. */
|
||||||
|
|
@ -113,9 +114,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// --- PLAYING FUNCTIONS ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a song.
|
* Play a [song] with the [mode] specified. [mode] will default to the preferred song
|
||||||
* @param song The song to be played
|
* playback mode of the user if not specified.
|
||||||
* @param mode The [PlaybackMode] for it to be played in. Defaults to the preferred song playback mode of the user if not specified.
|
|
||||||
*/
|
*/
|
||||||
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
||||||
playbackManager.playSong(song, mode)
|
playbackManager.playSong(song, mode)
|
||||||
|
|
@ -154,15 +154,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.playParent(genre, shuffled)
|
playbackManager.playParent(genre, shuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shuffle all songs */
|
/** Play using a file URI. This will not play instantly during the initial startup sequence.*/
|
||||||
fun shuffleAll() {
|
fun playWithUri(uri: Uri, context: Context) {
|
||||||
playbackManager.shuffleAll()
|
// 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 */
|
/** Actually play with a given URI internally. The frontend doesn't play instantly. */
|
||||||
fun playWithIntent(intent: Intent, context: Context) {
|
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||||
val uri = intent.data ?: return
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
|
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
|
||||||
playSong(song)
|
playSong(song)
|
||||||
|
|
@ -170,6 +173,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shuffle all songs */
|
||||||
|
fun shuffleAll() {
|
||||||
|
playbackManager.shuffleAll()
|
||||||
|
}
|
||||||
|
|
||||||
// --- POSITION FUNCTIONS ---
|
// --- POSITION FUNCTIONS ---
|
||||||
|
|
||||||
/** Update the position and push it to [PlaybackStateManager] */
|
/** 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.
|
* Handle the file last-saved file intent, or restore playback, depending on the situation.
|
||||||
* @param context [Context] required.
|
|
||||||
*/
|
*/
|
||||||
fun restorePlaybackIfNeeded(context: Context) {
|
fun setupPlayback(context: Context) {
|
||||||
if (!playbackManager.isRestored && playbackManager.song == null) {
|
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 {
|
viewModelScope.launch {
|
||||||
playbackManager.getStateFromDatabase(context)
|
playbackManager.getStateFromDatabase(context)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.shouldHandleFileIntent
|
|
||||||
import org.oxycblt.auxio.ui.isEdgeOn
|
import org.oxycblt.auxio.ui.isEdgeOn
|
||||||
import org.oxycblt.auxio.ui.isIrregularLandscape
|
import org.oxycblt.auxio.ui.isIrregularLandscape
|
||||||
|
|
||||||
|
|
@ -106,16 +105,6 @@ class QueueFragment : Fragment() {
|
||||||
return binding.root
|
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
|
* Create the queue data that should be displayed
|
||||||
* @return The list of headers/songs that should be displayed.
|
* @return The list of headers/songs that should be displayed.
|
||||||
|
|
|
||||||
|
|
@ -598,6 +598,13 @@ class PlaybackStateManager private constructor() {
|
||||||
mHasPlayed = false
|
mHasPlayed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this instance as restored.
|
||||||
|
*/
|
||||||
|
fun setRestored() {
|
||||||
|
mIsRestored = true
|
||||||
|
}
|
||||||
|
|
||||||
// --- PERSISTENCE FUNCTIONS ---
|
// --- PERSISTENCE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue