Show sync progress
parent
5d4fd8b07e
commit
e3bea8b35b
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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 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,35 +304,46 @@ class TimelineQuery(private val mCtx: MainActivity) {
|
||||||
// Count number of updates
|
// Count number of updates
|
||||||
var updates = 0
|
var updates = 0
|
||||||
|
|
||||||
// Iterate all images from system store
|
try {
|
||||||
for (image in SystemImage.cursor(
|
// Iterate all images from system store
|
||||||
mCtx,
|
for (image in SystemImage.cursor(
|
||||||
SystemImage.IMAGE_URI,
|
mCtx,
|
||||||
selection,
|
SystemImage.IMAGE_URI,
|
||||||
selectionArgs,
|
selection,
|
||||||
null
|
selectionArgs,
|
||||||
)) {
|
null
|
||||||
insertItemDb(image)
|
)) {
|
||||||
updates++
|
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
|
// Reset sync status
|
||||||
for (video in SystemImage.cursor(
|
synchronized(this) {
|
||||||
mCtx,
|
syncStatus = -1
|
||||||
SystemImage.VIDEO_URI,
|
|
||||||
selection,
|
|
||||||
selectionArgs,
|
|
||||||
null
|
|
||||||
)) {
|
|
||||||
insertItemDb(video)
|
|
||||||
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()
|
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
Loading…
Reference in New Issue