diff --git a/build.gradle b/build.gradle index ec5f9aab..970b4c4c 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 { @@ -118,6 +125,7 @@ repositories { dependencies { // Core + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.0' implementation 'androidx.appcompat:appcompat-resources:1.4.0' @@ -130,6 +138,12 @@ dependencies { // Material3 implementation 'com.google.android.material:material:1.6.0-alpha01' + + // FastAdapter + implementation("com.mikepenz:fastadapter:5.6.0") + implementation("com.mikepenz:fastadapter-extensions-diff:5.6.0") + implementation("com.mikepenz:fastadapter-extensions-binding:5.6.0") + // Coil implementation 'io.coil-kt:coil:1.4.0' @@ -148,12 +162,13 @@ dependencies { // Coroutines / Lifecycle implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' // Room implementation 'androidx.room:room-runtime:2.4.0' implementation 'androidx.room:room-ktx:2.4.0' + implementation 'androidx.room:room-rxjava3:2.4.0' kapt 'androidx.room:room-compiler:2.4.0' } diff --git a/gradle.properties b/gradle.properties index 00b46820..8323ddb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ org.gradle.daemon=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cb2384a6..69f2ab82 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Dec 16 21:31:46 IST 2021 +#Thu Dec 23 21:52:45 IST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 0df126de..dc2a4421 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -8,7 +8,8 @@ - + { - jobScheduler.cancel(Common.JOB_ID_SYNC) + is Preferences.AutoSync.Never -> { + 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, - ComponentName(this, SyncService.Job::class.java) + is Preferences.AutoSync.Wifi -> { + autoSync( + jobScheduler = jobScheduler, + connectionType = JobInfo.NETWORK_TYPE_UNMETERED + ) + } + is Preferences.AutoSync.WifiBattery -> { + if (isCharging(this)) { + autoSync( + jobScheduler = jobScheduler, + connectionType = JobInfo.NETWORK_TYPE_UNMETERED ) - .setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY) - .apply { - if (Android.sdk(26)) { - setRequiresBatteryNotLow(true) - setRequiresStorageNotLow(true) - } - if (Android.sdk(24)) { - setPeriodic(period, JobInfo.getMinFlexMillis()) - } else { - setPeriodic(period) - } - } - .build()) + } Unit } + is Preferences.AutoSync.Always -> { + autoSync( + jobScheduler = jobScheduler, + connectionType = JobInfo.NETWORK_TYPE_ANY + ) + } }::class.java } } + private fun autoSync(jobScheduler: JobScheduler, connectionType: Int) { + val period = 12.hours.inWholeMilliseconds + jobScheduler.schedule( + JobInfo + .Builder( + JOB_ID_SYNC, + ComponentName(this, SyncService.Job::class.java) + ) + .setRequiredNetworkType(connectionType) + .apply { + if (Android.sdk(26)) { + setRequiresBatteryNotLow(true) + setRequiresStorageNotLow(true) + } + if (Android.sdk(24)) setPeriodic(period, JobInfo.getMinFlexMillis()) + else setPeriodic(period) + } + .build() + ) + } + + private fun isCharging(context: Context): Boolean { + val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + val plugged = intent!!.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) + return plugged == BatteryManager.BATTERY_PLUGGED_AC + || plugged == BatteryManager.BATTERY_PLUGGED_USB + || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS + } + private fun updateProxy() { val type = Preferences[Preferences.Key.ProxyType].proxyType val host = Preferences[Preferences.Key.ProxyHost] @@ -176,14 +207,14 @@ class MainApplication : Application(), ImageLoaderFactory { } } } - val proxy = socketAddress?.let { Proxy(type, socketAddress) } + val proxy = socketAddress?.let { Proxy(type, it) } Downloader.proxy = proxy } private fun forceSyncAll() { - Database.RepositoryAdapter.getAll(null).forEach { + db.repositoryDao.all.mapNotNull { it.trueData }.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/Preferences.kt b/src/main/kotlin/com/looker/droidify/content/Preferences.kt index 6eec3171..c5ec5dd8 100644 --- a/src/main/kotlin/com/looker/droidify/content/Preferences.kt +++ b/src/main/kotlin/com/looker/droidify/content/Preferences.kt @@ -3,8 +3,8 @@ package com.looker.droidify.content import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration -import com.looker.droidify.Common.PREFS_LANGUAGE -import com.looker.droidify.Common.PREFS_LANGUAGE_DEFAULT +import com.looker.droidify.PREFS_LANGUAGE +import com.looker.droidify.PREFS_LANGUAGE_DEFAULT import com.looker.droidify.R import com.looker.droidify.entity.ProductItem import com.looker.droidify.utility.extension.android.Android @@ -18,8 +18,8 @@ import java.net.Proxy object Preferences { private lateinit var preferences: SharedPreferences - private val _subject = MutableSharedFlow>() - val subject = _subject.asSharedFlow() + private val mutableSubject = MutableSharedFlow>() + val subject = mutableSubject.asSharedFlow() private val keys = sequenceOf( Key.Language, @@ -39,12 +39,14 @@ object Preferences { fun init(context: Context) { preferences = - context.getSharedPreferences("${context.packageName}_preferences", - Context.MODE_PRIVATE) + context.getSharedPreferences( + "${context.packageName}_preferences", + Context.MODE_PRIVATE + ) preferences.registerOnSharedPreferenceChangeListener { _, keyString -> CoroutineScope(Dispatchers.Default).launch { keys[keyString]?.let { - _subject.emit(it) + mutableSubject.emit(it) } } } @@ -167,10 +169,11 @@ object Preferences { sealed class AutoSync(override val valueString: String) : Enumeration { override val values: List - get() = listOf(Never, Wifi, Always) + get() = listOf(Never, Wifi, WifiBattery, Always) object Never : AutoSync("never") object Wifi : AutoSync("wifi") + object WifiBattery : AutoSync("wifi-battery") object Always : AutoSync("always") } diff --git a/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt b/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt index 77aa4aa1..c5f493e4 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 = packageName + version_code = versionCode + } + ) + else db.lockDao.delete(packageName) } } } 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..cd4fb593 100644 --- a/src/main/kotlin/com/looker/droidify/database/DAOs.kt +++ b/src/main/kotlin/com/looker/droidify/database/DAOs.kt @@ -1,57 +1,272 @@ 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 +import io.reactivex.rxjava3.core.Flowable -@Dao -interface RepositoryDao { + +interface BaseDao { @Insert - @Throws(SQLException::class) - fun insert(vararg repository: Repository) + fun insert(vararg product: T) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertReplace(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 { + @get:Query("SELECT COUNT(_id) FROM repository") + val count: Int + + fun put(repository: com.looker.droidify.entity.Repository): com.looker.droidify.entity.Repository { + repository.let { + val dbRepo = Repository().apply { + if (it.id >= 0L) id = it.id + enabled = if (it.enabled) 1 else 0 + deleted = false + data = it + } + val newId = if (it.id > 0L) update(dbRepo).toLong() else returnInsert(dbRepo) + return if (newId != repository.id) repository.copy(id = newId) else repository + } } + @Insert + 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 * FROM repository WHERE deleted == 0 ORDER BY _id ASC") + val allFlowable: Flowable> + @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())) + } + + @RawQuery + fun queryList( + query: SupportSQLiteQuery + ): List + + // TODO optimize and simplify + @Transaction + fun queryList( + installed: Boolean, updates: Boolean, searchQuery: String, + section: ProductItem.Section, order: ProductItem.Order + ): List { + 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 queryList(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 +274,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 { + insertReplace(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/Database.kt b/src/main/kotlin/com/looker/droidify/database/Database.kt deleted file mode 100644 index 68f0f965..00000000 --- a/src/main/kotlin/com/looker/droidify/database/Database.kt +++ /dev/null @@ -1,828 +0,0 @@ -package com.looker.droidify.database - -import android.content.ContentValues -import android.content.Context -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.os.CancellationSignal -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -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.utility.extension.android.asSequence -import com.looker.droidify.utility.extension.android.firstOrNull -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 io.reactivex.rxjava3.core.Observable -import java.io.ByteArrayOutputStream - -object Database { - fun init(context: Context): Boolean { - val helper = Helper(context) - db = helper.writableDatabase - if (helper.created) { - for (repository in Repository.defaultRepositories) { - RepositoryAdapter.put(repository) - } - } - return helper.created || helper.updated - } - - private lateinit var db: SQLiteDatabase - - private interface Table { - val memory: Boolean - val innerName: String - val createTable: String - val createIndex: String? - get() = null - - val databasePrefix: String - get() = if (memory) "memory." else "" - - val name: String - get() = "$databasePrefix$innerName" - - fun formatCreateTable(name: String): String { - return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})" - } - - val createIndexPairFormatted: Pair? - get() = createIndex?.let { - Pair( - "CREATE INDEX ${innerName}_index ON $innerName ($it)", - "CREATE INDEX ${name}_index ON $innerName ($it)" - ) - } - } - - private object Schema { - object Repository : Table { - const val ROW_ID = "_id" - const val ROW_ENABLED = "enabled" - const val ROW_DELETED = "deleted" - const val ROW_DATA = "data" - - override val memory = false - override val innerName = "repository" - override val createTable = """ - $ROW_ID INTEGER PRIMARY KEY AUTOINCREMENT, - $ROW_ENABLED INTEGER NOT NULL, - $ROW_DELETED INTEGER NOT NULL, - $ROW_DATA BLOB NOT NULL - """ - } - - object Product : Table { - 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" - - override val memory = false - override val innerName = "product" - override val createTable = """ - $ROW_REPOSITORY_ID INTEGER NOT NULL, - $ROW_PACKAGE_NAME TEXT NOT NULL, - $ROW_NAME TEXT NOT NULL, - $ROW_SUMMARY TEXT NOT NULL, - $ROW_DESCRIPTION TEXT NOT NULL, - $ROW_ADDED INTEGER NOT NULL, - $ROW_UPDATED INTEGER NOT NULL, - $ROW_VERSION_CODE INTEGER NOT NULL, - $ROW_SIGNATURES TEXT NOT NULL, - $ROW_COMPATIBLE INTEGER NOT NULL, - $ROW_DATA BLOB NOT NULL, - $ROW_DATA_ITEM BLOB NOT NULL, - PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME) - """ - override val createIndex = ROW_PACKAGE_NAME - } - - object Category : Table { - const val ROW_REPOSITORY_ID = "repository_id" - const val ROW_PACKAGE_NAME = "package_name" - const val ROW_NAME = "name" - - override val memory = false - override val innerName = "category" - override val createTable = """ - $ROW_REPOSITORY_ID INTEGER NOT NULL, - $ROW_PACKAGE_NAME TEXT NOT NULL, - $ROW_NAME TEXT NOT NULL, - PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME, $ROW_NAME) - """ - override val createIndex = "$ROW_PACKAGE_NAME, $ROW_NAME" - } - - object Installed : Table { - const val ROW_PACKAGE_NAME = "package_name" - const val ROW_VERSION = "version" - const val ROW_VERSION_CODE = "version_code" - const val ROW_SIGNATURE = "signature" - - override val memory = true - override val innerName = "installed" - override val createTable = """ - $ROW_PACKAGE_NAME TEXT PRIMARY KEY, - $ROW_VERSION TEXT NOT NULL, - $ROW_VERSION_CODE INTEGER NOT NULL, - $ROW_SIGNATURE TEXT NOT NULL - """ - } - - object Lock : Table { - const val ROW_PACKAGE_NAME = "package_name" - const val ROW_VERSION_CODE = "version_code" - - override val memory = true - override val innerName = "lock" - override val createTable = """ - $ROW_PACKAGE_NAME TEXT PRIMARY KEY, - $ROW_VERSION_CODE INTEGER NOT NULL - """ - } - - object Synthetic { - const val ROW_CAN_UPDATE = "can_update" - const val ROW_MATCH_RANK = "match_rank" - } - } - - private class Helper(context: Context) : SQLiteOpenHelper(context, "droidify", null, 1) { - var created = false - private set - var updated = false - private set - - override fun onCreate(db: SQLiteDatabase) = Unit - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = - onVersionChange(db) - - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = - onVersionChange(db) - - private fun onVersionChange(db: SQLiteDatabase) { - handleTables(db, true, Schema.Product, Schema.Category) - this.updated = true - } - - override fun onOpen(db: SQLiteDatabase) { - val create = handleTables(db, false, Schema.Repository) - val updated = handleTables(db, create, Schema.Product, Schema.Category) - db.execSQL("ATTACH DATABASE ':memory:' AS memory") - handleTables(db, false, Schema.Installed, Schema.Lock) - handleIndexes( - db, - Schema.Repository, - Schema.Product, - Schema.Category, - Schema.Installed, - Schema.Lock - ) - dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category) - this.created = this.created || create - this.updated = this.updated || create || updated - } - } - - private fun handleTables(db: SQLiteDatabase, recreate: Boolean, vararg tables: Table): Boolean { - val shouldRecreate = recreate || tables.any { - val sql = db.query( - "${it.databasePrefix}sqlite_master", columns = arrayOf("sql"), - selection = Pair("type = ? AND name = ?", arrayOf("table", it.innerName)) - ) - .use { it.firstOrNull()?.getString(0) }.orEmpty() - it.formatCreateTable(it.innerName) != sql - } - return shouldRecreate && run { - val shouldVacuum = tables.map { - db.execSQL("DROP TABLE IF EXISTS ${it.name}") - db.execSQL(it.formatCreateTable(it.name)) - !it.memory - } - if (shouldVacuum.any { it } && !db.inTransaction()) { - db.execSQL("VACUUM") - } - true - } - } - - private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) { - val shouldVacuum = tables.map { - val sqls = db.query( - "${it.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"), - selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", it.innerName)) - ) - .use { - it.asSequence() - .mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } } - .toList() - } - .filter { !it.first.startsWith("sqlite_") } - val createIndexes = it.createIndexPairFormatted?.let { listOf(it) }.orEmpty() - createIndexes.map { it.first } != sqls.map { it.second } && run { - for (name in sqls.map { it.first }) { - db.execSQL("DROP INDEX IF EXISTS $name") - } - for (createIndexPair in createIndexes) { - db.execSQL(createIndexPair.second) - } - !it.memory - } - } - if (shouldVacuum.any { it } && !db.inTransaction()) { - db.execSQL("VACUUM") - } - } - - private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) { - val tables = db.query( - "sqlite_master", columns = arrayOf("name"), - selection = Pair("type = ?", arrayOf("table")) - ) - .use { it.asSequence().mapNotNull { it.getString(0) }.toList() } - .filter { !it.startsWith("sqlite_") && !it.startsWith("android_") } - .toSet() - neededTables.mapNotNull { if (it.memory) null else it.name } - if (tables.isNotEmpty()) { - for (table in tables) { - db.execSQL("DROP TABLE IF EXISTS $table") - } - if (!db.inTransaction()) { - db.execSQL("VACUUM") - } - } - } - - sealed class Subject { - object Repositories : Subject() - data class Repository(val id: Long) : Subject() - object Products : Subject() - } - - private val observers = mutableMapOf Unit>>() - - private fun dataObservable(subject: Subject): (Boolean, () -> Unit) -> Unit = - { register, observer -> - synchronized(observers) { - val set = observers[subject] ?: run { - val set = mutableSetOf<() -> Unit>() - observers[subject] = set - set - } - if (register) { - set += observer - } else { - set -= observer - } - } - } - - fun observable(subject: Subject): Observable { - return Observable.create { - val callback: () -> Unit = { it.onNext(Unit) } - val dataObservable = dataObservable(subject) - dataObservable(true, callback) - it.setCancellable { dataObservable(false, callback) } - } - } - - private fun notifyChanged(vararg subjects: Subject) { - synchronized(observers) { - subjects.asSequence().mapNotNull { observers[it] }.flatten().forEach { it() } - } - } - - private fun SQLiteDatabase.insertOrReplace( - replace: Boolean, - table: String, - contentValues: ContentValues, - ): Long { - return if (replace) replace(table, null, contentValues) else insert( - table, - null, - contentValues - ) - } - - private fun SQLiteDatabase.query( - table: String, columns: Array? = null, - selection: Pair>? = null, orderBy: String? = null, - signal: CancellationSignal? = null, - ): Cursor { - return query( - false, - table, - columns, - selection?.first, - selection?.second, - null, - null, - orderBy, - null, - signal - ) - } - - private fun Cursor.observable(subject: Subject): ObservableCursor { - return ObservableCursor(this, dataObservable(subject)) - } - - 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() - } - - object RepositoryAdapter { - // Done in put - internal fun putWithoutNotification(repository: Repository, shouldReplace: Boolean): Long { - return db.insertOrReplace(shouldReplace, Schema.Repository.name, ContentValues().apply { - if (shouldReplace) { - put(Schema.Repository.ROW_ID, repository.id) - } - put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0) - put(Schema.Repository.ROW_DELETED, 0) - put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize)) - }) - } - - // Done - fun put(repository: Repository): Repository { - val shouldReplace = repository.id >= 0L - val newId = putWithoutNotification(repository, shouldReplace) - val id = if (shouldReplace) repository.id else newId - notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products) - return if (newId != repository.id) repository.copy(id = newId) else repository - } - - // Done - fun get(id: Long): Repository? { - return db.query( - Schema.Repository.name, - selection = Pair( - "${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0", - arrayOf(id.toString()) - ) - ) - .use { it.firstOrNull()?.let(::transform) } - } - - // Done - // MAYBE signal has to be considered - fun getAll(signal: CancellationSignal?): List { - return db.query( - Schema.Repository.name, - selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), - signal = signal - ).use { it.asSequence().map(::transform).toList() } - } - - // Done Pair instead - // MAYBE signal has to be considered - fun getAllDisabledDeleted(signal: CancellationSignal?): Set> { - return db.query( - Schema.Repository.name, - columns = arrayOf(Schema.Repository.ROW_ID, Schema.Repository.ROW_DELETED), - selection = Pair( - "${Schema.Repository.ROW_ENABLED} == 0 OR ${Schema.Repository.ROW_DELETED} != 0", - emptyArray() - ), - signal = signal - ).use { - it.asSequence().map { - Pair( - it.getLong(it.getColumnIndex(Schema.Repository.ROW_ID)), - it.getInt(it.getColumnIndex(Schema.Repository.ROW_DELETED)) != 0 - ) - }.toSet() - } - } - - // Done - fun markAsDeleted(id: Long) { - db.update(Schema.Repository.name, ContentValues().apply { - put(Schema.Repository.ROW_DELETED, 1) - }, "${Schema.Repository.ROW_ID} = ?", arrayOf(id.toString())) - notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products) - } - - // Done - fun cleanup(pairs: Set>) { - val result = pairs.windowed(10, 10, true).map { - val idsString = it.joinToString(separator = ", ") { it.first.toString() } - val productsCount = db.delete( - Schema.Product.name, - "${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)", null - ) - val categoriesCount = db.delete( - Schema.Category.name, - "${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)", null - ) - val deleteIdsString = it.asSequence().filter { it.second } - .joinToString(separator = ", ") { it.first.toString() } - if (deleteIdsString.isNotEmpty()) { - db.delete( - Schema.Repository.name, - "${Schema.Repository.ROW_ID} IN ($deleteIdsString)", - null - ) - } - productsCount != 0 || categoriesCount != 0 - } - if (result.any { it }) { - notifyChanged(Subject.Products) - } - } - - // get the cursor in the specific table. Unnecessary with Room - fun query(signal: CancellationSignal?): Cursor { - return db.query( - Schema.Repository.name, - selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), - signal = signal - ).observable(Subject.Repositories) - } - - // Unnecessary with Room - fun transform(cursor: Cursor): Repository { - return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA)) - .jsonParse { - Repository.deserialize(it).apply { - this.id = cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)) - } - } - } - } - - object ProductAdapter { - // Done - fun get(packageName: String, signal: CancellationSignal?): List { - return db.query( - Schema.Product.name, - columns = arrayOf( - Schema.Product.ROW_REPOSITORY_ID, - Schema.Product.ROW_DESCRIPTION, - Schema.Product.ROW_DATA - ), - selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), - signal = signal - ).use { it.asSequence().map(::transform).toList() } - } - - // Done - fun getCount(repositoryId: Long): Int { - return db.query( - Schema.Product.name, columns = arrayOf("COUNT (*)"), - selection = Pair( - "${Schema.Product.ROW_REPOSITORY_ID} = ?", - arrayOf(repositoryId.toString()) - ) - ) - .use { it.firstOrNull()?.getInt(0) ?: 0 } - } - - // Complex left to wiring phase - fun query( - installed: Boolean, updates: Boolean, searchQuery: String, - section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?, - ): Cursor { - val builder = QueryBuilder() - - val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND - product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND - product.${Schema.Product.ROW_SIGNATURES} != ''""" - - builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, - product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME}, - product.${Schema.Product.ROW_SUMMARY}, installed.${Schema.Installed.ROW_VERSION}, - (COALESCE(lock.${Schema.Lock.ROW_VERSION_CODE}, -1) NOT IN (0, product.${Schema.Product.ROW_VERSION_CODE}) AND - product.${Schema.Product.ROW_COMPATIBLE} != 0 AND product.${Schema.Product.ROW_VERSION_CODE} > - COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches) - AS ${Schema.Synthetic.ROW_CAN_UPDATE}, product.${Schema.Product.ROW_COMPATIBLE}, - product.${Schema.Product.ROW_DATA_ITEM},""" - - if (searchQuery.isNotEmpty()) { - builder += """(((product.${Schema.Product.ROW_NAME} LIKE ? OR - product.${Schema.Product.ROW_SUMMARY} LIKE ?) * 7) | - ((product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ?) * 3) | - (product.${Schema.Product.ROW_DESCRIPTION} LIKE ?)) AS ${Schema.Synthetic.ROW_MATCH_RANK},""" - builder %= List(4) { "%$searchQuery%" } - } else { - builder += "0 AS ${Schema.Synthetic.ROW_MATCH_RANK}," - } - - builder += """MAX((product.${Schema.Product.ROW_COMPATIBLE} AND - (installed.${Schema.Installed.ROW_SIGNATURE} IS NULL OR $signatureMatches)) || - PRINTF('%016X', product.${Schema.Product.ROW_VERSION_CODE})) FROM ${Schema.Product.name} AS product""" - builder += """JOIN ${Schema.Repository.name} AS repository - ON product.${Schema.Product.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID}""" - builder += """LEFT JOIN ${Schema.Lock.name} AS lock - ON product.${Schema.Product.ROW_PACKAGE_NAME} = lock.${Schema.Lock.ROW_PACKAGE_NAME}""" - - if (!installed && !updates) { - builder += "LEFT" - } - builder += """JOIN ${Schema.Installed.name} AS installed - ON product.${Schema.Product.ROW_PACKAGE_NAME} = installed.${Schema.Installed.ROW_PACKAGE_NAME}""" - - if (section is ProductItem.Section.Category) { - builder += """JOIN ${Schema.Category.name} AS category - ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}""" - } - - builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND - repository.${Schema.Repository.ROW_DELETED} == 0""" - - if (section is ProductItem.Section.Category) { - builder += "AND category.${Schema.Category.ROW_NAME} = ?" - builder %= section.name - } else if (section is ProductItem.Section.Repository) { - builder += "AND product.${Schema.Product.ROW_REPOSITORY_ID} = ?" - builder %= section.id.toString() - } - - if (searchQuery.isNotEmpty()) { - builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0""" - } - - builder += "GROUP BY product.${Schema.Product.ROW_PACKAGE_NAME} HAVING 1" - - if (updates) { - builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" - } - builder += "ORDER BY" - - if (searchQuery.isNotEmpty()) { - builder += """${Schema.Synthetic.ROW_MATCH_RANK} DESC,""" - } - - when (order) { - ProductItem.Order.NAME -> Unit - ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC," - ProductItem.Order.LAST_UPDATE -> builder += "product.${Schema.Product.ROW_UPDATED} DESC," - }::class - builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC" - - return builder.query(db, signal).observable(Subject.Products) - } - - // Unnecessary with Room - private fun transform(cursor: Cursor): Product { - return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA)) - .jsonParse { - Product.deserialize(it).apply { - this.repositoryId = cursor - .getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)) - this.description = cursor - .getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)) - } - } - } - - // Unnecessary with Room - fun transformItem(cursor: Cursor): ProductItem { - return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM)) - .jsonParse { - ProductItem.deserialize(it).apply { - this.repositoryId = cursor - .getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)) - this.packageName = cursor - .getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME)) - this.name = cursor - .getString(cursor.getColumnIndex(Schema.Product.ROW_NAME)) - this.summary = cursor - .getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)) - this.installedVersion = cursor - .getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)) - .orEmpty() - this.compatible = cursor - .getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0 - this.canUpdate = cursor - .getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0 - this.matchRank = cursor - .getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)) - } - } - } - } - - object CategoryAdapter { - // Done - fun getAll(signal: CancellationSignal?): Set { - val builder = QueryBuilder() - - builder += """SELECT DISTINCT category.${Schema.Category.ROW_NAME} - FROM ${Schema.Category.name} AS category - JOIN ${Schema.Repository.name} AS repository - ON category.${Schema.Category.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID} - WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND - repository.${Schema.Repository.ROW_DELETED} == 0""" - - return builder.query(db, signal).use { - it.asSequence() - .map { it.getString(it.getColumnIndex(Schema.Category.ROW_NAME)) }.toSet() - } - } - } - - object InstalledAdapter { - // Done - fun get(packageName: String, signal: CancellationSignal?): InstalledItem? { - return db.query( - Schema.Installed.name, - columns = arrayOf( - Schema.Installed.ROW_PACKAGE_NAME, Schema.Installed.ROW_VERSION, - Schema.Installed.ROW_VERSION_CODE, Schema.Installed.ROW_SIGNATURE - ), - selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), - signal = signal - ).use { it.firstOrNull()?.let(::transform) } - } - - // Done in insert - private fun put(installedItem: InstalledItem, notify: Boolean) { - db.insertOrReplace(true, Schema.Installed.name, ContentValues().apply { - put(Schema.Installed.ROW_PACKAGE_NAME, installedItem.packageName) - put(Schema.Installed.ROW_VERSION, installedItem.version) - put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode) - put(Schema.Installed.ROW_SIGNATURE, installedItem.signature) - }) - if (notify) { - notifyChanged(Subject.Products) - } - } - - // Done in insert - fun put(installedItem: InstalledItem) = put(installedItem, true) - - // Done in insert - fun putAll(installedItems: List) { - db.beginTransaction() - try { - db.delete(Schema.Installed.name, null, null) - installedItems.forEach { put(it, false) } - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } - - // Done - fun delete(packageName: String) { - val count = db.delete( - Schema.Installed.name, - "${Schema.Installed.ROW_PACKAGE_NAME} = ?", - arrayOf(packageName) - ) - if (count > 0) { - notifyChanged(Subject.Products) - } - } - - // Unnecessary with Room - private fun transform(cursor: Cursor): InstalledItem { - return InstalledItem( - cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_PACKAGE_NAME)), - cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)), - cursor.getLong(cursor.getColumnIndex(Schema.Installed.ROW_VERSION_CODE)), - cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_SIGNATURE)) - ) - } - } - - object LockAdapter { - // Done in insert (Lock object instead of pair) - private fun put(lock: Pair, notify: Boolean) { - db.insertOrReplace(true, Schema.Lock.name, ContentValues().apply { - put(Schema.Lock.ROW_PACKAGE_NAME, lock.first) - put(Schema.Lock.ROW_VERSION_CODE, lock.second) - }) - if (notify) { - notifyChanged(Subject.Products) - } - } - - // Done in insert (Lock object instead of pair) - fun put(lock: Pair) = put(lock, true) - - // Done in insert (Lock object instead of pair) - fun putAll(locks: List>) { - db.beginTransaction() - try { - db.delete(Schema.Lock.name, null, null) - locks.forEach { put(it, false) } - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } - - // Done - fun delete(packageName: String) { - db.delete(Schema.Lock.name, "${Schema.Lock.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)) - notifyChanged(Subject.Products) - } - } - - // TODO add temporary tables - object UpdaterAdapter { - private val Table.temporaryName: String - get() = "${name}_temporary" - - fun createTemporaryTable() { - db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") - db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") - db.execSQL(Schema.Product.formatCreateTable(Schema.Product.temporaryName)) - db.execSQL(Schema.Category.formatCreateTable(Schema.Category.temporaryName)) - } - - fun putTemporary(products: List) { - db.beginTransaction() - try { - for (product in products) { - // Format signatures like ".signature1.signature2." for easier select - val signatures = product.signatures.joinToString { ".$it" } - .let { if (it.isNotEmpty()) "$it." else "" } - db.insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply { - put(Schema.Product.ROW_REPOSITORY_ID, product.repositoryId) - put(Schema.Product.ROW_PACKAGE_NAME, product.packageName) - put(Schema.Product.ROW_NAME, product.name) - put(Schema.Product.ROW_SUMMARY, product.summary) - put(Schema.Product.ROW_DESCRIPTION, product.description) - put(Schema.Product.ROW_ADDED, product.added) - put(Schema.Product.ROW_UPDATED, product.updated) - put(Schema.Product.ROW_VERSION_CODE, product.versionCode) - put(Schema.Product.ROW_SIGNATURES, signatures) - put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0) - put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize)) - put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize)) - }) - for (category in product.categories) { - db.insertOrReplace( - true, - Schema.Category.temporaryName, - ContentValues().apply { - put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId) - put(Schema.Category.ROW_PACKAGE_NAME, product.packageName) - put(Schema.Category.ROW_NAME, category) - }) - } - } - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } - - fun finishTemporary(repository: Repository, success: Boolean) { - if (success) { - db.beginTransaction() - try { - db.delete( - Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?", - arrayOf(repository.id.toString()) - ) - db.delete( - Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?", - arrayOf(repository.id.toString()) - ) - db.execSQL("INSERT INTO ${Schema.Product.name} SELECT * FROM ${Schema.Product.temporaryName}") - db.execSQL("INSERT INTO ${Schema.Category.name} SELECT * FROM ${Schema.Category.temporaryName}") - RepositoryAdapter.putWithoutNotification(repository, true) - db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") - db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - if (success) { - notifyChanged( - Subject.Repositories, - Subject.Repository(repository.id), - Subject.Products - ) - } - } else { - db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") - db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") - } - } - } -} diff --git a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt index 2a8e02ea..24b1c18d 100644 --- a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt +++ b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt @@ -5,12 +5,15 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.looker.droidify.entity.Repository.Companion.defaultRepositories @Database( entities = [ Repository::class, Product::class, + ProductTemp::class, Category::class, + CategoryTemp::class, Installed::class, Lock::class ], version = 1 @@ -19,7 +22,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 @@ -39,6 +44,11 @@ abstract class DatabaseX : RoomDatabase() { .fallbackToDestructiveMigration() .allowMainThreadQueries() .build() + INSTANCE?.let { instance -> + if (instance.repositoryDao.count == 0) defaultRepositories.forEach { + instance.repositoryDao.put(it) + } + } } return INSTANCE!! } @@ -46,17 +56,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 + // TODO 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..5ee964ef 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,63 +16,72 @@ class Repository { var id: Long = 0 var enabled = 0 - var deleted = 0 + var deleted = false @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var data: Repository? = null + val trueData: Repository? + get() = data?.copy(id = id) + class IdAndDeleted { @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 +95,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/IndexV1Parser.kt b/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index 0d718a63..8c8d7954 100644 --- a/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -153,9 +153,9 @@ object IndexV1Parser { it.string("openCollective") -> donates += Product.Donate.OpenCollective( valueAsString ) - it.dictionary("localized") -> forEachKey { it -> - if (it.token == JsonToken.START_OBJECT) { - val locale = it.key + it.dictionary("localized") -> forEachKey { keyToken -> + if (keyToken.token == JsonToken.START_OBJECT) { + val locale = keyToken.key var name = "" var summary = "" var description = "" 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/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt index f058ccd0..a8154cdf 100644 --- a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -11,8 +11,8 @@ import android.net.Uri import android.os.IBinder import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat -import com.looker.droidify.Common.NOTIFICATION_CHANNEL_INSTALLER -import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER +import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER +import com.looker.droidify.NOTIFICATION_ID_INSTALLER import com.looker.droidify.MainActivity import com.looker.droidify.R import com.looker.droidify.utility.Utils diff --git a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt index 4de79e89..4286bfc6 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)?.trueData } 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.trueData } 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)?.trueData } ?.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..472e1691 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)?.trueData 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 424457b5..c43cf90f 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -11,6 +11,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 @@ -26,6 +27,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 status: Int?, val promptIntent: Intent?) : 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..7520df2e 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)?.trueData ) } .observeOn(AndroidSchedulers.mainThread()) diff --git a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt index 92c1d8e0..21e2bbea 100644 --- a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt @@ -33,7 +33,6 @@ import com.looker.droidify.utility.Utils.languagesList import com.looker.droidify.utility.Utils.translateLocale import com.looker.droidify.utility.extension.resources.* import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class SettingsFragment : ScreenFragment() { @@ -105,6 +104,7 @@ class SettingsFragment : ScreenFragment() { when (it) { Preferences.AutoSync.Never -> getString(R.string.never) Preferences.AutoSync.Wifi -> getString(R.string.only_on_wifi) + Preferences.AutoSync.WifiBattery -> getString(R.string.only_on_wifi_and_battery) Preferences.AutoSync.Always -> getString(R.string.always) } } diff --git a/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt index dd79c909..404395fe 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.trueData } } } .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 aaf4041b..6f01ca70 100644 --- a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt +++ b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt @@ -7,12 +7,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.NOTIFICATION_ID_DOWNLOADING -import com.looker.droidify.Common.NOTIFICATION_ID_SYNCING -import com.looker.droidify.Common.NOTIFICATION_CHANNEL_DOWNLOADING -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 diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 57a4f451..bd1a9702 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -12,15 +12,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.NOTIFICATION_ID_UPDATES -import com.looker.droidify.Common.NOTIFICATION_ID_SYNCING -import com.looker.droidify.Common.NOTIFICATION_CHANNEL_SYNCING -import com.looker.droidify.Common.NOTIFICATION_CHANNEL_UPDATES -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 @@ -31,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 @@ -104,7 +99,7 @@ class SyncService : ConnectionService() { } fun sync(request: SyncRequest) { - val ids = Database.RepositoryAdapter.getAll(null) + val ids = db.repositoryDao.all.mapNotNull { it.trueData } .asSequence().filter { it.enabled }.map { it.id }.toList() sync(ids, request) } @@ -130,7 +125,7 @@ class SyncService : ConnectionService() { } 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) @@ -149,10 +144,10 @@ class SyncService : ConnectionService() { } fun deleteRepository(repositoryId: Long): Boolean { - val repository = Database.RepositoryAdapter.get(repositoryId) + val repository = db.repositoryDao.get(repositoryId)?.trueData return repository != null && run { setEnabled(repository, false) - Database.RepositoryAdapter.markAsDeleted(repository.id) + db.repositoryDao.markAsDeleted(repository.id) true } } @@ -160,10 +155,12 @@ 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( NOTIFICATION_CHANNEL_SYNCING, @@ -337,7 +334,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)?.trueData if (repository != null && repository.enabled) { val lastStarted = started val newStarted = @@ -382,7 +379,7 @@ class SyncService : ConnectionService() { } else if (started != Started.NO) { val disposable = RxUtils .querySingle { it -> - Database.ProductAdapter + db.productDao .query( installed = true, updates = true, @@ -392,7 +389,7 @@ class SyncService : ConnectionService() { signal = it ) .use { - it.asSequence().map(Database.ProductAdapter::transformItem) + it.asSequence().map { it.getProductItem() } .toList() } } diff --git a/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt index 66c4f6dc..aa7191fa 100644 --- a/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt @@ -2,15 +2,12 @@ package com.looker.droidify.ui.activities import android.content.Context import android.content.Intent -import android.database.Cursor import android.os.Bundle import android.view.* import android.view.inputmethod.InputMethodManager import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import androidx.loader.app.LoaderManager -import androidx.loader.content.Loader import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration @@ -20,11 +17,10 @@ import androidx.navigation.ui.setupWithNavController import com.google.android.material.appbar.MaterialToolbar import com.looker.droidify.BuildConfig import com.looker.droidify.ContextWrapperX +import com.looker.droidify.MainApplication import com.looker.droidify.R import com.looker.droidify.content.Preferences import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database -import com.looker.droidify.database.QueryLoader import com.looker.droidify.databinding.ActivityMainXBinding import com.looker.droidify.installer.AppInstaller import com.looker.droidify.screen.* @@ -37,7 +33,7 @@ import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.text.nullIfEmpty import kotlinx.coroutines.launch -class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks { +class MainActivityX : AppCompatActivity() { companion object { const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES" const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL" @@ -67,6 +63,9 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks } }) + val db + get() = (application as MainApplication).db + lateinit var cursorOwner: CursorOwner private set @@ -230,79 +229,4 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks } syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment) } - - fun attachCursorOwner(callback: CursorOwner.Callback, request: CursorOwner.Request) { - val oldActiveRequest = viewModel.activeRequests[request.id] - if (oldActiveRequest?.callback != null && - oldActiveRequest.callback != callback && oldActiveRequest.cursor != null - ) { - oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null) - } - val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) { - callback.onCursorData(request, oldActiveRequest.cursor) - oldActiveRequest.cursor - } else { - null - } - viewModel.activeRequests[request.id] = CursorOwner.ActiveRequest(request, callback, cursor) - if (cursor == null) { - LoaderManager.getInstance(this).restartLoader(request.id, null, this) - } - } - - - fun detachCursorOwner(callback: CursorOwner.Callback) { - for (id in viewModel.activeRequests.keys) { - val activeRequest = viewModel.activeRequests[id]!! - if (activeRequest.callback == callback) { - viewModel.activeRequests[id] = activeRequest.copy(callback = null) - } - } - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - val request = viewModel.activeRequests[id]!!.request - return QueryLoader(this) { - when (request) { - is CursorOwner.Request.ProductsAvailable -> Database.ProductAdapter - .query( - installed = false, - updates = false, - searchQuery = request.searchQuery, - section = request.section, - order = request.order, - signal = it - ) - is CursorOwner.Request.ProductsInstalled -> Database.ProductAdapter - .query( - installed = true, - updates = false, - searchQuery = request.searchQuery, - section = request.section, - order = request.order, - signal = it - ) - is CursorOwner.Request.ProductsUpdates -> Database.ProductAdapter - .query( - installed = true, - updates = true, - searchQuery = request.searchQuery, - section = request.section, - order = request.order, - signal = it - ) - is CursorOwner.Request.Repositories -> Database.RepositoryAdapter.query(it) - } - } - } - - override fun onLoadFinished(loader: Loader, data: Cursor?) { - val activeRequest = viewModel.activeRequests[loader.id] - if (activeRequest != null) { - viewModel.activeRequests[loader.id] = activeRequest.copy(cursor = data) - activeRequest.callback?.onCursorData(activeRequest.request, data) - } - } - - override fun onLoaderReset(loader: Loader) = onLoadFinished(loader, null) } 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..681c50b8 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,15 +31,17 @@ 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 import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.* +import kotlin.collections.ArrayList class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { companion object { @@ -129,12 +130,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.trueData } } .map { it -> it.asSequence().map { Pair(it.id, it) }.toMap() .let { @@ -151,7 +156,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()) @@ -452,15 +457,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { } else Unit } AppDetailAdapter.Action.SHARE -> { - val sendIntent: Intent = Intent().apply { - this.action = Intent.ACTION_SEND - putExtra( - Intent.EXTRA_TEXT, - "https://www.f-droid.org/packages/${products[0].first.packageName}/" - ) - type = "text/plain" - } - startActivity(Intent.createChooser(sendIntent, null)) + shareIntent(packageName, products[0].first.name) } }::class } @@ -478,6 +475,21 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { } } + private fun shareIntent(packageName: String, appName: String) { + val shareIntent = Intent(Intent.ACTION_SEND) + val extraText = if (Android.sdk(24)) { + "https://www.f-droid.org/${resources.configuration.locales[0].language}/packages/${packageName}/" + } else "https://www.f-droid.org/${resources.configuration.locale.language}/packages/${packageName}/" + + + shareIntent.type = "text/plain" + shareIntent.putExtra(Intent.EXTRA_TITLE, appName) + shareIntent.putExtra(Intent.EXTRA_SUBJECT, appName) + shareIntent.putExtra(Intent.EXTRA_TEXT, extraText) + + startActivity(Intent.createChooser(shareIntent, "Where to Send?")) + } + override fun onPreferenceChanged(preference: ProductPreference) { lifecycleScope.launch { updateButtons(preference) } } 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..759e8a26 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.trueData } } } + .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/ui/fragments/ExploreFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt index 892186fa..dc07584a 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt @@ -5,34 +5,32 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.looker.droidify.R import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database import com.looker.droidify.databinding.FragmentExploreXBinding +import com.looker.droidify.entity.Repository import com.looker.droidify.ui.adapters.AppListAdapter import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX import com.looker.droidify.utility.RxUtils import com.looker.droidify.widget.RecyclerFastScroller import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { - override val viewModel: MainNavFragmentViewModelX by viewModels() + override lateinit var viewModel: MainNavFragmentViewModelX private lateinit var binding: FragmentExploreXBinding override val source = Source.AVAILABLE - private var repositoriesDisposable: Disposable? = null + private var repositories: Map = mapOf() override fun onCreateView( inflater: LayoutInflater, @@ -42,6 +40,9 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { super.onCreate(savedInstanceState) binding = FragmentExploreXBinding.inflate(inflater, container, false) binding.lifecycleOwner = this + val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db) + viewModel = ViewModelProvider(this, viewModelFactory) + .get(MainNavFragmentViewModelX::class.java) binding.recyclerView.apply { layoutManager = LinearLayoutManager(context) @@ -58,22 +59,13 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) - repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + viewModel.fillList(source) + viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } .observeOn(AndroidSchedulers.mainThread()) - .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } - } - - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - repositoriesDisposable?.dispose() - repositoriesDisposable = null + .subscribe { repositories = it } } override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt index ea427cc6..f09a3440 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt @@ -5,34 +5,36 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager -import com.looker.droidify.R +import androidx.recyclerview.widget.RecyclerView import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database import com.looker.droidify.databinding.FragmentInstalledXBinding -import com.looker.droidify.ui.adapters.AppListAdapter +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.entity.Repository +import com.looker.droidify.ui.items.HAppItem +import com.looker.droidify.ui.items.VAppItem import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX import com.looker.droidify.utility.RxUtils import com.looker.droidify.widget.RecyclerFastScroller +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.adapters.ItemAdapter import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { - override val viewModel: MainNavFragmentViewModelX by viewModels() + override lateinit var viewModel: MainNavFragmentViewModelX private lateinit var binding: FragmentInstalledXBinding + private val installedItemAdapter = ItemAdapter() + private var installedFastAdapter: FastAdapter? = null + private val updatedItemAdapter = ItemAdapter() + private var updatedFastAdapter: FastAdapter? = null + override val source = Source.INSTALLED - private var repositoriesDisposable: Disposable? = null + private var repositories: Map = mapOf() override fun onCreateView( inflater: LayoutInflater, @@ -42,12 +44,26 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { super.onCreate(savedInstanceState) binding = FragmentInstalledXBinding.inflate(inflater, container, false) binding.lifecycleOwner = this + val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db) + viewModel = ViewModelProvider(this, viewModelFactory) + .get(MainNavFragmentViewModelX::class.java) - binding.recyclerView.apply { - layoutManager = LinearLayoutManager(context) + installedFastAdapter = FastAdapter.with(installedItemAdapter) + installedFastAdapter?.setHasStableIds(true) + binding.installedRecycler.apply { + layoutManager = LinearLayoutManager(requireContext()) isMotionEventSplittingEnabled = false isVerticalScrollBarEnabled = false - adapter = AppListAdapter { mainActivityX.navigateProduct(it.packageName) } + adapter = installedFastAdapter + RecyclerFastScroller(this) + } + updatedFastAdapter = FastAdapter.with(updatedItemAdapter) + updatedFastAdapter?.setHasStableIds(true) + binding.updatedRecycler.apply { + layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + adapter = updatedFastAdapter RecyclerFastScroller(this) } return binding.root @@ -56,28 +72,26 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) - repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + viewModel.fillList(source) + viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } .observeOn(AndroidSchedulers.mainThread()) - .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } - } - - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - repositoriesDisposable?.dispose() - repositoriesDisposable = null + .subscribe { repositories = it } } override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { - // TODO create app list out of cursor and use those on the different RecycleViews - (binding.recyclerView.adapter as? AppListAdapter)?.apply { - this.cursor = cursor + // TODO get a list instead of the cursor + // TODO use LiveData and observers instead of listeners + val appItemList: List = listOf() + installedItemAdapter.set(appItemList + .map { VAppItem(it, repositories[it.repositoryId]) } + ) + updatedItemAdapter.set(appItemList.filter { it.canUpdate } + .map { HAppItem(it, repositories[it.repositoryId]) } + ) + /* lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { emptyText = when { @@ -88,6 +102,6 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { } } } - } + */ } } diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt index 778eb7c6..df7cc7b3 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt @@ -5,34 +5,36 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager -import com.looker.droidify.R +import androidx.recyclerview.widget.RecyclerView import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database import com.looker.droidify.databinding.FragmentLatestXBinding -import com.looker.droidify.ui.adapters.AppListAdapter +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.entity.Repository +import com.looker.droidify.ui.items.HAppItem +import com.looker.droidify.ui.items.VAppItem import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX import com.looker.droidify.utility.RxUtils import com.looker.droidify.widget.RecyclerFastScroller +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.adapters.ItemAdapter import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { - override val viewModel: MainNavFragmentViewModelX by viewModels() + override lateinit var viewModel: MainNavFragmentViewModelX private lateinit var binding: FragmentLatestXBinding - override val source = Source.UPDATES + private val updatedItemAdapter = ItemAdapter() + private var updatedFastAdapter: FastAdapter? = null + private val newItemAdapter = ItemAdapter() + private var newFastAdapter: FastAdapter? = null - private var repositoriesDisposable: Disposable? = null + override val source = Source.AVAILABLE + + private var repositories: Map = mapOf() override fun onCreateView( inflater: LayoutInflater, @@ -42,13 +44,26 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { super.onCreate(savedInstanceState) binding = FragmentLatestXBinding.inflate(inflater, container, false) binding.lifecycleOwner = this + val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db) + viewModel = ViewModelProvider(this, viewModelFactory) + .get(MainNavFragmentViewModelX::class.java) - binding.recyclerView.apply { - id = android.R.id.list - layoutManager = LinearLayoutManager(context) + updatedFastAdapter = FastAdapter.with(updatedItemAdapter) + updatedFastAdapter?.setHasStableIds(true) + binding.updatedRecycler.apply { + layoutManager = LinearLayoutManager(requireContext()) isMotionEventSplittingEnabled = false isVerticalScrollBarEnabled = false - adapter = AppListAdapter { mainActivityX.navigateProduct(it.packageName) } + adapter = updatedFastAdapter + RecyclerFastScroller(this) + } + newFastAdapter = FastAdapter.with(newItemAdapter) + newFastAdapter?.setHasStableIds(true) + binding.newRecycler.apply { + layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + adapter = newFastAdapter RecyclerFastScroller(this) } return binding.root @@ -57,28 +72,26 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) - repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) + viewModel.fillList(source) + viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } .observeOn(AndroidSchedulers.mainThread()) - .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } - } - - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - repositoriesDisposable?.dispose() - repositoriesDisposable = null + .subscribe { repositories = it } } override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { - // TODO create app list out of cursor and use those on the different RecycleViews - (binding.recyclerView.adapter as? AppListAdapter)?.apply { - this.cursor = cursor + // TODO get a list instead of the cursor + // TODO use LiveData and observers instead of listeners + val appItemList: List = listOf() + updatedItemAdapter.set(appItemList // .filter { !it.hasOneRelease } + .map { VAppItem(it, repositories[it.repositoryId]) } + ) + newItemAdapter.set(appItemList // .filter { it.hasOneRelease } + .map { HAppItem(it, repositories[it.repositoryId]) } + ) + /* lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { emptyText = when { @@ -89,6 +102,6 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { } } } - } + */ } } diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt index 3abcb73c..0b25f808 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt @@ -10,7 +10,7 @@ import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { val mainActivityX: MainActivityX get() = requireActivity() as MainActivityX - abstract val viewModel: MainNavFragmentViewModelX + abstract var viewModel: MainNavFragmentViewModelX abstract val source: Source open fun onBackPressed(): Boolean = false @@ -18,7 +18,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setSearchQuery(searchQuery: String) { viewModel.setSearchQuery(searchQuery) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -26,7 +26,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setSection(section: ProductItem.Section) { viewModel.setSection(section) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -34,7 +34,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setOrder(order: ProductItem.Order) { viewModel.setOrder(order) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -44,4 +44,37 @@ enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean AVAILABLE(R.string.available, true, true), INSTALLED(R.string.installed, false, true), UPDATES(R.string.updates, false, false) +} + +sealed class Request { + internal abstract val id: Int + + data class ProductsAvailable( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 1 + } + + data class ProductsInstalled( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 2 + } + + data class ProductsUpdates( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 3 + } + + object Repositories : Request() { + override val id: Int + get() = 4 + } } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/items/HAppItem.kt b/src/main/kotlin/com/looker/droidify/ui/items/HAppItem.kt new file mode 100644 index 00000000..5d12bb58 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/items/HAppItem.kt @@ -0,0 +1,42 @@ +package com.looker.droidify.ui.items + +import android.view.LayoutInflater +import android.view.ViewGroup +import coil.load +import coil.transform.RoundedCornersTransformation +import com.looker.droidify.R +import com.looker.droidify.databinding.ItemAppHorizXBinding +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.toPx +import com.mikepenz.fastadapter.binding.AbstractBindingItem + +class HAppItem(val item: ProductItem, val repository: Repository?) : + AbstractBindingItem() { + override val type: Int + get() = R.id.fastadapter_item + + override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) + : ItemAppHorizXBinding = ItemAppHorizXBinding.inflate(inflater, parent, false) + + override fun bindView(binding: ItemAppHorizXBinding, payloads: List) { + val (progressIcon, defaultIcon) = Utils.getDefaultApplicationIcons(binding.icon.context) + + binding.name.text = item.name + repository?.let { + binding.icon.load( + CoilDownloader.createIconUri( + binding.icon, item.packageName, + item.icon, item.metadataIcon, repository + ) + ) { + transformations(RoundedCornersTransformation(4.toPx)) + placeholder(progressIcon) + error(defaultIcon) + } + } + binding.version.text = if (item.canUpdate) item.version else item.installedVersion + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/items/VAppItem.kt b/src/main/kotlin/com/looker/droidify/ui/items/VAppItem.kt new file mode 100644 index 00000000..2a9a63b9 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/items/VAppItem.kt @@ -0,0 +1,79 @@ +package com.looker.droidify.ui.items + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.res.ResourcesCompat +import coil.load +import coil.transform.RoundedCornersTransformation +import com.looker.droidify.R +import com.looker.droidify.databinding.ItemAppVerticalXBinding +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.getColorFromAttr +import com.looker.droidify.utility.extension.resources.sizeScaled +import com.looker.droidify.utility.extension.resources.toPx +import com.looker.droidify.utility.extension.text.nullIfEmpty +import com.mikepenz.fastadapter.binding.AbstractBindingItem + +class VAppItem(val item: ProductItem, val repository: Repository?) : + AbstractBindingItem() { + override val type: Int + get() = R.id.fastadapter_item + + override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) + : ItemAppVerticalXBinding = ItemAppVerticalXBinding.inflate(inflater, parent, false) + + override fun bindView(binding: ItemAppVerticalXBinding, payloads: List) { + val (progressIcon, defaultIcon) = Utils.getDefaultApplicationIcons(binding.icon.context) + + binding.name.text = item.name + binding.summary.text = + if (item.name == item.summary) "" else item.summary + binding.summary.visibility = + if (binding.summary.text.isNotEmpty()) View.VISIBLE else View.GONE + + repository?.let { + binding.icon.load( + CoilDownloader.createIconUri( + binding.icon, item.packageName, + item.icon, item.metadataIcon, it + ) + ) { + transformations(RoundedCornersTransformation(4.toPx)) + placeholder(progressIcon) + error(defaultIcon) + } + } + binding.status.apply { + if (item.canUpdate) { + text = item.version + if (background == null) { + background = + ResourcesCompat.getDrawable( + binding.root.resources, + R.drawable.background_border, + context.theme + ) + resources.sizeScaled(6).let { setPadding(it, it, it, it) } + backgroundTintList = + context.getColorFromAttr(R.attr.colorSecondaryContainer) + setTextColor(context.getColorFromAttr(R.attr.colorSecondary)) + } + } else { + text = item.installedVersion.nullIfEmpty() ?: item.version + if (background != null) { + setPadding(0, 0, 0, 0) + setTextColor(binding.status.context.getColorFromAttr(android.R.attr.colorControlNormal)) + background = null + } + } + } + val enabled = item.compatible || item.installedVersion.isNotEmpty() + sequenceOf(binding.name, binding.status, binding.summary).forEach { + it.isEnabled = enabled + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt index bc3fb11c..687b4cc8 100644 --- a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt @@ -2,9 +2,8 @@ package com.looker.droidify.ui.viewmodels import androidx.lifecycle.ViewModel import com.looker.droidify.database.CursorOwner -import com.looker.droidify.ui.activities.MainActivityX -class MainActivityViewModelX() : ViewModel() { +class MainActivityViewModelX : ViewModel() { val activeRequests = mutableMapOf() } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt index 30832c56..6d463a91 100644 --- a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt @@ -1,15 +1,23 @@ package com.looker.droidify.ui.viewmodels +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.DatabaseX +import com.looker.droidify.database.Product import com.looker.droidify.entity.ProductItem +import com.looker.droidify.ui.fragments.Request import com.looker.droidify.ui.fragments.Source import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class MainNavFragmentViewModelX : ViewModel() { +class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { private val _order = MutableStateFlow(ProductItem.Order.LAST_UPDATE) private val _sections = MutableStateFlow(ProductItem.Section.All) @@ -32,7 +40,7 @@ class MainNavFragmentViewModelX : ViewModel() { started = SharingStarted.WhileSubscribed(5000) ) - fun request(source: Source): CursorOwner.Request { + fun request(source: Source): Request { var mSearchQuery = "" var mSections: ProductItem.Section = ProductItem.Section.All var mOrder: ProductItem.Order = ProductItem.Order.NAME @@ -42,17 +50,17 @@ class MainNavFragmentViewModelX : ViewModel() { launch { order.collect { if (source.order) mOrder = it } } } return when (source) { - Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( + Source.AVAILABLE -> Request.ProductsAvailable( mSearchQuery, mSections, mOrder ) - Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( + Source.INSTALLED -> Request.ProductsInstalled( mSearchQuery, mSections, mOrder ) - Source.UPDATES -> CursorOwner.Request.ProductsUpdates( + Source.UPDATES -> Request.ProductsUpdates( mSearchQuery, mSections, mOrder @@ -60,6 +68,46 @@ class MainNavFragmentViewModelX : ViewModel() { } } + var productsList = MediatorLiveData>() + + fun fillList(source: Source) { + viewModelScope.launch { + productsList.value = query(request(source))?.toMutableList() + } + } + + private suspend fun query(request: Request): List? { + return withContext(Dispatchers.IO) { + when (request) { + is Request.ProductsAvailable -> db.productDao + .queryList( + installed = false, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + is Request.ProductsInstalled -> db.productDao + .queryList( + installed = true, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + is Request.ProductsUpdates -> db.productDao + .queryList( + installed = true, + updates = true, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + else -> listOf() + } + } + } + fun setSection(newSection: ProductItem.Section, perform: () -> Unit) { viewModelScope.launch { if (newSection != sections.value) { @@ -86,4 +134,14 @@ class MainNavFragmentViewModelX : ViewModel() { } } } -} \ No newline at end of file + + class Factory(val db: DatabaseX) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MainNavFragmentViewModelX::class.java)) { + return MainNavFragmentViewModelX(db) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/utility/Utils.kt b/src/main/kotlin/com/looker/droidify/utility/Utils.kt index fff8f177..8ed0f609 100644 --- a/src/main/kotlin/com/looker/droidify/utility/Utils.kt +++ b/src/main/kotlin/com/looker/droidify/utility/Utils.kt @@ -7,26 +7,32 @@ import android.content.Context import android.content.pm.PackageInfo import android.content.pm.Signature import android.content.res.Configuration +import android.database.Cursor 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.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +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 import com.topjohnwu.superuser.Shell import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect +import java.io.ByteArrayOutputStream import java.security.MessageDigest import java.security.cert.Certificate import java.security.cert.CertificateEncodingException @@ -181,3 +187,51 @@ object Utils { } } + +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 diff --git a/src/main/res/layout/fragment_installed_x.xml b/src/main/res/layout/fragment_installed_x.xml index 92aedc5f..9bea4a68 100644 --- a/src/main/res/layout/fragment_installed_x.xml +++ b/src/main/res/layout/fragment_installed_x.xml @@ -130,7 +130,7 @@ app:layout_constraintTop_toBottomOf="@id/modeBar"> + + + app:layout_constraintTop_toBottomOf="@id/name" /> \ No newline at end of file diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 37092627..cf2a3d15 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -81,4 +81,9 @@ Odkaz zkopírován do schránky Typy instalací Odkazy + + %d aplikace má novou verzi. + + + \ No newline at end of file diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 48eee4d4..3032c1ce 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -177,4 +177,5 @@ Actualizar todos Aplicaciones instaladas Aplicaciones nuevas + Solo en Wi-Fi y Conectado \ No newline at end of file diff --git a/src/main/res/values-fa/strings.xml b/src/main/res/values-fa/strings.xml index c85a846c..da54ba31 100644 --- a/src/main/res/values-fa/strings.xml +++ b/src/main/res/values-fa/strings.xml @@ -177,4 +177,5 @@ نادیده گرفتن تمام نسخه‌های جدید پاسخ نادرست سرور. ناسازگار با %s + فقط در وای‌فای و به برق وصل‌شده \ No newline at end of file diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 6d88cd3a..fc5ed415 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -8,56 +8,56 @@ Musta Anti-ominaisuudet Sovellus - Toiminta epäonnistui - Lisää arkisto + Toiminto epäonnistui + Lisää tietovarasto Tätä sovellusta ei löytynyt - Tekijän sähköposti - Kirjoittajan verkkosivusto + Kehittäjän sähköposti + Kehittäjän verkkosivusto Saatavilla Vikojen jäljitin Poista - Pimeä - Ei voitu validoida %s + Tumma + Ei voitu validoida kohdetta %s Käännetty virheenkorjausta varten Vahvistus Yhdistetään… Sisältää ei-vapaata mediaa - Ei voitu ladata %s - Ei voitu synkronoida %s - Opintopisteet + Ei voitu ladata kohdetta %s + Ei voitu synkronoida kohdetta %s + Tekijät Peruuta - Arkistoa ei voi muokata, koska se synkronoidaan juuri nyt. + Tietovarastoa ei voida muokata, koska sen synkronointi on käynnissä. Muutosloki Muutokset Tarkistetaan tietovarastoa… Sormenjälki Väärä tiedostomuoto. - Muokkaa arkistoa + Muokkaa tietovarastoa Ladataan %s… - Lataaminen - Ladattu %s + Ladataan + Kohde %s ladattu Lahjoita Tiedot - Description - Poista arkisto\? - On mainontaa + Kuvaus + Poistetaanko tietovarasto\? + Sisältää mainontaa Ei-vapaita riippuvuuksia - On tietoturva-aukkoja - Virheellinen palvelinvastaus. + Tietoturvassa on haavoittuvuuksia + Virheellinen palvelimen vastaus. HTTP-välityspalvelin - Jätä kaikki uudet versiot huomiotta + Älä huomioi uusia versioita Jätä tämä versio huomiotta Sinun %1$s (API-versio %2$d) ei ole tuettu. %3$s Suurin API-versio on %d. - Vähimmäis API-versio on %d. + Varhaisin API-versio on %d. Puuttuvat ominaisuudet. Tämä versio on vanhempi kuin laitteeseesi asennettu versio. Poista se ensin. Yhteensopimaton versio Yhteensopimattomat versiot Näytä sovellusversiot, jotka eivät ole yhteensopivia laitteen kanssa - Yhteensopimaton %s + Yhteensopimaton kohteen %s kanssa Asenna - Asennuksen tyypit + Asennustyypit Asennettu Ei voitu tarkistaa eheyttä. Virheellinen osoite @@ -66,74 +66,74 @@ Virheelliset käyttöoikeudet. Virheellinen allekirjoitus. Virheellinen käyttäjänimen muoto - Laukaisu + Avaa Lisenssi %s -lisenssi - Valo + Vaalea Linkki kopioitu leikepöydälle Linkit Lista-animaatiot Näytä listan animaatio pääsivulla Yhdistäminen %s Nimi - Verkko virhe. - Koskaan - Sovellusten uudet versiot saatavilla + Verkkovirhe. + Ei koskaan + Päivityksiä saatavilla - %d -sovelluksella on uusi versio. - %d -sovelluksia, joilla on uusia versioita. + Uusi versio on saatavilla yhteen sovellukseen. + Uusi versio saatavilla %d sovellukseen. - Ei käytettävissä olevia sovelluksia + Ei sovelluksia saatavilla Ei asennettuja sovelluksia Kuvausta ei ole saatavilla. - Ei löytynyt tällaisia sovelluksia + Haulla ei löytynyt sovelluksia Toimittanut %s - Proxy-isäntä - Proxy portti - Proxy tyyppi + Välityspalvelimen osoite + Välityspalvelimen portti + Välityspalvelimen tyyppi Äskettäin päivitetty Tietovarastot - Varasto + Tietovarasto Vaatii %s Hiljainen asennus Salli pääkäyttäjän oikeudet hiljaisiin asennuksiin Tallenna Tallennetaan tietoja… Kuvakaappaukset - Vain Wi-Fi - Avaa %s\? + Vain Wi-Fi-yhteydellä + Avataanko %s\? Muut Indeksitiedostoa ei voitu analysoida. Salasana Salasana puuttuu - Luvat + Käyttöoikeudet +%d lisää Asetukset - Käsitellään %1$s… - Hankkeen verkkosivusto - Edistää muita kuin ilmaisia verkkopalveluja + Käsitellään kohdetta %1$s… + Verkkosivusto + Edistää ei-vapaita verkkopalveluja Haku - Valitse peili - Ei proxy + Valitse peilipalvelin + Ei välityspalvelinta Ilmoita sovellusten uusista versioista Näytä ilmoitus, kun uusia versioita on saatavilla - Yhteensopiva vain %s - Osake + Yhteensopiva vain arkkitehtuurilla %s + Jaa Näytä lisää Näytä vanhemmat versiot - OKEI - Seuraa toimintaasi tai raportoi siitä + OK + Seuraa tai raportoi toimintaasi Poista asennus Tuntematon Tuntematon virhe. Tuntematon: %s - Merkitsemätön + Allekirjoittamaton Epävakaat päivitykset Vahvistamaton Alkuperäinen lähdekoodi ei ole vapaa Verkkosivut Mitä uutta - Odotan latauksen aloittamista… + Odotetaan latauksen aloittamista… Käyttäjätunnus puuttuu Käyttäjänimi Allekirjoitus %s @@ -141,12 +141,12 @@ Koko Ohita SOCKS-välityspalvelin - Lajittelu järjestys + Lajittelujärjestys Lähdekoodi Lähdekoodi ei enää saatavilla Suositeltu - Synkronoi arkistot - Synkronoi arkistot automaattisesti + Synkronoi tietovarastot + Synkronoi tietovarastot automaattisesti Synkronointi Synkronoidaan %s… Teemat @@ -157,13 +157,13 @@ Päivitys Päivitykset Edistää ei-vapaita ohjelmia - Tätä arkistoa ei ole vielä käytetty. Ota se käyttöön nähdäksesi siinä olevat sovellukset. - Proxy - Allekirjoittamaton. Sovellusluetteloa ei voitu tarkistaa. Ole varovainen ladatessasi sovelluksia allekirjoittamattomista arkistoista. + Tätä tietovarastoa ei ole vielä käytetty. Ota se käyttöön nähdäksesi siinä olevat sovellukset. + Välityspalvelin + Allekirjoittamaton. Sovellusluetteloa ei voitu varmistaa. Ole varovainen ladatessasi sovelluksia allekirjoittamattomista tietovarastoista. Versio %s Indeksiä ei voitu validoida. Ehdota epävakaiden versioiden asentamista - Tämä versio on allekirjoitettu eri varmenteella kuin laitteeseesi asennettu varmenne. Poista se ensin. + Tämä versio on allekirjoitettu eri varmenteella kuin laitteellesi asennettu versio. Poista se ensin. Alustasi %1$s ei ole tuettu. Tuetut alustat: %2$s. Versiot Versio diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 9613935e..b5e68f4c 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -177,4 +177,5 @@ Nouveautés Nouvelles applications Tout mettre à jour + Uniquement en Wi-Fi et branché \ No newline at end of file diff --git a/src/main/res/values-in/strings.xml b/src/main/res/values-in/strings.xml new file mode 100644 index 00000000..ccc3d757 --- /dev/null +++ b/src/main/res/values-in/strings.xml @@ -0,0 +1,180 @@ + + + Pelacak bug + Platform %1$s Anda tidak didukung. Platform yang didukung: %2$s. + Semua aplikasi + Hapus + Anti-fitur + Situs pembuat + Tersedia + Batal + Tidak dapat mengedit repositori karena sedang mensinkronisasi. + Daftar perubahan + Perubahan + Memeriksa repositori… + Menghubungkan… + Mengandung media non-bebas + Tidak dapat mensinkronisasi %s + Tidak dapat memvalidasi %s + Kredit + Deskripsi + Detail + Donasi + %s telah diunduh + Mengunduh %s… + Ubah repositori + Format berkas tidak valid. + Sidik + Ada iklan + Ada celah keamanan + Respon server tidak valid. + Abaikan semua versi baru + Abaikan versi ini + %1$s Anda (versi API %2$d) tidak didukung. %3$s + Versi API maksimal %d. + Versi API minimal %d. + Fitur yang hilang. + Versi ini lebih tua dari yang ter-install di perangkat Anda. Uninstall terlebih dahulu. + Versi yang tidak cocok + Versi yang tidak cocok + Tipe Instalasi + Ter-install + Tidak dapat mengecek integritas. + Alamat tidak valid + Perlihatkan animasi list di halaman utama + Menggabungkan %s + Nama + Tidak pernah + Tanpa proxy + Beritahu tentang versi baru aplikasi + Hanya kompatibel dengan %s + Hanya di jaringan Wi-Fi + Sandi + Pengaturan + Situs projek + Disediakan oleh %s + Sinkronisasi repositori otomatis + Mensinkronisasi + Sistem + Tap untuk menginstall. + Target + Tema + Tema + Merekam atau melaporkan aktivitas Anda + Uninstall + Galat. + Sarankan menginstall versi tidak stabil + Pembaruan + Pembaruan + Menunggu mulai unduhan… + Jelajahi + Perbaharui semua + Urutkan & Saring + Aksi gagal + Tambah repositori + Semua aplikasi Anda terbaharukan + Selalu + E-mail pembuat + Dikompilasi untuk debugging + Alamat + Sudah ada + Hitam + Tidak dapat menemukan aplikasi itu + Aplikasi + Konfirmasi + Tidak dapat mengunduh %s + Hapus repositori\? + Ada dependensi non-bebas + Versi ini ditandatangani dengan sertifikat yang berbeda dari yang ter-install di perangkat Anda. Uninstall terlebih dahulu. + Install + Tidak ada aplikasi yang ter-install + Gelap + Mengunduh + HTTP proxy + Tidak cocok dengan %s + Perlihatkan versi aplikasi yang tidak cocok dengan perangkat + Format sidik tidak valid + Tidak dapat mengurai berkas indeks. + Metadata tidak valid. + Perizinan tidak valid. + Memproses %1$s… + Tandatangan tidak valid. + Lisensi %s + Animasi List + + %d aplikasi dengan versi baru. + + Format username tidak valid + Jalankan + Lisensi + Tautan disalin ke clipboard + Tautan + Terang + Galat jaringan. + Versi aplikasi baru tersedia + Tidak ada aplikasi yang tersedia + Perlihatkan notifikasi saat versi baru tersedia + Jumlah aplikasi + OK + Sandi tidak ditemukan + Perizinan + Proxy SOCKS + Tidak ada deskripsi. + Tidak dapat menemukan aplikasi tersebut + Hanya di jaringan Wi-Fi dan terhubung ke listrik + Buka %s\? + Lainnya + +%d lebih banyak + Port proxy + Repositori + Mempromosikan software non-bebas + Proxy + Mempromosikan situs non-bebas + Jenis proxy + Host proxy + Perlihatkan versi yang lebih tua + Diperbaharui akhir-akhir ini + Repositori + Perbolehkan perizinan root untuk instalasi senyap + Tidak ditandatangani. Tidak dapat memverifikasi daftar aplikasi. Hati-hati mengunduh aplikasi dari repositori yang tidak ditandatangani. + Menyimpan detail… + Pilih mirror + Pengurutan + Kode sumber + Username + Repositori ini belum dipakai. Aktifkan untuk melihat aplikasi di dalamnya. + Membutuhkan %s + Simpan + Cari + Instalasi Senyap + Tangkapan layar + Lewati + Kode sumber sudah tidak tersedia + Disarankan + Bagikan + Perlihatkan lebih banyak + Tandatangan %s + Ditandatangani dengan algoritma yang tidak aman + Ukuran + Versi + Sinkronisasi repositori + Mensinkronisasi %s… + Tidak ditandatangani + Pembaruan tidak stabil + Kode sumber tidak bebas + Tidak terverifikasi + Indeks tidak dapat divalidasi. + Tidak diketahui + Tidak diketahui: %s + Username tidak ada + Versi + Perlihatkan Lebih Sedikit + Terbaru + Versi %s + Situs + Bahasa + Personalisasi + Apa yang Baru + Aplikasi ter-install + Aplikasi baru + \ No newline at end of file diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 12a1ec00..7974326f 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -45,7 +45,7 @@ Contiene vulnerabilità Risposta del server non valida. Proxy HTTP - Ignora tuttie le nuove versioni + Ignora tutte le nuove versioni Ignora questa versione Il tuo %1$s (API version %2$d) non è supportato. %3$s La versione massima API è %d. @@ -180,4 +180,5 @@ Più recenti Ordina e filtra Nuove applicazioni + Solo su Wi-Fi e Plugged-In \ No newline at end of file diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..44d24b4c --- /dev/null +++ b/src/main/res/values-ja/strings.xml @@ -0,0 +1,173 @@ + + + 作者のウェブサイト + 操作に失敗しました + リポジトリ追加 + すべてのアプリが最新です + 既に存在しています + 常に + ブラック + 好ましくない可能性のある機能 + アプリ + 利用可能 + バグトラッカー + 現在同期中のため,リポジトリを編集できません。 + 変更内容 + 確認 + 接続中… + 非フリーのメディアを含みます + %s をダウンロードできませんでした + %s を同期できませんでした + %s を確認できませんでした + クレジット + ダーク + リポジトリを削除しますか\? + 説明 + 詳細 + %s をダウンロードしました + ダウンロード中 + %s をダウンロード中… + 無効なファイル形式です。 + 電子指紋 + 非フリーな依存関係を含みます + セキュリティ上の危険性を含みます + 無効なサーバー応答です。 + HTTPプロキシ + 不足している機能。 + 最大のAPIバージョンは %d です。 + 最小のAPIバージョンは %d です。 + 互換性のないバージョン + 互換性のないバージョン + インストール + 無効なアドレス + 無効な電子指紋形式 + 無効なパーミッションです。 + 無効なメタデータです。 + 無効な署名です。 + 無効なユーザー名形式 + ライセンス + ライト + リンク + リストアニメーション + メインページにリストアニメーションを表示 + %s をマージ + 名前 + ネットワーク エラー。 + なし + アプリの新バージョンが利用可能 + 利用可能なアプリはありません + インストール済みのアプリはありません + 説明がありません。 + プロキシなし + アプリの新バージョンを通知 + アプリの数 + OK + Wi-Fiのみ + %s を開きますか\? + その他 + インデックスファイルを解析できませんでした。 + パスワード + パスワードがありません + + %d 詳細 + %1$s を処理中… + 非フリーなネットワークサービスを推奨 + 非フリーなソフトウェアを推奨 + %s 提供 + プロキシホスト + プロキシポート + プロキシタイプ + 新規更新 + リポジトリ + アドレス + すべてのアプリ + アプリを発見できませんでした + キャンセル + リポジトリを編集 + 作者のメールアドレス + 更新履歴 + リポジトリのチェック中です… + 削除 + 寄付 + デバッグ用にコンパイル済み + 広告を含みます + このバージョンを無視 + すべての新バージョンを無視 + これはあなたのデバイスにインストールされているバージョンより古いバージョンです。先にアンインストールしてください。 + お使いの %1$s (APIバージョン %2$d) はサポートされていません。%3$s + インストール済み + 整合性を確認できませんでした。 + あなたの %1$s プラットフォームはサポート外です。サポートされているプラットフォーム: %2$s 。 + このバージョンは,デバイスにインストールされているアプリの証明書とは異なる証明書で署名されています。先にアンインストールしてください。 + デバイスと互換性のないアプリケーションのバージョンを表示 + %s とは互換性がありません + リンクがクリップボードにコピーされました + + %d 個のアプリで新バージョンが利用可能です。 + + 新しいバージョンが利用可能なときに通知を表示します + 権限 + インストールの種類 + %s ライセンス + 起動 + %s とのみ互換性があります + そのようなアプリは見つかりませんでした + 設定 + プロジェクトのウェブサイト + プロキシ + リポジトリ + このリポジトリは未だ使用されていません。オンにすると、アプリケーションを見ることができます。 + 同期中 + 不明 + 不明なエラーです。 + ユーザーネーム + 上流のソースコードは非フリーです + インストール済みのアプリケーション + Wi-Fi接続時と充電時のみ + サイレントインストール + 古いバージョンを見る + 安全ではないアルゴリズムで署名されています + サイズ + スキップ + 並び変え + ソースコード + ソースコードは使用できません + 提案 + リポジトリを同期 + 自動的にリポジトリを同期 + システム + タップしてインストールします。 + あなたのアクティビティを追跡、報告します + アンインストール + 不安定なアップデート + ウェブサイト + 言語 + 表示を減らす + 最新 + 全てをアップデート + 新しいアプリケーション + サイレントインストールのためにスーパーユーザー権限を許可 + 共有 + もっと見る + アップデート + 不安定なバージョンのインストールを提案する + バージョン + ダウンロード開始を待っています… + 保存 + テーマ + スクリーンショット + ミラーを選択 + 検索 + インデックスが検証できません。 + SOCKSプロキシ + ユーザーネームがありません + 並べ替えとフィルター + 詳細を保存しています… + 署名 %s + %sを同期中… + 署名なし + アップデート + 新機能 + バージョン + 未確認 + テーマ + \ No newline at end of file diff --git a/src/main/res/values-nb-rNO/strings.xml b/src/main/res/values-nb-rNO/strings.xml index 9217da0c..f6f28540 100644 --- a/src/main/res/values-nb-rNO/strings.xml +++ b/src/main/res/values-nb-rNO/strings.xml @@ -2,18 +2,18 @@ Finnes allerede Alltid - Svart - Forfatter e-post + Amoled + Utviklerens e-postadresse Utviklerens nettside Endringslogg Kan ikke redigere pakkebrønnen siden den synkroniseres akkurat nå. - Kontrollerer depotet… + Sjekker pakkebrønn Kompilert for avlusing Bekreftelse Kunne ikke laste ned %s - Kobler til… + Kobler til Inneholder ufri media - Slett depotet\? + Vil du slette pakkebrønnen\? Lastet ned %s Laster ned Fingeravtrykk @@ -36,13 +36,13 @@ %s-lisens Lenke kopiert til utklippstavle Listeanimasjoner - Vis liste animasjon på hovedsiden + Skru på listeanimasjoner på hovedsiden Nye versjoner av programmer tilgjengelig - Ingen programmer tilgjengelig + Ingen programmer tilgjengelige Ingen programmer installert Ingen beskrivelse tilgjengelig. Åpne %s\? - Fant ingen slike programmer + Fant ingen samsvarende programmer Gi merknad om nye versjoner av programmer Antall programmer Kun kompatibelt med %s @@ -51,7 +51,7 @@ Tilganger +%d til Innstillinger - Behandler %1$s… + Behandler %1$s Promoterer ufrie nettverkstjenester Tilbudt av %s Mellomtjenervert @@ -94,12 +94,12 @@ Versjon %s Venter på å laste ned… Handlingen mislyktes - Alle programmene dine er av nyeste dato + Alt er av nyeste dato Kjør Vis en merknad når nye versjoner er tilgjengelig Legg til pakkebrønn Bidragsytere - Nedlasting %s… + Laster ned %s Nettverksfeil. Ingen mellomtjener Kun på Wi-Fi @@ -108,14 +108,14 @@ Versjoner Detaljer Manglende funksjoner. - Finner ikke det programmet + Fant ikke programmet Endringer Kunne ikke synkronisere %s Mørk Beskrivelse Doner Rediger pakkebrønn - Vis programversjoner som ikke er kompatible med enheten + Vis programversjoner som er ukompatible med enheten Installert Ugyldig metadata. @@ -166,9 +166,9 @@ Kildekoden er ikke lenger tilgjengelig Oppgraderinger Seneste - Denne pakkebrønnen har ikke blitt brukt enda. Skru den på for å vise programmene i den. - Oppgradering - Språk + Denne pakkebrønnen har ikke blitt brukt enda. Du må skru den på for å vise programmene den tilbyr. + Oppgrader + Språ Personalisering Vis mindre Siste @@ -177,4 +177,5 @@ Installerte programmer Sorter og filtrer Nye programmer + Kun på Wi-Fi tilkoblet lader \ No newline at end of file diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index d108a8aa..08f7e7c4 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1,14 +1,14 @@ - A ação falhou + Ação falhou Adicionar repositório Endereço Todos os aplicativos - Todos os seus aplicativos estão atualizados + Todos os teus aplicativos estão atualizados Já existe Sempre - Escuro - Características indesejadas + Preto + Anti-funções Aplicativo Não foi possível encontrar esse aplicativo E-mail do autor @@ -180,4 +180,5 @@ Mais recente Explorar Aplicativos instalados + Apenas em Wi-Fi e Plugado \ No newline at end of file diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 3dedb7be..a14e1dd3 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -178,4 +178,7 @@ Исследуйте Обновить все Установленные приложения + Последние + Сортировать и фильтровать + Только при Wi-Fi и подключении к сети \ No newline at end of file diff --git a/src/main/res/values-tr/strings.xml b/src/main/res/values-tr/strings.xml index 80ca5136..cf87ce33 100644 --- a/src/main/res/values-tr/strings.xml +++ b/src/main/res/values-tr/strings.xml @@ -178,4 +178,5 @@ En yeni Tümünü güncelle Keşfet + Yalnızca Wi-Fi\'de ve Prize Takılı \ No newline at end of file diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 6ddeacc2..fd964d90 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -177,4 +177,7 @@ Оновити все Встановлені додатки Нові додатки + Останні + Сортувати та фільтрувати + Лише при Wi-Fi і підключеному до мережі \ No newline at end of file diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index dbbb45ab..f7b63136 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -179,4 +179,5 @@ 更新全部 新程序 已安装的程序 + 仅在 Wi-Fi 和充电情况下 \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0a8d7b5a..6469fdc2 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -99,6 +99,7 @@ OK Only compatible with %s Only on Wi-Fi + Only on Wi-Fi and Plugged-In Open %s? Other Could not parse the index file.