diff --git a/build.gradle b/build.gradle index a4c3f477..b3eebd41 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,13 @@ android { versionCode = 43 versionName = "0.4.3" vectorDrawables.useSupportLibrary = true + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + arguments += ["room.incremental": "true"] + } + } } sourceSets.all { diff --git a/src/main/kotlin/com/looker/droidify/Common.kt b/src/main/kotlin/com/looker/droidify/Common.kt index 446a2f83..5f948509 100644 --- a/src/main/kotlin/com/looker/droidify/Common.kt +++ b/src/main/kotlin/com/looker/droidify/Common.kt @@ -1,13 +1,39 @@ package com.looker.droidify -object Common { - const val NOTIFICATION_CHANNEL_SYNCING = "syncing" - const val NOTIFICATION_CHANNEL_UPDATES = "updates" - const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading" +const val NOTIFICATION_CHANNEL_SYNCING = "syncing" +const val NOTIFICATION_CHANNEL_UPDATES = "updates" +const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading" - const val NOTIFICATION_ID_SYNCING = 1 - const val NOTIFICATION_ID_UPDATES = 2 - const val NOTIFICATION_ID_DOWNLOADING = 3 +const val NOTIFICATION_ID_SYNCING = 1 +const val NOTIFICATION_ID_UPDATES = 2 +const val NOTIFICATION_ID_DOWNLOADING = 3 + +const val ROW_REPOSITORY_ID = "repository_id" +const val ROW_PACKAGE_NAME = "package_name" +const val ROW_NAME = "name" +const val ROW_SUMMARY = "summary" +const val ROW_DESCRIPTION = "description" +const val ROW_ADDED = "added" +const val ROW_UPDATED = "updated" +const val ROW_VERSION_CODE = "version_code" +const val ROW_SIGNATURES = "signatures" +const val ROW_COMPATIBLE = "compatible" +const val ROW_DATA = "data" +const val ROW_DATA_ITEM = "data_item" +const val ROW_VERSION = "version" +const val ROW_SIGNATURE = "signature" +const val ROW_ID = "_id" +const val ROW_ENABLED = "enabled" +const val ROW_DELETED = "deleted" +const val ROW_CAN_UPDATE = "can_update" +const val ROW_MATCH_RANK = "match_rank" +const val ROW_REPOSITORY_NAME = "repository" +const val ROW_PRODUCT_NAME = "product" +const val ROW_CATEGORY_NAME = "category" +const val ROW_INSTALLED_NAME = "memory_installed" +const val ROW_LOCK_NAME = "memory_lock" + +const val JOB_ID_SYNC = 1 const val PREFS_LANGUAGE = "languages" const val PREFS_LANGUAGE_DEFAULT = "system" diff --git a/src/main/kotlin/com/looker/droidify/MainApplication.kt b/src/main/kotlin/com/looker/droidify/MainApplication.kt index c1ccd68a..838a3188 100644 --- a/src/main/kotlin/com/looker/droidify/MainApplication.kt +++ b/src/main/kotlin/com/looker/droidify/MainApplication.kt @@ -10,7 +10,7 @@ import coil.ImageLoaderFactory import com.looker.droidify.content.Cache import com.looker.droidify.content.Preferences import com.looker.droidify.content.ProductPreferences -import com.looker.droidify.database.Database +import com.looker.droidify.database.DatabaseX import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.network.CoilDownloader import com.looker.droidify.network.Downloader @@ -29,19 +29,21 @@ import java.net.Proxy @Suppress("unused") class MainApplication : Application(), ImageLoaderFactory { + lateinit var db: DatabaseX + override fun onCreate() { super.onCreate() - val databaseUpdated = Database.init(this) + db = DatabaseX.getInstance(applicationContext) Preferences.init(this) ProductPreferences.init(this) - RepositoryUpdater.init() + RepositoryUpdater.init(this) listenApplications() listenPreferences() - if (databaseUpdated) { + /*if (databaseUpdated) { forceSyncAll() - } + }*/ Cache.cleanup(this) updateSyncJob(false) @@ -66,9 +68,9 @@ class MainApplication : Application(), ImageLoaderFactory { null } if (packageInfo != null) { - Database.InstalledAdapter.put(packageInfo.toInstalledItem()) + db.installedDao.put(packageInfo.toInstalledItem()) } else { - Database.InstalledAdapter.delete(packageName) + db.installedDao.delete(packageName) } } } @@ -82,7 +84,7 @@ class MainApplication : Application(), ImageLoaderFactory { val installedItems = packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag) .map { it.toInstalledItem() } - Database.InstalledAdapter.putAll(installedItems) + db.installedDao.put(*installedItems.toTypedArray()) } private fun listenPreferences() { @@ -125,19 +127,19 @@ class MainApplication : Application(), ImageLoaderFactory { private fun updateSyncJob(force: Boolean) { val jobScheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler - val reschedule = force || !jobScheduler.allPendingJobs.any { it.id == Common.JOB_ID_SYNC } + val reschedule = force || !jobScheduler.allPendingJobs.any { it.id == JOB_ID_SYNC } if (reschedule) { val autoSync = Preferences[Preferences.Key.AutoSync] when (autoSync) { Preferences.AutoSync.Never -> { - jobScheduler.cancel(Common.JOB_ID_SYNC) + jobScheduler.cancel(JOB_ID_SYNC) } Preferences.AutoSync.Wifi, Preferences.AutoSync.Always -> { val period = 12 * 60 * 60 * 1000L // 12 hours val wifiOnly = autoSync == Preferences.AutoSync.Wifi jobScheduler.schedule(JobInfo .Builder( - Common.JOB_ID_SYNC, + JOB_ID_SYNC, ComponentName(this, SyncService.Job::class.java) ) .setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY) @@ -181,9 +183,9 @@ class MainApplication : Application(), ImageLoaderFactory { } private fun forceSyncAll() { - Database.RepositoryAdapter.getAll(null).forEach { + db.repositoryDao.all.mapNotNull { it.data }.forEach { if (it.lastModified.isNotEmpty() || it.entityTag.isNotEmpty()) { - Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = "")) + db.repositoryDao.put(it.copy(lastModified = "", entityTag = "")) } } Connection(SyncService::class.java, onBind = { connection, binder -> diff --git a/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt b/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt index 77aa4aa1..96c3ee5d 100644 --- a/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt +++ b/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt @@ -2,7 +2,8 @@ package com.looker.droidify.content import android.content.Context import android.content.SharedPreferences -import com.looker.droidify.database.Database +import com.looker.droidify.database.DatabaseX +import com.looker.droidify.database.Lock import com.looker.droidify.entity.ProductPreference import com.looker.droidify.utility.extension.json.Json import com.looker.droidify.utility.extension.json.parseDictionary @@ -21,17 +22,30 @@ object ProductPreferences { private lateinit var preferences: SharedPreferences private val mutableSubject = MutableSharedFlow>() private val subject = mutableSubject.asSharedFlow() + lateinit var db: DatabaseX fun init(context: Context) { + db = DatabaseX.getInstance(context) preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE) - Database.LockAdapter.putAll(preferences.all.keys - .mapNotNull { packageName -> - this[packageName].databaseVersionCode?.let { Pair(packageName, it) } - }) + db.lockDao.insert(*preferences.all.keys + .mapNotNull { pName -> + this[pName].databaseVersionCode?.let { + Lock().apply { + package_name = pName + version_code = it + } + } + } + .toTypedArray() + ) CoroutineScope(Dispatchers.Default).launch { subject.collect { (packageName, versionCode) -> - if (versionCode != null) Database.LockAdapter.put(Pair(packageName, versionCode)) - else Database.LockAdapter.delete(packageName) + if (versionCode != null) db.lockDao.insert(Lock().apply { + package_name = pName + version_code = versionCode + } + ) + else db.lockDao.delete(pName) } } } diff --git a/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt index 818721bf..67cc459b 100644 --- a/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt +++ b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt @@ -87,9 +87,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { override fun onCreateLoader(id: Int, args: Bundle?): Loader { val request = activeRequests[id]!!.request + val db = DatabaseX.getInstance(requireContext()) return QueryLoader(requireContext()) { when (request) { - is Request.ProductsAvailable -> Database.ProductAdapter + is Request.ProductsAvailable -> db.productDao .query( installed = false, updates = false, @@ -98,7 +99,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { order = request.order, signal = it ) - is Request.ProductsInstalled -> Database.ProductAdapter + is Request.ProductsInstalled -> db.productDao .query( installed = true, updates = false, @@ -107,7 +108,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { order = request.order, signal = it ) - is Request.ProductsUpdates -> Database.ProductAdapter + is Request.ProductsUpdates -> db.productDao .query( installed = true, updates = true, @@ -116,7 +117,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { order = request.order, signal = it ) - is Request.Repositories -> Database.RepositoryAdapter.query(it) + is Request.Repositories -> db.repositoryDao.allCursor } } } diff --git a/src/main/kotlin/com/looker/droidify/database/DAOs.kt b/src/main/kotlin/com/looker/droidify/database/DAOs.kt index d332a6a0..774b410d 100644 --- a/src/main/kotlin/com/looker/droidify/database/DAOs.kt +++ b/src/main/kotlin/com/looker/droidify/database/DAOs.kt @@ -1,57 +1,171 @@ package com.looker.droidify.database -import android.database.SQLException +import android.database.Cursor +import android.os.CancellationSignal import androidx.room.* +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import com.looker.droidify.* +import com.looker.droidify.entity.ProductItem -@Dao -interface RepositoryDao { - @Insert - @Throws(SQLException::class) - fun insert(vararg repository: Repository) + +interface BaseDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg product: T) @Update(onConflict = OnConflictStrategy.REPLACE) - fun update(vararg repository: Repository?) + fun update(vararg obj: T): Int - fun put(repository: Repository) { - if (repository.id >= 0L) update(repository) else insert(repository) + @Delete + fun delete(obj: T) +} + +@Dao +interface RepositoryDao : BaseDao { + fun put(repository: com.looker.droidify.entity.Repository): com.looker.droidify.entity.Repository { + repository.let { + val dbRepo = Repository().apply { + id = it.id + enabled = if (it.enabled) 1 else 0 + deleted = false + data = it + } + val newId = if (repository.id >= 0L) update(dbRepo).toLong() else returnInsert(dbRepo) + return if (newId != repository.id) repository.copy(id = newId) else repository + } } + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun returnInsert(product: Repository): Long + @Query("SELECT * FROM repository WHERE _id = :id and deleted == 0") fun get(id: Long): Repository? + @get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC") + val allCursor: Cursor + @get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC") val all: List @get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC") val allDisabledDeleted: List - @Delete - fun delete(repository: Repository) - @Query("DELETE FROM repository WHERE _id = :id") fun deleteById(vararg id: Long): Int + // TODO optimize @Update(onConflict = OnConflictStrategy.REPLACE) fun markAsDeleted(id: Long) { - update(get(id).apply { this?.deleted = 1 }) + get(id).apply { this?.deleted = true }?.let { update(it) } } } @Dao -interface ProductDao { +interface ProductDao : BaseDao { @Query("SELECT COUNT(*) FROM product WHERE repository_id = :id") fun countForRepository(id: Long): Long @Query("SELECT * FROM product WHERE package_name = :packageName") - fun get(packageName: String): Product? + fun get(packageName: String): List @Query("DELETE FROM product WHERE repository_id = :id") fun deleteById(vararg id: Long): Int + + @RawQuery + fun query( + query: SupportSQLiteQuery + ): Cursor + + // TODO optimize and simplify + @Transaction + fun query( + installed: Boolean, updates: Boolean, searchQuery: String, + section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal? + ): Cursor { + val builder = QueryBuilder() + + val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND + product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND + product.${ROW_SIGNATURES} != ''""" + + builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID}, + product.${ROW_PACKAGE_NAME}, product.${ROW_NAME}, + product.${ROW_SUMMARY}, installed.${ROW_VERSION}, + (COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND + product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} > + COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches) + AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE}, + product.${ROW_DATA_ITEM},""" + + if (searchQuery.isNotEmpty()) { + builder += """(((product.${ROW_NAME} LIKE ? OR + product.${ROW_SUMMARY} LIKE ?) * 7) | + ((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) | + (product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},""" + builder %= List(4) { "%$searchQuery%" } + } else { + builder += "0 AS ${ROW_MATCH_RANK}," + } + + builder += """MAX((product.${ROW_COMPATIBLE} AND + (installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) || + PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product""" + builder += """JOIN $ROW_REPOSITORY_NAME AS repository + ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}""" + builder += """LEFT JOIN $ROW_LOCK_NAME AS lock + ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}""" + + if (!installed && !updates) { + builder += "LEFT" + } + builder += """JOIN $ROW_INSTALLED_NAME AS installed + ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}""" + + if (section is ProductItem.Section.Category) { + builder += """JOIN $ROW_CATEGORY_NAME AS category + ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}""" + } + + builder += """WHERE repository.${ROW_ENABLED} != 0 AND + repository.${ROW_DELETED} == 0""" + + if (section is ProductItem.Section.Category) { + builder += "AND category.${ROW_NAME} = ?" + builder %= section.name + } else if (section is ProductItem.Section.Repository) { + builder += "AND product.${ROW_REPOSITORY_ID} = ?" + builder %= section.id.toString() + } + + if (searchQuery.isNotEmpty()) { + builder += """AND $ROW_MATCH_RANK > 0""" + } + + builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1" + + if (updates) { + builder += "AND $ROW_CAN_UPDATE" + } + builder += "ORDER BY" + + if (searchQuery.isNotEmpty()) { + builder += """$ROW_MATCH_RANK DESC,""" + } + + when (order) { + ProductItem.Order.NAME -> Unit + ProductItem.Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC," + ProductItem.Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC," + }::class + builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC" + + return query(SimpleSQLiteQuery(builder.build())) + } } @Dao -interface CategoryDao { - @Query( +interface CategoryDao : BaseDao { + @get:Query( """SELECT DISTINCT category.name FROM category AS category JOIN repository AS repository @@ -59,31 +173,85 @@ interface CategoryDao { WHERE repository.enabled != 0 AND repository.deleted == 0""" ) - fun getAll(): List + val allNames: List @Query("DELETE FROM category WHERE repository_id = :id") fun deleteById(vararg id: Long): Int } @Dao -interface InstalledDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - @Throws(SQLException::class) - fun insert(vararg installed: Installed) +interface InstalledDao : BaseDao { + fun put(vararg isntalled: com.looker.droidify.entity.InstalledItem) { + isntalled.forEach { + insert(Installed(it.packageName).apply { + version = it.version + version_code = it.versionCode + signature = it.signature + }) + } + } - @Query("SELECT * FROM installed WHERE package_name = :packageName") - fun get(packageName: String): Installed? + @Query("SELECT * FROM memory_installed WHERE package_name = :packageName") + fun get(packageName: String): Cursor - @Query("DELETE FROM installed WHERE package_name = :packageName") + @Query("DELETE FROM memory_installed WHERE package_name = :packageName") fun delete(packageName: String) } @Dao -interface LockDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - @Throws(SQLException::class) - fun insert(vararg lock: Lock) - - @Query("DELETE FROM lock WHERE package_name = :packageName") +interface LockDao : BaseDao { + @Query("DELETE FROM memory_lock WHERE package_name = :packageName") fun delete(packageName: String) +} + +@Dao +interface ProductTempDao : BaseDao { + @get:Query("SELECT * FROM temporary_product") + val all: Array + + @Query("DELETE FROM temporary_product") + fun emptyTable() + + @Insert + fun insertCategory(vararg product: CategoryTemp) + + @Transaction + fun putTemporary(products: List) { + products.forEach { + val signatures = it.signatures.joinToString { ".$it" } + .let { if (it.isNotEmpty()) "$it." else "" } + insert(it.let { + ProductTemp().apply { + repository_id = it.repositoryId + package_name = it.packageName + name = it.name + summary = it.summary + description = it.description + added = it.added + updated = it.updated + version_code = it.versionCode + this.signatures = signatures + compatible = if (it.compatible) 1 else 0 + data = it + data_item = it.item() + } + }) + it.categories.forEach { category -> + insertCategory(CategoryTemp().apply { + repository_id = it.repositoryId + package_name = it.packageName + name = category + }) + } + } + } +} + +@Dao +interface CategoryTempDao : BaseDao { + @get:Query("SELECT * FROM temporary_category") + val all: Array + + @Query("DELETE FROM temporary_category") + fun emptyTable() } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt index 2a8e02ea..72a10b27 100644 --- a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt +++ b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt @@ -10,7 +10,9 @@ import androidx.room.TypeConverters entities = [ Repository::class, Product::class, + ProductTemp::class, Category::class, + CategoryTemp::class, Installed::class, Lock::class ], version = 1 @@ -19,7 +21,9 @@ import androidx.room.TypeConverters abstract class DatabaseX : RoomDatabase() { abstract val repositoryDao: RepositoryDao abstract val productDao: ProductDao + abstract val productTempDao: ProductTempDao abstract val categoryDao: CategoryDao + abstract val categoryTempDao: CategoryTempDao abstract val installedDao: InstalledDao abstract val lockDao: LockDao @@ -46,17 +50,33 @@ abstract class DatabaseX : RoomDatabase() { } fun cleanUp(pairs: Set>) { - val result = pairs.windowed(10, 10, true).map { - val ids = it.map { it.first }.toLongArray() - val productsCount = productDao.deleteById(*ids) - val categoriesCount = categoryDao.deleteById(*ids) - val deleteIds = it.filter { it.second }.map { it.first }.toLongArray() - repositoryDao.deleteById(*deleteIds) - productsCount != 0 || categoriesCount != 0 + runInTransaction { + val result = pairs.windowed(10, 10, true).map { + val ids = it.map { it.first }.toLongArray() + val productsCount = productDao.deleteById(*ids) + val categoriesCount = categoryDao.deleteById(*ids) + val deleteIds = it.filter { it.second }.map { it.first }.toLongArray() + repositoryDao.deleteById(*deleteIds) + productsCount != 0 || categoriesCount != 0 + } } // Use live objects and observers instead /*if (result.any { it }) { com.looker.droidify.database.Database.notifyChanged(com.looker.droidify.database.Database.Subject.Products) }*/ } + + fun finishTemporary(repository: com.looker.droidify.entity.Repository, success: Boolean) { + runInTransaction { + if (success) { + productDao.deleteById(repository.id) + categoryDao.deleteById(repository.id) + productDao.insert(*(productTempDao.all)) + categoryDao.insert(*(categoryTempDao.all)) + repositoryDao.put(repository) + } + productTempDao.emptyTable() + categoryTempDao.emptyTable() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt index c81618dd..86d3bcb1 100644 --- a/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt +++ b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt @@ -33,6 +33,8 @@ class QueryBuilder { this.arguments += arguments } + fun build() = builder.toString() + fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor { val query = builder.toString() val arguments = arguments.toTypedArray() @@ -47,4 +49,4 @@ class QueryBuilder { } return db.rawQuery(query, arguments, signal) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/database/Tables.kt b/src/main/kotlin/com/looker/droidify/database/Tables.kt index 930fd59b..cda3236e 100644 --- a/src/main/kotlin/com/looker/droidify/database/Tables.kt +++ b/src/main/kotlin/com/looker/droidify/database/Tables.kt @@ -4,11 +4,10 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverter -import com.looker.droidify.database.Database.jsonGenerate -import com.looker.droidify.database.Database.jsonParse -import com.looker.droidify.entity.Product import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.Repository +import com.looker.droidify.utility.jsonGenerate +import com.looker.droidify.utility.jsonParse @Entity class Repository { @@ -17,7 +16,7 @@ class Repository { var id: Long = 0 var enabled = 0 - var deleted = 0 + var deleted = false @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var data: Repository? = null @@ -26,54 +25,60 @@ class Repository { @ColumnInfo(name = "_id") var id = 0L - var deleted = 0 + var deleted = false } } -@Entity(primaryKeys = ["repository_id", "package_name"]) -class Product { - var repository_id: Long = 0 +@Entity(tableName = "product", primaryKeys = ["repository_id", "package_name"]) +open class Product { + var repository_id = 0L var package_name = "" var name = "" var summary = "" var description = "" - var added = 0 - var updated = 0 - var version_code = 0 + var added = 0L + var updated = 0L + var version_code = 0L var signatures = "" var compatible = 0 @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - var data: Product? = null + var data: com.looker.droidify.entity.Product? = null @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var data_item: ProductItem? = null } -@Entity(primaryKeys = ["repository_id", "package_name", "name"]) -class Category { +@Entity(tableName = "temporary_product") +class ProductTemp : Product() + +@Entity(tableName = "category", primaryKeys = ["repository_id", "package_name", "name"]) +open class Category { var repository_id: Long = 0 var package_name = "" var name = "" } -@Entity -class Installed { +@Entity(tableName = "temporary_category") +class CategoryTemp : Category() + +@Entity(tableName = "memory_installed") +class Installed(pName: String = "") { @PrimaryKey - var package_name = "" + var package_name = pName var version = "" - var version_code = 0 + var version_code = 0L var signature = "" } -@Entity +@Entity(tableName = "memory_lock") class Lock { @PrimaryKey var package_name = "" - var version_code = 0 + var version_code = 0L } object Converters { @@ -87,11 +92,12 @@ object Converters { @TypeConverter @JvmStatic - fun toProduct(byteArray: ByteArray) = byteArray.jsonParse { Product.deserialize(it) } + fun toProduct(byteArray: ByteArray) = + byteArray.jsonParse { com.looker.droidify.entity.Product.deserialize(it) } @TypeConverter @JvmStatic - fun toByteArray(product: Product) = jsonGenerate(product::serialize) + fun toByteArray(product: com.looker.droidify.entity.Product) = jsonGenerate(product::serialize) @TypeConverter @JvmStatic diff --git a/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt index cc003092..15270351 100644 --- a/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt +++ b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt @@ -3,7 +3,7 @@ package com.looker.droidify.index import android.content.Context import android.net.Uri import com.looker.droidify.content.Cache -import com.looker.droidify.database.Database +import com.looker.droidify.database.DatabaseX import com.looker.droidify.entity.Product import com.looker.droidify.entity.Release import com.looker.droidify.entity.Repository @@ -59,29 +59,28 @@ object RepositoryUpdater { private val updaterLock = Any() private val cleanupLock = Any() + lateinit var db: DatabaseX - fun init() { - + fun init(context: Context) { + db = DatabaseX.getInstance(context) var lastDisabled = setOf() Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + //.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) .flatMapSingle { RxUtils.querySingle { - Database.RepositoryAdapter.getAllDisabledDeleted( - it - ) + db.repositoryDao.allDisabledDeleted } } .forEach { it -> - val newDisabled = it.asSequence().filter { !it.second }.map { it.first }.toSet() + val newDisabled = it.asSequence().filter { !it.deleted }.map { it.id }.toSet() val disabled = newDisabled - lastDisabled lastDisabled = newDisabled - val deleted = it.asSequence().filter { it.second }.map { it.first }.toSet() + val deleted = it.asSequence().filter { it.deleted }.map { it.id }.toSet() if (disabled.isNotEmpty() || deleted.isNotEmpty()) { val pairs = (disabled.asSequence().map { Pair(it, false) } + deleted.asSequence().map { Pair(it, true) }).toSet() - synchronized(cleanupLock) { Database.RepositoryAdapter.cleanup(pairs) } + synchronized(cleanupLock) { db.cleanUp(pairs) } } } } @@ -193,12 +192,14 @@ object RepositoryUpdater { file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit, ): Boolean { var rollback = true + val db = DatabaseX.getInstance(context) return synchronized(updaterLock) { try { val jarFile = JarFile(file, true) val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry val total = indexEntry.size - Database.UpdaterAdapter.createTemporaryTable() + db.productTempDao.emptyTable() + db.categoryTempDao.emptyTable() val features = context.packageManager.systemAvailableFeatures .asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen") @@ -231,7 +232,7 @@ object RepositoryUpdater { } products += transformProduct(product, features, unstable) if (products.size >= 50) { - Database.UpdaterAdapter.putTemporary(products) + db.productTempDao.putTemporary(products) products.clear() } } @@ -249,7 +250,7 @@ object RepositoryUpdater { throw InterruptedException() } if (products.isNotEmpty()) { - Database.UpdaterAdapter.putTemporary(products) + db.productTempDao.putTemporary(products) products.clear() } Pair(changedRepository, certificateFromIndex) @@ -334,7 +335,7 @@ object RepositoryUpdater { progress.toLong(), totalCount.toLong() ) - Database.UpdaterAdapter.putTemporary(products + db.productTempDao.putTemporary(products .map { transformProduct(it, features, unstable) }) } } @@ -407,7 +408,7 @@ object RepositoryUpdater { } callback(Stage.COMMIT, 0, null) synchronized(cleanupLock) { - Database.UpdaterAdapter.finishTemporary( + db.finishTemporary( commitRepository, true ) @@ -423,7 +424,7 @@ object RepositoryUpdater { } finally { file.delete() if (rollback) { - Database.UpdaterAdapter.finishTemporary(repository, false) + db.finishTemporary(repository, false) } } } diff --git a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt index 4de79e89..3adca521 100644 --- a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt @@ -17,7 +17,6 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.looker.droidify.R -import com.looker.droidify.database.Database import com.looker.droidify.databinding.EditRepositoryBinding import com.looker.droidify.entity.Repository import com.looker.droidify.network.Downloader @@ -154,7 +153,7 @@ class EditRepositoryFragment() : ScreenFragment() { } if (savedInstanceState == null) { - val repository = repositoryId?.let(Database.RepositoryAdapter::get) + val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.data } if (repository == null) { val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -233,7 +232,7 @@ class EditRepositoryFragment() : ScreenFragment() { } lifecycleScope.launch { - val list = Database.RepositoryAdapter.getAll(null) + val list = screenActivity.db.repositoryDao.all.mapNotNull { it.data } takenAddresses = list.asSequence().filter { it.id != repositoryId } .flatMap { (it.mirrors + it.address).asSequence() } .map { it.withoutKnownPath }.toSet() @@ -449,10 +448,10 @@ class EditRepositoryFragment() : ScreenFragment() { MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager) invalidateState() } else { - val repository = repositoryId?.let(Database.RepositoryAdapter::get) + val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.data } ?.edit(address, fingerprint, authentication) ?: Repository.newRepository(address, fingerprint, authentication) - val changedRepository = Database.RepositoryAdapter.put(repository) + val changedRepository = screenActivity.db.repositoryDao.put(repository) if (repositoryId == null && changedRepository.enabled) { binder.sync(changedRepository) } diff --git a/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt b/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt index f4bbf172..f873d3a2 100644 --- a/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt +++ b/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt @@ -9,11 +9,11 @@ import com.google.android.material.card.MaterialCardView import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.textview.MaterialTextView import com.looker.droidify.R -import com.looker.droidify.database.Database import com.looker.droidify.entity.Repository import com.looker.droidify.utility.extension.resources.clear import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.inflate +import com.looker.droidify.utility.getRepository import com.looker.droidify.widget.CursorRecyclerAdapter class RepositoriesAdapter( @@ -45,7 +45,7 @@ class RepositoriesAdapter( } private fun getRepository(position: Int): Repository { - return Database.RepositoryAdapter.transform(moveTo(position)) + return moveTo(position).getRepository() } override fun onCreateViewHolder( diff --git a/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt index bd1904fa..82beff60 100644 --- a/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt @@ -12,7 +12,6 @@ import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.widget.NestedScrollView import androidx.lifecycle.lifecycleScope import com.looker.droidify.R -import com.looker.droidify.database.Database import com.looker.droidify.databinding.TitleTextItemBinding import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService @@ -99,7 +98,7 @@ class RepositoryFragment() : ScreenFragment() { } private fun updateRepositoryView() { - val repository = Database.RepositoryAdapter.get(repositoryId) + val repository = screenActivity.db.repositoryDao.get(repositoryId)?.data val layout = layout!! layout.removeAllViews() if (repository == null) { @@ -125,7 +124,7 @@ class RepositoryFragment() : ScreenFragment() { if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { layout.addTitleText( R.string.number_of_applications, - Database.ProductAdapter.getCount(repository.id).toString() + screenActivity.db.productDao.countForRepository(repository.id).toString() ) } } else { diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt index ffb9d57a..f1b927df 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -10,6 +10,7 @@ import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.google.android.material.circularreveal.CircularRevealFrameLayout +import com.looker.droidify.MainApplication import com.looker.droidify.R import com.looker.droidify.content.Preferences import com.looker.droidify.database.CursorOwner @@ -25,6 +26,9 @@ abstract class ScreenActivity : AppCompatActivity() { private const val STATE_FRAGMENT_STACK = "fragmentStack" } + val db + get() = (application as MainApplication).db + sealed class SpecialIntent { object Updates : SpecialIntent() class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent() diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenshotsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenshotsFragment.kt index e921a28c..93320579 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenshotsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenshotsFragment.kt @@ -19,7 +19,7 @@ import androidx.viewpager2.widget.ViewPager2 import coil.load import com.google.android.material.imageview.ShapeableImageView import com.looker.droidify.R -import com.looker.droidify.database.Database +import com.looker.droidify.database.DatabaseX import com.looker.droidify.entity.Product import com.looker.droidify.entity.Repository import com.looker.droidify.graphics.PaddingDrawable @@ -68,6 +68,7 @@ class ScreenshotsFragment() : DialogFragment() { val window = dialog.window val decorView = window?.decorView + val db = DatabaseX.getInstance(requireContext()) if (window != null) { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -132,13 +133,17 @@ class ScreenshotsFragment() : DialogFragment() { var restored = false productDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Products)) + //.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } } + .flatMapSingle { + RxUtils.querySingle { + db.productDao.get(packageName).mapNotNull { it?.data } + } + } .map { it -> Pair( it.find { it.repositoryId == repositoryId }, - Database.RepositoryAdapter.get(repositoryId) + db.repositoryDao.get(repositoryId)?.data ) } .observeOn(AndroidSchedulers.mainThread()) diff --git a/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt index dd79c909..86853e10 100644 --- a/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt @@ -19,7 +19,6 @@ import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.textview.MaterialTextView import com.looker.droidify.R import com.looker.droidify.content.Preferences -import com.looker.droidify.database.Database import com.looker.droidify.databinding.TabsToolbarBinding import com.looker.droidify.entity.ProductItem import com.looker.droidify.service.Connection @@ -235,9 +234,9 @@ class TabsFragment : ScreenFragment() { } categoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Products)) + //.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } } + .flatMapSingle { RxUtils.querySingle { screenActivity.db.categoryDao.allNames } } .observeOn(AndroidSchedulers.mainThread()) .subscribe { setSectionsAndUpdate( @@ -246,9 +245,9 @@ class TabsFragment : ScreenFragment() { ) } repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + //.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } } } .observeOn(AndroidSchedulers.mainThread()) .subscribe { it -> setSectionsAndUpdate(null, it.asSequence().filter { it.enabled } diff --git a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt index 27db7c13..327139d5 100644 --- a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt +++ b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt @@ -9,10 +9,7 @@ import android.content.Intent import android.net.Uri import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat -import com.looker.droidify.BuildConfig -import com.looker.droidify.Common -import com.looker.droidify.MainActivity -import com.looker.droidify.R +import com.looker.droidify.* import com.looker.droidify.content.Cache import com.looker.droidify.entity.Release import com.looker.droidify.entity.Repository @@ -121,7 +118,7 @@ class DownloadService : ConnectionService() { } else { cancelTasks(packageName) cancelCurrentTask(packageName) - notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) + notificationManager.cancel(task.notificationTag, NOTIFICATION_ID_DOWNLOADING) tasks += task if (currentTask == null) { handleDownload() @@ -146,7 +143,7 @@ class DownloadService : ConnectionService() { if (Android.sdk(26)) { NotificationChannel( - Common.NOTIFICATION_CHANNEL_DOWNLOADING, + NOTIFICATION_CHANNEL_DOWNLOADING, getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW ) .apply { setShowBadge(false) } @@ -209,9 +206,9 @@ class DownloadService : ConnectionService() { private fun showNotificationError(task: Task, errorType: ErrorType) { notificationManager.notify(task.notificationTag, - Common.NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_DOWNLOADING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setAutoCancel(true) .setSmallIcon(android.R.drawable.stat_sys_warning) .setColor( @@ -276,8 +273,8 @@ class DownloadService : ConnectionService() { private fun showNotificationInstall(task: Task) { notificationManager.notify( - task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + task.notificationTag, NOTIFICATION_ID_DOWNLOADING, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setAutoCancel(true) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setColor( @@ -367,7 +364,7 @@ class DownloadService : ConnectionService() { private val stateNotificationBuilder by lazy { NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setSmallIcon(android.R.drawable.stat_sys_download) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -389,7 +386,7 @@ class DownloadService : ConnectionService() { private fun publishForegroundState(force: Boolean, state: State) { if (force || currentTask != null) { currentTask = currentTask?.copy(lastState = state) - startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { + startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { when (state) { is State.Connecting -> { setContentTitle(getString(R.string.downloading_FORMAT, state.name)) diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 3982b6fb..2ff49219 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -13,12 +13,9 @@ import android.text.style.ForegroundColorSpan import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment -import com.looker.droidify.BuildConfig -import com.looker.droidify.Common -import com.looker.droidify.MainActivity -import com.looker.droidify.R +import com.looker.droidify.* import com.looker.droidify.content.Preferences -import com.looker.droidify.database.Database +import com.looker.droidify.database.DatabaseX import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.Repository import com.looker.droidify.index.RepositoryUpdater @@ -28,6 +25,7 @@ import com.looker.droidify.utility.extension.android.asSequence import com.looker.droidify.utility.extension.android.notificationManager import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.text.formatSize +import com.looker.droidify.utility.getProductItem import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers @@ -99,7 +97,7 @@ class SyncService : ConnectionService() { } fun sync(request: SyncRequest) { - val ids = Database.RepositoryAdapter.getAll(null) + val ids = db.repositoryDao.all.mapNotNull { it.data } .asSequence().filter { it.enabled }.map { it.id }.toList() sync(ids, request) } @@ -120,12 +118,12 @@ class SyncService : ConnectionService() { fun setUpdateNotificationBlocker(fragment: Fragment?) { updateNotificationBlockerFragment = fragment?.let(::WeakReference) if (fragment != null) { - notificationManager.cancel(Common.NOTIFICATION_ID_UPDATES) + notificationManager.cancel(NOTIFICATION_ID_UPDATES) } } fun setEnabled(repository: Repository, enabled: Boolean): Boolean { - Database.RepositoryAdapter.put(repository.enable(enabled)) + db.repositoryDao.put(repository.enable(enabled)) if (enabled) { if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) { tasks += Task(repository.id, true) @@ -144,10 +142,10 @@ class SyncService : ConnectionService() { } fun deleteRepository(repositoryId: Long): Boolean { - val repository = Database.RepositoryAdapter.get(repositoryId) + val repository = db.repositoryDao.get(repositoryId)?.data return repository != null && run { setEnabled(repository, false) - Database.RepositoryAdapter.markAsDeleted(repository.id) + db.repositoryDao.markAsDeleted(repository.id) true } } @@ -155,19 +153,21 @@ class SyncService : ConnectionService() { private val binder = Binder() override fun onBind(intent: Intent): Binder = binder + lateinit var db: DatabaseX override fun onCreate() { super.onCreate() + db = DatabaseX.getInstance(applicationContext) if (Android.sdk(26)) { NotificationChannel( - Common.NOTIFICATION_CHANNEL_SYNCING, + NOTIFICATION_CHANNEL_SYNCING, getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW ) .apply { setShowBadge(false) } .let(notificationManager::createNotificationChannel) NotificationChannel( - Common.NOTIFICATION_CHANNEL_UPDATES, + NOTIFICATION_CHANNEL_UPDATES, getString(R.string.updates), NotificationManager.IMPORTANCE_LOW ) .let(notificationManager::createNotificationChannel) @@ -210,8 +210,8 @@ class SyncService : ConnectionService() { private fun showNotificationError(repository: Repository, exception: Exception) { notificationManager.notify( - "repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) + "repository-${repository.id}", NOTIFICATION_ID_SYNCING, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_SYNCING) .setSmallIcon(android.R.drawable.stat_sys_warning) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -237,7 +237,7 @@ class SyncService : ConnectionService() { private val stateNotificationBuilder by lazy { NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) + .Builder(this, NOTIFICATION_CHANNEL_SYNCING) .setSmallIcon(R.drawable.ic_sync) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -260,7 +260,7 @@ class SyncService : ConnectionService() { if (force || currentTask?.lastState != state) { currentTask = currentTask?.copy(lastState = state) if (started == Started.MANUAL) { - startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { + startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { when (state) { is State.Connecting -> { setContentTitle(getString(R.string.syncing_FORMAT, state.name)) @@ -330,7 +330,7 @@ class SyncService : ConnectionService() { if (currentTask == null) { if (tasks.isNotEmpty()) { val task = tasks.removeAt(0) - val repository = Database.RepositoryAdapter.get(task.repositoryId) + val repository = db.repositoryDao.get(task.repositoryId)?.data if (repository != null && repository.enabled) { val lastStarted = started val newStarted = @@ -376,7 +376,7 @@ class SyncService : ConnectionService() { if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { val disposable = RxUtils .querySingle { it -> - Database.ProductAdapter + db.productDao .query( installed = true, updates = true, @@ -386,8 +386,7 @@ class SyncService : ConnectionService() { signal = it ) .use { - it.asSequence().map(Database.ProductAdapter::transformItem) - .toList() + it.asSequence().map { it.getProductItem() }.toList() } } .subscribeOn(Schedulers.io()) @@ -419,8 +418,8 @@ class SyncService : ConnectionService() { val maxUpdates = 5 fun T.applyHack(callback: T.() -> Unit): T = apply(callback) notificationManager.notify( - Common.NOTIFICATION_ID_UPDATES, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) + NOTIFICATION_ID_UPDATES, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_UPDATES) .setSmallIcon(R.drawable.ic_new_releases) .setContentTitle(getString(R.string.new_updates_available)) .setContentText( diff --git a/src/main/kotlin/com/looker/droidify/ui/adapters/AppListAdapter.kt b/src/main/kotlin/com/looker/droidify/ui/adapters/AppListAdapter.kt index bbd70dbc..d9968a16 100644 --- a/src/main/kotlin/com/looker/droidify/ui/adapters/AppListAdapter.kt +++ b/src/main/kotlin/com/looker/droidify/ui/adapters/AppListAdapter.kt @@ -16,13 +16,13 @@ import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.textview.MaterialTextView import com.looker.droidify.R import com.looker.droidify.content.Preferences -import com.looker.droidify.database.Database import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.Repository import com.looker.droidify.network.CoilDownloader import com.looker.droidify.utility.Utils import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.text.nullIfEmpty +import com.looker.droidify.utility.getProductItem import com.looker.droidify.widget.CursorRecyclerAdapter class AppListAdapter(private val onClick: (ProductItem) -> Unit) : @@ -113,7 +113,7 @@ class AppListAdapter(private val onClick: (ProductItem) -> Unit) : } private fun getProductItem(position: Int): ProductItem { - return Database.ProductAdapter.transformItem(moveTo(position)) + return moveTo(position).getProductItem() } override fun onCreateViewHolder( diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/AppDetailFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/AppDetailFragment.kt index ae8c1929..08c3c5ec 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/AppDetailFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/AppDetailFragment.kt @@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.looker.droidify.R import com.looker.droidify.content.ProductPreferences -import com.looker.droidify.database.Database import com.looker.droidify.entity.* import com.looker.droidify.installer.AppInstaller import com.looker.droidify.screen.MessageDialog @@ -32,6 +31,7 @@ import com.looker.droidify.utility.Utils.rootInstallerEnabled import com.looker.droidify.utility.Utils.startUpdate import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.text.trimAfter +import com.looker.droidify.utility.getInstalledItem import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable @@ -129,12 +129,16 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { var first = true productDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Products)) + //.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } } + .flatMapSingle { + RxUtils.querySingle { + screenActivity.db.productDao.get(packageName).mapNotNull { it?.data } + } + } .flatMapSingle { products -> RxUtils - .querySingle { Database.RepositoryAdapter.getAll(it) } + .querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } } .map { it -> it.asSequence().map { Pair(it.id, it) }.toMap() .let { @@ -151,7 +155,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { } .flatMapSingle { products -> RxUtils - .querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) } + .querySingle { Nullable(screenActivity.db.installedDao.get(packageName).getInstalledItem()) } .map { Pair(products, it) } } .observeOn(AndroidSchedulers.mainThread()) diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/AppListFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/AppListFragment.kt index fb6ba6a5..33735a0c 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/AppListFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/AppListFragment.kt @@ -13,7 +13,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.looker.droidify.R import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database import com.looker.droidify.entity.ProductItem import com.looker.droidify.screen.BaseFragment import com.looker.droidify.ui.adapters.AppListAdapter @@ -78,10 +77,10 @@ class AppListFragment() : BaseFragment(), CursorOwner.Callback { screenActivity.cursorOwner.attach(this, viewModel.request(source)) repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + //.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } - .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } + .flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } } } + .map { it.asSequence().map { Pair(it.id, it) }.toMap() } .observeOn(AndroidSchedulers.mainThread()) .subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it } } diff --git a/src/main/kotlin/com/looker/droidify/utility/Utils.kt b/src/main/kotlin/com/looker/droidify/utility/Utils.kt index 58e42d43..517503d0 100644 --- a/src/main/kotlin/com/looker/droidify/utility/Utils.kt +++ b/src/main/kotlin/com/looker/droidify/utility/Utils.kt @@ -6,18 +6,20 @@ import android.content.pm.Signature import android.content.res.Configuration import android.graphics.drawable.Drawable import android.os.Build -import com.looker.droidify.BuildConfig -import com.looker.droidify.Common.PREFS_LANGUAGE_DEFAULT -import com.looker.droidify.R +import com.looker.droidify.* import com.looker.droidify.content.Preferences import com.looker.droidify.entity.InstalledItem import com.looker.droidify.entity.Product +import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.Repository import com.looker.droidify.service.Connection import com.looker.droidify.service.DownloadService import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.singleSignature import com.looker.droidify.utility.extension.android.versionCodeCompat +import com.looker.droidify.utility.extension.json.Json +import com.looker.droidify.utility.extension.json.parseDictionary +import com.looker.droidify.utility.extension.json.writeDictionary import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getDrawableCompat import com.looker.droidify.utility.extension.text.hex @@ -167,3 +169,51 @@ object Utils { else -> Locale(localeCode) } } + +fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA)) + .jsonParse { + Product.deserialize(it).apply { + this.repositoryId = getLong(getColumnIndex(ROW_REPOSITORY_ID)) + this.description = getString(getColumnIndex(ROW_DESCRIPTION)) + } + } + + +fun Cursor.getProductItem(): ProductItem = getBlob(getColumnIndex(ROW_DATA_ITEM)) + .jsonParse { + ProductItem.deserialize(it).apply { + this.repositoryId = getLong(getColumnIndex(ROW_REPOSITORY_ID)) + this.packageName = getString(getColumnIndex(ROW_PACKAGE_NAME)) + this.name = getString(getColumnIndex(ROW_NAME)) + this.summary = getString(getColumnIndex(ROW_SUMMARY)) + this.installedVersion = getString(getColumnIndex(ROW_VERSION)) + .orEmpty() + this.compatible = getInt(getColumnIndex(ROW_COMPATIBLE)) != 0 + this.canUpdate = getInt(getColumnIndex(ROW_CAN_UPDATE)) != 0 + this.matchRank = getInt(getColumnIndex(ROW_MATCH_RANK)) + } + } + +fun Cursor.getRepository(): Repository = getBlob(getColumnIndex(ROW_DATA)) + .jsonParse { + Repository.deserialize(it).apply { + this.id = getLong(getColumnIndex(ROW_ID)) + } + } + +fun Cursor.getInstalledItem(): InstalledItem = InstalledItem( + getString(getColumnIndex(ROW_PACKAGE_NAME)), + getString(getColumnIndex(ROW_VERSION)), + getLong(getColumnIndex(ROW_VERSION_CODE)), + getString(getColumnIndex(ROW_SIGNATURE)) +) + +fun ByteArray.jsonParse(callback: (JsonParser) -> T): T { + return Json.factory.createParser(this).use { it.parseDictionary(callback) } +} + +fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray { + val outputStream = ByteArrayOutputStream() + Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) } + return outputStream.toByteArray() +} \ No newline at end of file