Show sync progress
parent
5d4fd8b07e
commit
e3bea8b35b
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
Loading…
Reference in New Issue