Refactor
parent
0eee69bacb
commit
2f065e6d12
|
@ -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)) {
|
||||
|
|
|
@ -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<String, String>? {
|
||||
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<String, String>?) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,10 @@ class ConfigService(private val mCtx: Context) {
|
|||
private var mEnabledBuckets: List<String>? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of enabled local folders
|
||||
* @return The list of enabled local folders
|
||||
*/
|
||||
var enabledBucketIds: List<String>
|
||||
get() {
|
||||
if (mEnabledBuckets != null) return mEnabledBuckets!!
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<Long, () -> 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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<Long>): List<SystemImage> {
|
||||
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<Long>, 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 {
|
||||
|
|
Loading…
Reference in New Issue