Update UI code

Make some misc changes to the code that runs behind the UI.
This commit is contained in:
OxygenCobalt 2021-01-24 11:48:55 -07:00
parent a5100b31ab
commit 3851c59f4b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 92 additions and 106 deletions

View file

@ -55,7 +55,6 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// Kotlin
//noinspection DifferentStdlibGradleVersion
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

View file

@ -20,7 +20,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.Accent
import org.oxycblt.auxio.ui.fixAnimationInfoMemoryLeak
import org.oxycblt.auxio.ui.fixAnimInfoLeak
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.isTablet
import org.oxycblt.auxio.ui.toColor
@ -115,7 +115,7 @@ class MainFragment : Fragment() {
override fun onDestroyView() {
super.onDestroyView()
fixAnimationInfoMemoryLeak()
fixAnimInfoLeak()
}
/**

View file

@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.fixAnimationInfoMemoryLeak
import org.oxycblt.auxio.ui.fixAnimInfoLeak
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.memberBinding
@ -25,9 +25,7 @@ import org.oxycblt.auxio.ui.memberBinding
abstract class DetailFragment : Fragment() {
protected val detailModel: DetailViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels()
protected val binding: FragmentDetailBinding by memberBinding(
FragmentDetailBinding::inflate
)
protected val binding by memberBinding(FragmentDetailBinding::inflate)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
@ -48,7 +46,7 @@ abstract class DetailFragment : Fragment() {
override fun onDestroyView() {
super.onDestroyView()
fixAnimationInfoMemoryLeak()
fixAnimInfoLeak()
}
/**

View file

@ -20,7 +20,6 @@ import org.oxycblt.auxio.music.processing.MusicLoader
/**
* An intermediary [Fragment] that asks for the READ_EXTERNAL_STORAGE permission and runs
* the music loading process in the background.
* FIXME: Leak that occurs when skipping load
* @author OxygenCobalt
*/
class LoadingFragment : Fragment(R.layout.fragment_loading) {

View file

@ -29,9 +29,7 @@ import org.oxycblt.auxio.ui.memberBinding
class CompactPlaybackFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private val binding: FragmentCompactPlaybackBinding by memberBinding(
FragmentCompactPlaybackBinding::inflate
)
private val binding by memberBinding(FragmentCompactPlaybackBinding::inflate)
override fun onCreateView(
inflater: LayoutInflater,

View file

@ -19,7 +19,7 @@ import org.oxycblt.auxio.logD
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.ui.Accent
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.ui.toColor
import org.oxycblt.auxio.ui.toStateList
/**
* A [Fragment] that displays more information about the song, along with more media controls.
@ -29,8 +29,7 @@ import org.oxycblt.auxio.ui.toColor
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private val binding: FragmentPlaybackBinding by memberBinding(FragmentPlaybackBinding::inflate) {
// Marquee must be disabled on destruction to prevent memory leaks
private val binding by memberBinding(FragmentPlaybackBinding::inflate) {
playbackSong.isSelected = false
}
@ -40,7 +39,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
private val controlColor: ColorStateList by lazy {
ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
R.color.control_color.toStateList(requireContext())
}
override fun onCreateView(
@ -49,6 +48,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
savedInstanceState: Bundle?
): View {
// TODO: Add a swipe-to-next-track function using a ViewPager
// Would require writing my own variant though to avoid index updates
val normalTextColor = binding.playbackDurationCurrent.currentTextColor

View file

@ -240,7 +240,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
uploadMetadataToSession(it)
notification.setMetadata(this, it, settingsManager.colorizeNotif) {
startForegroundOrNotify("Song")
startForegroundOrNotify()
}
return
@ -254,7 +254,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
override fun onModeUpdate(mode: PlaybackMode) {
notification.updateMode(this)
startForegroundOrNotify("Mode")
startForegroundOrNotify()
}
override fun onPlayingUpdate(isPlaying: Boolean) {
@ -262,13 +262,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
player.play()
notification.updatePlaying(this)
audioFocusManager.requestFocus()
startForegroundOrNotify("Play")
startForegroundOrNotify()
startPollingPosition()
} else {
player.pause()
notification.updatePlaying(this)
startForegroundOrNotify("Pause")
startForegroundOrNotify()
}
}
@ -283,18 +283,18 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
}
notification.updateExtraAction(this, settingsManager.useAltNotifAction)
startForegroundOrNotify("Loop")
startForegroundOrNotify()
}
override fun onShuffleUpdate(isShuffling: Boolean) {
if (settingsManager.useAltNotifAction) {
notification.updateExtraAction(this, settingsManager.useAltNotifAction)
startForegroundOrNotify("Shuffle update")
startForegroundOrNotify()
}
}
override fun onSeekConfirm(position: Long) {
override fun onSeek(position: Long) {
player.seekTo(position)
}
@ -309,7 +309,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
override fun onColorizeNotifUpdate(doColorize: Boolean) {
playbackManager.song?.let {
notification.setMetadata(this, it, settingsManager.colorizeNotif) {
startForegroundOrNotify("Colorize update")
startForegroundOrNotify()
}
}
}
@ -317,13 +317,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
override fun onNotifActionUpdate(useAltAction: Boolean) {
notification.updateExtraAction(this, useAltAction)
startForegroundOrNotify("Notif action update")
startForegroundOrNotify()
}
override fun onShowCoverUpdate(showCovers: Boolean) {
playbackManager.song?.let {
notification.setMetadata(this, it, settingsManager.colorizeNotif) {
startForegroundOrNotify("Cover update")
startForegroundOrNotify()
}
}
}
@ -331,7 +331,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
playbackManager.song?.let { song ->
notification.setMetadata(this, song, settingsManager.colorizeNotif) {
startForegroundOrNotify("Quality cover update")
startForegroundOrNotify()
}
}
}
@ -388,7 +388,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
playbackManager.song?.let {
notification.setMetadata(this, it, settingsManager.colorizeNotif) {
if (playbackManager.isPlaying) {
startForegroundOrNotify("Restore")
startForegroundOrNotify()
} else {
stopForegroundAndNotification()
}
@ -437,15 +437,14 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
/**
* Bring the service into the foreground and show the notification, or refresh the notification.
* @param reason (Debug) The reason for this call.
*/
private fun startForegroundOrNotify(reason: String) {
private fun startForegroundOrNotify() {
// Don't start foreground if:
// - The playback hasnt even started
// - The playback hasnt been restored
// - There is nothing to play
if (playbackManager.hasPlayed && playbackManager.isRestored && playbackManager.song != null) {
logD("Starting foreground/notifying because of $reason")
logD("Starting foreground/notifying")
if (!isForeground) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -484,7 +483,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
// Play/Pause if any of the keys are play/pause
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
playbackManager.setPlaying(!playbackManager.isPlaying)
true
}
@ -554,10 +553,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
private fun onGain() {
if (settingsManager.doAudioFocus) {
if (player.volume == VOLUME_DUCK && playbackManager.isPlaying) {
player.volume = VOLUME_DUCK
animateVolume(VOLUME_DUCK, VOLUME_FULL)
unduck()
} else if (pauseWasFromAudioFocus) {
playbackManager.setPlayingStatus(true)
playbackManager.setPlaying(true)
}
pauseWasFromAudioFocus = false
@ -567,7 +565,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
private fun onLoss() {
if (settingsManager.doAudioFocus && playbackManager.isPlaying) {
pauseWasFromAudioFocus = true
playbackManager.setPlayingStatus(false)
playbackManager.setPlaying(false)
}
}
@ -577,14 +575,16 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
}
}
private fun animateVolume(from: Float, to: Float) {
private fun unduck() {
player.volume = VOLUME_DUCK
ValueAnimator().apply {
setFloatValues(from, to)
setFloatValues(VOLUME_DUCK, VOLUME_FULL)
duration = DUCK_DURATION
addListener(
onStart = { player.volume = from },
onCancel = { player.volume = to },
onEnd = { player.volume = to }
onStart = { player.volume = VOLUME_DUCK },
onCancel = { player.volume = VOLUME_FULL },
onEnd = { player.volume = VOLUME_FULL }
)
addUpdateListener {
player.volume = it.animatedValue as Float
@ -609,7 +609,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev()
NotificationUtils.ACTION_PLAY_PAUSE -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
playbackManager.setPlaying(!playbackManager.isPlaying)
}
NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next()
NotificationUtils.ACTION_EXIT -> stop()
@ -643,7 +643,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device connected, resuming...")
playbackManager.setPlayingStatus(true)
playbackManager.setPlaying(true)
}
}
@ -654,7 +654,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device disconnected, pausing...")
playbackManager.setPlayingStatus(false)
playbackManager.setPlaying(false)
}
}
@ -662,7 +662,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
* Stop if the X button was clicked from the notification
*/
private fun stop() {
playbackManager.setPlayingStatus(false)
playbackManager.setPlaying(false)
stopForegroundAndNotification()
}
}

View file

@ -281,7 +281,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
fun invertPlayingStatus() {
enableAnimation()
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
playbackManager.setPlaying(!playbackManager.isPlaying)
}
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */

View file

@ -59,9 +59,7 @@ class QueueFragment : Fragment() {
insets.systemWindowInsetTop
}
(parent as View).updatePadding(
top = top
)
(parent as View).updatePadding(top = top)
insets
}
@ -77,7 +75,7 @@ class QueueFragment : Fragment() {
helper.attachToRecyclerView(this)
}
// --- VIEWMODEL SETUP ---
// --- VIEWMODEL SETUP ----
playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) {

View file

@ -187,11 +187,7 @@ class PlaybackStateManager private constructor() {
resetLoopMode()
updatePlayback(song)
// Depending on the configuration, keep the shuffle mode on.
setShuffling(settingsManager.keepShuffle && mIsShuffling, keepSong = true)
mIndex = mQueue.indexOf(song)
}
/**
@ -246,7 +242,7 @@ class PlaybackStateManager private constructor() {
mPosition = 0
if (!mIsPlaying) {
setPlayingStatus(true)
setPlaying(true)
}
}
@ -265,14 +261,15 @@ class PlaybackStateManager private constructor() {
}
/**
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeekConfirm] to notify
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* @param position The position to seek to in millis.
* @see setPosition
*/
fun seekTo(position: Long) {
mPosition = position
callbacks.forEach { it.onSeekConfirm(position) }
callbacks.forEach { it.onSeek(position) }
}
// --- QUEUE FUNCTIONS ---
@ -340,13 +337,9 @@ class PlaybackStateManager private constructor() {
mIndex = 0
forceQueueUpdate()
// The whole point here is making the playback pause and loop, so duplicate
// the updatePlayback code instead of using it with a useless arg tacked on.
mSong = mQueue[0]
mPosition = 0
setPlayingStatus(false)
setPlaying(false)
mIsInUserQueue = false
}
@ -505,11 +498,11 @@ class PlaybackStateManager private constructor() {
/**
* Set the shuffle status. Updates the queue accordingly
* @param value Whether the queue should be shuffled or not.
* @param shuffling Whether the queue should be shuffled or not.
* @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled
*/
fun setShuffling(value: Boolean, keepSong: Boolean) {
mIsShuffling = value
fun setShuffling(shuffling: Boolean, keepSong: Boolean) {
mIsShuffling = shuffling
if (mIsShuffling) {
genShuffle(keepSong, mIsInUserQueue)
@ -524,10 +517,7 @@ class PlaybackStateManager private constructor() {
* @param useLastSong Whether to use the last song in the queue instead of the current one
* @return A new shuffled queue
*/
private fun genShuffle(
keepSong: Boolean,
useLastSong: Boolean
) {
private fun genShuffle(keepSong: Boolean, useLastSong: Boolean) {
val lastSong = if (useLastSong) mQueue[0] else mSong
logD("Shuffling queue")
@ -551,10 +541,7 @@ class PlaybackStateManager private constructor() {
* @param keepSong Whether the current song should be kept as the queue is unshuffled
* @param useLastSong Whether to use the previous song for the index calculations.
*/
private fun resetShuffle(
keepSong: Boolean,
useLastSong: Boolean
) {
private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) {
val lastSong = if (useLastSong) mQueue[mIndex] else mSong
mQueue = when (mMode) {
@ -575,15 +562,15 @@ class PlaybackStateManager private constructor() {
/**
* Set the current playing status
* @param value Whether the playback should be playing or paused.
* @param playing Whether the playback should be playing or paused.
*/
fun setPlayingStatus(value: Boolean) {
if (mIsPlaying != value) {
if (value) {
fun setPlaying(playing: Boolean) {
if (mIsPlaying != playing) {
if (playing) {
mHasPlayed = true
}
mIsPlaying = value
mIsPlaying = playing
}
}
@ -592,7 +579,7 @@ class PlaybackStateManager private constructor() {
*/
fun rewind() {
seekTo(0)
setPlayingStatus(true)
setPlaying(true)
}
/**
@ -724,7 +711,7 @@ class PlaybackStateManager private constructor() {
mIndex = playbackState.index
callbacks.forEach {
it.onSeekConfirm(mPosition)
it.onSeek(mPosition)
it.onModeUpdate(mMode)
it.onRestoreFinish()
}
@ -839,7 +826,7 @@ class PlaybackStateManager private constructor() {
fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {}
fun onLoopUpdate(mode: LoopMode) {}
fun onSeekConfirm(position: Long) {}
fun onSeek(position: Long) {}
fun onInUserQueueUpdate(isInUserQueue: Boolean) {}
fun onRestoreFinish() {}
}

View file

@ -24,10 +24,11 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.Accent
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.fixAnimationInfoMemoryLeak
import org.oxycblt.auxio.ui.fixAnimInfoLeak
import org.oxycblt.auxio.ui.getSpans
import org.oxycblt.auxio.ui.requireCompatActivity
import org.oxycblt.auxio.ui.toColor
import org.oxycblt.auxio.ui.toStateList
/**
* A [Fragment] that allows for the searching of the entire music library.
@ -73,9 +74,7 @@ class SearchFragment : Fragment() {
binding.searchTextLayout.apply {
boxStrokeColor = accent
hintTextColor = ColorStateList.valueOf(accent)
setEndIconTintList(
ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
)
setEndIconTintList(R.color.control_color.toStateList(context))
}
binding.searchEditText.addTextChangedListener {
@ -133,7 +132,7 @@ class SearchFragment : Fragment() {
override fun onDestroyView() {
super.onDestroyView()
fixAnimationInfoMemoryLeak()
fixAnimInfoLeak()
}
override fun onResume() {

View file

@ -37,6 +37,11 @@ class SearchViewModel : ViewModel() {
mFilterMode = settingsManager.searchFilterMode
}
/**
* Perform a search of the music library. Will push results to [searchResults].
* @param query The query to use
* @param context [Context] required to create the headers
*/
fun doSearch(query: String, context: Context) {
mLastQuery = query

View file

@ -53,9 +53,7 @@ data class Accent(
/**
* Get a [ColorStateList] of the accent
*/
fun getStateList(context: Context): ColorStateList {
return ColorStateList.valueOf(color.toColor(context))
}
fun getStateList(context: Context): ColorStateList = color.toStateList(context)
/**
* Get the name (in bold) and the hex value of a accent.

View file

@ -83,7 +83,8 @@ fun String.createToast(context: Context) {
}
/**
* Require an [AppCompatActivity]
* Ensure that a not-null [AppCompatActivity] will be returned.
* @throws IllegalStateException When there is no activity or if the activity is null
*/
fun Fragment.requireCompatActivity(): AppCompatActivity {
val activity = requireActivity()
@ -122,6 +123,13 @@ fun Int.toColor(context: Context): Int {
}
}
/**
* Resolve a color and turn it into a [ColorStateList]
* @param context [Context] required
* @return The resolved color as a [ColorStateList]
*/
fun Int.toStateList(context: Context): ColorStateList = ColorStateList.valueOf(toColor(context))
// --- CONFIGURATION ---
/**
@ -209,16 +217,14 @@ private fun isSystemBarOnBottom(activity: Activity): Boolean {
// --- HACKY NIGHTMARES ---
/**
* Use ***R E F L E C T I O N*** to fix a memory leak where mAnimationInfo will keep a reference to
* its focused view.
*
* I can't believe I have to do this.
* Use reflection to fix a memory leak in the [Fragment] source code where the focused view will
* never be cleared. I can't believe I have to do this.
*/
fun Fragment.fixAnimationInfoMemoryLeak() {
fun Fragment.fixAnimInfoLeak() {
try {
Fragment::class.java.getDeclaredMethod("setFocusedView", View::class.java).let {
it.isAccessible = true
it.invoke(this, null)
Fragment::class.java.getDeclaredMethod("setFocusedView", View::class.java).apply {
isAccessible = true
invoke(this@fixAnimInfoLeak, null)
}
} catch (e: Exception) {
logE("mAnimationInfo leak fix failed.")

View file

@ -2,32 +2,31 @@ package org.oxycblt.auxio.ui
import android.os.Looper
import android.view.LayoutInflater
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* A delegate that creates a binding that can be used as a member variable without nullability or
* memory leaks.
* @param bindingFactory The ViewBinding inflation method that should be used
* @param onDestroy Any code that should be run when the binding is destroyed
* @param inflate The ViewBinding inflation method that should be used
*/
fun <T : ViewBinding> Fragment.memberBinding(
bindingFactory: (LayoutInflater) -> T,
fun <T : ViewDataBinding> Fragment.memberBinding(
inflate: (LayoutInflater) -> T,
onDestroy: T.() -> Unit = {}
) = FragmentBinderDelegate(this, bindingFactory, onDestroy)
) = MemberBinder(this, inflate, onDestroy)
/**
* The delegate for the [memberBinding] shortcut function.
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
*/
class FragmentBinderDelegate<T : ViewBinding>(
class MemberBinder<T : ViewDataBinding>(
private val fragment: Fragment,
private val inflate: (LayoutInflater) -> T,
private val onDestroy: T.() -> Unit
@ -36,7 +35,7 @@ class FragmentBinderDelegate<T : ViewBinding>(
init {
fragment.observeOwnerThroughCreation {
lifecycle.addObserver(this@FragmentBinderDelegate)
lifecycle.addObserver(this@MemberBinder)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB