Skip to content

Commit 42a829c

Browse files
committed
list fixes
1 parent bb81043 commit 42a829c

File tree

7 files changed

+97
-404
lines changed

7 files changed

+97
-404
lines changed

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/AlbumAdapter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class AlbumAdapter(
5353
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
5454
super.onBindViewHolder(holder, position, payloads)
5555
if (layoutType == LayoutType.GRID) {
56-
val item = list[position]
56+
val item = list?.second[position] ?: return
5757
holder.itemView.setOnLongClickListener {
5858
val popupMenu = PopupMenu(it.context, it)
5959
onMenu(item, popupMenu)

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt

Lines changed: 47 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ package org.akanework.gramophone.ui.adapters
2020
import android.annotation.SuppressLint
2121
import android.content.res.Configuration
2222
import android.net.Uri
23-
import android.os.Handler
24-
import android.os.Looper
2523
import android.util.Log
2624
import android.view.LayoutInflater
2725
import android.view.View
@@ -45,16 +43,13 @@ import com.google.android.material.button.MaterialButton
4543
import kotlinx.coroutines.CoroutineScope
4644
import kotlinx.coroutines.Dispatchers
4745
import kotlinx.coroutines.ExperimentalCoroutinesApi
46+
import kotlinx.coroutines.NonCancellable
4847
import kotlinx.coroutines.cancel
4948
import kotlinx.coroutines.flow.Flow
5049
import kotlinx.coroutines.flow.MutableStateFlow
51-
import kotlinx.coroutines.flow.SharedFlow
52-
import kotlinx.coroutines.flow.SharingStarted
50+
import kotlinx.coroutines.flow.combine
5351
import kotlinx.coroutines.flow.first
5452
import kotlinx.coroutines.flow.flatMapLatest
55-
import kotlinx.coroutines.flow.map
56-
import kotlinx.coroutines.flow.onEach
57-
import kotlinx.coroutines.flow.shareIn
5853
import kotlinx.coroutines.launch
5954
import kotlinx.coroutines.runBlocking
6055
import kotlinx.coroutines.sync.Semaphore
@@ -84,16 +79,14 @@ abstract class BaseAdapter<T>(
8479
val ownsView: Boolean,
8580
defaultLayoutType: LayoutType,
8681
private val isSubFragment: Boolean = false,
87-
private val rawOrderExposed: Boolean = false,
82+
rawOrderExposed: Boolean = false,
8883
private val allowDiffUtils: Boolean = false,
8984
private val canSort: Boolean = true,
9085
private val fallbackSpans: Int = 1
9186
) : AdapterFragment.BaseInterface<BaseAdapter<T>.ViewHolder>(), PopupTextProvider, ItemHeightHelper {
9287

9388
val context = fragment.requireContext()
9489
protected val liveDataAgent = MutableStateFlow(liveData)
95-
@OptIn(ExperimentalCoroutinesApi::class)
96-
private val flow = liveDataAgent.flatMapLatest { it }
9790
protected inline val mainActivity
9891
get() = context as MainActivity
9992
internal inline val layoutInflater: LayoutInflater
@@ -110,10 +103,8 @@ abstract class BaseAdapter<T>(
110103
override val itemHeightHelper by lazy {
111104
DefaultItemHeightHelper.concatItemHeightHelper(decorAdapter, { 1 }, this)
112105
}
113-
private var lastList: List<T>? = null
114-
protected val list = ArrayList<T>((flow as? SharedFlow<List<T>>)?.replayCache?.lastOrNull()?.size ?: 0)
106+
protected var list: Pair<List<T>, List<T>>
115107
private var layoutManager: RecyclerView.LayoutManager? = null
116-
private var listLock = Semaphore(1)
117108
protected var recyclerView: MyRecyclerView? = null
118109
private set
119110

@@ -173,14 +164,27 @@ abstract class BaseAdapter<T>(
173164
else
174165
initialSortType
175166
)
176-
private val comparator: SharedFlow<Sorter.HintedComparator<T>?> = sortType.map {
177-
sorter.getComparator(it)
178-
}.shareIn(CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed(5000))
167+
@OptIn(ExperimentalCoroutinesApi::class)
168+
private val flow = liveDataAgent.flatMapLatest { it }.combine(sortType) { it, st ->
169+
it to ArrayList(it).apply {
170+
val cmp = sorter.getComparator(st)
171+
if (st == Sorter.Type.NativeOrderDescending) {
172+
reverse()
173+
} else if (st != Sorter.Type.NativeOrder) {
174+
sortWith { o1, o2 ->
175+
if (isPinned(o1) && !isPinned(o2)) -1
176+
else if (!isPinned(o1) && isPinned(o2)) 1
177+
else cmp?.compare(o1, o2) ?: 0
178+
}
179+
}
180+
}.toList()
181+
}
179182
val sortTypes: Set<Sorter.Type>
180183
get() = if (canSort) sorter.getSupportedTypes() else setOf(Sorter.Type.None)
181184

182185
init {
183-
updateListInternal(runBlocking { flow.first() }, now = true, canDiff = false)
186+
list = runBlocking { flow.first() }
187+
onListUpdated()
184188
layoutType =
185189
if (prefLayoutType != LayoutType.NONE && prefLayoutType != defaultLayoutType && !isSubFragment)
186190
prefLayoutType
@@ -210,11 +214,27 @@ abstract class BaseAdapter<T>(
210214
applyLayoutManager()
211215
}
212216
}
213-
this.scope = CoroutineScope(Dispatchers.Default)
214-
this.scope!!.launch {
217+
if (scope != null)
218+
throw IllegalStateException("scope != null in onAttachedToRecyclerView")
219+
scope = CoroutineScope(Dispatchers.Default)
220+
scope!!.launch {
215221
flow.collect {
216-
withContext(Dispatchers.Main) {
217-
updateListInternal(it, now = false, canDiff = true)
222+
// The replay cache may cause us seeing the same list more than one. Make sure to
223+
// use === (reference equals) to avoid performance hit.
224+
if (list === it) return@collect
225+
val diff = if ((list.second.isNotEmpty<T>() && it.second.isNotEmpty<T>())
226+
|| allowDiffUtils)
227+
DiffUtil.calculateDiff(SongDiffCallback(list.second, it.second))
228+
else null
229+
val sizeChanged = list.second.size != it.second.size
230+
withContext(Dispatchers.Main + NonCancellable) {
231+
list = it
232+
if (diff != null)
233+
diff.dispatchUpdatesTo(this@BaseAdapter)
234+
else
235+
notifyDataSetChanged()
236+
if (sizeChanged) decorAdapter.updateSongCounter()
237+
onListUpdated()
218238
}
219239
}
220240
}
@@ -225,8 +245,8 @@ abstract class BaseAdapter<T>(
225245
if (layoutType == LayoutType.GRID) {
226246
recyclerView.removeItemDecoration(gridPaddingDecoration)
227247
}
228-
this.scope!!.cancel()
229-
this.scope = null
248+
scope!!.cancel()
249+
scope = null
230250
this.recyclerView = null
231251
if (ownsView) {
232252
recyclerView.layoutManager = null
@@ -246,7 +266,7 @@ abstract class BaseAdapter<T>(
246266
recyclerView?.scrollToPosition(scrollPosition)
247267
}
248268

249-
override fun getItemCount(): Int = list.size
269+
override fun getItemCount(): Int = list.second.size
250270

251271
override fun onCreateViewHolder(
252272
parent: ViewGroup,
@@ -258,64 +278,6 @@ abstract class BaseAdapter<T>(
258278

259279
fun sort(selector: Sorter.Type) {
260280
sortType.value = selector
261-
//updateListInternal(null, now = false, canDiff = true) TODO
262-
}
263-
264-
@SuppressLint("NotifyDataSetChanged")
265-
private suspend fun sort(srcList: List<T>, canDiff: Boolean) {
266-
val newList = ArrayList(srcList)
267-
if (!listLock.tryAcquire()) {
268-
throw IllegalStateException("listLock already held, add now = true to the caller (I am ${javaClass.name})")
269-
}
270-
try {
271-
val diff = withContext(Dispatchers.Default) {
272-
val st = sortType.first()
273-
val cmp = comparator.first()
274-
if (st == Sorter.Type.NativeOrderDescending) {
275-
newList.reverse()
276-
} else if (st != Sorter.Type.NativeOrder) {
277-
newList.sortWith { o1, o2 ->
278-
if (isPinned(o1) && !isPinned(o2)) -1
279-
else if (!isPinned(o1) && isPinned(o2)) 1
280-
else cmp?.compare(o1, o2) ?: 0
281-
}
282-
}
283-
if (((list.isNotEmpty() && newList.isNotEmpty()) || allowDiffUtils) && canDiff)
284-
DiffUtil.calculateDiff(SongDiffCallback(list, newList)) else null
285-
}
286-
list.clear()
287-
list.addAll(newList)
288-
if (diff != null)
289-
diff.dispatchUpdatesTo(this@BaseAdapter)
290-
else
291-
notifyDataSetChanged()
292-
if (list.size != newList.size) decorAdapter.updateSongCounter()
293-
onListUpdated()
294-
} finally {
295-
listLock.release()
296-
}
297-
}
298-
299-
fun updateListInternal(newList: List<T>? = null, now: Boolean, canDiff: Boolean) {
300-
// The replay cache may cause us seeing the same list more than one.
301-
if (newList != null) {
302-
if (lastList === newList) return
303-
lastList = newList
304-
}
305-
val list = lastList
306-
if (list == null)
307-
throw IllegalArgumentException("updateListInternal called with null value but no value is cached")
308-
if (now || scope == null) {
309-
runBlocking {
310-
sort(list, canDiff)
311-
}
312-
} else {
313-
scope!!.launch {
314-
withContext(Dispatchers.Main) {
315-
sort(list, canDiff)
316-
}
317-
}
318-
}
319281
}
320282

321283
protected open fun onListUpdated() {}
@@ -346,7 +308,7 @@ abstract class BaseAdapter<T>(
346308
}
347309
}
348310
}
349-
val item = list[position]
311+
val item = list.second[position]
350312
holder.title.text = titleOf(item) ?: virtualTitleOf(item)
351313
holder.subTitle.text = subTitleOf(item)
352314
holder.songCover.load(coverOf(item)) {
@@ -466,7 +428,7 @@ abstract class BaseAdapter<T>(
466428
}
467429

468430
protected fun toRawPos(item: T): Int {
469-
return lastList!!.indexOf(item)
431+
return list.first.indexOf(item)
470432
}
471433

472434
final override fun getPopupText(view: View, position: Int): CharSequence {
@@ -475,7 +437,7 @@ abstract class BaseAdapter<T>(
475437
// if this crashes with IndexOutOfBoundsException, list access isn't guarded enough?
476438
// lib only ever gets popup text for what RecyclerView believes to be the first view
477439
return (if (position >= 1)
478-
sorter.getFastScrollHintFor(list[position - 1], sortType.value)
440+
sorter.getFastScrollHintFor(list.second[position - 1], sortType.value)
479441
else null) ?: "-"
480442
}
481443

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseDecorAdapter.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ open class BaseDecorAdapter<T : BaseAdapter<*>>(
6464
if (adapter is SongAdapter) View.VISIBLE else View.GONE
6565
holder.counter.text = context.resources.getQuantityString(pluralStr, count, count)
6666
holder.sortButton.visibility =
67-
if (adapter.sortType != Sorter.Type.None || adapter.ownsView) View.VISIBLE else View.GONE
67+
if (adapter.sortType.value != Sorter.Type.None || adapter.ownsView) View.VISIBLE else View.GONE
6868
holder.sortButton.setOnClickListener { view ->
6969
val popupMenu = PopupMenu(context, view)
7070
popupMenu.inflate(R.menu.sort_menu)
@@ -93,16 +93,16 @@ open class BaseDecorAdapter<T : BaseAdapter<*>>(
9393
popupMenu.menu.findItem(it.key).isVisible = adapter.ownsView
9494
}
9595
popupMenu.menu.findItem(R.id.display).isVisible = adapter.ownsView
96-
if (adapter.sortType != Sorter.Type.None) {
97-
when (adapter.sortType) {
96+
if (adapter.sortType.value != Sorter.Type.None) {
97+
when (adapter.sortType.value) {
9898
in buttonMap.values -> {
9999
popupMenu.menu.findItem(
100100
buttonMap.entries
101-
.first { it.value == adapter.sortType }.key
101+
.first { it.value == adapter.sortType.value }.key
102102
).isChecked = true
103103
}
104104

105-
else -> throw IllegalStateException("Invalid sortType ${adapter.sortType.name}")
105+
else -> throw IllegalStateException("Invalid sortType ${adapter.sortType.value.name}")
106106
}
107107
}
108108
if (adapter.ownsView) {

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/DetailedFolderAdapter.kt

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ import org.akanework.gramophone.ui.fragments.AdapterFragment
4545
import uk.akane.libphonograph.items.FileNode
4646

4747
class DetailedFolderAdapter(
48-
private val fragment: Fragment
49-
) : AdapterFragment.BaseInterface<RecyclerView.ViewHolder>(), Observer<FileNode> {
48+
private val fragment: Fragment,
49+
private val isDetailed: Boolean
50+
) : AdapterFragment.BaseInterface<RecyclerView.ViewHolder>() {
5051
private val mainActivity = fragment.requireActivity() as MainActivity
51-
private val liveData = mainActivity.reader.folderStructureFlow
52+
private val liveData = if (isDetailed) mainActivity.reader.folderStructureFlow
53+
else mainActivity.reader.shallowFolderFlow
5254
private var scope: CoroutineScope? = null
5355
private val folderPopAdapter: FolderPopAdapter = FolderPopAdapter(this)
5456
private val folderAdapter: FolderListAdapter =
@@ -83,17 +85,27 @@ class DetailedFolderAdapter(
8385

8486
override fun onDetachedFromRecyclerView(recyclerView: MyRecyclerView) {
8587
super.onDetachedFromRecyclerView(recyclerView)
86-
this.scope!!.cancel()
87-
this.scope = null
88+
scope!!.cancel()
89+
scope = null
8890
recyclerView.layoutManager = null
8991
}
9092

91-
override fun getPopupText(view: View, position: Int): CharSequence {
92-
return "-"
93-
}
94-
95-
override fun onChanged(value: FileNode) {
93+
fun onChanged(value: FileNode) {
9694
root = value
95+
if (fileNodePath.isEmpty() && isDetailed) {
96+
val stg = value.folderList.values.firstOrNull { it.folderName == "storage" }
97+
val emu = stg?.folderList?.values?.firstOrNull { it.folderName == "emulated" }
98+
val usr = emu?.folderList?.values?.firstOrNull()
99+
if (stg != null) {
100+
fileNodePath.add(stg.folderName)
101+
}
102+
if (emu != null) {
103+
fileNodePath.add(emu.folderName)
104+
}
105+
if (usr != null) {
106+
fileNodePath.add(usr.folderName)
107+
}
108+
}
97109
update(null)
98110
}
99111

@@ -148,6 +160,22 @@ class DetailedFolderAdapter(
148160
}
149161
}
150162

163+
override fun getPopupText(view: View, position: Int): CharSequence {
164+
var newPos = position
165+
if (newPos < folderPopAdapter.itemCount) {
166+
return "-"
167+
}
168+
newPos -= folderPopAdapter.itemCount
169+
if (newPos < folderAdapter.itemCount) {
170+
return folderAdapter.getPopupText(view, newPos)
171+
}
172+
newPos -= folderAdapter.itemCount
173+
if (newPos < songAdapter.itemCount) {
174+
return songAdapter.getPopupText(view, newPos + 1)
175+
}
176+
throw IllegalStateException()
177+
}
178+
151179
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
152180
throw UnsupportedOperationException()
153181

0 commit comments

Comments
 (0)