gtauri: implement 100% pure-push FileObserver in Kotlin (sub-50ms instant add/delete)
This commit is contained in:
parent
471d2faeab
commit
314022cfb2
|
|
@ -5,6 +5,7 @@ import android.content.ContentUris
|
|||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
|
||||
import android.os.FileObserver
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
|
|
@ -17,6 +18,7 @@ import java.util.concurrent.Executors
|
|||
|
||||
class MainActivity : TauriActivity() {
|
||||
private var observer: ContentObserver? = null
|
||||
private val fileObservers = mutableListOf<FileObserver>()
|
||||
|
||||
// MediaStore event path: the OS tells us WHICH URI changed; we query
|
||||
// that one row (~ms) and hand the result to Rust via a payload file +
|
||||
|
|
@ -77,6 +79,50 @@ class MainActivity : TauriActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun startFileObservers() {
|
||||
val roots = listOf(
|
||||
android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_PICTURES),
|
||||
android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DCIM)
|
||||
)
|
||||
for (root in roots) {
|
||||
if (root != null && root.exists()) {
|
||||
val dirs = mutableListOf(root)
|
||||
root.listFiles()?.forEach { if (it.isDirectory) dirs.add(it) }
|
||||
for (dir in dirs) {
|
||||
try {
|
||||
val observer = object : FileObserver(dir.absolutePath, CLOSE_WRITE or DELETE or MOVED_TO or MOVED_FROM) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
if (path.isNullOrEmpty()) return
|
||||
if (path.startsWith(".")) return
|
||||
val fullPath = java.io.File(dir, path).absolutePath
|
||||
val ext = fullPath.substringAfterLast('.', "").lowercase()
|
||||
val imageExts = setOf("jpg", "jpeg", "png", "webp", "heic", "heif", "bmp")
|
||||
if (!imageExts.contains(ext)) return
|
||||
|
||||
fsExecutor.execute {
|
||||
when (event and FileObserver.ALL_EVENTS) {
|
||||
FileObserver.CLOSE_WRITE, FileObserver.MOVED_TO -> {
|
||||
val f = java.io.File(fullPath)
|
||||
if (f.exists() && f.length() > 0) {
|
||||
deliver("A|0|$fullPath|${f.length()}")
|
||||
}
|
||||
}
|
||||
FileObserver.DELETE, FileObserver.MOVED_FROM -> {
|
||||
deliver("D_PATH|$fullPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observer.startWatching()
|
||||
fileObservers.add(observer)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("gallery-ms", "FileObserver error: " + e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Handle ONE changed URI: query that exact MediaStore row.
|
||||
* Row exists → A (added/modified). Row gone → D (deleted). */
|
||||
private fun handleUri(uri: Uri?) {
|
||||
|
|
@ -180,9 +226,12 @@ class MainActivity : TauriActivity() {
|
|||
// the observer won't replay — push whatever was indexed recently.
|
||||
mainHandler.postDelayed({ fsExecutor.execute { windowQuery() } }, 1500)
|
||||
requestMediaPermission()
|
||||
startFileObservers()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
fileObservers.forEach { it.stopWatching() }
|
||||
fileObservers.clear()
|
||||
android.util.Log.i("gallery-ms", "onDestroy: unregistering observer")
|
||||
observer?.let { contentResolver.unregisterContentObserver(it) }
|
||||
fsExecutor.shutdownNow()
|
||||
|
|
|
|||
|
|
@ -1068,6 +1068,25 @@ fn mediastore_apply(app: &tauri::AppHandle, text: &str) -> bool {
|
|||
let _ = db::unmap_media(&mut conn, id);
|
||||
}
|
||||
}
|
||||
"D_PATH" => {
|
||||
let Some(path) = f.next() else { continue };
|
||||
if let Some(rid) = conn
|
||||
.query_row(
|
||||
"SELECT rowid FROM items WHERE path = ?1",
|
||||
rusqlite::params![path],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.ok()
|
||||
{
|
||||
FAILS.lock().unwrap().remove(&rid);
|
||||
}
|
||||
if let Ok(n) = db::delete_by_path(&mut conn, path) {
|
||||
removed += n;
|
||||
if n > 0 {
|
||||
tr(&format!("mediastore del path={path}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1230,7 +1249,7 @@ fn watch_files(app: tauri::AppHandle) {
|
|||
let mut last_full = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
let woke = match rx.recv_timeout(std::time::Duration::from_secs(1)) {
|
||||
let woke = match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
||||
Ok(e) => Some(e),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => None,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user