Implement BUID

pull/653/merge
Varun Patil 2023-10-04 14:59:47 -07:00
parent c04cc12e88
commit 55e5c05d54
9 changed files with 116 additions and 67 deletions

View File

@ -47,14 +47,14 @@ class NativeX(private val mCtx: MainActivity) {
val DAY = Regex("^/api/days/\\d+$") val DAY = Regex("^/api/days/\\d+$")
val IMAGE_INFO = Regex("^/api/image/info/\\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_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_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/[0-9a-f]+$")
val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$") val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$")
} }
@ -118,7 +118,7 @@ class NativeX(private val mCtx: MainActivity) {
} }
@JavascriptInterface @JavascriptInterface
fun playVideo(auid: Long, fileid: Long, urlsArray: String) { fun playVideo(auid: String, fileid: Long, urlsArray: String) {
mCtx.threadPool.submit { mCtx.threadPool.submit {
// Get URI of remote videos // Get URI of remote videos
val urls = JSONArray(urlsArray) val urls = JSONArray(urlsArray)
@ -169,11 +169,16 @@ class NativeX(private val mCtx: MainActivity) {
} }
@JavascriptInterface @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 { mCtx.threadPool.submit {
val parsed = JSONArray(auids) val auidArray = JSONArray(auids)
val list = List(parsed.length()) { parsed.getLong(it) } val buidArray = JSONArray(buids)
query.setHasRemote(list, value) 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) { } catch (e: Exception) {
Log.w(TAG, "handleRequest: ", e) Log.w(TAG, "handleRequest: " + e.message)
makeErrorResponse() makeErrorResponse()
} }
@ -238,13 +243,13 @@ class NativeX(private val mCtx: MainActivity) {
} else if (path.matches(API.IMAGE_PREVIEW)) { } else if (path.matches(API.IMAGE_PREVIEW)) {
makeResponse(image.getPreview(parts[3].toLong()), "image/jpeg") makeResponse(image.getPreview(parts[3].toLong()), "image/jpeg")
} else if (path.matches(API.IMAGE_FULL)) { } 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)) { } else if (path.matches(API.SHARE_URL)) {
makeResponse(dlService!!.shareUrl(URLDecoder.decode(parts[4], "UTF-8"))) makeResponse(dlService!!.shareUrl(URLDecoder.decode(parts[4], "UTF-8")))
} else if (path.matches(API.SHARE_BLOB)) { } else if (path.matches(API.SHARE_BLOB)) {
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]))
} else if (path.matches(API.CONFIG_ALLOW_MEDIA)) { } else if (path.matches(API.CONFIG_ALLOW_MEDIA)) {
permissions.setAllowMedia(true) permissions.setAllowMedia(true)
if (permissions.requestMediaPermissionSync()) { if (permissions.requestMediaPermissionSync()) {
@ -276,8 +281,8 @@ class NativeX(private val mCtx: MainActivity) {
return response return response
} }
private fun parseIds(ids: String): List<Long> { private fun parseIds(ids: String): List<String> {
return ids.trim().split(",").map { it.toLong() } return ids.trim().split(",")
} }
fun doMediaSync(forceFull: Boolean) { fun doMediaSync(forceFull: Boolean) {

View File

@ -9,7 +9,7 @@ import gallery.memories.R
import gallery.memories.mapper.Photo import gallery.memories.mapper.Photo
@Database(entities = [Photo::class], version = 11) @Database(entities = [Photo::class], version = 34)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun photoDao(): PhotoDao abstract fun photoDao(): PhotoDao

View File

@ -25,7 +25,7 @@ interface PhotoDao {
fun getPhotosByFileIds(fileIds: List<Long>): List<Photo> fun getPhotosByFileIds(fileIds: List<Long>): List<Photo>
@Query("SELECT * FROM photos WHERE auid IN (:auids)") @Query("SELECT * FROM photos WHERE auid IN (:auids)")
fun getPhotosByAUIDs(auids: List<Long>): List<Photo> fun getPhotosByAUIDs(auids: List<String>): List<Photo>
@Query("UPDATE photos SET flag=1") @Query("UPDATE photos SET flag=1")
fun flagAll() fun flagAll()
@ -42,6 +42,6 @@ interface PhotoDao {
@Query("SELECT bucket_id, bucket_name FROM photos GROUP BY bucket_id") @Query("SELECT bucket_id, bucket_name FROM photos GROUP BY bucket_id")
fun getBuckets(): List<Bucket> fun getBuckets(): List<Bucket>
@Query("UPDATE photos SET has_remote=:v WHERE auid IN (:auids)") @Query("UPDATE photos SET has_remote=:v WHERE auid IN (:auids) OR buid IN (:buids)")
fun setHasRemote(auids: List<Long>, v: Boolean) fun setHasRemote(auids: List<String>, buids: List<String>, v: Boolean)
} }

View File

@ -19,6 +19,7 @@ class Fields {
const val DATETAKEN = "datetaken" const val DATETAKEN = "datetaken"
const val EPOCH = "epoch" const val EPOCH = "epoch"
const val AUID = "auid" const val AUID = "auid"
const val BUID = "buid"
const val DAYID = "dayid" const val DAYID = "dayid"
const val ISVIDEO = "isvideo" const val ISVIDEO = "isvideo"
const val VIDEO_DURATION = "video_duration" const val VIDEO_DURATION = "video_duration"

View File

@ -9,6 +9,7 @@ import androidx.room.PrimaryKey
tableName = "photos", indices = [ tableName = "photos", indices = [
Index(value = ["local_id"]), Index(value = ["local_id"]),
Index(value = ["auid"]), Index(value = ["auid"]),
Index(value = ["buid"]),
Index(value = ["dayid"]), Index(value = ["dayid"]),
Index(value = ["flag"]), Index(value = ["flag"]),
Index(value = ["bucket_id"]), Index(value = ["bucket_id"]),
@ -18,7 +19,8 @@ import androidx.room.PrimaryKey
data class Photo( data class Photo(
@PrimaryKey(autoGenerate = true) val id: Int? = null, @PrimaryKey(autoGenerate = true) val id: Int? = null,
@ColumnInfo(name = "local_id") val localId: Long, @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 = "mtime") val mtime: Long,
@ColumnInfo(name = "date_taken") val dateTaken: Long, @ColumnInfo(name = "date_taken") val dateTaken: Long,
@ColumnInfo(name = "dayid") val dayId: Long, @ColumnInfo(name = "dayid") val dayId: Long,

View File

@ -10,9 +10,11 @@ import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.math.BigInteger
import java.security.MessageDigest
class SystemImage { class SystemImage {
var fileId = 0L; var fileId = 0L
var baseName = "" var baseName = ""
var mimeType = "" var mimeType = ""
var dateTaken = 0L var dateTaken = 0L
@ -163,7 +165,6 @@ class SystemImage {
.put(Fields.Photo.SIZE, size) .put(Fields.Photo.SIZE, size)
.put(Fields.Photo.ETAG, mtime.toString()) .put(Fields.Photo.ETAG, mtime.toString())
.put(Fields.Photo.EPOCH, epoch) .put(Fields.Photo.EPOCH, epoch)
.put(Fields.Photo.AUID, auid)
if (isVideo) { if (isVideo) {
obj.put(Fields.Photo.ISVIDEO, 1) obj.put(Fields.Photo.ISVIDEO, 1)
@ -179,13 +180,22 @@ class SystemImage {
return dateTaken / 1000 return dateTaken / 1000
} }
/** The UTC dateTaken timestamp of the image. */ val exifInterface
val utcDate get() : ExifInterface? {
get(): Long { if (isVideo) return null
// Get EXIF date using ExifInterface if image try {
if (!isVideo) { return ExifInterface(dataPath)
} catch (e: Exception) {
Log.e(TAG, "Failed to read EXIF daddta: " + e.message)
return null
}
}
/** The UTC dateTaken timestamp of the image. */
fun utcDate(exif: ExifInterface?): Long {
// Get EXIF date using ExifInterface if image
if (exif != null) {
try { try {
val exif = ExifInterface(dataPath)
val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME) val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME)
?: throw IOException() ?: throw IOException()
val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss") val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss")
@ -194,7 +204,7 @@ class SystemImage {
return it.time / 1000 return it.time / 1000
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to read EXIF data: " + e.message) Log.e(TAG, "Failed to read EXIF datetime: " + e.message)
} }
} }
@ -202,24 +212,38 @@ class SystemImage {
return (dateTaken + TimeZone.getDefault().getOffset(dateTaken).toLong()) / 1000 return (dateTaken + TimeZone.getDefault().getOffset(dateTaken).toLong()) / 1000
} }
/** The auid of the image. */ fun auid(): String {
val auid return md5("$epoch$size")
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 database Photo object corresponding to the SystemImage. */ 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 val photo
get(): Photo { get(): Photo {
val dateCache = utcDate val exif = exifInterface
val dateCache = utcDate(exif)
return Photo( return Photo(
localId = fileId, localId = fileId,
auid = auid, auid = auid(),
buid = buid(exif),
mtime = mtime, mtime = mtime,
dateTaken = dateCache, dateTaken = dateCache,
dayId = dateCache / 86400, dayId = dateCache / 86400,
@ -230,4 +254,9 @@ class SystemImage {
hasRemote = false 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')
}
} }

View File

@ -108,7 +108,7 @@ import java.util.concurrent.CountDownLatch
* @return True if the image was shared * @return True if the image was shared
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun shareLocal(auid: Long): Boolean { fun shareLocal(auid: String): Boolean {
val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) val sysImgs = query.getSystemImagesByAUIDs(listOf(auid))
if (sysImgs.isEmpty()) throw Exception("Image not found locally") if (sysImgs.isEmpty()) throw Exception("Image not found locally")
val uri = sysImgs[0].uri val uri = sysImgs[0].uri

View File

@ -48,7 +48,7 @@ import java.io.ByteArrayOutputStream
* @return The full image as a JPEG byte array * @return The full image as a JPEG byte array
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun getFull(auid: Long): ByteArray { fun getFull(auid: String): ByteArray {
val sysImgs = query.getSystemImagesByAUIDs(listOf(auid)) val sysImgs = query.getSystemImagesByAUIDs(listOf(auid))
if (sysImgs.isEmpty()) { if (sysImgs.isEmpty()) {
throw Exception("Image not found") throw Exception("Image not found")

View File

@ -130,7 +130,7 @@ class TimelineQuery(private val mCtx: MainActivity) {
* @param auids List of AUIDs * @param auids List of AUIDs
* @return List of SystemImage * @return List of SystemImage
*/ */
fun getSystemImagesByAUIDs(auids: List<Long>): List<SystemImage> { fun getSystemImagesByAUIDs(auids: List<String>): List<SystemImage> {
val photos = mPhotoDao.getPhotosByAUIDs(auids) val photos = mPhotoDao.getPhotosByAUIDs(auids)
if (photos.isEmpty()) return listOf() if (photos.isEmpty()) return listOf()
return SystemImage.getByIds(mCtx, photos.map { it.localId }) return SystemImage.getByIds(mCtx, photos.map { it.localId })
@ -157,23 +157,32 @@ class TimelineQuery(private val mCtx: MainActivity) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getDay(dayId: Long): JSONArray { fun getDay(dayId: Long): JSONArray {
// Get the photos for the day from DB // Get the photos for the day from DB
val fileIds = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds) val photos = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds)
.map { it.localId }.toMutableList() .map { it.localId to it }.toMap()
if (fileIds.isEmpty()) return JSONArray()
if (photos.isEmpty()) return JSONArray()
val fileIds = photos.keys.toMutableList()
// Get latest metadata from system table // 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 // Mark file exists
fileIds.remove(image.fileId) fileIds.remove(image.fileId)
// Add missing dayId to JSON // Add missing fields to JSON
image.json.put(Fields.Photo.DAYID, dayId) 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) } }.let { JSONArray(it) }
// Remove files that were not found // Remove files that were not found
mPhotoDao.deleteFileIds(fileIds) mPhotoDao.deleteFileIds(fileIds)
return photos return response
} }
/** /**
@ -222,7 +231,7 @@ class TimelineQuery(private val mCtx: MainActivity) {
* @return JSON response * @return JSON response
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun delete(auids: List<Long>, dry: Boolean): JSONObject { fun delete(auids: List<String>, dry: Boolean): JSONObject {
synchronized(this) { synchronized(this) {
if (deleting) throw Exception("Already deleting another set of images") if (deleting) throw Exception("Already deleting another set of images")
deleting = true deleting = true
@ -405,10 +414,13 @@ class TimelineQuery(private val mCtx: MainActivity) {
return return
} }
// Convert to photo
val photo = image.photo
// Delete file with same local_id and insert new one // Delete file with same local_id and insert new one
mPhotoDao.deleteFileIds(listOf(fileId)) mPhotoDao.deleteFileIds(listOf(fileId))
mPhotoDao.insert(image.photo) mPhotoDao.insert(photo)
Log.v(TAG, "Inserted file to local DB: $fileId / $baseName") 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 auids List of AUIDs
* @param value Value to set * @param value Value to set
*/ */
fun setHasRemote(auids: List<Long>, value: Boolean) { fun setHasRemote(auids: List<String>, buids: List<String>, value: Boolean) {
mPhotoDao.setHasRemote(auids, value) mPhotoDao.setHasRemote(auids, buids, value)
} }
/** /**