refactor(android): move SAF batch exists and modtime queries out of MainActivitySafScan.kt

safExistsBatch and getSafFileModTimes answer per-URI document queries
for library and history checks and do not take part in the scan, so
they move verbatim into SafBatchQueries.kt. The file header of
MainActivitySafScan.kt now describes what is left there.
This commit is contained in:
zarzet committed 2026-10-19 05:16:33 +07:00
1 parent 671b5a2502
commit eeda955363
2 files changed
+82 -72

No files matched your search

@@ -1,7 +1,5 @@
package com.zarz.spotiflac
import android.net.Uri
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import org.json.JSONArray
import org.json.JSONObject
@@ -11,8 +9,10 @@ import java.io.IOException
import java.security.MessageDigest
import java.util.Locale
// SAF library-scan subsystem: tree walking, incremental diff, CUE resolution,
// and scan-progress state shared with the progress stream in MainActivity.
// SAF library scans: the full and incremental tree scans, the pause and
// cancel check, and scan-progress state shared with the progress stream in
// MainActivity. Listing, checkpoint, CUE, history inspection and batch query
// helpers live in the Saf*.kt files beside this one.
internal fun MainActivity.buildStableLibraryId(filePath: String): String {
val digest = MessageDigest.getInstance("SHA-1")
@@ -928,71 +928,3 @@ internal fun MainActivity.scanSafTreeIncremental(
spill.raw(",\"skippedCount\":$skippedCount,\"totalFiles\":$totalFiles}")
return spill.result()
}
// A failed DocumentsProvider query must never be mistaken for a missing file.
// Unlike DocumentFile.exists(), this keeps null cursors/exceptions inconclusive.
internal fun MainActivity.safExistsBatch(urisJson: String): String {
val result = JSONObject()
val uris = JSONArray(urisJson)
val accessibleTrees = mutableMapOf<String, Boolean>()
val permissions = contentResolver.persistedUriPermissions
val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
for (index in 0 until uris.length()) {
val path = uris.optString(index)
if (path.isBlank()) continue
var status = "unknown"
try {
val uri = Uri.parse(path)
val treeAccessible = if (DocumentsContract.isTreeUri(uri)) {
val treeId = DocumentsContract.getTreeDocumentId(uri)
val tree = DocumentsContract.buildTreeDocumentUri(uri.authority, treeId)
accessibleTrees.getOrPut(tree.toString()) {
val granted = permissions.any { it.uri == tree && it.isReadPermission }
if (!granted) false else {
val root = DocumentsContract.buildDocumentUriUsingTree(tree, treeId)
contentResolver.query(root, projection, null, null, null)?.use {
it.moveToFirst()
} == true
}
}
} else false
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
status = if (cursor.moveToFirst()) "found"
else if (treeAccessible &&
!cursor.extras.getBoolean(DocumentsContract.EXTRA_LOADING, false))
"missing" else "unknown"
}
} catch (_: Exception) {
// Revoked permission, offline storage and provider errors: retain row.
}
result.put(path, status)
}
return result.toString()
}
/**
* Resolve SAF file last-modified values for a list of content URIs.
* Returns JSON object mapping uri -> lastModified (unix millis).
*/
internal fun MainActivity.getSafFileModTimes(urisJson: String): String {
val result = JSONObject()
val uris = try {
JSONArray(urisJson)
} catch (_: Exception) {
JSONArray()
}
for (i in 0 until uris.length()) {
val uriStr = uris.optString(i, "")
if (uriStr.isBlank()) continue
try {
val uri = Uri.parse(uriStr)
val doc = DocumentFile.fromSingleUri(this, uri)
if (doc != null && doc.exists()) {
result.put(uriStr, doc.lastModified())
}
} catch (_: Exception) {}
}
return result.toString()
}
@@ -0,0 +1,78 @@
package com.zarz.spotiflac
import android.net.Uri
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import org.json.JSONArray
import org.json.JSONObject
// Batch SAF document queries for library and history checks: existence
// status per URI and last-modified values per URI.
// A failed DocumentsProvider query must never be mistaken for a missing file.
// Unlike DocumentFile.exists(), this keeps null cursors/exceptions inconclusive.
internal fun MainActivity.safExistsBatch(urisJson: String): String {
val result = JSONObject()
val uris = JSONArray(urisJson)
val accessibleTrees = mutableMapOf<String, Boolean>()
val permissions = contentResolver.persistedUriPermissions
val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
for (index in 0 until uris.length()) {
val path = uris.optString(index)
if (path.isBlank()) continue
var status = "unknown"
try {
val uri = Uri.parse(path)
val treeAccessible = if (DocumentsContract.isTreeUri(uri)) {
val treeId = DocumentsContract.getTreeDocumentId(uri)
val tree = DocumentsContract.buildTreeDocumentUri(uri.authority, treeId)
accessibleTrees.getOrPut(tree.toString()) {
val granted = permissions.any { it.uri == tree && it.isReadPermission }
if (!granted) false else {
val root = DocumentsContract.buildDocumentUriUsingTree(tree, treeId)
contentResolver.query(root, projection, null, null, null)?.use {
it.moveToFirst()
} == true
}
}
} else false
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
status = if (cursor.moveToFirst()) "found"
else if (treeAccessible &&
!cursor.extras.getBoolean(DocumentsContract.EXTRA_LOADING, false))
"missing" else "unknown"
}
} catch (_: Exception) {
// Revoked permission, offline storage and provider errors: retain row.
}
result.put(path, status)
}
return result.toString()
}
/**
* Resolve SAF file last-modified values for a list of content URIs.
* Returns JSON object mapping uri -> lastModified (unix millis).
*/
internal fun MainActivity.getSafFileModTimes(urisJson: String): String {
val result = JSONObject()
val uris = try {
JSONArray(urisJson)
} catch (_: Exception) {
JSONArray()
}
for (i in 0 until uris.length()) {
val uriStr = uris.optString(i, "")
if (uriStr.isBlank()) continue
try {
val uri = Uri.parse(uriStr)
val doc = DocumentFile.fromSingleUri(this, uri)
if (doc != null && doc.exists()) {
result.put(uriStr, doc.lastModified())
}
} catch (_: Exception) {}
}
return result.toString()
}