Skip to content

Commit 12f79fc

Browse files
committed
wip
1 parent f98a8d9 commit 12f79fc

8 files changed

Lines changed: 79 additions & 42 deletions

File tree

app/src/main/java/org/akanework/gramophone/logic/utils/PauseableFlows.kt

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.akanework.gramophone.logic.utils
22

3-
import android.util.Log
43
import androidx.lifecycle.Lifecycle
54
import androidx.lifecycle.LifecycleOwner
65
import androidx.lifecycle.lifecycleScope
76
import androidx.lifecycle.repeatOnLifecycle
87
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.Dispatchers
99
import kotlinx.coroutines.ExperimentalCoroutinesApi
1010
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
1111
import kotlinx.coroutines.Job
@@ -19,11 +19,14 @@ import kotlinx.coroutines.flow.Flow
1919
import kotlinx.coroutines.flow.FlowCollector
2020
import kotlinx.coroutines.flow.MutableStateFlow
2121
import kotlinx.coroutines.flow.SharedFlow
22+
import kotlinx.coroutines.flow.SharingCommand
2223
import kotlinx.coroutines.flow.SharingStarted
24+
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
2325
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
2426
import kotlinx.coroutines.flow.StateFlow
2527
import kotlinx.coroutines.flow.channelFlow
2628
import kotlinx.coroutines.flow.collect
29+
import kotlinx.coroutines.flow.combine
2730
import kotlinx.coroutines.flow.emptyFlow
2831
import kotlinx.coroutines.flow.first
2932
import kotlinx.coroutines.flow.flatMapLatest
@@ -39,7 +42,7 @@ import kotlin.coroutines.EmptyCoroutineContext
3942
import kotlin.time.Duration
4043

