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 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<Long> {
return ids.trim().split(",").map { it.toLong() }
private fun parseIds(ids: String): List<String> {
return ids.trim().split(",")
}
fun doMediaSync(forceFull: Boolean) {

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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,

View File

@ -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')
}
}

View File

@ -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

View File

@ -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")

View File

@ -130,7 +130,7 @@ class TimelineQuery(private val mCtx: MainActivity) {
* @param auids List of AUIDs
* @return List of SystemImage
*/
fun getSystemImagesByAUIDs(auids: List<Long>): List<SystemImage> {
fun getSystemImagesByAUIDs(auids: List<String>): List<SystemImage> {
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<Long>, dry: Boolean): JSONObject {
fun delete(auids: List<String>, 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<Long>, value: Boolean) {
mPhotoDao.setHasRemote(auids, value)
fun setHasRemote(auids: List<String>, buids: List<String>, value: Boolean) {
mPhotoDao.setHasRemote(auids, buids, value)
}
/**