diff --git a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt index 555f73c6e..9d01ffccd 100644 --- a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt +++ b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt @@ -49,6 +49,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -71,7 +72,7 @@ import uk.akane.libphonograph.items.Item abstract class BaseAdapter( protected val fragment: Fragment, - liveData: Flow>?, + liveData: Flow>, sortHelper: Sorter.Helper, naturalOrderHelper: Sorter.NaturalOrderHelper?, initialSortType: Sorter.Type, @@ -86,8 +87,7 @@ abstract class BaseAdapter( ) : AdapterFragment.BaseInterface.ViewHolder>(), PopupTextProvider, ItemHeightHelper { val context = fragment.requireContext() - protected val listAgent = if (liveData == null) MutableStateFlow(listOf()) else null - protected val liveDataAgent = MutableStateFlow((liveData ?: listAgent)!!) + protected val liveDataAgent = MutableStateFlow(liveData) @OptIn(ExperimentalCoroutinesApi::class) private val flow = liveDataAgent.flatMapLatest { it } protected inline val mainActivity @@ -100,15 +100,13 @@ abstract class BaseAdapter( context.resources.getDimensionPixelSize(R.dimen.larger_list_height) private var gridHeight: Int? = null private var lockedInGridSize = false - private var lastList: List? = null private val sorter = Sorter(sortHelper, naturalOrderHelper, rawOrderExposed) val decorAdapter by lazy { createDecorAdapter() } override val concatAdapter by lazy { ConcatAdapter(decorAdapter, this) } override val itemHeightHelper by lazy { DefaultItemHeightHelper.concatItemHeightHelper(decorAdapter, { 1 }, this) } - private val handler = Handler(Looper.getMainLooper()) - private val rawList = ArrayList((flow as? SharedFlow>)?.replayCache?.lastOrNull()?.size ?: 0) + private var lastList: List? = null protected val list = ArrayList((flow as? SharedFlow>)?.replayCache?.lastOrNull()?.size ?: 0) private var comparator: Sorter.HintedComparator? = null private var layoutManager: RecyclerView.LayoutManager? = null @@ -186,18 +184,7 @@ abstract class BaseAdapter( prefSortType else initialSortType - /* - if (flow is SharedFlow>) { // TODO this can never succeed - flow.replayCache.lastOrNull()?.let { - updateListInternal(it, now = true, canDiff = false) - } - } else { - // TODO if we don't have a SharedFlow we'd be missing the above fast path and waste - // cycles reloading the list after view was layout-ed. should use viewmodel in - // callers to keep permanent SharedFlow and remove this code path entirely - Log.w("BaseAdapter", "liveData is non-null but not SharedFlow") - } - */ + updateListInternal(runBlocking { flow.first() }, now = true, canDiff = false) layoutType = if (prefLayoutType != LayoutType.NONE && prefLayoutType != defaultLayoutType && !isSubFragment) prefLayoutType @@ -279,16 +266,13 @@ abstract class BaseAdapter( } @SuppressLint("NotifyDataSetChanged") - private fun sort(srcList: List? = null, canDiff: Boolean): () -> () -> Unit { - // Ensure rawList is only accessed on UI thread - // and ensure calls to this method go in order - // to prevent funny IndexOutOfBoundsException crashes - val newList = ArrayList(srcList ?: rawList) + private suspend fun sort(srcList: List, canDiff: Boolean) { + val newList = ArrayList(srcList) if (!listLock.tryAcquire()) { throw IllegalStateException("listLock already held, add now = true to the caller (I am ${javaClass.name})") } - return { - try { + try { + val diff = withContext(Dispatchers.Default) { if (sortType == Sorter.Type.NativeOrderDescending) { newList.reverse() } else if (sortType != Sorter.Type.NativeOrder) { @@ -298,54 +282,39 @@ abstract class BaseAdapter( else comparator?.compare(o1, o2) ?: 0 } } - val diff = - if (((list.isNotEmpty() && newList.isNotEmpty()) || allowDiffUtils) && canDiff) - DiffUtil.calculateDiff(SongDiffCallback(list, newList)) else null - val oldCount = list.size - val newCount = newList.size - { - try { - if (srcList != null) { - rawList.clear() - rawList.addAll(srcList) - } - list.clear() - list.addAll(newList) - if (diff != null) - diff.dispatchUpdatesTo(this) - else - notifyDataSetChanged() - if (oldCount != newCount) decorAdapter.updateSongCounter() - onListUpdated() - } finally { - listLock.release() - } - } - } catch (e: Exception) { - listLock.release() - throw e + if (((list.isNotEmpty() && newList.isNotEmpty()) || allowDiffUtils) && canDiff) + DiffUtil.calculateDiff(SongDiffCallback(list, newList)) else null } - } - } - - fun updateList(newList: List, canDiff: Boolean) { // now is true for all callees - // TODO what about now / canDiff - runBlocking { - listAgent!!.emit(newList) + list.clear() + list.addAll(newList) + if (diff != null) + diff.dispatchUpdatesTo(this@BaseAdapter) + else + notifyDataSetChanged() + if (list.size != newList.size) decorAdapter.updateSongCounter() + onListUpdated() + } finally { + listLock.release() } } fun updateListInternal(newList: List? = null, now: Boolean, canDiff: Boolean) { // The replay cache may cause us seeing the same list more than one. - if (lastList === newList) return - lastList = newList - val doSort = sort(newList, canDiff) - if (now || scope == null) doSort()() - else { + if (newList != null) { + if (lastList === newList) return + lastList = newList + } + val list = lastList + if (list == null) + throw IllegalArgumentException("updateListInternal called with null value but no value is cached") + if (now || scope == null) { + runBlocking { + sort(list, canDiff) + } + } else { scope!!.launch { - val apply = doSort() - handler.post { - apply() + withContext(Dispatchers.Main) { + sort(list, canDiff) } } } @@ -499,7 +468,7 @@ abstract class BaseAdapter( } protected fun toRawPos(item: T): Int { - return rawList.indexOf(item) + return lastList!!.indexOf(item) } final override fun getPopupText(view: View, position: Int): CharSequence { diff --git a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/DetailedFolderAdapter.kt b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/DetailedFolderAdapter.kt index 1e1dd4723..d7fed6a2a 100644 --- a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/DetailedFolderAdapter.kt +++ b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/DetailedFolderAdapter.kt @@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer +import androidx.media3.common.MediaItem import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager @@ -32,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.zhanghai.android.fastscroll.PopupTextProvider @@ -51,8 +53,9 @@ class DetailedFolderAdapter( private val folderPopAdapter: FolderPopAdapter = FolderPopAdapter(this) private val folderAdapter: FolderListAdapter = FolderListAdapter(listOf(), mainActivity, this) + private val songList = MutableStateFlow(listOf()) private val songAdapter: SongAdapter = - SongAdapter(fragment, listOf(), false, null, false) + SongAdapter(fragment, songList, false, null, false) override val concatAdapter: ConcatAdapter = ConcatAdapter(this, folderPopAdapter, folderAdapter, songAdapter) override val itemHeightHelper: ItemHeightHelper? = null @@ -116,7 +119,7 @@ class DetailedFolderAdapter( val doUpdate = { canDiff: Boolean -> folderPopAdapter.enabled = fileNodePath.isNotEmpty() folderAdapter.updateList(item?.folderList?.values ?: listOf(), canDiff) - songAdapter.updateList(item?.songList ?: listOf(), false) + songList.value = item?.songList ?: listOf() } recyclerView.let { if (it == null || invertedDirection == null) { diff --git a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/FolderAdapter.kt b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/FolderAdapter.kt index 99d8c44fd..b129ee321 100644 --- a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/FolderAdapter.kt +++ b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/FolderAdapter.kt @@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer +import androidx.media3.common.MediaItem import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager @@ -32,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.zhanghai.android.fastscroll.PopupTextProvider @@ -51,8 +53,9 @@ class FolderAdapter( private val folderPopAdapter: FolderPopAdapter = FolderPopAdapter(this) private val folderAdapter: FolderListAdapter = FolderListAdapter(listOf(), mainActivity, this) + private val songList = MutableStateFlow(listOf()) private val songAdapter: SongAdapter = - SongAdapter(fragment, listOf(), false, null, false) + SongAdapter(fragment, songList, false, null, false) override val concatAdapter: ConcatAdapter = ConcatAdapter(this, folderPopAdapter, folderAdapter, songAdapter) override val itemHeightHelper: ItemHeightHelper? = null @@ -126,7 +129,7 @@ class FolderAdapter( val doUpdate = { canDiff: Boolean -> folderPopAdapter.enabled = fileNodePath.isNotEmpty() folderAdapter.updateList(item?.folderList?.values ?: listOf(), canDiff) - songAdapter.updateList(item?.songList ?: listOf(), false) + songList.value = item?.songList ?: listOf() } recyclerView.let { if (it == null || invertedDirection == null) { diff --git a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/SongAdapter.kt b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/SongAdapter.kt index ce6ed4b5b..1cf49b86f 100644 --- a/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/SongAdapter.kt +++ b/app/src/main/kotlin/org/akanework/gramophone/ui/adapters/SongAdapter.kt @@ -54,7 +54,7 @@ import uk.akane.libphonograph.manipulator.ItemManipulator */ class SongAdapter( fragment: Fragment, - songList: Flow>? = (fragment.requireActivity() as MainActivity).reader.songListFlow, + songList: Flow> = (fragment.requireActivity() as MainActivity).reader.songListFlow, canSort: Boolean, helper: Sorter.NaturalOrderHelper?, ownsView: Boolean, @@ -82,30 +82,6 @@ class SongAdapter( fallbackSpans = fallbackSpans ) { - constructor( - fragment: Fragment, - songList: List, - canSort: Boolean, - helper: Sorter.NaturalOrderHelper?, - ownsView: Boolean, - isSubFragment: Boolean = false, - allowDiffUtils: Boolean = false, - rawOrderExposed: Boolean = !isSubFragment, - fallbackSpans: Int = 1 - ) : this( - fragment, - null, - canSort, - helper, - ownsView, - isSubFragment, - allowDiffUtils, - rawOrderExposed, - fallbackSpans - ) { - updateList(songList, false) - } - fun getSongList() = list fun getActivity() = mainActivity diff --git a/app/src/main/kotlin/org/akanework/gramophone/ui/fragments/SearchFragment.kt b/app/src/main/kotlin/org/akanework/gramophone/ui/fragments/SearchFragment.kt index 195c0eaf3..0642ff326 100644 --- a/app/src/main/kotlin/org/akanework/gramophone/ui/fragments/SearchFragment.kt +++ b/app/src/main/kotlin/org/akanework/gramophone/ui/fragments/SearchFragment.kt @@ -32,6 +32,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.appbar.AppBarLayout import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import org.akanework.gramophone.R import org.akanework.gramophone.logic.closeKeyboard @@ -64,9 +65,10 @@ class SearchFragment : BaseFragment(false) { appBarLayout.enableEdgeToEdgePaddingListener() editText = rootView.findViewById(R.id.edit_text) val recyclerView = rootView.findViewById(R.id.recyclerview) + val songList = MutableStateFlow(listOf()) val songAdapter = SongAdapter( - this, listOf(), + this, songList, true, null, false, isSubFragment = true, allowDiffUtils = true, rawOrderExposed = true ) @@ -83,7 +85,7 @@ class SearchFragment : BaseFragment(false) { editText.addTextChangedListener { rawText -> // TODO sort results by match quality? (using NaturalOrderHelper) if (rawText.isNullOrBlank()) { - songAdapter.updateList(listOf(), true) + songList.value = listOf() } else { // make sure the user doesn't edit away our text while we are filtering val text = rawText.toString() @@ -104,9 +106,7 @@ class SearchFragment : BaseFragment(false) { it ) } - handler.post { - songAdapter.updateList(filteredList, true) - } + songList.value = filteredList.toList() } } } diff --git a/build.gradle.kts b/build.gradle.kts index 3ff98cee6..981c8cd20 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - val agpVersion = "8.7.2" + val agpVersion = "8.8.0-beta01" id("com.android.application") version agpVersion apply false id("com.android.library") version agpVersion apply false id("com.android.test") version agpVersion apply false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 68e8816d7..fb602ee2a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libphonograph b/libphonograph index 363020373..b767d9b4b 160000 --- a/libphonograph +++ b/libphonograph @@ -1 +1 @@ -Subproject commit 3630203732128022b440fa0dd26e9345d8a5ffcd +Subproject commit b767d9b4b67e16ae8d5f57152663c34a4c62e582