4144
interface PauseManager : CoroutineContext.Element {
42-
val isPaused: Flow<Boolean>
45+
val isPaused: StateFlow<Boolean>
4346

4447
override val key: CoroutineContext.Key<*> get() = Key
4548
companion object Key : CoroutineContext.Key<PauseManager>
@@ -67,22 +70,34 @@ object EmptyPauseManager : PauseManager {
6770
}
6871

6972
@OptIn(ExperimentalCoroutinesApi::class)
70-
class CountingPauseManager : PauseManager {
71-
private val flows = MutableStateFlow(listOf<Flow<Boolean>>())
72-
override val isPaused = flows.mapLatest { it.find { !it.first() } == null } // will pause when 0 items
73+
class CountingPauseManager(paused: SharingStarted) : PauseManager {
74+
private val flows = MutableStateFlow(listOf<PauseManager>())
75+
override val isPaused = paused.command(flows.flatMapLatest {
76+
combine(it.map { it.isPaused }) { it.size - it.count { it } }
77+
}.stateIn(CoroutineScope(Dispatchers.Default), Eagerly, 0))
78+
.mapLatest {
79+
when (it) {
80+
SharingCommand.START -> false
81+
SharingCommand.STOP,
82+
SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> true
83+
}
84+
}.stateIn(CoroutineScope(Dispatchers.Default), Eagerly, true)
85+
//override val isPaused = flows.flatMapLatest {
86+
// combine(it.map { it.isPaused }) { !it.contains(false) }
87+
//}.stateIn(CoroutineScope(Dispatchers.Default), Eagerly, true)
7388

7489
fun add(other: PauseManager) {
75-
flows.value += other.isPaused
90+
flows.value += other
7691
}
7792

7893
fun remove(other: PauseManager) {
79-
flows.value -= other.isPaused
94+
flows.value -= other
8095
}
8196
}
8297

8398
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
84-
class PauseManagingSharedFlow<T>() : SharedFlow<T> {
85-
private val pauseManager = CountingPauseManager()
99+
class PauseManagingSharedFlow<T>(paused: SharingStarted) : SharedFlow<T> {
100+
private val pauseManager = CountingPauseManager(paused)
86101
lateinit var sharedFlow: SharedFlow<T>
87102

88103
override val replayCache: List<T>
@@ -91,13 +106,11 @@ class PauseManagingSharedFlow<T>() : SharedFlow<T> {
91106
override suspend fun collect(collector: FlowCollector<T>): Nothing {
92107
val pm = currentCoroutineContext()[PauseManager] ?: EmptyPauseManager
93108
try {
94-
Log.w("Tag", java.lang.IllegalStateException("register"))
95109
pauseManager.add(pm)
96110
withContext(pauseManager) {
97111
sharedFlow.collect(collector)
98112
}
99113
} finally {
100-
Log.w("Tag", java.lang.IllegalStateException("remove"))
101114
pauseManager.remove(pm)
102115
}
103116
}
@@ -106,9 +119,10 @@ class PauseManagingSharedFlow<T>() : SharedFlow<T> {
106119
fun <T> Flow<T>.sharePauseableIn(
107120
scope: CoroutineScope,
108121
started: SharingStarted,
122+
paused: SharingStarted,
109123
replay: Int = 0
110124
): SharedFlow<T> {
111-
val wrapper = PauseManagingSharedFlow<T>()
125+
val wrapper = PauseManagingSharedFlow<T>(paused)
112126
wrapper.sharedFlow = shareIn(CoroutineScope(scope.coroutineContext + Job(scope.coroutineContext[Job])
113127
+ wrapper.pauseManager), started, replay)
114128
return wrapper
@@ -117,8 +131,8 @@ class PauseManagingSharedFlow<T>() : SharedFlow<T> {
117131
}
118132

119133
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
120-
class PauseManagingStateFlow<T>() : StateFlow<T> {
121-
private val pauseManager = CountingPauseManager()
134+
class PauseManagingStateFlow<T>(paused: SharingStarted) : StateFlow<T> {
135+
private val pauseManager = CountingPauseManager(paused)
122136
lateinit var stateFlow: StateFlow<T>
123137

124138
override val replayCache: List<T>
@@ -143,9 +157,10 @@ class PauseManagingStateFlow<T>() : StateFlow<T> {
143157
fun <T> Flow<T>.statePauseableIn(
144158
scope: CoroutineScope,
145159
started: SharingStarted,
160+
paused: SharingStarted,
146161
initialValue: T
147162
): SharedFlow<T> {
148-
val wrapper = PauseManagingStateFlow<T>()
163+
val wrapper = PauseManagingStateFlow<T>(paused)
149164
wrapper.stateFlow = stateIn(CoroutineScope(scope.coroutineContext + Job(scope.coroutineContext[Job])
150165
+ wrapper.pauseManager), started, initialValue)
151166
return wrapper
@@ -158,14 +173,8 @@ suspend fun <T> repeatFlowWhenUnpaused(enforcePauseable: Boolean = false, block:
158173
val pauseManager = currentCoroutineContext()[PauseManager]
159174
return if (pauseManager != null) {
160175
pauseManager.isPaused.flatMapLatest {
161-
Log.e("hi", "hii ${pauseManager.isPaused.first()}")
162-
if (!it) {
163-
try {
164-
flowOf(block())
165-
} finally {
166-
Log.e("hi", "byee ${pauseManager.isPaused.first()}")
167-
}
168-
}
176+
if (!it)
177+
flowOf(block())
169178
else emptyFlow()
170179
}
171180
} else {

app/src/main/java/org/akanework/gramophone/logic/utils/SdScanner.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import android.provider.MediaStore
1010
import android.util.Log
1111
import androidx.core.content.getSystemService
1212
import androidx.core.util.Consumer
13-
import org.akanework.gramophone.BuildConfig
1413
import java.io.File
1514
import java.io.IOException
1615

@@ -210,10 +209,6 @@ class SdScanner(private val context: Context, var progressFrequencyMs: Int = 250
210209
}
211210

212211
fun scanEverything(context: Context, progressFrequencyMs: Int = 250, listener: Consumer<SimpleProgress>? = null) {
213-
if (BuildConfig.DEBUG) { // TODO
214-
listener?.accept(SimpleProgress().apply { set(SimpleProgress.Step.DONE, "", 0) })
215-
return
216-
}
217212
val scanner = SdScanner(context, progressFrequencyMs)
218213
if (listener != null) {
219214
scanner.progress.addListener { t ->

app/src/main/java/org/akanework/gramophone/ui/MainActivity.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
5656
import kotlinx.coroutines.CoroutineScope
5757
import kotlinx.coroutines.Dispatchers
5858
import kotlinx.coroutines.FlowPreview
59-
import kotlinx.coroutines.flow.debounce
6059
import kotlinx.coroutines.flow.first
6160
import kotlinx.coroutines.flow.firstOrNull
6261
import kotlinx.coroutines.launch
@@ -196,8 +195,7 @@ class MainActivity : AppCompatActivity() {
196195
Toast.makeText(this@MainActivity, R.string.edit_playlist_failed, Toast.LENGTH_LONG).show()
197196
return
198197
}
199-
// TODO debounce(50) sucks, cant we have up to date values with first()
200-
val playlists = runBlocking { reader.playlistListFlow.debounce(50).first().filter { it.id != null } }
198+
val playlists = runBlocking { reader.playlistListFlow.first().filter { it.id != null } }
201199
MaterialAlertDialogBuilder(this)
202200
.setTitle(R.string.add_to_playlist)
203201
.setIcon(R.drawable.ic_playlist_play)
@@ -262,8 +260,10 @@ class MainActivity : AppCompatActivity() {
262260
val songs = data.getStringArrayList("Songs")!!.map { File(it) }
263261
CoroutineScope(Dispatchers.Default).launch {
264262
try {
265-
// TODO add mode
266-
ItemManipulator.setPlaylistContent(this@MainActivity, path, songs)
263+
if (add)
264+
ItemManipulator.addToPlaylist(this@MainActivity, path, songs)
265+
else
266+
ItemManipulator.setPlaylistContent(this@MainActivity, path, songs)
267267
} catch (e: Exception) {
268268
Log.e("MainActivity", Log.getStackTraceString(e))
269269
withContext(Dispatchers.Main) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ abstract class BaseAdapter<T>(
177177
}
178178
}
179179
}.toList()
180-
}.sharePauseableIn(CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed(), replay = 1) // TODO !!! 5000
180+
}.sharePauseableIn(CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed(5000),
181+
SharingStarted.WhileSubscribed(2000), replay = 1)
181182
val sortTypes: Set<Sorter.Type>
182183
get() = if (canSort) sorter.getSupportedTypes() else setOf(Sorter.Type.None)
183184

app/src/main/java/uk/akane/libphonograph/manipulator/ItemManipulator.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ object ItemManipulator {
8484
}
8585
}
8686

87+
fun addToPlaylist(context: Context, out: File, songs: List<File>) {
88+
if (!out.exists())
89+
throw IllegalArgumentException("tried to change playlist $out that doesn't exist")
90+
setPlaylistContent(context, out, PlaylistSerializer.read(out) + songs)
91+
}
92+
8793
fun setPlaylistContent(context: Context, out: File, songs: List<File>) {
8894
if (!out.exists())
8995
throw IllegalArgumentException("tried to change playlist $out that doesn't exist")

app/src/main/java/uk/akane/libphonograph/manipulator/PlaylistSerializer.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ object PlaylistSerializer {
2020
write(context, format, outFile, songs)
2121
}
2222

23+
@Throws(UnsupportedPlaylistFormatException::class)
24+
fun read(outFile: File): List<File> {
25+
val format = when (outFile.extension.lowercase()) {
26+
"m3u", "m3u8" -> PlaylistFormat.M3u
27+
// "xspf" -> PlaylistFormat.Xspf
28+
// "wpl" -> PlaylistFormat.Wpl
29+
// "pls" -> PlaylistFormat.Pls
30+
else -> throw UnsupportedPlaylistFormatException(outFile.extension)
31+
}
32+
return read(format, outFile)
33+
}
34+
35+
private fun read(format: PlaylistFormat, outFile: File): List<File> {
36+
return when (format) {
37+
PlaylistFormat.M3u -> {
38+
val lines = outFile.readLines()
39+
lines.filter { !it.startsWith('#') }.map { outFile.resolveSibling(it) }
40+
}
41+
PlaylistFormat.Xspf -> TODO()
42+
PlaylistFormat.Wpl -> TODO()
43+
PlaylistFormat.Pls -> TODO()
44+
}
45+
}
46+
2347
private fun write(context: Context, format: PlaylistFormat, outFile: File, songs: List<File>) {
2448
when (format) {
2549
PlaylistFormat.M3u -> {

app/src/main/java/uk/akane/libphonograph/reader/FlowReader.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import android.provider.MediaStore
55
import android.util.Log
66
import androidx.media3.common.MediaItem
7+
import kotlinx.coroutines.CoroutineName
78
import kotlinx.coroutines.CoroutineScope
89
import kotlinx.coroutines.Dispatchers
910
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -53,11 +54,12 @@ class FlowReader(
5354
private var awaitingRefresh = false
5455
var hadFirstRefresh = true
5556
private set
56-
private val scope = CoroutineScope(Dispatchers.IO)
57+
private val scope = CoroutineScope(Dispatchers.IO + CoroutineName("FlowReader"))
5758
private val finishRefreshTrigger = MutableSharedFlow<Unit>(replay = 0)
5859
private val manualRefreshTrigger = MutableSharedFlow<Unit>(replay = 1)
5960
init {
60-
manualRefreshTrigger.tryEmit(Unit)
61+
if (!manualRefreshTrigger.tryEmit(Unit))
62+
throw IllegalStateException()
6163
}
6264
// Start observing as soon as class gets instantiated. ContentObservers are cheap, and more
6365
// importantly, this allows us to skip the expensive Reader call if nothing changed while we
@@ -72,14 +74,14 @@ class FlowReader(
7274
// These expensive Reader calls are only done if we have someone (UI) observing the result AND
7375
// something changed. The PauseableFlows mechanism allows us to skip any unnecessary work.
7476
private val rawPlaylistFlow = rawPlaylistVersionFlow
75-
.conflateAndBlockWhenPaused(true)
77+
//.conflateAndBlockWhenPaused(true) TODO!
7678
.flatMapLatest {
7779
manualRefreshTrigger.mapLatest { _ ->
7880
if (context.hasAudioPermission())
7981
Reader.fetchPlaylists(context).first
8082
else emptyList()
8183
}
82-
}
84+
}.sharePauseableIn(scope, WhileSubscribed(20000), WhileSubscribed(2000), replay = 1)
8385
private val readerFlow: Flow<ReaderResult> =
8486
shouldIncludeExtraFormatFlow.distinctUntilChanged().flatMapLatest { shouldIncludeExtraFormat ->
8587
shouldUseEnhancedCoverReadingFlow.distinctUntilChanged()
@@ -123,7 +125,7 @@ class FlowReader(
123125
finishRefreshTrigger.emit(Unit)
124126
awaitingRefresh = true
125127
hadFirstRefresh = true
126-
}.sharePauseableIn(scope, WhileSubscribed(), replay = 1) // TODO 20000
128+
}.sharePauseableIn(scope, WhileSubscribed(20000), WhileSubscribed(2000), replay = 1)
127129
val idMapFlow: Flow<Map<Long, MediaItem>> = readerFlow.map { it.idMap!! }
128130
val songListFlow: Flow<List<MediaItem>> = readerFlow.map { it.songList }
129131
private val recentlyAddedFlow = recentlyAddedFilterSecondFlow.distinctUntilChanged()
@@ -135,7 +137,7 @@ class FlowReader(
135137
)
136138
else
137139
null
138-
}
140+
}.sharePauseableIn(scope, WhileSubscribed(20000), WhileSubscribed(2000), replay = 1)
139141
private val mappedPlaylistsFlow = idMapFlow.combine(rawPlaylistFlow) { idMap, rawPlaylists ->
140142
rawPlaylists.map { it.toPlaylist(idMap) }
141143
}
@@ -146,7 +148,7 @@ class FlowReader(
146148
val dateListFlow: Flow<List<Date>> = readerFlow.map { it.dateList!! }
147149
val playlistListFlow = mappedPlaylistsFlow.combine(recentlyAddedFlow) { mappedPlaylists, recentlyAdded ->
148150
if (recentlyAdded != null) mappedPlaylists + recentlyAdded else mappedPlaylists
149-
}.sharePauseableIn(scope, WhileSubscribed(), replay = 1) // TODO 20000
151+
}
150152
val folderStructureFlow: Flow<FileNode> = readerFlow.map { it.folderStructure!! }
151153
val shallowFolderFlow: Flow<FileNode> = readerFlow.map { it.shallowFolder!! }
152154
val foldersFlow: Flow<Set<String>> = readerFlow.map { it.folders!! }

app/src/main/java/uk/akane/libphonograph/reader/Reader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ internal object Reader {
477477
while (cursor.moveToNext()) {
478478
val last = first
479479
first = cursor.getLong(column2)
480-
while (last != null && first + 1 != last) {
480+
while (last != null && first + 1 < last) {
481481
content.add(null)
482482
first++
483483
}

0 commit comments

Comments
 (0)