From 2f065e6d127549b326bd86403d3aa5ed2e8eae90 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 1 Oct 2023 19:37:35 -0700 Subject: [PATCH] Refactor --- app/src/main/java/gallery/memories/NativeX.kt | 90 +++++++++---------- .../memories/service/AccountService.kt | 54 +++++++++-- .../gallery/memories/service/ConfigService.kt | 4 + .../service/DownloadBroadcastReceiver.kt | 6 +- .../memories/service/DownloadService.kt | 33 ++++++- .../gallery/memories/service/ImageService.kt | 13 ++- .../gallery/memories/service/TimelineQuery.kt | 62 ++++++++++--- 7 files changed, 197 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/gallery/memories/NativeX.kt b/app/src/main/java/gallery/memories/NativeX.kt index 5a8430c8..ae9df528 100644 --- a/app/src/main/java/gallery/memories/NativeX.kt +++ b/app/src/main/java/gallery/memories/NativeX.kt @@ -24,21 +24,6 @@ import java.net.URLDecoder val image = ImageService(mCtx, query) val account = AccountService(mCtx) - object API { - val DAYS = Regex("^/api/days$") - val DAY = Regex("^/api/days/\\d+$") - - val IMAGE_INFO = Regex("^/api/image/info/\\d+$") - val IMAGE_DELETE = Regex("^/api/image/delete/\\d+(,\\d+)*$") - - val IMAGE_PREVIEW = Regex("^/image/preview/\\d+$") - val IMAGE_FULL = Regex("^/image/full/\\d+$") - - val SHARE_URL = Regex("^/api/share/url/.+$") - val SHARE_BLOB = Regex("^/api/share/blob/.+$") - val SHARE_LOCAL = Regex("^/api/share/local/\\d+$") - } - init { dlService = DownloadService(mCtx, query) } @@ -52,38 +37,19 @@ import java.net.URLDecoder query.destroy() } - fun handleRequest(request: WebResourceRequest): WebResourceResponse { - val path = request.url.path ?: return makeErrorResponse() + object API { + val DAYS = Regex("^/api/days$") + val DAY = Regex("^/api/days/\\d+$") - val response = try { - when (request.method) { - "GET" -> { - routerGet(request) - } - "OPTIONS" -> { - WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream("".toByteArray())) - } - else -> { - throw Exception("Method Not Allowed") - } - } - } catch (e: Exception) { - Log.w(TAG, "handleRequest: ", e) - makeErrorResponse() - } + val IMAGE_INFO = Regex("^/api/image/info/\\d+$") + val IMAGE_DELETE = Regex("^/api/image/delete/\\d+(,\\d+)*$") - // Allow CORS from all origins - response.responseHeaders = mutableMapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*" - ) + val IMAGE_PREVIEW = Regex("^/image/preview/\\d+$") + val IMAGE_FULL = Regex("^/image/full/\\d+$") - // Cache image responses for 7 days - if (path.matches(API.IMAGE_PREVIEW) || path.matches(API.IMAGE_FULL)) { - response.responseHeaders["Cache-Control"] = "max-age=604800" - } - - return response + val SHARE_URL = Regex("^/api/share/url/.+$") + val SHARE_BLOB = Regex("^/api/share/blob/.+$") + val SHARE_LOCAL = Regex("^/api/share/local/\\d+$") } @JavascriptInterface @@ -188,6 +154,40 @@ import java.net.URLDecoder return query.localFolders.toString() } + fun handleRequest(request: WebResourceRequest): WebResourceResponse { + val path = request.url.path ?: return makeErrorResponse() + + val response = try { + when (request.method) { + "GET" -> { + routerGet(request) + } + "OPTIONS" -> { + WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream("".toByteArray())) + } + else -> { + throw Exception("Method Not Allowed") + } + } + } catch (e: Exception) { + Log.w(TAG, "handleRequest: ", e) + makeErrorResponse() + } + + // Allow CORS from all origins + response.responseHeaders = mutableMapOf( + "Access-Control-Allow-Origin" to "*", + "Access-Control-Allow-Headers" to "*" + ) + + // Cache image responses for 7 days + if (path.matches(API.IMAGE_PREVIEW) || path.matches(API.IMAGE_FULL)) { + response.responseHeaders["Cache-Control"] = "max-age=604800" + } + + return response + } + @Throws(Exception::class) private fun routerGet(request: WebResourceRequest): WebResourceResponse { val path = request.url.path ?: return makeErrorResponse() @@ -196,7 +196,7 @@ import java.net.URLDecoder return if (path.matches(API.DAYS)) { makeResponse(query.getDays()) } else if (path.matches(API.DAY)) { - makeResponse(query.getByDayId(parts[3].toLong())) + makeResponse(query.getDay(parts[3].toLong())) } else if (path.matches(API.IMAGE_INFO)) { makeResponse(query.getImageInfo(parts[4].toLong())) } else if (path.matches(API.IMAGE_DELETE)) { diff --git a/app/src/main/java/gallery/memories/service/AccountService.kt b/app/src/main/java/gallery/memories/service/AccountService.kt index d00d3b61..54a3fb37 100644 --- a/app/src/main/java/gallery/memories/service/AccountService.kt +++ b/app/src/main/java/gallery/memories/service/AccountService.kt @@ -26,12 +26,11 @@ class AccountService(private val mCtx: MainActivity) { var authHeader: String? = null var memoriesUrl: String? = null - private fun toast(message: String) { - mCtx.runOnUiThread { - Toast.makeText(mCtx, message, Toast.LENGTH_LONG).show() - } - } - + /** + * Login to a server + * @param baseUrl The base URL of the server + * @param loginFlowUrl The login flow URL + */ fun login(baseUrl: String, loginFlowUrl: String) { // Make POST request to login flow URL val client = OkHttpClient() @@ -84,6 +83,12 @@ class AccountService(private val mCtx: MainActivity) { }.start() } + /** + * Poll the login flow URL until we get a login token + * @param pollUrl The login flow URL + * @param pollToken The login token + * @param baseUrl The base URL of the server + */ private fun pollLogin(pollUrl: String, pollToken: String, baseUrl: String) { mCtx.binding.webview.post { mCtx.binding.webview.loadUrl("file:///android_asset/sync.html") @@ -141,6 +146,10 @@ class AccountService(private val mCtx: MainActivity) { } } + /** + * Check if the credentials are valid and the server version is supported + * Makes a toast to the user if something is wrong + */ fun checkCredentialsAndVersion() { if (memoriesUrl == null) return @@ -192,6 +201,9 @@ class AccountService(private val mCtx: MainActivity) { } } + /** + * Handle a logout. Delete the stored credentials and go back to the login screen. + */ fun loggedOut() { toast(mCtx.getString(R.string.err_logged_out)) deleteCredentials() @@ -200,6 +212,12 @@ class AccountService(private val mCtx: MainActivity) { } } + /** + * Store the credentials + * @param url The URL to store + * @param user The username to store + * @param password The password to store + */ fun storeCredentials(url: String, user: String, password: String) { mCtx.getSharedPreferences("credentials", 0).edit() .putString("memoriesUrl", url) @@ -210,6 +228,10 @@ class AccountService(private val mCtx: MainActivity) { setAuthHeader(Pair(user, password)) } + /** + * Get the stored credentials + * @return The stored credentials + */ fun getCredentials(): Pair? { val prefs = mCtx.getSharedPreferences("credentials", 0) memoriesUrl = prefs.getString("memoriesUrl", null) @@ -219,6 +241,9 @@ class AccountService(private val mCtx: MainActivity) { return Pair(user, password) } + /** + * Delete the stored credentials + */ fun deleteCredentials() { authHeader = null memoriesUrl = null @@ -229,10 +254,17 @@ class AccountService(private val mCtx: MainActivity) { .apply() } + /** + * Refresh the authorization header + */ fun refreshAuthHeader() { setAuthHeader(getCredentials()) } + /** + * Set the authorization header + * @param credentials The credentials to use + */ private fun setAuthHeader(credentials: Pair?) { if (credentials != null) { val auth = "${credentials.first}:${credentials.second}" @@ -241,4 +273,14 @@ class AccountService(private val mCtx: MainActivity) { } authHeader = null } + + /** + * Show a toast on the UI thread + * @param message The message to show + */ + private fun toast(message: String) { + mCtx.runOnUiThread { + Toast.makeText(mCtx, message, Toast.LENGTH_LONG).show() + } + } } \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/ConfigService.kt b/app/src/main/java/gallery/memories/service/ConfigService.kt index 670c2ac3..2fd97de6 100644 --- a/app/src/main/java/gallery/memories/service/ConfigService.kt +++ b/app/src/main/java/gallery/memories/service/ConfigService.kt @@ -8,6 +8,10 @@ class ConfigService(private val mCtx: Context) { private var mEnabledBuckets: List? = null } + /** + * Get the list of enabled local folders + * @return The list of enabled local folders + */ var enabledBucketIds: List get() { if (mEnabledBuckets != null) return mEnabledBuckets!! diff --git a/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt b/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt index 32c9afae..026ed3b7 100644 --- a/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt +++ b/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt @@ -3,9 +3,13 @@ package gallery.memories.service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import androidx.media3.common.util.UnstableApi import gallery.memories.NativeX -class DownloadBroadcastReceiver : BroadcastReceiver() { +@UnstableApi class DownloadBroadcastReceiver : BroadcastReceiver() { + /** + * Callback when download is complete + */ override fun onReceive(context: Context, intent: Intent) { NativeX.dlService?.runDownloadCallback(intent) } diff --git a/app/src/main/java/gallery/memories/service/DownloadService.kt b/app/src/main/java/gallery/memories/service/DownloadService.kt index 201683be..a1e87dab 100644 --- a/app/src/main/java/gallery/memories/service/DownloadService.kt +++ b/app/src/main/java/gallery/memories/service/DownloadService.kt @@ -9,11 +9,16 @@ import android.webkit.CookieManager import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.collection.ArrayMap +import androidx.media3.common.util.UnstableApi import java.util.concurrent.CountDownLatch -class DownloadService(private val mActivity: AppCompatActivity, private val query: TimelineQuery) { +@UnstableApi class DownloadService(private val mActivity: AppCompatActivity, private val query: TimelineQuery) { private val mDownloads: MutableMap Unit> = ArrayMap() + /** + * Callback when download is complete + * @param intent The intent that triggered the callback + */ fun runDownloadCallback(intent: Intent) { if (mActivity.isDestroyed) return @@ -31,6 +36,12 @@ class DownloadService(private val mActivity: AppCompatActivity, private val quer } } + /** + * Queue a download + * @param url The URL to download + * @param filename The filename to save the download as + * @return The download ID + */ fun queue(url: String, filename: String): Long { val uri = Uri.parse(url) val manager = mActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager @@ -52,6 +63,11 @@ class DownloadService(private val mActivity: AppCompatActivity, private val quer return manager.enqueue(request) } + /** + * Share a URL as a string + * @param url The URL to share + * @return True if the URL was shared + */ fun shareUrl(url: String): Boolean { val intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" @@ -60,6 +76,11 @@ class DownloadService(private val mActivity: AppCompatActivity, private val quer return true } + /** + * Share a URL as a blob + * @param url The URL to share + * @return True if the URL was shared + */ @Throws(Exception::class) fun shareBlobFromUrl(url: String): Boolean { val id = queue(url, "") @@ -81,6 +102,11 @@ class DownloadService(private val mActivity: AppCompatActivity, private val quer return true } + /** + * Share a local image + * @param auid The AUID of the image to share + * @return True if the image was shared + */ @Throws(Exception::class) fun shareLocal(auid: Long): Boolean { val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) @@ -95,6 +121,11 @@ class DownloadService(private val mActivity: AppCompatActivity, private val quer return true } + /** + * Get the URI of a downloaded file from download ID + * @param downloadId The download ID + * @return The URI of the downloaded file + */ private fun getDownloadedFileURI(downloadId: Long): String? { val downloadManager = mActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager diff --git a/app/src/main/java/gallery/memories/service/ImageService.kt b/app/src/main/java/gallery/memories/service/ImageService.kt index d0bf2012..399dd113 100644 --- a/app/src/main/java/gallery/memories/service/ImageService.kt +++ b/app/src/main/java/gallery/memories/service/ImageService.kt @@ -6,9 +6,15 @@ import android.graphics.Bitmap import android.graphics.ImageDecoder import android.os.Build import android.provider.MediaStore +import androidx.media3.common.util.UnstableApi import java.io.ByteArrayOutputStream -class ImageService(private val mCtx: Context, private val query: TimelineQuery) { +@UnstableApi class ImageService(private val mCtx: Context, private val query: TimelineQuery) { + /** + * Get a preview image for a given image ID + * @param id The image ID + * @return The preview image as a JPEG byte array + */ @Throws(Exception::class) fun getPreview(id: Long): ByteArray { val bitmap = @@ -36,6 +42,11 @@ class ImageService(private val mCtx: Context, private val query: TimelineQuery) return stream.toByteArray() } + /** + * Get a full image for a given image ID + * @param id The image ID + * @return The full image as a JPEG byte array + */ @Throws(Exception::class) fun getFull(auid: Long): ByteArray { val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) diff --git a/app/src/main/java/gallery/memories/service/TimelineQuery.kt b/app/src/main/java/gallery/memories/service/TimelineQuery.kt index 4d612b70..f32111ae 100644 --- a/app/src/main/java/gallery/memories/service/TimelineQuery.kt +++ b/app/src/main/java/gallery/memories/service/TimelineQuery.kt @@ -105,14 +105,36 @@ class TimelineQuery(private val mCtx: MainActivity) { return observer } + /** + * Get system images by AUIDs + * @param auids List of AUIDs + */ fun getSystemImagesByAUIDs(auids: List): List { val photos = mPhotoDao.getPhotosByAUIDs(auids) if (photos.isEmpty()) return listOf() return SystemImage.getByIds(mCtx, photos.map { it.localId }) } + /** + * Get the days response for local files. + * @return JSON response + */ @Throws(JSONException::class) - fun getByDayId(dayId: Long): JSONArray { + fun getDays(): JSONArray { + return mPhotoDao.getDays(mConfigService.enabledBucketIds).map { + JSONObject() + .put(Fields.Day.DAYID, it.dayId) + .put(Fields.Day.COUNT, it.count) + }.let { JSONArray(it) } + } + + /** + * Get the day response for local files. + * @param dayId Day ID + * @return JSON response + */ + @Throws(JSONException::class) + fun getDay(dayId: Long): JSONArray { // Get the photos for the day from DB val fileIds = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds) .map { it.localId }.toMutableList() @@ -133,15 +155,6 @@ class TimelineQuery(private val mCtx: MainActivity) { return photos } - @Throws(JSONException::class) - fun getDays(): JSONArray { - return mPhotoDao.getDays(mConfigService.enabledBucketIds).map { - JSONObject() - .put(Fields.Day.DAYID, it.dayId) - .put(Fields.Day.COUNT, it.count) - }.let { JSONArray(it) } - } - @Throws(Exception::class) fun getImageInfo(id: Long): JSONObject { val photos = mPhotoDao.getPhotosByFileIds(listOf(id)) @@ -176,6 +189,12 @@ class TimelineQuery(private val mCtx: MainActivity) { } + /** + * Delete images from local database and system store. + * @param auids List of AUIDs + * @param dry Dry run (returns whether confirmation will be needed) + * @return JSON response + */ @Throws(Exception::class) fun delete(auids: List, dry: Boolean): JSONObject { synchronized(this) { @@ -237,6 +256,11 @@ class TimelineQuery(private val mCtx: MainActivity) { return response } + /** + * Sync local database with system store. + * @param startTime Only sync files modified after this time + * @return Number of updated files + */ private fun syncDb(startTime: Long): Int { // Date modified is in seconds, not millis val syncTime = Instant.now().toEpochMilli() / 1000; @@ -287,6 +311,10 @@ class TimelineQuery(private val mCtx: MainActivity) { return updates } + /** + * Sync local database with system store. + * @return Number of updated files + */ fun syncDeltaDb(): Int { // Get last sync time val syncTime = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0) @@ -294,6 +322,11 @@ class TimelineQuery(private val mCtx: MainActivity) { return syncDb(syncTime) } + /** + * Sync local database with system store. + * Runs a full synchronization pass, flagging all files for removal. + * @return Number of updated files + */ fun syncFullDb() { // Flag all images for removal mPhotoDao.flagAll() @@ -305,6 +338,10 @@ class TimelineQuery(private val mCtx: MainActivity) { mPhotoDao.deleteFlagged() } + /** + * Insert item into local database. + * @param image SystemImage + */ @SuppressLint("SimpleDateFormat") private fun insertItemDb(image: SystemImage) { val fileId = image.fileId @@ -325,7 +362,10 @@ class TimelineQuery(private val mCtx: MainActivity) { Log.v(TAG, "Inserted file to local DB: $fileId / $baseName") } - /** This is in timeline query because it calls the database service */ + /** + * Active local folders response. + * This is in timeline query because it calls the database service. + */ var localFolders: JSONArray get() { return mPhotoDao.getBuckets().map {