Switch to room
parent
ec8c125b0e
commit
6dfe308268
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,17 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="deploymentTargetDropDown">
|
<component name="deploymentTargetDropDown">
|
||||||
<runningDeviceTargetSelectedWithDropDown>
|
|
||||||
<Target>
|
|
||||||
<type value="RUNNING_DEVICE_TARGET" />
|
|
||||||
<deviceKey>
|
|
||||||
<Key>
|
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
|
||||||
<value value="C:\Users\varun\.android\avd\Pixel_6_API_33_2.avd" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</runningDeviceTargetSelectedWithDropDown>
|
|
||||||
<targetSelectedWithDropDown>
|
<targetSelectedWithDropDown>
|
||||||
<Target>
|
<Target>
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -6,7 +6,7 @@
|
||||||
</list>
|
</list>
|
||||||
</component>
|
</component>
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'com.google.devtools.ksp' version '1.9.0-1.0.13'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -22,28 +23,35 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def mediaVersion = "1.0.1"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
def media_version = "1.1.1"
|
||||||
|
def room_version = "2.5.2"
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
|
||||||
|
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||||
implementation "androidx.media3:media3-exoplayer:$mediaVersion"
|
implementation "androidx.media3:media3-exoplayer:$media_version"
|
||||||
implementation "androidx.media3:media3-ui:$mediaVersion"
|
implementation "androidx.media3:media3-ui:$media_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-hls:$mediaVersion"
|
implementation "androidx.media3:media3-exoplayer-hls:$media_version"
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||||
|
ksp "androidx.room:room-compiler:$room_version"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
||||||
implementation "io.github.g00fy2:versioncompare:1.5.0"
|
implementation "io.github.g00fy2:versioncompare:1.5.0"
|
||||||
}
|
}
|
|
@ -386,7 +386,7 @@ import gallery.memories.databinding.ActivityMainBinding
|
||||||
fun refreshTimeline(force: Boolean = false) {
|
fun refreshTimeline(force: Boolean = false) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// Check webview is loaded
|
// Check webview is loaded
|
||||||
if (binding?.webview?.url == null) return@runOnUiThread
|
if (binding.webview.url == null) return@runOnUiThread
|
||||||
|
|
||||||
// Schedule for resume if not active
|
// Schedule for resume if not active
|
||||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) || force) {
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) || force) {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package gallery.memories.dao
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room.databaseBuilder
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import gallery.memories.R
|
||||||
|
import gallery.memories.mapper.Photo
|
||||||
|
|
||||||
|
|
||||||
|
@Database(entities = [Photo::class], version = 8)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun photoDao(): PhotoDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATABASE_NAME = "memories_room"
|
||||||
|
@Volatile private var INSTANCE: AppDatabase? = null
|
||||||
|
|
||||||
|
fun get(context: Context): AppDatabase {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
synchronized(AppDatabase::class.java) {
|
||||||
|
val ctx = context.applicationContext
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = databaseBuilder(ctx, AppDatabase::class.java, DATABASE_NAME)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(callbacks(ctx))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callbacks(ctx: Context): Callback {
|
||||||
|
return object : Callback() {
|
||||||
|
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||||
|
super.onDestructiveMigration(db)
|
||||||
|
|
||||||
|
// retrigger synchronization whenever database is destructed
|
||||||
|
ctx.getSharedPreferences(ctx.getString(R.string.preferences_key), 0).edit()
|
||||||
|
.remove(ctx.getString(R.string.preferences_last_sync_time))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package gallery.memories.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import gallery.memories.mapper.Bucket
|
||||||
|
import gallery.memories.mapper.Day
|
||||||
|
import gallery.memories.mapper.Photo
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PhotoDao {
|
||||||
|
@Query("SELECT 1")
|
||||||
|
fun ping(): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM photos WHERE dayid=:dayId AND bucket_id IN (:buckets)")
|
||||||
|
fun getPhotosByDay(dayId: Long, buckets: List<String>): List<Photo>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM photos WHERE local_id IN (:fileIds)")
|
||||||
|
fun getPhotosByFileIds(fileIds: List<Long>): List<Photo>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM photos WHERE auid IN (:auids)")
|
||||||
|
fun getPhotosByAUIDs(auids: List<Long>): List<Photo>
|
||||||
|
|
||||||
|
@Query("SELECT dayid, COUNT(local_id) AS count FROM photos WHERE bucket_id IN (:bucketIds) GROUP BY dayid")
|
||||||
|
fun getDays(bucketIds: List<String>): List<Day>
|
||||||
|
|
||||||
|
@Query("DELETE FROM photos WHERE local_id IN (:fileIds)")
|
||||||
|
fun deleteFileIds(fileIds: List<Long>)
|
||||||
|
|
||||||
|
@Query("UPDATE photos SET flag=1")
|
||||||
|
fun flagAll()
|
||||||
|
|
||||||
|
@Query("UPDATE photos SET flag=0 WHERE local_id=:fileId")
|
||||||
|
fun unflag(fileId: Long)
|
||||||
|
|
||||||
|
@Query("DELETE FROM photos WHERE flag=1")
|
||||||
|
fun deleteFlagged()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(vararg photos: Photo)
|
||||||
|
|
||||||
|
@Query("SELECT bucket_id, bucket_name FROM photos GROUP BY bucket_id")
|
||||||
|
fun getBuckets(): List<Bucket>
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package gallery.memories.mapper
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
|
||||||
|
data class Bucket (
|
||||||
|
@ColumnInfo(name="bucket_id") val id: String,
|
||||||
|
@ColumnInfo(name="bucket_name") val name: String,
|
||||||
|
)
|
|
@ -1,37 +1,8 @@
|
||||||
package gallery.memories.mapper
|
package gallery.memories.mapper
|
||||||
|
|
||||||
import android.database.Cursor
|
import androidx.room.ColumnInfo
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class Day {
|
data class Day (
|
||||||
val dayId: Long
|
@ColumnInfo(name="dayid") val dayId: Long,
|
||||||
val count: Long
|
@ColumnInfo(name="count") val count: Long
|
||||||
|
)
|
||||||
companion object {
|
|
||||||
val FIELDS get(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
Photo.FIELD_DAY_ID,
|
|
||||||
"COUNT(${Photo.FIELD_LOCAL_ID})"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unpack(cursor: Cursor): List<Day> {
|
|
||||||
val days = mutableListOf<Day>()
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
days.add(Day(cursor))
|
|
||||||
}
|
|
||||||
return days
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(cursor: Cursor) {
|
|
||||||
dayId = cursor.getLong(0)
|
|
||||||
count = cursor.getLong(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val json get(): JSONObject {
|
|
||||||
return JSONObject()
|
|
||||||
.put(Fields.Day.DAYID, dayId)
|
|
||||||
.put(Fields.Day.COUNT, count)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +1,19 @@
|
||||||
package gallery.memories.mapper
|
package gallery.memories.mapper
|
||||||
|
|
||||||
import android.database.Cursor
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
class Photo {
|
@Entity(tableName="photos")
|
||||||
val id: Long
|
data class Photo (
|
||||||
val localId: Long
|
@PrimaryKey(autoGenerate = true) val id: Int? = null,
|
||||||
val auid: Long
|
@ColumnInfo(name="local_id") val localId: Long,
|
||||||
val mtime: Long
|
@ColumnInfo(name="auid") val auid: Long,
|
||||||
val dateTaken: Long
|
@ColumnInfo(name="mtime") val mtime: Long,
|
||||||
val dayId: Long
|
@ColumnInfo(name="date_taken") val dateTaken: Long,
|
||||||
val basename: String
|
@ColumnInfo(name="dayid") val dayId: Long,
|
||||||
val bucketId: Long
|
@ColumnInfo(name="basename") val baseName: String,
|
||||||
val bucketName: String
|
@ColumnInfo(name="bucket_id") val bucketId: Long,
|
||||||
val flag: Int
|
@ColumnInfo(name="bucket_name") val bucketName: String,
|
||||||
|
@ColumnInfo(name="flag") val flag: Int
|
||||||
companion object {
|
)
|
||||||
val FIELD_ID = "id"
|
|
||||||
val FIELD_LOCAL_ID = "local_id"
|
|
||||||
val FIELD_AUID = "auid"
|
|
||||||
val FIELD_MTIME = "mtime"
|
|
||||||
val FIELD_DATE_TAKEN = "date_taken"
|
|
||||||
val FIELD_DAY_ID = "dayid"
|
|
||||||
val FIELD_BASENAME = "basename"
|
|
||||||
val FIELD_BUCKET_ID = "bucket_id"
|
|
||||||
val FIELD_BUCKET_NAME = "bucket_name"
|
|
||||||
val FIELD_FLAG = "flag"
|
|
||||||
|
|
||||||
val FIELDS get(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
FIELD_ID,
|
|
||||||
FIELD_LOCAL_ID,
|
|
||||||
FIELD_AUID,
|
|
||||||
FIELD_MTIME,
|
|
||||||
FIELD_DATE_TAKEN,
|
|
||||||
FIELD_DAY_ID,
|
|
||||||
FIELD_BASENAME,
|
|
||||||
FIELD_BUCKET_ID,
|
|
||||||
FIELD_BUCKET_NAME,
|
|
||||||
FIELD_FLAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val FIELDS_CREATE get(): String {
|
|
||||||
return """
|
|
||||||
$FIELD_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
$FIELD_LOCAL_ID INTEGER,
|
|
||||||
$FIELD_AUID INTEGER,
|
|
||||||
$FIELD_MTIME INTEGER,
|
|
||||||
$FIELD_DATE_TAKEN INTEGER,
|
|
||||||
$FIELD_DAY_ID INTEGER,
|
|
||||||
$FIELD_BASENAME TEXT,
|
|
||||||
$FIELD_BUCKET_ID INTEGER,
|
|
||||||
$FIELD_BUCKET_NAME TEXT,
|
|
||||||
$FIELD_FLAG INTEGER
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unpack(cursor: Cursor): List<Photo> {
|
|
||||||
val photos = mutableListOf<Photo>()
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
photos.add(Photo(cursor))
|
|
||||||
}
|
|
||||||
return photos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(cursor: Cursor) {
|
|
||||||
id = cursor.getLong(0)
|
|
||||||
localId = cursor.getLong(1)
|
|
||||||
auid = cursor.getLong(2)
|
|
||||||
mtime = cursor.getLong(3)
|
|
||||||
dateTaken = cursor.getLong(4)
|
|
||||||
dayId = cursor.getLong(5)
|
|
||||||
basename = cursor.getString(6)
|
|
||||||
bucketId = cursor.getLong(7)
|
|
||||||
bucketName = cursor.getString(8)
|
|
||||||
flag = cursor.getInt(9)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -190,4 +190,19 @@ class SystemImage {
|
||||||
|
|
||||||
return crc.value
|
return crc.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val photo get(): Photo {
|
||||||
|
val dateCache = utcDate
|
||||||
|
return Photo(
|
||||||
|
localId = fileId,
|
||||||
|
auid = auid,
|
||||||
|
mtime = mtime,
|
||||||
|
dateTaken = dateCache,
|
||||||
|
dayId = dateCache / 86400,
|
||||||
|
baseName = baseName,
|
||||||
|
bucketId = bucketId,
|
||||||
|
bucketName = bucketName,
|
||||||
|
flag = 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -5,20 +5,21 @@ import gallery.memories.R
|
||||||
|
|
||||||
class ConfigService(private val mCtx: Context) {
|
class ConfigService(private val mCtx: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
private var mEnabledBuckets: Set<String>? = null
|
private var mEnabledBuckets: List<String>? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
var enabledBucketIds: Set<String>
|
var enabledBucketIds: List<String>
|
||||||
get() {
|
get() {
|
||||||
if (mEnabledBuckets != null) return mEnabledBuckets!!
|
if (mEnabledBuckets != null) return mEnabledBuckets!!
|
||||||
mEnabledBuckets = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0)
|
mEnabledBuckets = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0)
|
||||||
.getStringSet(mCtx.getString(R.string.preferences_enabled_local_folders), null) ?: setOf()
|
.getStringSet(mCtx.getString(R.string.preferences_enabled_local_folders), null)?.toList()
|
||||||
|
?: listOf()
|
||||||
return mEnabledBuckets!!
|
return mEnabledBuckets!!
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mEnabledBuckets = value
|
mEnabledBuckets = value
|
||||||
mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit()
|
mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit()
|
||||||
.putStringSet(mCtx.getString(R.string.preferences_enabled_local_folders), value)
|
.putStringSet(mCtx.getString(R.string.preferences_enabled_local_folders), value.toSet())
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,137 +0,0 @@
|
||||||
package gallery.memories.service
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
|
||||||
import gallery.memories.R
|
|
||||||
import gallery.memories.mapper.Day
|
|
||||||
import gallery.memories.mapper.Photo
|
|
||||||
import gallery.memories.mapper.SystemImage
|
|
||||||
|
|
||||||
class DbService(val context: Context) : SQLiteOpenHelper(context, "memories", null, 45) {
|
|
||||||
companion object {
|
|
||||||
val MEMORIES = "memories"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
|
||||||
db.execSQL("CREATE TABLE $MEMORIES (${Photo.FIELDS_CREATE})")
|
|
||||||
|
|
||||||
// Add index on local_id, dayid, and flag
|
|
||||||
db.execSQL("CREATE INDEX images_local_id ON $MEMORIES (${Photo.FIELD_LOCAL_ID})")
|
|
||||||
db.execSQL("CREATE INDEX images_auid ON $MEMORIES (${Photo.FIELD_AUID})")
|
|
||||||
db.execSQL("CREATE INDEX images_dayid ON $MEMORIES (${Photo.FIELD_DAY_ID})")
|
|
||||||
db.execSQL("CREATE INDEX images_flag ON $MEMORIES (${Photo.FIELD_FLAG})")
|
|
||||||
db.execSQL("CREATE INDEX images_bucket ON $MEMORIES (${Photo.FIELD_BUCKET_ID})")
|
|
||||||
db.execSQL("CREATE INDEX images_bucket_dayid ON $MEMORIES (${Photo.FIELD_BUCKET_ID}, ${Photo.FIELD_DAY_ID})")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
||||||
database.execSQL("DROP TABLE IF EXISTS $MEMORIES")
|
|
||||||
|
|
||||||
// Reset sync time
|
|
||||||
context.getSharedPreferences(context.getString(R.string.preferences_key), 0).edit()
|
|
||||||
.remove(context.getString(R.string.preferences_last_sync_time))
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
onCreate(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initialize(): DbService {
|
|
||||||
writableDatabase
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotosByDay(dayId: Long, buckets: Set<String>): List<Photo> {
|
|
||||||
if (buckets.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
val bs = buckets.joinToString(",")
|
|
||||||
val where = "${Photo.FIELD_DAY_ID} = ? AND ${Photo.FIELD_BUCKET_ID} IN ($bs)"
|
|
||||||
readableDatabase.query(MEMORIES, Photo.FIELDS, where, arrayOf(
|
|
||||||
dayId.toString(),
|
|
||||||
), null, null, null).use { cursor ->
|
|
||||||
return Photo.unpack(cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotosBy(field: String, vals: List<Long>): List<Photo> {
|
|
||||||
if (vals.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
val vls = vals.joinToString(",")
|
|
||||||
readableDatabase.query(MEMORIES, Photo.FIELDS, "$field IN ($vls)", null,
|
|
||||||
null, null, null
|
|
||||||
).use { cursor ->
|
|
||||||
return Photo.unpack(cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotosByFileIds(fileIds: List<Long>): List<Photo> {
|
|
||||||
return getPhotosBy(Photo.FIELD_LOCAL_ID, fileIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotosByAUIDs(auids: List<Long>): List<Photo> {
|
|
||||||
return getPhotosBy(Photo.FIELD_AUID, auids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDays(bucketIds: Set<String>): List<Day> {
|
|
||||||
if (bucketIds.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
val bs = bucketIds.joinToString(",")
|
|
||||||
val where = "${Photo.FIELD_BUCKET_ID} IN ($bs)"
|
|
||||||
readableDatabase.query(MEMORIES, Day.FIELDS, where, null,
|
|
||||||
Photo.FIELD_DAY_ID, null, null
|
|
||||||
).use { cursor ->
|
|
||||||
return Day.unpack(cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteFileIds(fileIds: List<Long>) {
|
|
||||||
if (fileIds.isEmpty()) return
|
|
||||||
|
|
||||||
val ids = fileIds.joinToString(",")
|
|
||||||
writableDatabase.delete(MEMORIES, "${Photo.FIELD_LOCAL_ID} IN ($ids)", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun flagAll() {
|
|
||||||
writableDatabase.execSQL("UPDATE $MEMORIES SET ${Photo.FIELD_FLAG} = 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unflag(fileId: Long) {
|
|
||||||
writableDatabase.execSQL("UPDATE $MEMORIES SET ${Photo.FIELD_FLAG} = 0 WHERE ${Photo.FIELD_LOCAL_ID} = $fileId")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteFlagged() {
|
|
||||||
writableDatabase.delete(MEMORIES, "${Photo.FIELD_FLAG} = 1", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun insertImage(image: SystemImage) {
|
|
||||||
val dateTaken = image.utcDate
|
|
||||||
|
|
||||||
ContentValues().apply {
|
|
||||||
put(Photo.FIELD_LOCAL_ID, image.fileId)
|
|
||||||
put(Photo.FIELD_MTIME, image.mtime)
|
|
||||||
put(Photo.FIELD_DATE_TAKEN, dateTaken)
|
|
||||||
put(Photo.FIELD_DAY_ID, dateTaken / 86400)
|
|
||||||
put(Photo.FIELD_AUID, image.auid)
|
|
||||||
put(Photo.FIELD_BASENAME, image.baseName)
|
|
||||||
put(Photo.FIELD_BUCKET_ID, image.bucketId)
|
|
||||||
put(Photo.FIELD_BUCKET_NAME, image.bucketName)
|
|
||||||
}.let {
|
|
||||||
writableDatabase.insert(MEMORIES, null, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBuckets(): Map<String, String> {
|
|
||||||
val ret = mutableMapOf<String, String>()
|
|
||||||
|
|
||||||
readableDatabase.query(MEMORIES, arrayOf(Photo.FIELD_BUCKET_ID, Photo.FIELD_BUCKET_NAME),
|
|
||||||
null, null, Photo.FIELD_BUCKET_ID, null, null
|
|
||||||
).use { cursor ->
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
ret[cursor.getString(0)] = cursor.getString(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ import androidx.exifinterface.media.ExifInterface
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import gallery.memories.MainActivity
|
import gallery.memories.MainActivity
|
||||||
import gallery.memories.R
|
import gallery.memories.R
|
||||||
|
import gallery.memories.dao.AppDatabase
|
||||||
import gallery.memories.mapper.Fields
|
import gallery.memories.mapper.Fields
|
||||||
import gallery.memories.mapper.Response
|
import gallery.memories.mapper.Response
|
||||||
import gallery.memories.mapper.SystemImage
|
import gallery.memories.mapper.SystemImage
|
||||||
|
@ -26,9 +27,12 @@ import java.time.Instant
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
@UnstableApi class TimelineQuery(private val mCtx: MainActivity) {
|
@UnstableApi class TimelineQuery(private val mCtx: MainActivity) {
|
||||||
private val mDbService = DbService(mCtx).initialize()
|
|
||||||
private val mConfigService = ConfigService(mCtx)
|
|
||||||
private val TAG = TimelineQuery::class.java.simpleName
|
private val TAG = TimelineQuery::class.java.simpleName
|
||||||
|
private val mConfigService = ConfigService(mCtx)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
private val mDb = AppDatabase.get(mCtx)
|
||||||
|
private val mPhotoDao = mDb.photoDao()
|
||||||
|
|
||||||
// Photo deletion events
|
// Photo deletion events
|
||||||
var deleting = false
|
var deleting = false
|
||||||
|
@ -50,6 +54,7 @@ import java.util.concurrent.CountDownLatch
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
|
mPhotoDao.ping()
|
||||||
if (syncDeltaDb() > 0) {
|
if (syncDeltaDb() > 0) {
|
||||||
mCtx.refreshTimeline()
|
mCtx.refreshTimeline()
|
||||||
}
|
}
|
||||||
|
@ -103,8 +108,8 @@ import java.util.concurrent.CountDownLatch
|
||||||
@Throws(JSONException::class)
|
@Throws(JSONException::class)
|
||||||
fun getByDayId(dayId: Long): JSONArray {
|
fun getByDayId(dayId: Long): JSONArray {
|
||||||
// Get the photos for the day from DB
|
// Get the photos for the day from DB
|
||||||
val dbPhotos = mDbService.getPhotosByDay(dayId, mConfigService.enabledBucketIds)
|
val fileIds = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds)
|
||||||
val fileIds = dbPhotos.map { it.localId }.toMutableList()
|
.map { it.localId }.toMutableList()
|
||||||
if (fileIds.isEmpty()) return JSONArray()
|
if (fileIds.isEmpty()) return JSONArray()
|
||||||
|
|
||||||
// Get latest metadata from system table
|
// Get latest metadata from system table
|
||||||
|
@ -117,19 +122,23 @@ import java.util.concurrent.CountDownLatch
|
||||||
}.let { JSONArray(it) }
|
}.let { JSONArray(it) }
|
||||||
|
|
||||||
// Remove files that were not found
|
// Remove files that were not found
|
||||||
mDbService.deleteFileIds(fileIds)
|
mPhotoDao.deleteFileIds(fileIds)
|
||||||
|
|
||||||
return photos
|
return photos
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(JSONException::class)
|
@Throws(JSONException::class)
|
||||||
fun getDays(): JSONArray {
|
fun getDays(): JSONArray {
|
||||||
return mDbService.getDays(mConfigService.enabledBucketIds).map { day -> day.json }.let { JSONArray(it) }
|
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)
|
@Throws(Exception::class)
|
||||||
fun getImageInfo(id: Long): JSONObject {
|
fun getImageInfo(id: Long): JSONObject {
|
||||||
val photos = mDbService.getPhotosByFileIds(listOf(id))
|
val photos = mPhotoDao.getPhotosByFileIds(listOf(id))
|
||||||
if (photos.isEmpty()) throw Exception("File not found in database")
|
if (photos.isEmpty()) throw Exception("File not found in database")
|
||||||
|
|
||||||
// Get image from system table
|
// Get image from system table
|
||||||
|
@ -172,7 +181,7 @@ import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get list of file IDs
|
// Get list of file IDs
|
||||||
val photos = mDbService.getPhotosByAUIDs(auids)
|
val photos = mPhotoDao.getPhotosByAUIDs(auids)
|
||||||
if (photos.isEmpty()) return Response.OK
|
if (photos.isEmpty()) return Response.OK
|
||||||
val fileIds = photos.map { it.localId }
|
val fileIds = photos.map { it.localId }
|
||||||
|
|
||||||
|
@ -206,7 +215,7 @@ import java.util.concurrent.CountDownLatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete from database
|
// Delete from database
|
||||||
mDbService.deleteFileIds(fileIds)
|
mPhotoDao.deleteFileIds(fileIds)
|
||||||
} finally {
|
} finally {
|
||||||
synchronized(this) { deleting = false }
|
synchronized(this) { deleting = false }
|
||||||
}
|
}
|
||||||
|
@ -252,13 +261,13 @@ import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
fun syncFullDb() {
|
fun syncFullDb() {
|
||||||
// Flag all images for removal
|
// Flag all images for removal
|
||||||
mDbService.flagAll()
|
mPhotoDao.flagAll()
|
||||||
|
|
||||||
// Sync all files, marking them in the process
|
// Sync all files, marking them in the process
|
||||||
syncDb(0L)
|
syncDb(0L)
|
||||||
|
|
||||||
// Clean up stale files
|
// Clean up stale files
|
||||||
mDbService.deleteFlagged()
|
mPhotoDao.deleteFlagged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
@ -267,38 +276,38 @@ import java.util.concurrent.CountDownLatch
|
||||||
val baseName = image.baseName
|
val baseName = image.baseName
|
||||||
|
|
||||||
// Check if file with local_id and mtime already exists
|
// Check if file with local_id and mtime already exists
|
||||||
val l = mDbService.getPhotosByFileIds(listOf(fileId))
|
val l = mPhotoDao.getPhotosByFileIds(listOf(fileId))
|
||||||
if (!l.isEmpty() && l[0].mtime == image.mtime) {
|
if (!l.isEmpty() && l[0].mtime == image.mtime) {
|
||||||
// File already exists, remove flag
|
// File already exists, remove flag
|
||||||
mDbService.unflag(fileId)
|
mPhotoDao.unflag(fileId)
|
||||||
Log.v(TAG, "File already exists: $fileId / $baseName")
|
Log.v(TAG, "File already exists: $fileId / $baseName")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete file with same local_id and insert new one
|
// Delete file with same local_id and insert new one
|
||||||
mDbService.deleteFileIds(listOf(fileId))
|
mPhotoDao.deleteFileIds(listOf(fileId))
|
||||||
mDbService.insertImage(image)
|
mPhotoDao.insert(image.photo)
|
||||||
Log.v(TAG, "Inserted file to local DB: $fileId / $baseName")
|
Log.v(TAG, "Inserted file to local DB: $fileId / $baseName")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is in timeline query because it calls the database service */
|
/** This is in timeline query because it calls the database service */
|
||||||
var localFolders: JSONArray
|
var localFolders: JSONArray
|
||||||
get() {
|
get() {
|
||||||
return mDbService.getBuckets().map {
|
return mPhotoDao.getBuckets().map {
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put(Fields.Bucket.ID, it.key)
|
.put(Fields.Bucket.ID, it.id)
|
||||||
.put(Fields.Bucket.NAME, it.value)
|
.put(Fields.Bucket.NAME, it.name)
|
||||||
.put(Fields.Bucket.ENABLED, mConfigService.enabledBucketIds.contains(it.key))
|
.put(Fields.Bucket.ENABLED, mConfigService.enabledBucketIds.contains(it.id))
|
||||||
}.let { JSONArray(it) }
|
}.let { JSONArray(it) }
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
val enabledSet = mutableSetOf<String>()
|
val enabled = mutableListOf<String>()
|
||||||
for (i in 0 until value.length()) {
|
for (i in 0 until value.length()) {
|
||||||
val obj = value.getJSONObject(i)
|
val obj = value.getJSONObject(i)
|
||||||
if (obj.getBoolean(Fields.Bucket.ENABLED)) {
|
if (obj.getBoolean(Fields.Bucket.ENABLED)) {
|
||||||
enabledSet.add(obj.getString(Fields.Bucket.ID))
|
enabled.add(obj.getString(Fields.Bucket.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mConfigService.enabledBucketIds = enabledSet
|
mConfigService.enabledBucketIds = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.8.10'
|
ext.kotlin_version = '1.9.0'
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '7.3.1' apply false
|
id 'com.android.application' version '8.1.0' apply false
|
||||||
id 'com.android.library' version '7.3.1' apply false
|
id 'com.android.library' version '8.1.0' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
|
||||||
}
|
}
|
|
@ -18,4 +18,6 @@ android.useAndroidX=true
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonFinalResIds=false
|
|
@ -1,6 +1,6 @@
|
||||||
#Tue May 02 23:46:18 PDT 2023
|
#Tue May 02 23:46:18 PDT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
Loading…
Reference in New Issue