From 55e5c05d5408b88223345d7d939c7bac586a24cc Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 4 Oct 2023 14:59:47 -0700 Subject: [PATCH] Implement BUID --- app/src/main/java/gallery/memories/NativeX.kt | 31 +++--- .../java/gallery/memories/dao/AppDatabase.kt | 2 +- .../java/gallery/memories/dao/PhotoDao.kt | 6 +- .../java/gallery/memories/mapper/Fields.kt | 1 + .../java/gallery/memories/mapper/Photo.kt | 4 +- .../gallery/memories/mapper/SystemImage.kt | 97 ++++++++++++------- .../memories/service/DownloadService.kt | 2 +- .../gallery/memories/service/ImageService.kt | 2 +- .../gallery/memories/service/TimelineQuery.kt | 38 +++++--- 9 files changed, 116 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/gallery/memories/NativeX.kt b/app/src/main/java/gallery/memories/NativeX.kt index dcbe416a..7a78a4d2 100644 --- a/app/src/main/java/gallery/memories/NativeX.kt +++ b/app/src/main/java/gallery/memories/NativeX.kt @@ -47,14 +47,14 @@ class NativeX(private val mCtx: MainActivity) { 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_DELETE = Regex("^/api/image/delete/[0-9a-f]+(,[0-9a-f]+)*$") val IMAGE_PREVIEW = Regex("^/image/preview/\\d+$") - val IMAGE_FULL = Regex("^/image/full/\\d+$") + val IMAGE_FULL = Regex("^/image/full/[0-9a-f]+$") val SHARE_URL = Regex("^/api/share/url/.+$") val SHARE_BLOB = Regex("^/api/share/blob/.+$") - val SHARE_LOCAL = Regex("^/api/share/local/\\d+$") + val SHARE_LOCAL = Regex("^/api/share/local/[0-9a-f]+$") val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$") } @@ -118,7 +118,7 @@ class NativeX(private val mCtx: MainActivity) { } @JavascriptInterface - fun playVideo(auid: Long, fileid: Long, urlsArray: String) { + fun playVideo(auid: String, fileid: Long, urlsArray: String) { mCtx.threadPool.submit { // Get URI of remote videos val urls = JSONArray(urlsArray) @@ -169,11 +169,16 @@ class NativeX(private val mCtx: MainActivity) { } @JavascriptInterface - fun setHasRemote(auids: String, value: Boolean) { + fun setHasRemote(auids: String, buids: String, value: Boolean) { + Log.v(TAG, "setHasRemote: auids=$auids, buids=$buids, value=$value") mCtx.threadPool.submit { - val parsed = JSONArray(auids) - val list = List(parsed.length()) { parsed.getLong(it) } - query.setHasRemote(list, value) + val auidArray = JSONArray(auids) + val buidArray = JSONArray(buids) + query.setHasRemote( + List(auidArray.length()) { auidArray.getString(it) }, + List(buidArray.length()) { buidArray.getString(it) }, + value + ) } } @@ -199,7 +204,7 @@ class NativeX(private val mCtx: MainActivity) { } } } catch (e: Exception) { - Log.w(TAG, "handleRequest: ", e) + Log.w(TAG, "handleRequest: " + e.message) makeErrorResponse() } @@ -238,13 +243,13 @@ class NativeX(private val mCtx: MainActivity) { } else if (path.matches(API.IMAGE_PREVIEW)) { makeResponse(image.getPreview(parts[3].toLong()), "image/jpeg") } else if (path.matches(API.IMAGE_FULL)) { - makeResponse(image.getFull(parts[3].toLong()), "image/jpeg") + makeResponse(image.getFull(parts[3]), "image/jpeg") } else if (path.matches(API.SHARE_URL)) { makeResponse(dlService!!.shareUrl(URLDecoder.decode(parts[4], "UTF-8"))) } else if (path.matches(API.SHARE_BLOB)) { makeResponse(dlService!!.shareBlobFromUrl(URLDecoder.decode(parts[4], "UTF-8"))) } else if (path.matches(API.SHARE_LOCAL)) { - makeResponse(dlService!!.shareLocal(parts[4].toLong())) + makeResponse(dlService!!.shareLocal(parts[4])) } else if (path.matches(API.CONFIG_ALLOW_MEDIA)) { permissions.setAllowMedia(true) if (permissions.requestMediaPermissionSync()) { @@ -276,8 +281,8 @@ class NativeX(private val mCtx: MainActivity) { return response } - private fun parseIds(ids: String): List { - return ids.trim().split(",").map { it.toLong() } + private fun parseIds(ids: String): List { + return ids.trim().split(",") } fun doMediaSync(forceFull: Boolean) { diff --git a/app/src/main/java/gallery/memories/dao/AppDatabase.kt b/app/src/main/java/gallery/memories/dao/AppDatabase.kt index c913e437..3cc21c2c 100644 --- a/app/src/main/java/gallery/memories/dao/AppDatabase.kt +++ b/app/src/main/java/gallery/memories/dao/AppDatabase.kt @@ -9,7 +9,7 @@ import gallery.memories.R import gallery.memories.mapper.Photo -@Database(entities = [Photo::class], version = 11) +@Database(entities = [Photo::class], version = 34) abstract class AppDatabase : RoomDatabase() { abstract fun photoDao(): PhotoDao diff --git a/app/src/main/java/gallery/memories/dao/PhotoDao.kt b/app/src/main/java/gallery/memories/dao/PhotoDao.kt index 5046f35f..092b617d 100644 --- a/app/src/main/java/gallery/memories/dao/PhotoDao.kt +++ b/app/src/main/java/gallery/memories/dao/PhotoDao.kt @@ -25,7 +25,7 @@ interface PhotoDao { fun getPhotosByFileIds(fileIds: List): List @Query("SELECT * FROM photos WHERE auid IN (:auids)") - fun getPhotosByAUIDs(auids: List): List + fun getPhotosByAUIDs(auids: List): List @Query("UPDATE photos SET flag=1") fun flagAll() @@ -42,6 +42,6 @@ interface PhotoDao { @Query("SELECT bucket_id, bucket_name FROM photos GROUP BY bucket_id") fun getBuckets(): List - @Query("UPDATE photos SET has_remote=:v WHERE auid IN (:auids)") - fun setHasRemote(auids: List, v: Boolean) + @Query("UPDATE photos SET has_remote=:v WHERE auid IN (:auids) OR buid IN (:buids)") + fun setHasRemote(auids: List, buids: List, v: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/mapper/Fields.kt b/app/src/main/java/gallery/memories/mapper/Fields.kt index e1121579..48cf6ff4 100644 --- a/app/src/main/java/gallery/memories/mapper/Fields.kt +++ b/app/src/main/java/gallery/memories/mapper/Fields.kt @@ -19,6 +19,7 @@ class Fields { const val DATETAKEN = "datetaken" const val EPOCH = "epoch" const val AUID = "auid" + const val BUID = "buid" const val DAYID = "dayid" const val ISVIDEO = "isvideo" const val VIDEO_DURATION = "video_duration" diff --git a/app/src/main/java/gallery/memories/mapper/Photo.kt b/app/src/main/java/gallery/memories/mapper/Photo.kt index 106da2e2..f49a9ac8 100644 --- a/app/src/main/java/gallery/memories/mapper/Photo.kt +++ b/app/src/main/java/gallery/memories/mapper/Photo.kt @@ -9,6 +9,7 @@ import androidx.room.PrimaryKey tableName = "photos", indices = [ Index(value = ["local_id"]), Index(value = ["auid"]), + Index(value = ["buid"]), Index(value = ["dayid"]), Index(value = ["flag"]), Index(value = ["bucket_id"]), @@ -18,7 +19,8 @@ import androidx.room.PrimaryKey data class Photo( @PrimaryKey(autoGenerate = true) val id: Int? = null, @ColumnInfo(name = "local_id") val localId: Long, - @ColumnInfo(name = "auid") val auid: Long, + @ColumnInfo(name = "auid") val auid: String, + @ColumnInfo(name = "buid") val buid: String, @ColumnInfo(name = "mtime") val mtime: Long, @ColumnInfo(name = "date_taken") val dateTaken: Long, @ColumnInfo(name = "dayid") val dayId: Long, diff --git a/app/src/main/java/gallery/memories/mapper/SystemImage.kt b/app/src/main/java/gallery/memories/mapper/SystemImage.kt index 96088a5e..ce857069 100644 --- a/app/src/main/java/gallery/memories/mapper/SystemImage.kt +++ b/app/src/main/java/gallery/memories/mapper/SystemImage.kt @@ -10,9 +10,11 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import org.json.JSONObject import java.io.IOException +import java.math.BigInteger +import java.security.MessageDigest class SystemImage { - var fileId = 0L; + var fileId = 0L var baseName = "" var mimeType = "" var dateTaken = 0L @@ -163,7 +165,6 @@ class SystemImage { .put(Fields.Photo.SIZE, size) .put(Fields.Photo.ETAG, mtime.toString()) .put(Fields.Photo.EPOCH, epoch) - .put(Fields.Photo.AUID, auid) if (isVideo) { obj.put(Fields.Photo.ISVIDEO, 1) @@ -179,47 +180,70 @@ class SystemImage { return dateTaken / 1000 } - /** The UTC dateTaken timestamp of the image. */ - val utcDate - get(): Long { - // Get EXIF date using ExifInterface if image - if (!isVideo) { - try { - val exif = ExifInterface(dataPath) - val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME) - ?: throw IOException() - val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss") - sdf.timeZone = TimeZone.GMT_ZONE - sdf.parse(exifDate).let { - return it.time / 1000 - } - } catch (e: Exception) { - Log.e(TAG, "Failed to read EXIF data: " + e.message) - } + val exifInterface + get() : ExifInterface? { + if (isVideo) return null + try { + return ExifInterface(dataPath) + } catch (e: Exception) { + Log.e(TAG, "Failed to read EXIF daddta: " + e.message) + return null } - - // No way to get the actual local date, so just assume current timezone - return (dateTaken + TimeZone.getDefault().getOffset(dateTaken).toLong()) / 1000 } - /** The auid of the image. */ - val auid - get(): Long { - val crc = java.util.zip.CRC32() - - // pass date taken + size as decimal string - crc.update((epoch.toString() + size.toString()).toByteArray()) - - return crc.value + /** The UTC dateTaken timestamp of the image. */ + fun utcDate(exif: ExifInterface?): Long { + // Get EXIF date using ExifInterface if image + if (exif != null) { + try { + val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME) + ?: throw IOException() + val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss") + sdf.timeZone = TimeZone.GMT_ZONE + sdf.parse(exifDate).let { + return it.time / 1000 + } + } catch (e: Exception) { + Log.e(TAG, "Failed to read EXIF datetime: " + e.message) + } } - /** The database Photo object corresponding to the SystemImage. */ + // No way to get the actual local date, so just assume current timezone + return (dateTaken + TimeZone.getDefault().getOffset(dateTaken).toLong()) / 1000 + } + + fun auid(): String { + return md5("$epoch$size") + } + + fun buid(exif: ExifInterface?): String { + var imageUniqueId = "size=$size" + if (exif != null) { + try { + val iuid = exif.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID) + ?: throw IOException() + imageUniqueId = "iuid=$iuid" + } catch (e: Exception) { + Log.e(TAG, "Failed to read EXIF unique ID ($baseName): " + e.message) + } + } + + return md5("$baseName$imageUniqueId"); + } + + /** + * The database Photo object corresponding to the SystemImage. + * This should ONLY be used for insertion into the database. + */ val photo get(): Photo { - val dateCache = utcDate + val exif = exifInterface + val dateCache = utcDate(exif) + return Photo( localId = fileId, - auid = auid, + auid = auid(), + buid = buid(exif), mtime = mtime, dateTaken = dateCache, dayId = dateCache / 86400, @@ -230,4 +254,9 @@ class SystemImage { hasRemote = false ) } + + private fun md5(input: String): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') + } } \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/DownloadService.kt b/app/src/main/java/gallery/memories/service/DownloadService.kt index a1e87dab..8807195d 100644 --- a/app/src/main/java/gallery/memories/service/DownloadService.kt +++ b/app/src/main/java/gallery/memories/service/DownloadService.kt @@ -108,7 +108,7 @@ import java.util.concurrent.CountDownLatch * @return True if the image was shared */ @Throws(Exception::class) - fun shareLocal(auid: Long): Boolean { + fun shareLocal(auid: String): Boolean { val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) if (sysImgs.isEmpty()) throw Exception("Image not found locally") val uri = sysImgs[0].uri diff --git a/app/src/main/java/gallery/memories/service/ImageService.kt b/app/src/main/java/gallery/memories/service/ImageService.kt index 399dd113..e26f075f 100644 --- a/app/src/main/java/gallery/memories/service/ImageService.kt +++ b/app/src/main/java/gallery/memories/service/ImageService.kt @@ -48,7 +48,7 @@ import java.io.ByteArrayOutputStream * @return The full image as a JPEG byte array */ @Throws(Exception::class) - fun getFull(auid: Long): ByteArray { + fun getFull(auid: String): ByteArray { val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) if (sysImgs.isEmpty()) { throw Exception("Image not found") diff --git a/app/src/main/java/gallery/memories/service/TimelineQuery.kt b/app/src/main/java/gallery/memories/service/TimelineQuery.kt index acf536d5..2534abd4 100644 --- a/app/src/main/java/gallery/memories/service/TimelineQuery.kt +++ b/app/src/main/java/gallery/memories/service/TimelineQuery.kt @@ -130,7 +130,7 @@ class TimelineQuery(private val mCtx: MainActivity) { * @param auids List of AUIDs * @return List of SystemImage */ - fun getSystemImagesByAUIDs(auids: List): List { + fun getSystemImagesByAUIDs(auids: List): List { val photos = mPhotoDao.getPhotosByAUIDs(auids) if (photos.isEmpty()) return listOf() return SystemImage.getByIds(mCtx, photos.map { it.localId }) @@ -157,23 +157,32 @@ class TimelineQuery(private val mCtx: MainActivity) { @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() - if (fileIds.isEmpty()) return JSONArray() + val photos = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds) + .map { it.localId to it }.toMap() + + if (photos.isEmpty()) return JSONArray() + val fileIds = photos.keys.toMutableList() // Get latest metadata from system table - val photos = SystemImage.getByIds(mCtx, fileIds).map { image -> + val response = SystemImage.getByIds(mCtx, fileIds).map { image -> // Mark file exists fileIds.remove(image.fileId) - // Add missing dayId to JSON - image.json.put(Fields.Photo.DAYID, dayId) + // Add missing fields to JSON + val json = image.json + photos[image.fileId]?.let { photo -> + json.put(Fields.Photo.AUID, photo.auid) + .put(Fields.Photo.BUID, photo.buid) + .put(Fields.Photo.DAYID, dayId) + } + + json }.let { JSONArray(it) } // Remove files that were not found mPhotoDao.deleteFileIds(fileIds) - return photos + return response } /** @@ -222,7 +231,7 @@ class TimelineQuery(private val mCtx: MainActivity) { * @return JSON response */ @Throws(Exception::class) - fun delete(auids: List, dry: Boolean): JSONObject { + fun delete(auids: List, dry: Boolean): JSONObject { synchronized(this) { if (deleting) throw Exception("Already deleting another set of images") deleting = true @@ -405,10 +414,13 @@ class TimelineQuery(private val mCtx: MainActivity) { return } + // Convert to photo + val photo = image.photo + // Delete file with same local_id and insert new one mPhotoDao.deleteFileIds(listOf(fileId)) - mPhotoDao.insert(image.photo) - Log.v(TAG, "Inserted file to local DB: $fileId / $baseName") + mPhotoDao.insert(photo) + Log.v(TAG, "Inserted file to local DB: $fileId / $baseName / $photo") } /** @@ -416,8 +428,8 @@ class TimelineQuery(private val mCtx: MainActivity) { * @param auids List of AUIDs * @param value Value to set */ - fun setHasRemote(auids: List, value: Boolean) { - mPhotoDao.setHasRemote(auids, value) + fun setHasRemote(auids: List, buids: List, value: Boolean) { + mPhotoDao.setHasRemote(auids, buids, value) } /**