Merge DB migration branch (Warning: possible conflict solving failures)

This commit is contained in:
machiav3lli 2021-12-24 13:20:38 +01:00
commit 5386c9d843
22 changed files with 477 additions and 175 deletions

View File

@ -25,6 +25,13 @@ android {
versionCode = 43 versionCode = 43
versionName = "0.4.3" versionName = "0.4.3"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
arguments += ["room.incremental": "true"]
}
}
} }
sourceSets.all { sourceSets.all {

View File

@ -1,13 +1,39 @@
package com.looker.droidify package com.looker.droidify
object Common { const val NOTIFICATION_CHANNEL_SYNCING = "syncing"
const val NOTIFICATION_CHANNEL_SYNCING = "syncing" const val NOTIFICATION_CHANNEL_UPDATES = "updates"
const val NOTIFICATION_CHANNEL_UPDATES = "updates" const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
const val NOTIFICATION_ID_SYNCING = 1 const val NOTIFICATION_ID_SYNCING = 1
const val NOTIFICATION_ID_UPDATES = 2 const val NOTIFICATION_ID_UPDATES = 2
const val NOTIFICATION_ID_DOWNLOADING = 3 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 = "languages"
const val PREFS_LANGUAGE_DEFAULT = "system" const val PREFS_LANGUAGE_DEFAULT = "system"

View File

@ -10,7 +10,7 @@ import coil.ImageLoaderFactory
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.content.ProductPreferences 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.index.RepositoryUpdater
import com.looker.droidify.network.CoilDownloader import com.looker.droidify.network.CoilDownloader
import com.looker.droidify.network.Downloader import com.looker.droidify.network.Downloader
@ -29,19 +29,21 @@ import java.net.Proxy
@Suppress("unused") @Suppress("unused")
class MainApplication : Application(), ImageLoaderFactory { class MainApplication : Application(), ImageLoaderFactory {
lateinit var db: DatabaseX
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val databaseUpdated = Database.init(this) db = DatabaseX.getInstance(applicationContext)
Preferences.init(this) Preferences.init(this)
ProductPreferences.init(this) ProductPreferences.init(this)
RepositoryUpdater.init() RepositoryUpdater.init(this)
listenApplications() listenApplications()
listenPreferences() listenPreferences()
if (databaseUpdated) { /*if (databaseUpdated) {
forceSyncAll() forceSyncAll()
} }*/
Cache.cleanup(this) Cache.cleanup(this)
updateSyncJob(false) updateSyncJob(false)
@ -66,9 +68,9 @@ class MainApplication : Application(), ImageLoaderFactory {
null null
} }
if (packageInfo != null) { if (packageInfo != null) {
Database.InstalledAdapter.put(packageInfo.toInstalledItem()) db.installedDao.put(packageInfo.toInstalledItem())
} else { } else {
Database.InstalledAdapter.delete(packageName) db.installedDao.delete(packageName)
} }
} }
} }
@ -82,7 +84,7 @@ class MainApplication : Application(), ImageLoaderFactory {
val installedItems = val installedItems =
packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag) packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag)
.map { it.toInstalledItem() } .map { it.toInstalledItem() }
Database.InstalledAdapter.putAll(installedItems) db.installedDao.put(*installedItems.toTypedArray())
} }
private fun listenPreferences() { private fun listenPreferences() {
@ -125,19 +127,19 @@ class MainApplication : Application(), ImageLoaderFactory {
private fun updateSyncJob(force: Boolean) { private fun updateSyncJob(force: Boolean) {
val jobScheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler 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) { if (reschedule) {
val autoSync = Preferences[Preferences.Key.AutoSync] val autoSync = Preferences[Preferences.Key.AutoSync]
when (autoSync) { when (autoSync) {
Preferences.AutoSync.Never -> { Preferences.AutoSync.Never -> {
jobScheduler.cancel(Common.JOB_ID_SYNC) jobScheduler.cancel(JOB_ID_SYNC)
} }
Preferences.AutoSync.Wifi, Preferences.AutoSync.Always -> { Preferences.AutoSync.Wifi, Preferences.AutoSync.Always -> {
val period = 12 * 60 * 60 * 1000L // 12 hours val period = 12 * 60 * 60 * 1000L // 12 hours
val wifiOnly = autoSync == Preferences.AutoSync.Wifi val wifiOnly = autoSync == Preferences.AutoSync.Wifi
jobScheduler.schedule(JobInfo jobScheduler.schedule(JobInfo
.Builder( .Builder(
Common.JOB_ID_SYNC, JOB_ID_SYNC,
ComponentName(this, SyncService.Job::class.java) ComponentName(this, SyncService.Job::class.java)
) )
.setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY) .setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY)
@ -181,9 +183,9 @@ class MainApplication : Application(), ImageLoaderFactory {
} }
private fun forceSyncAll() { private fun forceSyncAll() {
Database.RepositoryAdapter.getAll(null).forEach { db.repositoryDao.all.mapNotNull { it.data }.forEach {
if (it.lastModified.isNotEmpty() || it.entityTag.isNotEmpty()) { 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 -> Connection(SyncService::class.java, onBind = { connection, binder ->

View File

@ -2,7 +2,8 @@ package com.looker.droidify.content
import android.content.Context import android.content.Context
import android.content.SharedPreferences 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.entity.ProductPreference
import com.looker.droidify.utility.extension.json.Json import com.looker.droidify.utility.extension.json.Json
import com.looker.droidify.utility.extension.json.parseDictionary import com.looker.droidify.utility.extension.json.parseDictionary
@ -21,17 +22,30 @@ object ProductPreferences {
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private val mutableSubject = MutableSharedFlow<Pair<String, Long?>>() private val mutableSubject = MutableSharedFlow<Pair<String, Long?>>()
private val subject = mutableSubject.asSharedFlow() private val subject = mutableSubject.asSharedFlow()
lateinit var db: DatabaseX
fun init(context: Context) { fun init(context: Context) {
db = DatabaseX.getInstance(context)
preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE) preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE)
Database.LockAdapter.putAll(preferences.all.keys db.lockDao.insert(*preferences.all.keys
.mapNotNull { packageName -> .mapNotNull { pName ->
this[packageName].databaseVersionCode?.let { Pair(packageName, it) } this[pName].databaseVersionCode?.let {
}) Lock().apply {
package_name = pName
version_code = it
}
}
}
.toTypedArray()
)
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
subject.collect { (packageName, versionCode) -> subject.collect { (packageName, versionCode) ->
if (versionCode != null) Database.LockAdapter.put(Pair(packageName, versionCode)) if (versionCode != null) db.lockDao.insert(Lock().apply {
else Database.LockAdapter.delete(packageName) package_name = pName
version_code = versionCode
}
)
else db.lockDao.delete(pName)
} }
} }
} }

View File

@ -87,9 +87,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
val request = activeRequests[id]!!.request val request = activeRequests[id]!!.request
val db = DatabaseX.getInstance(requireContext())
return QueryLoader(requireContext()) { return QueryLoader(requireContext()) {
when (request) { when (request) {
is Request.ProductsAvailable -> Database.ProductAdapter is Request.ProductsAvailable -> db.productDao
.query( .query(
installed = false, installed = false,
updates = false, updates = false,
@ -98,7 +99,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order, order = request.order,
signal = it signal = it
) )
is Request.ProductsInstalled -> Database.ProductAdapter is Request.ProductsInstalled -> db.productDao
.query( .query(
installed = true, installed = true,
updates = false, updates = false,
@ -107,7 +108,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order, order = request.order,
signal = it signal = it
) )
is Request.ProductsUpdates -> Database.ProductAdapter is Request.ProductsUpdates -> db.productDao
.query( .query(
installed = true, installed = true,
updates = true, updates = true,
@ -116,7 +117,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order, order = request.order,
signal = it signal = it
) )
is Request.Repositories -> Database.RepositoryAdapter.query(it) is Request.Repositories -> db.repositoryDao.allCursor
} }
} }
} }

View File

@ -1,57 +1,171 @@
package com.looker.droidify.database package com.looker.droidify.database
import android.database.SQLException import android.database.Cursor
import android.os.CancellationSignal
import androidx.room.* 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 { interface BaseDao<T> {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
@Throws(SQLException::class) fun insert(vararg product: T)
fun insert(vararg repository: Repository)
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg repository: Repository?) fun update(vararg obj: T): Int
fun put(repository: Repository) { @Delete
if (repository.id >= 0L) update(repository) else insert(repository) fun delete(obj: T)
}
@Dao
interface RepositoryDao : BaseDao<Repository> {
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") @Query("SELECT * FROM repository WHERE _id = :id and deleted == 0")
fun get(id: Long): Repository? 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") @get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
val all: List<Repository> val all: List<Repository>
@get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC") @get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC")
val allDisabledDeleted: List<Repository.IdAndDeleted> val allDisabledDeleted: List<Repository.IdAndDeleted>
@Delete
fun delete(repository: Repository)
@Query("DELETE FROM repository WHERE _id = :id") @Query("DELETE FROM repository WHERE _id = :id")
fun deleteById(vararg id: Long): Int fun deleteById(vararg id: Long): Int
// TODO optimize
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)
fun markAsDeleted(id: Long) { fun markAsDeleted(id: Long) {
update(get(id).apply { this?.deleted = 1 }) get(id).apply { this?.deleted = true }?.let { update(it) }
} }
} }
@Dao @Dao
interface ProductDao { interface ProductDao : BaseDao<Product> {
@Query("SELECT COUNT(*) FROM product WHERE repository_id = :id") @Query("SELECT COUNT(*) FROM product WHERE repository_id = :id")
fun countForRepository(id: Long): Long fun countForRepository(id: Long): Long
@Query("SELECT * FROM product WHERE package_name = :packageName") @Query("SELECT * FROM product WHERE package_name = :packageName")
fun get(packageName: String): Product? fun get(packageName: String): List<Product?>
@Query("DELETE FROM product WHERE repository_id = :id") @Query("DELETE FROM product WHERE repository_id = :id")
fun deleteById(vararg id: Long): Int 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 @Dao
interface CategoryDao { interface CategoryDao : BaseDao<Category> {
@Query( @get:Query(
"""SELECT DISTINCT category.name """SELECT DISTINCT category.name
FROM category AS category FROM category AS category
JOIN repository AS repository JOIN repository AS repository
@ -59,31 +173,85 @@ interface CategoryDao {
WHERE repository.enabled != 0 AND WHERE repository.enabled != 0 AND
repository.deleted == 0""" repository.deleted == 0"""
) )
fun getAll(): List<String> val allNames: List<String>
@Query("DELETE FROM category WHERE repository_id = :id") @Query("DELETE FROM category WHERE repository_id = :id")
fun deleteById(vararg id: Long): Int fun deleteById(vararg id: Long): Int
} }
@Dao @Dao
interface InstalledDao { interface InstalledDao : BaseDao<Installed> {
@Insert(onConflict = OnConflictStrategy.REPLACE) fun put(vararg isntalled: com.looker.droidify.entity.InstalledItem) {
@Throws(SQLException::class) isntalled.forEach {
fun insert(vararg installed: Installed) insert(Installed(it.packageName).apply {
version = it.version
version_code = it.versionCode
signature = it.signature
})
}
}
@Query("SELECT * FROM installed WHERE package_name = :packageName") @Query("SELECT * FROM memory_installed WHERE package_name = :packageName")
fun get(packageName: String): Installed? 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) fun delete(packageName: String)
} }
@Dao @Dao
interface LockDao { interface LockDao : BaseDao<Lock> {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Query("DELETE FROM memory_lock WHERE package_name = :packageName")
@Throws(SQLException::class)
fun insert(vararg lock: Lock)
@Query("DELETE FROM lock WHERE package_name = :packageName")
fun delete(packageName: String) fun delete(packageName: String)
} }
@Dao
interface ProductTempDao : BaseDao<ProductTemp> {
@get:Query("SELECT * FROM temporary_product")
val all: Array<ProductTemp>
@Query("DELETE FROM temporary_product")
fun emptyTable()
@Insert
fun insertCategory(vararg product: CategoryTemp)
@Transaction
fun putTemporary(products: List<com.looker.droidify.entity.Product>) {
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<CategoryTemp> {
@get:Query("SELECT * FROM temporary_category")
val all: Array<CategoryTemp>
@Query("DELETE FROM temporary_category")
fun emptyTable()
}

View File

@ -10,7 +10,9 @@ import androidx.room.TypeConverters
entities = [ entities = [
Repository::class, Repository::class,
Product::class, Product::class,
ProductTemp::class,
Category::class, Category::class,
CategoryTemp::class,
Installed::class, Installed::class,
Lock::class Lock::class
], version = 1 ], version = 1
@ -19,7 +21,9 @@ import androidx.room.TypeConverters
abstract class DatabaseX : RoomDatabase() { abstract class DatabaseX : RoomDatabase() {
abstract val repositoryDao: RepositoryDao abstract val repositoryDao: RepositoryDao
abstract val productDao: ProductDao abstract val productDao: ProductDao
abstract val productTempDao: ProductTempDao
abstract val categoryDao: CategoryDao abstract val categoryDao: CategoryDao
abstract val categoryTempDao: CategoryTempDao
abstract val installedDao: InstalledDao abstract val installedDao: InstalledDao
abstract val lockDao: LockDao abstract val lockDao: LockDao
@ -46,17 +50,33 @@ abstract class DatabaseX : RoomDatabase() {
} }
fun cleanUp(pairs: Set<Pair<Long, Boolean>>) { fun cleanUp(pairs: Set<Pair<Long, Boolean>>) {
val result = pairs.windowed(10, 10, true).map { runInTransaction {
val ids = it.map { it.first }.toLongArray() val result = pairs.windowed(10, 10, true).map {
val productsCount = productDao.deleteById(*ids) val ids = it.map { it.first }.toLongArray()
val categoriesCount = categoryDao.deleteById(*ids) val productsCount = productDao.deleteById(*ids)
val deleteIds = it.filter { it.second }.map { it.first }.toLongArray() val categoriesCount = categoryDao.deleteById(*ids)
repositoryDao.deleteById(*deleteIds) val deleteIds = it.filter { it.second }.map { it.first }.toLongArray()
productsCount != 0 || categoriesCount != 0 repositoryDao.deleteById(*deleteIds)
productsCount != 0 || categoriesCount != 0
}
} }
// Use live objects and observers instead // Use live objects and observers instead
/*if (result.any { it }) { /*if (result.any { it }) {
com.looker.droidify.database.Database.notifyChanged(com.looker.droidify.database.Database.Subject.Products) 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()
}
}
} }

View File

@ -33,6 +33,8 @@ class QueryBuilder {
this.arguments += arguments this.arguments += arguments
} }
fun build() = builder.toString()
fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor { fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor {
val query = builder.toString() val query = builder.toString()
val arguments = arguments.toTypedArray() val arguments = arguments.toTypedArray()

View File

@ -4,11 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverter 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.ProductItem
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.utility.jsonGenerate
import com.looker.droidify.utility.jsonParse
@Entity @Entity
class Repository { class Repository {
@ -17,7 +16,7 @@ class Repository {
var id: Long = 0 var id: Long = 0
var enabled = 0 var enabled = 0
var deleted = 0 var deleted = false
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data: Repository? = null var data: Repository? = null
@ -26,54 +25,60 @@ class Repository {
@ColumnInfo(name = "_id") @ColumnInfo(name = "_id")
var id = 0L var id = 0L
var deleted = 0 var deleted = false
} }
} }
@Entity(primaryKeys = ["repository_id", "package_name"]) @Entity(tableName = "product", primaryKeys = ["repository_id", "package_name"])
class Product { open class Product {
var repository_id: Long = 0 var repository_id = 0L
var package_name = "" var package_name = ""
var name = "" var name = ""
var summary = "" var summary = ""
var description = "" var description = ""
var added = 0 var added = 0L
var updated = 0 var updated = 0L
var version_code = 0 var version_code = 0L
var signatures = "" var signatures = ""
var compatible = 0 var compatible = 0
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data: Product? = null var data: com.looker.droidify.entity.Product? = null
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data_item: ProductItem? = null var data_item: ProductItem? = null
} }
@Entity(primaryKeys = ["repository_id", "package_name", "name"]) @Entity(tableName = "temporary_product")
class Category { class ProductTemp : Product()
@Entity(tableName = "category", primaryKeys = ["repository_id", "package_name", "name"])
open class Category {
var repository_id: Long = 0 var repository_id: Long = 0
var package_name = "" var package_name = ""
var name = "" var name = ""
} }
@Entity @Entity(tableName = "temporary_category")
class Installed { class CategoryTemp : Category()
@Entity(tableName = "memory_installed")
class Installed(pName: String = "") {
@PrimaryKey @PrimaryKey
var package_name = "" var package_name = pName
var version = "" var version = ""
var version_code = 0 var version_code = 0L
var signature = "" var signature = ""
} }
@Entity @Entity(tableName = "memory_lock")
class Lock { class Lock {
@PrimaryKey @PrimaryKey
var package_name = "" var package_name = ""
var version_code = 0 var version_code = 0L
} }
object Converters { object Converters {
@ -87,11 +92,12 @@ object Converters {
@TypeConverter @TypeConverter
@JvmStatic @JvmStatic
fun toProduct(byteArray: ByteArray) = byteArray.jsonParse { Product.deserialize(it) } fun toProduct(byteArray: ByteArray) =
byteArray.jsonParse { com.looker.droidify.entity.Product.deserialize(it) }
@TypeConverter @TypeConverter
@JvmStatic @JvmStatic
fun toByteArray(product: Product) = jsonGenerate(product::serialize) fun toByteArray(product: com.looker.droidify.entity.Product) = jsonGenerate(product::serialize)
@TypeConverter @TypeConverter
@JvmStatic @JvmStatic

View File

@ -3,7 +3,7 @@ package com.looker.droidify.index
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.looker.droidify.content.Cache 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.Product
import com.looker.droidify.entity.Release import com.looker.droidify.entity.Release
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
@ -59,29 +59,28 @@ object RepositoryUpdater {
private val updaterLock = Any() private val updaterLock = Any()
private val cleanupLock = Any() private val cleanupLock = Any()
lateinit var db: DatabaseX
fun init() { fun init(context: Context) {
db = DatabaseX.getInstance(context)
var lastDisabled = setOf<Long>() var lastDisabled = setOf<Long>()
Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { .flatMapSingle {
RxUtils.querySingle { RxUtils.querySingle {
Database.RepositoryAdapter.getAllDisabledDeleted( db.repositoryDao.allDisabledDeleted
it
)
} }
} }
.forEach { it -> .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 val disabled = newDisabled - lastDisabled
lastDisabled = newDisabled 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()) { if (disabled.isNotEmpty() || deleted.isNotEmpty()) {
val pairs = (disabled.asSequence().map { Pair(it, false) } + val pairs = (disabled.asSequence().map { Pair(it, false) } +
deleted.asSequence().map { Pair(it, true) }).toSet() 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, file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit,
): Boolean { ): Boolean {
var rollback = true var rollback = true
val db = DatabaseX.getInstance(context)
return synchronized(updaterLock) { return synchronized(updaterLock) {
try { try {
val jarFile = JarFile(file, true) val jarFile = JarFile(file, true)
val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry
val total = indexEntry.size val total = indexEntry.size
Database.UpdaterAdapter.createTemporaryTable() db.productTempDao.emptyTable()
db.categoryTempDao.emptyTable()
val features = context.packageManager.systemAvailableFeatures val features = context.packageManager.systemAvailableFeatures
.asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen") .asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen")
@ -231,7 +232,7 @@ object RepositoryUpdater {
} }
products += transformProduct(product, features, unstable) products += transformProduct(product, features, unstable)
if (products.size >= 50) { if (products.size >= 50) {
Database.UpdaterAdapter.putTemporary(products) db.productTempDao.putTemporary(products)
products.clear() products.clear()
} }
} }
@ -249,7 +250,7 @@ object RepositoryUpdater {
throw InterruptedException() throw InterruptedException()
} }
if (products.isNotEmpty()) { if (products.isNotEmpty()) {
Database.UpdaterAdapter.putTemporary(products) db.productTempDao.putTemporary(products)
products.clear() products.clear()
} }
Pair(changedRepository, certificateFromIndex) Pair(changedRepository, certificateFromIndex)
@ -334,7 +335,7 @@ object RepositoryUpdater {
progress.toLong(), progress.toLong(),
totalCount.toLong() totalCount.toLong()
) )
Database.UpdaterAdapter.putTemporary(products db.productTempDao.putTemporary(products
.map { transformProduct(it, features, unstable) }) .map { transformProduct(it, features, unstable) })
} }
} }
@ -407,7 +408,7 @@ object RepositoryUpdater {
} }
callback(Stage.COMMIT, 0, null) callback(Stage.COMMIT, 0, null)
synchronized(cleanupLock) { synchronized(cleanupLock) {
Database.UpdaterAdapter.finishTemporary( db.finishTemporary(
commitRepository, commitRepository,
true true
) )
@ -423,7 +424,7 @@ object RepositoryUpdater {
} finally { } finally {
file.delete() file.delete()
if (rollback) { if (rollback) {
Database.UpdaterAdapter.finishTemporary(repository, false) db.finishTemporary(repository, false)
} }
} }
} }

View File

@ -17,7 +17,6 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.Database
import com.looker.droidify.databinding.EditRepositoryBinding import com.looker.droidify.databinding.EditRepositoryBinding
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.network.Downloader import com.looker.droidify.network.Downloader
@ -154,7 +153,7 @@ class EditRepositoryFragment() : ScreenFragment() {
} }
if (savedInstanceState == null) { if (savedInstanceState == null) {
val repository = repositoryId?.let(Database.RepositoryAdapter::get) val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.data }
if (repository == null) { if (repository == null) {
val clipboardManager = val clipboardManager =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@ -233,7 +232,7 @@ class EditRepositoryFragment() : ScreenFragment() {
} }
lifecycleScope.launch { lifecycleScope.launch {
val list = Database.RepositoryAdapter.getAll(null) val list = screenActivity.db.repositoryDao.all.mapNotNull { it.data }
takenAddresses = list.asSequence().filter { it.id != repositoryId } takenAddresses = list.asSequence().filter { it.id != repositoryId }
.flatMap { (it.mirrors + it.address).asSequence() } .flatMap { (it.mirrors + it.address).asSequence() }
.map { it.withoutKnownPath }.toSet() .map { it.withoutKnownPath }.toSet()
@ -449,10 +448,10 @@ class EditRepositoryFragment() : ScreenFragment() {
MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager) MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager)
invalidateState() invalidateState()
} else { } else {
val repository = repositoryId?.let(Database.RepositoryAdapter::get) val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.data }
?.edit(address, fingerprint, authentication) ?.edit(address, fingerprint, authentication)
?: Repository.newRepository(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) { if (repositoryId == null && changedRepository.enabled) {
binder.sync(changedRepository) binder.sync(changedRepository)
} }

View File

@ -9,11 +9,11 @@ import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.Database
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.utility.extension.resources.clear import com.looker.droidify.utility.extension.resources.clear
import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getColorFromAttr
import com.looker.droidify.utility.extension.resources.inflate import com.looker.droidify.utility.extension.resources.inflate
import com.looker.droidify.utility.getRepository
import com.looker.droidify.widget.CursorRecyclerAdapter import com.looker.droidify.widget.CursorRecyclerAdapter
class RepositoriesAdapter( class RepositoriesAdapter(
@ -45,7 +45,7 @@ class RepositoriesAdapter(
} }
private fun getRepository(position: Int): Repository { private fun getRepository(position: Int): Repository {
return Database.RepositoryAdapter.transform(moveTo(position)) return moveTo(position).getRepository()
} }
override fun onCreateViewHolder( override fun onCreateViewHolder(

View File

@ -12,7 +12,6 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.Database
import com.looker.droidify.databinding.TitleTextItemBinding import com.looker.droidify.databinding.TitleTextItemBinding
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService import com.looker.droidify.service.SyncService
@ -99,7 +98,7 @@ class RepositoryFragment() : ScreenFragment() {
} }
private fun updateRepositoryView() { private fun updateRepositoryView() {
val repository = Database.RepositoryAdapter.get(repositoryId) val repository = screenActivity.db.repositoryDao.get(repositoryId)?.data
val layout = layout!! val layout = layout!!
layout.removeAllViews() layout.removeAllViews()
if (repository == null) { if (repository == null) {
@ -125,7 +124,7 @@ class RepositoryFragment() : ScreenFragment() {
if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) {
layout.addTitleText( layout.addTitleText(
R.string.number_of_applications, R.string.number_of_applications,
Database.ProductAdapter.getCount(repository.id).toString() screenActivity.db.productDao.countForRepository(repository.id).toString()
) )
} }
} else { } else {

View File

@ -10,6 +10,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.circularreveal.CircularRevealFrameLayout import com.google.android.material.circularreveal.CircularRevealFrameLayout
import com.looker.droidify.MainApplication
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.database.CursorOwner import com.looker.droidify.database.CursorOwner
@ -25,6 +26,9 @@ abstract class ScreenActivity : AppCompatActivity() {
private const val STATE_FRAGMENT_STACK = "fragmentStack" private const val STATE_FRAGMENT_STACK = "fragmentStack"
} }
val db
get() = (application as MainApplication).db
sealed class SpecialIntent { sealed class SpecialIntent {
object Updates : SpecialIntent() object Updates : SpecialIntent()
class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent() class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent()

View File

@ -19,7 +19,7 @@ import androidx.viewpager2.widget.ViewPager2
import coil.load import coil.load
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import com.looker.droidify.R 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.Product
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.graphics.PaddingDrawable import com.looker.droidify.graphics.PaddingDrawable
@ -68,6 +68,7 @@ class ScreenshotsFragment() : DialogFragment() {
val window = dialog.window val window = dialog.window
val decorView = window?.decorView val decorView = window?.decorView
val db = DatabaseX.getInstance(requireContext())
if (window != null) { if (window != null) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -132,13 +133,17 @@ class ScreenshotsFragment() : DialogFragment() {
var restored = false var restored = false
productDisposable = Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } } .flatMapSingle {
RxUtils.querySingle {
db.productDao.get(packageName).mapNotNull { it?.data }
}
}
.map { it -> .map { it ->
Pair( Pair(
it.find { it.repositoryId == repositoryId }, it.find { it.repositoryId == repositoryId },
Database.RepositoryAdapter.get(repositoryId) db.repositoryDao.get(repositoryId)?.data
) )
} }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View File

@ -19,7 +19,6 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.database.Database
import com.looker.droidify.databinding.TabsToolbarBinding import com.looker.droidify.databinding.TabsToolbarBinding
import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.ProductItem
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
@ -235,9 +234,9 @@ class TabsFragment : ScreenFragment() {
} }
categoriesDisposable = Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { screenActivity.db.categoryDao.allNames } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
setSectionsAndUpdate( setSectionsAndUpdate(
@ -246,9 +245,9 @@ class TabsFragment : ScreenFragment() {
) )
} }
repositoriesDisposable = Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { it -> .subscribe { it ->
setSectionsAndUpdate(null, it.asSequence().filter { it.enabled } setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }

View File

@ -9,10 +9,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.looker.droidify.BuildConfig import com.looker.droidify.*
import com.looker.droidify.Common
import com.looker.droidify.MainActivity
import com.looker.droidify.R
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.entity.Release import com.looker.droidify.entity.Release
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
@ -121,7 +118,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
} else { } else {
cancelTasks(packageName) cancelTasks(packageName)
cancelCurrentTask(packageName) cancelCurrentTask(packageName)
notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) notificationManager.cancel(task.notificationTag, NOTIFICATION_ID_DOWNLOADING)
tasks += task tasks += task
if (currentTask == null) { if (currentTask == null) {
handleDownload() handleDownload()
@ -146,7 +143,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
if (Android.sdk(26)) { if (Android.sdk(26)) {
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_DOWNLOADING, NOTIFICATION_CHANNEL_DOWNLOADING,
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW
) )
.apply { setShowBadge(false) } .apply { setShowBadge(false) }
@ -209,9 +206,9 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun showNotificationError(task: Task, errorType: ErrorType) { private fun showNotificationError(task: Task, errorType: ErrorType) {
notificationManager.notify(task.notificationTag, notificationManager.notify(task.notificationTag,
Common.NOTIFICATION_ID_DOWNLOADING, NOTIFICATION_ID_DOWNLOADING,
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(android.R.drawable.stat_sys_warning) .setSmallIcon(android.R.drawable.stat_sys_warning)
.setColor( .setColor(
@ -276,8 +273,8 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun showNotificationInstall(task: Task) { private fun showNotificationInstall(task: Task) {
notificationManager.notify( notificationManager.notify(
task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat task.notificationTag, NOTIFICATION_ID_DOWNLOADING, NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
.setColor( .setColor(
@ -367,7 +364,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private val stateNotificationBuilder by lazy { private val stateNotificationBuilder by lazy {
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -389,7 +386,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun publishForegroundState(force: Boolean, state: State) { private fun publishForegroundState(force: Boolean, state: State) {
if (force || currentTask != null) { if (force || currentTask != null) {
currentTask = currentTask?.copy(lastState = state) currentTask = currentTask?.copy(lastState = state)
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
when (state) { when (state) {
is State.Connecting -> { is State.Connecting -> {
setContentTitle(getString(R.string.downloading_FORMAT, state.name)) setContentTitle(getString(R.string.downloading_FORMAT, state.name))

View File

@ -13,12 +13,9 @@ import android.text.style.ForegroundColorSpan
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.looker.droidify.BuildConfig import com.looker.droidify.*
import com.looker.droidify.Common
import com.looker.droidify.MainActivity
import com.looker.droidify.R
import com.looker.droidify.content.Preferences 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.ProductItem
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.index.RepositoryUpdater 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.android.notificationManager
import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getColorFromAttr
import com.looker.droidify.utility.extension.text.formatSize 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
@ -99,7 +97,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
} }
fun sync(request: SyncRequest) { 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() .asSequence().filter { it.enabled }.map { it.id }.toList()
sync(ids, request) sync(ids, request)
} }
@ -120,12 +118,12 @@ class SyncService : ConnectionService<SyncService.Binder>() {
fun setUpdateNotificationBlocker(fragment: Fragment?) { fun setUpdateNotificationBlocker(fragment: Fragment?) {
updateNotificationBlockerFragment = fragment?.let(::WeakReference) updateNotificationBlockerFragment = fragment?.let(::WeakReference)
if (fragment != null) { if (fragment != null) {
notificationManager.cancel(Common.NOTIFICATION_ID_UPDATES) notificationManager.cancel(NOTIFICATION_ID_UPDATES)
} }
} }
fun setEnabled(repository: Repository, enabled: Boolean): Boolean { fun setEnabled(repository: Repository, enabled: Boolean): Boolean {
Database.RepositoryAdapter.put(repository.enable(enabled)) db.repositoryDao.put(repository.enable(enabled))
if (enabled) { if (enabled) {
if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) { if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) {
tasks += Task(repository.id, true) tasks += Task(repository.id, true)
@ -144,10 +142,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
} }
fun deleteRepository(repositoryId: Long): Boolean { fun deleteRepository(repositoryId: Long): Boolean {
val repository = Database.RepositoryAdapter.get(repositoryId) val repository = db.repositoryDao.get(repositoryId)?.data
return repository != null && run { return repository != null && run {
setEnabled(repository, false) setEnabled(repository, false)
Database.RepositoryAdapter.markAsDeleted(repository.id) db.repositoryDao.markAsDeleted(repository.id)
true true
} }
} }
@ -155,19 +153,21 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private val binder = Binder() private val binder = Binder()
override fun onBind(intent: Intent): Binder = binder override fun onBind(intent: Intent): Binder = binder
lateinit var db: DatabaseX
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
db = DatabaseX.getInstance(applicationContext)
if (Android.sdk(26)) { if (Android.sdk(26)) {
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_SYNCING, NOTIFICATION_CHANNEL_SYNCING,
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
) )
.apply { setShowBadge(false) } .apply { setShowBadge(false) }
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_UPDATES, NOTIFICATION_CHANNEL_UPDATES,
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW getString(R.string.updates), NotificationManager.IMPORTANCE_LOW
) )
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
@ -210,8 +210,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private fun showNotificationError(repository: Repository, exception: Exception) { private fun showNotificationError(repository: Repository, exception: Exception) {
notificationManager.notify( notificationManager.notify(
"repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat "repository-${repository.id}", NOTIFICATION_ID_SYNCING, NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) .Builder(this, NOTIFICATION_CHANNEL_SYNCING)
.setSmallIcon(android.R.drawable.stat_sys_warning) .setSmallIcon(android.R.drawable.stat_sys_warning)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -237,7 +237,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private val stateNotificationBuilder by lazy { private val stateNotificationBuilder by lazy {
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) .Builder(this, NOTIFICATION_CHANNEL_SYNCING)
.setSmallIcon(R.drawable.ic_sync) .setSmallIcon(R.drawable.ic_sync)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -260,7 +260,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
if (force || currentTask?.lastState != state) { if (force || currentTask?.lastState != state) {
currentTask = currentTask?.copy(lastState = state) currentTask = currentTask?.copy(lastState = state)
if (started == Started.MANUAL) { if (started == Started.MANUAL) {
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
when (state) { when (state) {
is State.Connecting -> { is State.Connecting -> {
setContentTitle(getString(R.string.syncing_FORMAT, state.name)) setContentTitle(getString(R.string.syncing_FORMAT, state.name))
@ -330,7 +330,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
if (currentTask == null) { if (currentTask == null) {
if (tasks.isNotEmpty()) { if (tasks.isNotEmpty()) {
val task = tasks.removeAt(0) 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) { if (repository != null && repository.enabled) {
val lastStarted = started val lastStarted = started
val newStarted = val newStarted =
@ -376,7 +376,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
val disposable = RxUtils val disposable = RxUtils
.querySingle { it -> .querySingle { it ->
Database.ProductAdapter db.productDao
.query( .query(
installed = true, installed = true,
updates = true, updates = true,
@ -386,8 +386,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
signal = it signal = it
) )
.use { .use {
it.asSequence().map(Database.ProductAdapter::transformItem) it.asSequence().map { it.getProductItem() }.toList()
.toList()
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -419,8 +418,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
val maxUpdates = 5 val maxUpdates = 5
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback) fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
notificationManager.notify( notificationManager.notify(
Common.NOTIFICATION_ID_UPDATES, NotificationCompat NOTIFICATION_ID_UPDATES, NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) .Builder(this, NOTIFICATION_CHANNEL_UPDATES)
.setSmallIcon(R.drawable.ic_new_releases) .setSmallIcon(R.drawable.ic_new_releases)
.setContentTitle(getString(R.string.new_updates_available)) .setContentTitle(getString(R.string.new_updates_available))
.setContentText( .setContentText(

View File

@ -16,13 +16,13 @@ import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.database.Database
import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.ProductItem
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.network.CoilDownloader import com.looker.droidify.network.CoilDownloader
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.utility.extension.text.nullIfEmpty import com.looker.droidify.utility.extension.text.nullIfEmpty
import com.looker.droidify.utility.getProductItem
import com.looker.droidify.widget.CursorRecyclerAdapter import com.looker.droidify.widget.CursorRecyclerAdapter
class AppListAdapter(private val onClick: (ProductItem) -> Unit) : class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
@ -113,7 +113,7 @@ class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
} }
private fun getProductItem(position: Int): ProductItem { private fun getProductItem(position: Int): ProductItem {
return Database.ProductAdapter.transformItem(moveTo(position)) return moveTo(position).getProductItem()
} }
override fun onCreateViewHolder( override fun onCreateViewHolder(

View File

@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.ProductPreferences import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.Database
import com.looker.droidify.entity.* import com.looker.droidify.entity.*
import com.looker.droidify.installer.AppInstaller import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.screen.MessageDialog 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.Utils.startUpdate
import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.android.*
import com.looker.droidify.utility.extension.text.trimAfter 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
@ -129,12 +129,16 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
var first = true var first = true
productDisposable = Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } } .flatMapSingle {
RxUtils.querySingle {
screenActivity.db.productDao.get(packageName).mapNotNull { it?.data }
}
}
.flatMapSingle { products -> .flatMapSingle { products ->
RxUtils RxUtils
.querySingle { Database.RepositoryAdapter.getAll(it) } .querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } }
.map { it -> .map { it ->
it.asSequence().map { Pair(it.id, it) }.toMap() it.asSequence().map { Pair(it.id, it) }.toMap()
.let { .let {
@ -151,7 +155,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
} }
.flatMapSingle { products -> .flatMapSingle { products ->
RxUtils RxUtils
.querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) } .querySingle { Nullable(screenActivity.db.installedDao.get(packageName).getInstalledItem()) }
.map { Pair(products, it) } .map { Pair(products, it) }
} }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View File

@ -13,7 +13,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.CursorOwner import com.looker.droidify.database.CursorOwner
import com.looker.droidify.database.Database
import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.ProductItem
import com.looker.droidify.screen.BaseFragment import com.looker.droidify.screen.BaseFragment
import com.looker.droidify.ui.adapters.AppListAdapter import com.looker.droidify.ui.adapters.AppListAdapter
@ -78,10 +77,10 @@ class AppListFragment() : BaseFragment(), CursorOwner.Callback {
screenActivity.cursorOwner.attach(this, viewModel.request(source)) screenActivity.cursorOwner.attach(this, viewModel.request(source))
repositoriesDisposable = Observable.just(Unit) 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()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.data } } }
.map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } .map { it.asSequence().map { Pair(it.id, it) }.toMap() }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it } .subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it }
} }

View File

@ -6,18 +6,20 @@ import android.content.pm.Signature
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import com.looker.droidify.BuildConfig import com.looker.droidify.*
import com.looker.droidify.Common.PREFS_LANGUAGE_DEFAULT
import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.entity.InstalledItem import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.entity.Product import com.looker.droidify.entity.Product
import com.looker.droidify.entity.ProductItem
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService import com.looker.droidify.service.DownloadService
import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.Android
import com.looker.droidify.utility.extension.android.singleSignature import com.looker.droidify.utility.extension.android.singleSignature
import com.looker.droidify.utility.extension.android.versionCodeCompat 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.getColorFromAttr
import com.looker.droidify.utility.extension.resources.getDrawableCompat import com.looker.droidify.utility.extension.resources.getDrawableCompat
import com.looker.droidify.utility.extension.text.hex import com.looker.droidify.utility.extension.text.hex
@ -167,3 +169,51 @@ object Utils {
else -> Locale(localeCode) 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 <T> 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()
}