diff --git a/app/src/main/java/gallery/memories/MainActivity.kt b/app/src/main/java/gallery/memories/MainActivity.kt index c8f14ffa..b17099e3 100644 --- a/app/src/main/java/gallery/memories/MainActivity.kt +++ b/app/src/main/java/gallery/memories/MainActivity.kt @@ -15,7 +15,6 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -63,8 +62,8 @@ class MainActivity : AppCompatActivity() { // Initialize services nativex = NativeX(this) - // Ensure storage permissions - ensureStoragePermissions() + // Sync if permission is available + nativex.doMediaSync(false) // Load JavaScript initializeWebView() @@ -198,49 +197,6 @@ class MainActivity : AppCompatActivity() { return false } - fun ensureStoragePermissions() { - val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - - // we need all of these - val isGranted = permissions.all { it.value } - - // start synchronization if granted - if (isGranted) { - val needFullSync = !hasMediaPermission() - - // Run DB operations in separate thread - Thread { - // 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() - } else { - Log.w(TAG, "Storage permission not available") - } - - // Persist that we have it now - setHasMediaPermission(isGranted) - } - - // Request media read permission - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - requestPermissionLauncher.launch( - arrayOf( - android.Manifest.permission.READ_MEDIA_IMAGES, - android.Manifest.permission.READ_MEDIA_VIDEO, - ) - ) - } else { - requestPermissionLauncher.launch(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE)) - } - } - fun initializePlayer(uris: Array, uid: String) { if (player != null) { if (playerUid.equals(uid)) return @@ -374,17 +330,6 @@ class MainActivity : AppCompatActivity() { } } - fun hasMediaPermission(): Boolean { - return getSharedPreferences(getString(R.string.preferences_key), 0) - .getBoolean(getString(R.string.preferences_has_media_permission), false) - } - - private fun setHasMediaPermission(v: Boolean) { - getSharedPreferences(getString(R.string.preferences_key), 0).edit() - .putBoolean(getString(R.string.preferences_has_media_permission), v) - .apply() - } - fun refreshTimeline(force: Boolean = false) { runOnUiThread { // Check webview is loaded diff --git a/app/src/main/java/gallery/memories/NativeX.kt b/app/src/main/java/gallery/memories/NativeX.kt index 743ec29d..e318fdd6 100644 --- a/app/src/main/java/gallery/memories/NativeX.kt +++ b/app/src/main/java/gallery/memories/NativeX.kt @@ -12,6 +12,7 @@ import gallery.memories.service.AccountService import gallery.memories.service.DownloadService import gallery.memories.service.HttpService import gallery.memories.service.ImageService +import gallery.memories.service.PermissionsService import gallery.memories.service.TimelineQuery import org.json.JSONArray import java.io.ByteArrayInputStream @@ -26,6 +27,7 @@ class NativeX(private val mCtx: MainActivity) { val image = ImageService(mCtx, query) val http = HttpService() val account = AccountService(mCtx, http) + val permissions = PermissionsService(mCtx).register() init { dlService = DownloadService(mCtx, query) @@ -53,6 +55,8 @@ class NativeX(private val mCtx: MainActivity) { val SHARE_URL = Regex("^/api/share/url/.+$") val SHARE_BLOB = Regex("^/api/share/blob/.+$") val SHARE_LOCAL = Regex("^/api/share/local/\\d+$") + + val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$") } @JavascriptInterface @@ -157,6 +161,16 @@ class NativeX(private val mCtx: MainActivity) { return query.localFolders.toString() } + @JavascriptInterface + fun configHasMediaPermission(): Boolean { + return permissions.hasAllowMedia() && permissions.hasMediaPermission() + } + + @JavascriptInterface + fun getSyncStatus(): Int { + return query.syncStatus + } + fun handleRequest(request: WebResourceRequest): WebResourceResponse { val path = request.url.path ?: return makeErrorResponse() @@ -225,6 +239,12 @@ class NativeX(private val mCtx: MainActivity) { makeResponse(dlService!!.shareBlobFromUrl(URLDecoder.decode(parts[4], "UTF-8"))) } else if (path.matches(API.SHARE_LOCAL)) { makeResponse(dlService!!.shareLocal(parts[4].toLong())) + } else if (path.matches(API.CONFIG_ALLOW_MEDIA)) { + permissions.setAllowMedia(true) + if (permissions.requestMediaPermissionSync()) { + doMediaSync(true) // separate thread + } + makeResponse("done") } else { throw Exception("Path did not match any known API route: $path") } @@ -253,4 +273,22 @@ class NativeX(private val mCtx: MainActivity) { private fun parseIds(ids: String): List { return ids.trim().split(",").map { it.toLong() } } + + fun doMediaSync(forceFull: Boolean) { + if (permissions.hasAllowMedia()) { + // Full sync if this is the first time permission was granted + val fullSync = forceFull || !permissions.hasMediaPermission() + + Thread { + // Block for media permission + if (!permissions.requestMediaPermissionSync()) return@Thread + + // Full sync requested + if (fullSync) query.syncFullDb() + + // Run delta sync and register hooks + query.initialize() + }.start() + } + } } \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/HttpService.kt b/app/src/main/java/gallery/memories/service/HttpService.kt index 4ef26b0b..2fb12594 100644 --- a/app/src/main/java/gallery/memories/service/HttpService.kt +++ b/app/src/main/java/gallery/memories/service/HttpService.kt @@ -63,6 +63,9 @@ class HttpService { // Get host name val host = Uri.parse(url).host + // Clear webview history + webView.clearHistory() + // Set authorization header webView.loadUrl(url!!, mapOf("Authorization" to authHeader)) diff --git a/app/src/main/java/gallery/memories/service/PermissionsService.kt b/app/src/main/java/gallery/memories/service/PermissionsService.kt new file mode 100644 index 00000000..94908a6b --- /dev/null +++ b/app/src/main/java/gallery/memories/service/PermissionsService.kt @@ -0,0 +1,80 @@ +package gallery.memories.service + +import android.os.Build +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.media3.common.util.UnstableApi +import gallery.memories.MainActivity +import gallery.memories.R +import java.util.concurrent.CountDownLatch + +@UnstableApi class PermissionsService(private val activity: MainActivity) { + var isGranted: Boolean = false + var latch: CountDownLatch? = null + lateinit var requestPermissionLauncher: ActivityResultLauncher> + + fun register(): PermissionsService { + requestPermissionLauncher = activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + // we need all of these + isGranted = permissions.all { it.value } + + // Persist that we have it now + setHasMediaPermission(isGranted) + + // Release latch + latch?.countDown() + } + + return this + } + + /** + * Requests media permission and blocks until it is granted + */ + fun requestMediaPermissionSync(): Boolean { + if (isGranted) return true + + // Wait for response + latch = CountDownLatch(1) + + // Request media read permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch( + arrayOf( + android.Manifest.permission.READ_MEDIA_IMAGES, + android.Manifest.permission.READ_MEDIA_VIDEO, + ) + ) + } else { + requestPermissionLauncher.launch(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE)) + } + + latch?.await() + + return isGranted + } + + fun hasMediaPermission(): Boolean { + return activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0) + .getBoolean(activity.getString(R.string.preferences_has_media_permission), false) + } + + private fun setHasMediaPermission(v: Boolean) { + activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0).edit() + .putBoolean(activity.getString(R.string.preferences_has_media_permission), v) + .apply() + } + + fun hasAllowMedia(): Boolean { + return activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0) + .getBoolean(activity.getString(R.string.preferences_allow_media), false) + } + + fun setAllowMedia(v: Boolean) { + activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0).edit() + .putBoolean(activity.getString(R.string.preferences_allow_media), v) + .apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/TimelineQuery.kt b/app/src/main/java/gallery/memories/service/TimelineQuery.kt index 0752b97c..00220cd7 100644 --- a/app/src/main/java/gallery/memories/service/TimelineQuery.kt +++ b/app/src/main/java/gallery/memories/service/TimelineQuery.kt @@ -45,6 +45,11 @@ class TimelineQuery(private val mCtx: MainActivity) { var videoObserver: ContentObserver? = null var refreshPending: Boolean = false + // Status of synchronization process + // -1 = not started + // >0 = number of files updated + var syncStatus = -1 + init { // Register intent launcher for callback deleteIntentLauncher = @@ -299,35 +304,46 @@ class TimelineQuery(private val mCtx: MainActivity) { // Count number of updates var updates = 0 - // Iterate all images from system store - for (image in SystemImage.cursor( - mCtx, - SystemImage.IMAGE_URI, - selection, - selectionArgs, - null - )) { - insertItemDb(image) - updates++ + try { + // Iterate all images from system store + for (image in SystemImage.cursor( + mCtx, + SystemImage.IMAGE_URI, + selection, + selectionArgs, + null + )) { + insertItemDb(image) + updates++ + syncStatus = updates + } + + // Iterate all videos from system store + for (video in SystemImage.cursor( + mCtx, + SystemImage.VIDEO_URI, + selection, + selectionArgs, + null + )) { + insertItemDb(video) + updates++ + syncStatus = updates + } + + // Store last sync time + mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit() + .putLong(mCtx.getString(R.string.preferences_last_sync_time), syncTime) + .apply() + } catch (e: Exception) { + Log.e(TAG, "Error syncing database", e) } - // Iterate all videos from system store - for (video in SystemImage.cursor( - mCtx, - SystemImage.VIDEO_URI, - selection, - selectionArgs, - null - )) { - insertItemDb(video) - updates++ + // Reset sync status + synchronized(this) { + syncStatus = -1 } - // Store last sync time - mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit() - .putLong(mCtx.getString(R.string.preferences_last_sync_time), syncTime) - .apply() - // Number of updated files return updates } @@ -337,6 +353,12 @@ class TimelineQuery(private val mCtx: MainActivity) { * @return Number of updated files */ fun syncDeltaDb(): Int { + // Exit if already running + synchronized(this) { + if (syncStatus != -1) return 0 + syncStatus = 0 + } + // Get last sync time val syncTime = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0) .getLong(mCtx.getString(R.string.preferences_last_sync_time), 0L) @@ -349,6 +371,12 @@ class TimelineQuery(private val mCtx: MainActivity) { * @return Number of updated files */ fun syncFullDb() { + // Exit if already running + synchronized(this) { + if (syncStatus != -1) return + syncStatus = 0 + } + // Flag all images for removal mPhotoDao.flagAll() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c56bd78f..54ae511b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ themeDark lastDbSyncTime hasMediaPermission + allowMedia enabledLocalFolders