Show sync progress

pull/653/merge
Varun Patil 2023-10-02 13:33:13 -07:00
parent 5d4fd8b07e
commit e3bea8b35b
6 changed files with 177 additions and 82 deletions

View File

@ -15,7 +15,6 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -63,8 +62,8 @@ class MainActivity : AppCompatActivity() {
// Initialize services // Initialize services
nativex = NativeX(this) nativex = NativeX(this)
// Ensure storage permissions // Sync if permission is available
ensureStoragePermissions() nativex.doMediaSync(false)
// Load JavaScript // Load JavaScript
initializeWebView() initializeWebView()
@ -198,49 +197,6 @@ class MainActivity : AppCompatActivity() {
return false 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<Uri>, uid: String) { fun initializePlayer(uris: Array<Uri>, uid: String) {
if (player != null) { if (player != null) {
if (playerUid.equals(uid)) return 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) { fun refreshTimeline(force: Boolean = false) {
runOnUiThread { runOnUiThread {
// Check webview is loaded // Check webview is loaded

View File

@ -12,6 +12,7 @@ import gallery.memories.service.AccountService
import gallery.memories.service.DownloadService import gallery.memories.service.DownloadService
import gallery.memories.service.HttpService import gallery.memories.service.HttpService
import gallery.memories.service.ImageService import gallery.memories.service.ImageService
import gallery.memories.service.PermissionsService
import gallery.memories.service.TimelineQuery import gallery.memories.service.TimelineQuery
import org.json.JSONArray import org.json.JSONArray
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -26,6 +27,7 @@ class NativeX(private val mCtx: MainActivity) {
val image = ImageService(mCtx, query) val image = ImageService(mCtx, query)
val http = HttpService() val http = HttpService()
val account = AccountService(mCtx, http) val account = AccountService(mCtx, http)
val permissions = PermissionsService(mCtx).register()
init { init {
dlService = DownloadService(mCtx, query) dlService = DownloadService(mCtx, query)
@ -53,6 +55,8 @@ class NativeX(private val mCtx: MainActivity) {
val SHARE_URL = Regex("^/api/share/url/.+$") val SHARE_URL = Regex("^/api/share/url/.+$")
val SHARE_BLOB = Regex("^/api/share/blob/.+$") val SHARE_BLOB = Regex("^/api/share/blob/.+$")
val SHARE_LOCAL = Regex("^/api/share/local/\\d+$") val SHARE_LOCAL = Regex("^/api/share/local/\\d+$")
val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$")
} }
@JavascriptInterface @JavascriptInterface
@ -157,6 +161,16 @@ class NativeX(private val mCtx: MainActivity) {
return query.localFolders.toString() 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 { fun handleRequest(request: WebResourceRequest): WebResourceResponse {
val path = request.url.path ?: return makeErrorResponse() 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"))) makeResponse(dlService!!.shareBlobFromUrl(URLDecoder.decode(parts[4], "UTF-8")))
} else if (path.matches(API.SHARE_LOCAL)) { } else if (path.matches(API.SHARE_LOCAL)) {
makeResponse(dlService!!.shareLocal(parts[4].toLong())) 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 { } else {
throw Exception("Path did not match any known API route: $path") 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<Long> { private fun parseIds(ids: String): List<Long> {
return ids.trim().split(",").map { it.toLong() } 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()
}
}
} }

View File

@ -63,6 +63,9 @@ class HttpService {
// Get host name // Get host name
val host = Uri.parse(url).host val host = Uri.parse(url).host
// Clear webview history
webView.clearHistory()
// Set authorization header // Set authorization header
webView.loadUrl(url!!, mapOf("Authorization" to authHeader)) webView.loadUrl(url!!, mapOf("Authorization" to authHeader))

View File

@ -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<Array<String>>
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()
}
}

View File

@ -45,6 +45,11 @@ class TimelineQuery(private val mCtx: MainActivity) {
var videoObserver: ContentObserver? = null var videoObserver: ContentObserver? = null
var refreshPending: Boolean = false var refreshPending: Boolean = false
// Status of synchronization process
// -1 = not started
// >0 = number of files updated
var syncStatus = -1
init { init {
// Register intent launcher for callback // Register intent launcher for callback
deleteIntentLauncher = deleteIntentLauncher =
@ -299,6 +304,7 @@ class TimelineQuery(private val mCtx: MainActivity) {
// Count number of updates // Count number of updates
var updates = 0 var updates = 0
try {
// Iterate all images from system store // Iterate all images from system store
for (image in SystemImage.cursor( for (image in SystemImage.cursor(
mCtx, mCtx,
@ -309,6 +315,7 @@ class TimelineQuery(private val mCtx: MainActivity) {
)) { )) {
insertItemDb(image) insertItemDb(image)
updates++ updates++
syncStatus = updates
} }
// Iterate all videos from system store // Iterate all videos from system store
@ -321,12 +328,21 @@ class TimelineQuery(private val mCtx: MainActivity) {
)) { )) {
insertItemDb(video) insertItemDb(video)
updates++ updates++
syncStatus = updates
} }
// Store last sync time // Store last sync time
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()
} catch (e: Exception) {
Log.e(TAG, "Error syncing database", e)
}
// Reset sync status
synchronized(this) {
syncStatus = -1
}
// Number of updated files // Number of updated files
return updates return updates
@ -337,6 +353,12 @@ class TimelineQuery(private val mCtx: MainActivity) {
* @return Number of updated files * @return Number of updated files
*/ */
fun syncDeltaDb(): Int { fun syncDeltaDb(): Int {
// Exit if already running
synchronized(this) {
if (syncStatus != -1) return 0
syncStatus = 0
}
// 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)
@ -349,6 +371,12 @@ class TimelineQuery(private val mCtx: MainActivity) {
* @return Number of updated files * @return Number of updated files
*/ */
fun syncFullDb() { fun syncFullDb() {
// Exit if already running
synchronized(this) {
if (syncStatus != -1) return
syncStatus = 0
}
// Flag all images for removal // Flag all images for removal
mPhotoDao.flagAll() mPhotoDao.flagAll()

View File

@ -7,6 +7,7 @@
<string name="preferences_theme_dark">themeDark</string> <string name="preferences_theme_dark">themeDark</string>
<string name="preferences_last_sync_time">lastDbSyncTime</string> <string name="preferences_last_sync_time">lastDbSyncTime</string>
<string name="preferences_has_media_permission">hasMediaPermission</string> <string name="preferences_has_media_permission">hasMediaPermission</string>
<string name="preferences_allow_media">allowMedia</string>
<string name="preferences_enabled_local_folders">enabledLocalFolders</string> <string name="preferences_enabled_local_folders">enabledLocalFolders</string>
<!-- https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome --> <!-- https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome -->