diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e5b20bdf..c445f0722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Rounded images are more nuanced - Shuffle and Repeat mode buttons now have more contrast when they are turned on +#### What's Fixed +- Fixed crash on certain devices running Android 10 and lower when a differing theme from the system theme was used. + #### What's Changed - All cover art is now cropped to a 1:1 aspect ratio @@ -14,6 +17,7 @@ - Switches now have a disabled state - Reworked dynamic color usage - Reworked logging +- Upgrade ExoPlayer to v2.17.0 [Eliminates custom fork] ## v2.2.1 #### What's Improved diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 18dcc05f3..44d940065 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -57,6 +57,7 @@ class AlbumDetailFragment : DetailFragment() { ): View { detailModel.setAlbum(args.albumId) + val binding = FragmentDetailBinding.inflate(layoutInflater) val detailAdapter = AlbumDetailAdapter( playbackModel, detailModel, doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) }, @@ -67,7 +68,7 @@ class AlbumDetailFragment : DetailFragment() { binding.lifecycleOwner = viewLifecycleOwner - setupToolbar(detailModel.curAlbum.value!!, R.menu.menu_album_detail) { itemId -> + setupToolbar(detailModel.curAlbum.value!!, binding, R.menu.menu_album_detail) { itemId -> when (itemId) { R.id.action_play_next -> { playbackModel.playNext(detailModel.curAlbum.value!!) @@ -85,7 +86,7 @@ class AlbumDetailFragment : DetailFragment() { } } - setupRecycler(detailAdapter) { pos -> + setupRecycler(binding, detailAdapter) { pos -> val item = detailAdapter.currentList[pos] item is Header || item is ActionHeader || item is Album } @@ -113,7 +114,7 @@ class AlbumDetailFragment : DetailFragment() { is Song -> { if (detailModel.curAlbum.value!!.id == item.album.id) { logD("Navigating to a song in this album") - scrollToItem(item.id, detailAdapter) + scrollToItem(item.id, binding, detailAdapter) detailModel.finishNavToItem() } else { logD("Navigating to another album") @@ -185,7 +186,11 @@ class AlbumDetailFragment : DetailFragment() { /** * Scroll to an song using its [id]. */ - private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) { + private fun scrollToItem( + id: Long, + binding: FragmentDetailBinding, + adapter: AlbumDetailAdapter + ) { // Calculate where the item for the currently played song is val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 60f112b4c..f908d2fe5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.Album @@ -51,6 +52,7 @@ class ArtistDetailFragment : DetailFragment() { ): View { detailModel.setArtist(args.artistId) + val binding = FragmentDetailBinding.inflate(layoutInflater) val detailAdapter = ArtistDetailAdapter( playbackModel, doOnClick = { data -> @@ -74,8 +76,8 @@ class ArtistDetailFragment : DetailFragment() { binding.lifecycleOwner = viewLifecycleOwner - setupToolbar(detailModel.curArtist.value!!) - setupRecycler(detailAdapter) { pos -> + setupToolbar(detailModel.curArtist.value!!, binding) + setupRecycler(binding, detailAdapter) { pos -> // If the item is an ActionHeader we need to also make the item full-width val item = detailAdapter.currentList[pos] item is Header || item is ActionHeader || item is Artist diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index a29a934f3..bf7ae2c49 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -23,13 +23,13 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.view.children import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.Navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.logD @@ -40,7 +40,6 @@ import org.oxycblt.auxio.util.logD abstract class DetailFragment : Fragment() { protected val detailModel: DetailViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() - protected val binding by memberBinding(FragmentDetailBinding::inflate) override fun onResume() { super.onResume() @@ -61,6 +60,7 @@ abstract class DetailFragment : Fragment() { */ protected fun setupToolbar( data: MusicParent, + binding: FragmentDetailBinding, @MenuRes menuId: Int = -1, onMenuClick: ((itemId: Int) -> Boolean)? = null ) { @@ -87,6 +87,7 @@ abstract class DetailFragment : Fragment() { * Shortcut method for recyclerview setup */ protected fun setupRecycler( + binding: FragmentDetailBinding, detailAdapter: RecyclerView.Adapter, gridLookup: (Int) -> Boolean ) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index a14d4378e..96bc663ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.Album @@ -51,6 +52,7 @@ class GenreDetailFragment : DetailFragment() { ): View { detailModel.setGenre(args.genreId) + val binding = FragmentDetailBinding.inflate(inflater) val detailAdapter = GenreDetailAdapter( playbackModel, doOnClick = { song -> @@ -65,8 +67,8 @@ class GenreDetailFragment : DetailFragment() { binding.lifecycleOwner = viewLifecycleOwner - setupToolbar(detailModel.curGenre.value!!) - setupRecycler(detailAdapter) { pos -> + setupToolbar(detailModel.curGenre.value!!, binding) + setupRecycler(binding, detailAdapter) { pos -> val item = detailAdapter.currentList[pos] item is Header || item is ActionHeader || item is Genre } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 277279989..f51ea56f8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.toDate @@ -43,6 +44,10 @@ class AlbumListFragment : HomeListFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + val binding = FragmentHomeListBinding.inflate(layoutInflater) + + // / --- UI SETUP --- + binding.lifecycleOwner = viewLifecycleOwner val adapter = AlbumAdapter( @@ -54,7 +59,7 @@ class AlbumListFragment : HomeListFragment() { ::newMenu ) - setupRecycler(R.id.home_album_list, adapter, homeModel.albums) + setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums) return binding.root } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index b9a26711e..eb3d96f19 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.ui.ArtistViewHolder @@ -40,6 +41,10 @@ class ArtistListFragment : HomeListFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + val binding = FragmentHomeListBinding.inflate(layoutInflater) + + // / --- UI SETUP --- + binding.lifecycleOwner = viewLifecycleOwner val adapter = ArtistAdapter( @@ -51,7 +56,7 @@ class ArtistListFragment : HomeListFragment() { ::newMenu ) - setupRecycler(R.id.home_artist_list, adapter, homeModel.artists) + setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists) return binding.root } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index bfd44685d..93482d032 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.ui.GenreViewHolder @@ -40,6 +41,10 @@ class GenreListFragment : HomeListFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + val binding = FragmentHomeListBinding.inflate(layoutInflater) + + // / --- UI SETUP --- + binding.lifecycleOwner = viewLifecycleOwner val adapter = GenreAdapter( @@ -51,7 +56,7 @@ class GenreListFragment : HomeListFragment() { ::newMenu ) - setupRecycler(R.id.home_genre_list, adapter, homeModel.genres) + setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres) return binding.root } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 25a73343c..9bca96f19 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -28,7 +28,6 @@ import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.music.Item import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.util.applySpans /** @@ -36,10 +35,6 @@ import org.oxycblt.auxio.util.applySpans * @author OxygenCobalt */ abstract class HomeListFragment : Fragment() { - protected val binding: FragmentHomeListBinding by memberBinding( - FragmentHomeListBinding::inflate - ) - protected val homeModel: HomeViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() @@ -50,6 +45,7 @@ abstract class HomeListFragment : Fragment() { protected fun setupRecycler( @IdRes uniqueId: Int, + binding: FragmentHomeListBinding, homeAdapter: HomeAdapter, homeData: LiveData>, ) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 93bb437d1..60cdd62cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.ui.DisplayMode @@ -41,6 +42,10 @@ class SongListFragment : HomeListFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + val binding = FragmentHomeListBinding.inflate(layoutInflater) + + // / --- UI SETUP --- + binding.lifecycleOwner = viewLifecycleOwner val adapter = SongsAdapter( @@ -50,7 +55,7 @@ class SongListFragment : HomeListFragment() { ::newMenu ) - setupRecycler(R.id.home_song_list, adapter, homeModel.songs) + setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs) return binding.root } 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 b9864f665..5fd4514d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -32,7 +32,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.playback.state.LoopMode -import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -44,17 +43,19 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat class PlaybackFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() - private val binding by memberBinding(FragmentPlaybackBinding::inflate) { - playbackSong.isSelected = false // Clear marquee to prevent a memory leak - } + private var mLastBinding: FragmentPlaybackBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + val binding = FragmentPlaybackBinding.inflate(layoutInflater) val queueItem: MenuItem + // See onDestroyView for why we do this + mLastBinding = binding + // --- UI SETUP --- binding.lifecycleOwner = viewLifecycleOwner @@ -92,6 +93,8 @@ class PlaybackFragment : Fragment() { // Make marquee of song title work binding.playbackSong.isSelected = true binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition + + // Abuse the play/pause FAB (see style definition for more info) binding.playbackPlayPause.post { binding.playbackPlayPause.stateListAnimator = null } @@ -156,6 +159,15 @@ class PlaybackFragment : Fragment() { return binding.root } + override fun onDestroyView() { + super.onDestroyView() + + // playbackSong will leak if we don't disable marquee, keep the binding around + // so that we can turn it off when we destroy the view. + mLastBinding?.playbackSong?.isSelected = false + mLastBinding = null + } + private fun navigateUp() { // This is a dumb and fragile hack but this fragment isn't part of the navigation stack // so we can't really do much diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt b/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt deleted file mode 100644 index 811f8d2e1..000000000 --- a/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * MemberBinder.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.ui - -import android.view.LayoutInflater -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.Fragment -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.LifecycleOwner -import org.oxycblt.auxio.util.assertMainThread -import org.oxycblt.auxio.util.inflater -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 inflate The ViewBinding inflation method that should be used - * @param onDestroy What to do when the binding is destroyed - */ -fun Fragment.memberBinding( - inflate: (LayoutInflater) -> T, - onDestroy: T.() -> Unit = {} -) = MemberBinder(this, inflate, onDestroy) - -/** - * The delegate for the [memberBinding] shortcut function. - * Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding) - * @author OxygenCobalt - */ -class MemberBinder( - private val fragment: Fragment, - private val inflate: (LayoutInflater) -> T, - private val onDestroy: T.() -> Unit -) : ReadOnlyProperty, LifecycleObserver, LifecycleEventObserver { - private var fragmentBinding: T? = null - - init { - fragment.observeOwnerThroughCreation { - lifecycle.addObserver(this@MemberBinder) - } - } - - override fun getValue(thisRef: Fragment, property: KProperty<*>): T { - assertMainThread() - - val binding = fragmentBinding - - // If the fragment is already initialized, then just return that. - if (binding != null) { - return binding - } - - val lifecycle = fragment.viewLifecycleOwner.lifecycle - - check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - "Fragment views are destroyed" - } - - // Otherwise create the binding and return that. - return inflate(thisRef.requireContext().inflater).also { - fragmentBinding = it - } - } - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if (event == Lifecycle.Event.ON_DESTROY) { - fragmentBinding?.onDestroy() - fragmentBinding = null - } - } - - private inline fun Fragment.observeOwnerThroughCreation( - crossinline viewOwner: LifecycleOwner.() -> Unit - ) { - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - super.onCreate(owner) - - viewLifecycleOwnerLiveData.observe(this@observeOwnerThroughCreation) { - it.viewOwner() - } - } - }) - } -} diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 16248789f..6ee1a3b84 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -53,9 +53,7 @@ is separated into three phases: - Set up ViewModel instances and LiveData observers `findViewById` is to **only** be used when interfacing with non-Auxio views. Otherwise, view-binding should be -used in all cases. If one needs to keep track of a view-binding outside of `onCreateView`, then one can declare -a binding `by memberBinding(BindingClass::inflate)` in order to have a binding that properly disposes itself -on lifecycle events. +used in all cases. Avoid usages of databinding outside of the `onCreateView` step unless absolutely necessary. At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as view-binding is still used. @@ -290,7 +288,6 @@ Shared views and view configuration models. This contains: - Customized views such as `EdgeAppBarLayout` and `EdgeRecyclerView`, which add some extra functionality not provided by default - Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied to a specific feature. - `newMenu` and `ActionMenu`, which automates menu creation for most data types -- `memberBinding` and `MemberBinder`, which allows for ViewBindings to be used as a member variable without memory leaks or nullability issues. #### `.util` Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method.