@@ -22,62 +22,69 @@ import android.content.ContentResolver
2222import android.content.Context
2323import android.net.Uri
2424import android.provider.DocumentsContract
25- import android.util.Log
26- import kotlinx.coroutines.CoroutineScope
27- import kotlinx.coroutines.Dispatchers
25+ import androidx.core.net.toUri
2826import kotlinx.coroutines.ExperimentalCoroutinesApi
2927import kotlinx.coroutines.coroutineScope
3028import kotlinx.coroutines.flow.Flow
3129import kotlinx.coroutines.flow.SharingStarted
30+ import kotlinx.coroutines.flow.asFlow
3231import kotlinx.coroutines.flow.emptyFlow
3332import kotlinx.coroutines.flow.flow
3433import kotlinx.coroutines.flow.map
3534import kotlinx.coroutines.flow.mapNotNull
35+ import kotlinx.coroutines.flow.merge
3636import kotlinx.coroutines.flow.shareIn
3737import kotlinx.coroutines.flow.takeWhile
3838import org.oxycblt.musikr.fs.MusicLocation
3939import org.oxycblt.musikr.fs.Path
40+ import org.oxycblt.musikr.fs.path.DocumentPathFactory
4041
4142internal interface DeviceFS {
42- suspend fun explore (locations : Flow <MusicLocation >): Flow <DeviceDirectory >
43+ fun explore (locations : Flow <MusicLocation >, fileTree : FileTree ): Flow <DeviceDirectory >
4344
4445 companion object {
4546 fun from (context : Context , withHidden : Boolean ): DeviceFS =
46- DeviceFSImpl (context.contentResolverSafe, JsonFileTreeCache (context), withHidden)
47+ DeviceFSImpl (context.contentResolverSafe, withHidden)
4748 }
4849}
4950
5051@OptIn(ExperimentalCoroutinesApi ::class )
5152private class DeviceFSImpl (
5253 private val contentResolver : ContentResolver ,
53- private val cache : FileTreeCache ,
5454 private val withHidden : Boolean
5555) : DeviceFS {
56- override fun explore (locations : Flow <MusicLocation >): Flow < DeviceDirectory > {
56+ override fun explore (locations : Flow <MusicLocation >, fileTree : FileTree ) =
5757 locations.mapNotNull { location ->
58- val treeDocumentId =
59- DocumentsContract .getTreeDocumentId(location.uri)
60- val uri = DocumentsContract .buildDocumentUriUsingTree(location.uri, treeDocumentId)
61- val modifiedMs = contentResolver.useQuery(
62- uri,
63- arrayOf(DocumentsContract .Document .COLUMN_LAST_MODIFIED )
64- ) { cursor ->
65- if (! cursor.moveToFirst()) return @useQuery null
66- val lastModifiedIndex =
67- cursor.getColumnIndexOrThrow(DocumentsContract .Document .COLUMN_LAST_MODIFIED )
68- cursor.getLong(lastModifiedIndex)
69- }
70- if (modifiedMs == null ) {
71- return @mapNotNull null
72- }
73- query(
74- location.uri,
75- treeDocumentId,
76- location.path,
77- modifiedMs,
78- null
79- )
58+ queryRoot(location, fileTree)
8059 }
60+
61+ private suspend fun queryRoot (
62+ location : MusicLocation ,
63+ fileTree : FileTree
64+ ): DeviceDirectory ? {
65+ val treeDocumentId =
66+ DocumentsContract .getTreeDocumentId(location.uri)
67+ val uri = DocumentsContract .buildDocumentUriUsingTree(location.uri, treeDocumentId)
68+ val modifiedMs = contentResolver.useQuery(
69+ uri,
70+ arrayOf(DocumentsContract .Document .COLUMN_LAST_MODIFIED )
71+ ) { cursor ->
72+ if (! cursor.moveToFirst()) return @useQuery null
73+ val lastModifiedIndex =
74+ cursor.getColumnIndexOrThrow(DocumentsContract .Document .COLUMN_LAST_MODIFIED )
75+ cursor.getLong(lastModifiedIndex)
76+ }
77+ if (modifiedMs == null ) {
78+ return null
79+ }
80+ return query(
81+ location.uri,
82+ treeDocumentId,
83+ location.path,
84+ modifiedMs,
85+ null ,
86+ fileTree
87+ )
8188 }
8289
8390 private suspend fun query (
@@ -86,9 +93,20 @@ private class DeviceFSImpl(
8693 path : Path ,
8794 modifiedMs : Long ,
8895 parent : DeviceDirectory ? ,
96+ fileTree : FileTree
8997 ): DeviceDirectory = coroutineScope {
98+ val uri = DocumentsContract .buildDocumentUriUsingTree(rootUri, treeDocumentId)
99+ val cached = fileTree.queryDirectory(uri)
100+ if (cached != null && cached.modifiedMs == modifiedMs) {
101+ return @coroutineScope hydrateCached(
102+ cached = cached,
103+ parentDir = parent,
104+ path = path,
105+ fileTree = fileTree
106+ )
107+ }
90108 val dir = DeviceDirectoryImpl (
91- uri = DocumentsContract .buildDocumentUriUsingTree(rootUri, treeDocumentId) ,
109+ uri = uri ,
92110 path = path,
93111 modifiedMs = modifiedMs,
94112 parent = parent,
@@ -111,6 +129,9 @@ private class DeviceFSImpl(
111129 val lastModifiedIndex =
112130 cursor.getColumnIndexOrThrow(DocumentsContract .Document .COLUMN_LAST_MODIFIED )
113131
132+ val childSubdirUris = mutableListOf<String >()
133+ val childFileUris = mutableListOf<String >()
134+
114135 while (cursor.moveToNext()) {
115136 val childId = cursor.getString(childUriIndex)
116137 val displayName = cursor.getString(displayNameIndex)
@@ -131,8 +152,10 @@ private class DeviceFSImpl(
131152 treeDocumentId = childId,
132153 path = newPath,
133154 modifiedMs = modifiedMs,
134- parent = dir
155+ parent = dir,
156+ fileTree = fileTree
135157 )
158+ childSubdirUris.add(subdir.uri.toString())
136159 emit(StreamedFile .More (subdir))
137160 } else {
138161 val size = cursor.getLong(sizeIndex)
@@ -146,9 +169,26 @@ private class DeviceFSImpl(
146169 modifiedMs = lastModified,
147170 parent = dir
148171 )
172+ val write = CachedFile (
173+ uri = childUri.toString(),
174+ name = displayName,
175+ modifiedMs = lastModified,
176+ mimeType = mimeType,
177+ size = size
178+ )
179+ fileTree.updateFile(childUri, write)
180+ childFileUris.add(childUri.toString())
149181 emit(StreamedFile .More (file))
150182 }
151183 }
184+ val writeDir = CachedDirectory (
185+ uri = uri.toString(),
186+ name = requireNotNull(path.name),
187+ modifiedMs = modifiedMs,
188+ subdirUris = childSubdirUris,
189+ fileUris = childFileUris
190+ )
191+ fileTree.updateDirectory(uri, writeDir)
152192 emit(StreamedFile .Done )
153193 }
154194 }.shareIn(this , SharingStarted .Eagerly , replay = Int .MAX_VALUE )
@@ -157,6 +197,42 @@ private class DeviceFSImpl(
157197 dir
158198 }
159199
200+ private fun hydrateCached (
201+ cached : CachedDirectory ,
202+ parentDir : DeviceDirectory ? ,
203+ path : Path ,
204+ fileTree : FileTree
205+ ): DeviceDirectory {
206+ val dir = DeviceDirectoryImpl (
207+ uri = cached.uri.toUri(),
208+ path = path,
209+ modifiedMs = cached.modifiedMs,
210+ parent = parentDir,
211+ children = emptyFlow()
212+ )
213+ dir.children = merge(
214+ cached.subdirUris.asFlow().map { subdirUriString ->
215+ val subdirUri = subdirUriString.toUri()
216+ val cachedSubdir = requireNotNull(fileTree.queryDirectory(subdirUri)) {
217+ " No cached subdir for $subdirUri , malformed cache! Rescan needed."
218+ }
219+ hydrateCached(cachedSubdir, dir, dir.path.file(cachedSubdir.name), fileTree)
220+ },
221+ cached.fileUris.asFlow().map {
222+ val cachedFile = requireNotNull(fileTree.queryFile(it.toUri()))
223+ DeviceFile (
224+ uri = it.toUri(),
225+ path = dir.path.file(cachedFile.name),
226+ modifiedMs = cachedFile.modifiedMs,
227+ mimeType = cachedFile.mimeType,
228+ size = cachedFile.size,
229+ parent = dir
230+ )
231+ }
232+ )
233+ return dir
234+ }
235+
160236 private sealed interface StreamedFile {
161237 data class More (val value : DeviceFSEntry ) : StreamedFile
162238 data object Done : StreamedFile
0 commit comments