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.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<Uri>, 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

View File

@ -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<Long> {
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
val host = Uri.parse(url).host
// Clear webview history
webView.clearHistory()
// Set authorization header
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 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()

View File

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