Implement BUID
parent
c04cc12e88
commit
55e5c05d54
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue