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.