Watch local changes

pull/653/merge
Varun Patil 2023-05-22 19:16:04 -07:00
parent 76f3a270c3
commit 79aecab377
3 changed files with 110 additions and 18 deletions

View File

@ -12,6 +12,7 @@ import android.view.WindowInsetsController
import android.webkit.* import android.webkit.*
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util import androidx.media3.common.util.Util
@ -35,6 +36,8 @@ import gallery.memories.databinding.ActivityMainBinding
private var mediaItemIndex = 0 private var mediaItemIndex = 0
private var playbackPosition = 0L private var playbackPosition = 0L
private var mNeedRefresh = false
private val memoriesRegex = Regex("/apps/memories/.*$") private val memoriesRegex = Regex("/apps/memories/.*$")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -72,6 +75,9 @@ import gallery.memories.databinding.ActivityMainBinding
if (playerUri != null && (Util.SDK_INT <= 23 || player == null)) { if (playerUri != null && (Util.SDK_INT <= 23 || player == null)) {
initializePlayer(playerUri!!, playerUid!!) initializePlayer(playerUri!!, playerUid!!)
} }
if (mNeedRefresh) {
refreshTimeline(true)
}
} }
public override fun onPause() { public override fun onPause() {
@ -181,15 +187,28 @@ import gallery.memories.databinding.ActivityMainBinding
} }
fun ensureStoragePermissions() { fun ensureStoragePermissions() {
val requestPermissionLauncher = val requestPermissionLauncher = registerForActivityResult(
registerForActivityResult( ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
ActivityResultContracts.RequestPermission() if (isGranted) {
) { isGranted: Boolean -> val needFullSync = !hasMediaPermission()
if (isGranted && !hasMediaPermission()) {
nativex.query.syncFullDb() // Run DB operations in separate thread
} Thread {
setHasMediaPermission(isGranted) // Full sync if this is the first time permission was granted
if (needFullSync) {
nativex.query.syncFullDb()
}
// Run delta sync and register hooks
nativex.query.initialize()
}.start()
} }
// Persist that we have it now
setHasMediaPermission(isGranted)
}
// Request media read permission
requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE) requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE)
} }
@ -301,4 +320,19 @@ import gallery.memories.databinding.ActivityMainBinding
.putBoolean(getString(R.string.preferences_has_media_permission), v) .putBoolean(getString(R.string.preferences_has_media_permission), v)
.apply() .apply()
} }
fun refreshTimeline(force: Boolean = false) {
runOnUiThread {
// Check webview is loaded
if (binding?.webview?.url == null) return@runOnUiThread
// Schedule for resume if not active
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) || force) {
mNeedRefresh = false
binding.webview.evaluateJavascript("window._nc_event_bus?.emit('files:file:created')", null)
} else {
mNeedRefresh = true
}
}
}
} }

View File

@ -42,11 +42,6 @@ import java.net.URLDecoder
init { init {
dlService = DownloadService(mCtx) dlService = DownloadService(mCtx)
// Synchronize the database if possible
if (mCtx.hasMediaPermission()) {
query.syncDeltaDb()
}
} }
companion object { companion object {
@ -55,6 +50,7 @@ import java.net.URLDecoder
fun destroy() { fun destroy() {
dlService = null dlService = null
query.destroy()
} }
fun handleRequest(request: WebResourceRequest): WebResourceResponse { fun handleRequest(request: WebResourceRequest): WebResourceResponse {

View File

@ -2,9 +2,11 @@ package gallery.memories.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.database.ContentObserver
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.icu.text.SimpleDateFormat import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
@ -13,9 +15,10 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import androidx.media3.common.util.UnstableApi
import gallery.memories.MainActivity
import gallery.memories.R import gallery.memories.R
import gallery.memories.mapper.Fields import gallery.memories.mapper.Fields
import gallery.memories.mapper.SystemImage import gallery.memories.mapper.SystemImage
@ -26,7 +29,7 @@ import java.io.IOException
import java.time.Instant import java.time.Instant
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
class TimelineQuery(private val mCtx: AppCompatActivity) { @UnstableApi class TimelineQuery(private val mCtx: MainActivity) {
private val mDb: SQLiteDatabase = DbService(mCtx).writableDatabase private val mDb: SQLiteDatabase = DbService(mCtx).writableDatabase
private val TAG = "TimelineQuery" private val TAG = "TimelineQuery"
@ -38,6 +41,11 @@ class TimelineQuery(private val mCtx: AppCompatActivity) {
// Caches // Caches
var mEnabledBuckets: Set<String>? = null var mEnabledBuckets: Set<String>? = null
// Observers
var imageObserver: ContentObserver? = null
var videoObserver: ContentObserver? = null
var refreshPending: Boolean = false
init { init {
// Register intent launcher for callback // Register intent launcher for callback
deleteIntentLauncher = mCtx.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult? -> deleteIntentLauncher = mCtx.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult? ->
@ -47,6 +55,57 @@ class TimelineQuery(private val mCtx: AppCompatActivity) {
} }
} }
fun initialize() {
if (syncDeltaDb() > 0) {
mCtx.refreshTimeline()
}
registerHooks()
}
fun destroy() {
if (imageObserver != null) {
mCtx.contentResolver.unregisterContentObserver(imageObserver!!)
}
if (videoObserver != null) {
mCtx.contentResolver.unregisterContentObserver(videoObserver!!)
}
}
fun registerHooks() {
imageObserver = registerContentObserver(SystemImage.IMAGE_URI)
videoObserver = registerContentObserver(SystemImage.VIDEO_URI)
}
private fun registerContentObserver(uri: Uri): ContentObserver {
val observer = @UnstableApi object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
// Debounce refreshes
synchronized(this@TimelineQuery) {
if (refreshPending) return
refreshPending = true
}
// Refresh after 750ms
Thread {
Thread.sleep(750)
synchronized(this@TimelineQuery) {
refreshPending = false
}
// Check if anything to update
if (syncDeltaDb() == 0 || mCtx.isDestroyed || mCtx.isFinishing) return@Thread
mCtx.refreshTimeline()
}.start()
}
}
mCtx.contentResolver.registerContentObserver(uri, true, observer)
return observer
}
@Throws(JSONException::class) @Throws(JSONException::class)
fun getByDayId(dayId: Long): JSONArray { fun getByDayId(dayId: Long): JSONArray {
// Filter for enabled buckets // Filter for enabled buckets
@ -230,7 +289,7 @@ class TimelineQuery(private val mCtx: AppCompatActivity) {
} }
} }
private fun syncDb(startTime: Long) { private fun syncDb(startTime: Long): Int {
// Date modified is in seconds, not millis // Date modified is in seconds, not millis
val syncTime = Instant.now().toEpochMilli() / 1000; val syncTime = Instant.now().toEpochMilli() / 1000;
@ -254,13 +313,16 @@ class TimelineQuery(private val mCtx: AppCompatActivity) {
mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit() mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit()
.putLong(mCtx.getString(R.string.preferences_last_sync_time), syncTime) .putLong(mCtx.getString(R.string.preferences_last_sync_time), syncTime)
.apply() .apply()
// Number of updated files
return files.size
} }
fun syncDeltaDb() { fun syncDeltaDb(): Int {
// Get last sync time // Get last sync time
val syncTime = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0) val syncTime = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0)
.getLong(mCtx.getString(R.string.preferences_last_sync_time), 0L) .getLong(mCtx.getString(R.string.preferences_last_sync_time), 0L)
syncDb(syncTime) return syncDb(syncTime)
} }
fun syncFullDb() { fun syncFullDb() {