Watch local changes
parent
76f3a270c3
commit
79aecab377
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue