mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-04-23 19:32:16 +00:00
Merge branch 'master' into installer-improvements
This commit is contained in:
commit
874370f34a
19
build.gradle
19
build.gradle
@ -25,6 +25,13 @@ android {
|
|||||||
versionCode = 43
|
versionCode = 43
|
||||||
versionName = "0.4.3"
|
versionName = "0.4.3"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||||
|
arguments += ["room.incremental": "true"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.all {
|
sourceSets.all {
|
||||||
@ -118,6 +125,7 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10'
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||||
implementation 'androidx.appcompat:appcompat-resources:1.4.0'
|
implementation 'androidx.appcompat:appcompat-resources:1.4.0'
|
||||||
@ -130,6 +138,12 @@ dependencies {
|
|||||||
// Material3
|
// Material3
|
||||||
implementation 'com.google.android.material:material:1.6.0-alpha01'
|
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
|
// Coil
|
||||||
implementation 'io.coil-kt:coil:1.4.0'
|
implementation 'io.coil-kt:coil:1.4.0'
|
||||||
|
|
||||||
@ -148,12 +162,13 @@ dependencies {
|
|||||||
|
|
||||||
// Coroutines / Lifecycle
|
// Coroutines / Lifecycle
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
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-core:1.6.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
implementation 'androidx.room:room-runtime:2.4.0'
|
implementation 'androidx.room:room-runtime:2.4.0'
|
||||||
implementation 'androidx.room:room-ktx: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'
|
kapt 'androidx.room:room-compiler:2.4.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
#Thu Dec 16 21:31:46 IST 2021
|
#Thu Dec 23 21:52:45 IST 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
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
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
@ -1,18 +1,41 @@
|
|||||||
package com.looker.droidify
|
package com.looker.droidify
|
||||||
|
|
||||||
object Common {
|
const val NOTIFICATION_CHANNEL_SYNCING = "syncing"
|
||||||
const val NOTIFICATION_CHANNEL_SYNCING = "syncing"
|
const val NOTIFICATION_CHANNEL_UPDATES = "updates"
|
||||||
const val NOTIFICATION_CHANNEL_UPDATES = "updates"
|
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
|
||||||
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
|
const val NOTIFICATION_CHANNEL_INSTALLER = "installed"
|
||||||
const val NOTIFICATION_CHANNEL_INSTALLER = "installed"
|
|
||||||
|
|
||||||
const val NOTIFICATION_ID_SYNCING = 1
|
const val NOTIFICATION_ID_SYNCING = 1
|
||||||
const val NOTIFICATION_ID_UPDATES = 2
|
const val NOTIFICATION_ID_UPDATES = 2
|
||||||
const val NOTIFICATION_ID_DOWNLOADING = 3
|
const val NOTIFICATION_ID_DOWNLOADING = 3
|
||||||
const val NOTIFICATION_ID_INSTALLER = 4
|
const val NOTIFICATION_ID_INSTALLER = 4
|
||||||
|
|
||||||
const val PREFS_LANGUAGE = "languages"
|
const val ROW_REPOSITORY_ID = "repository_id"
|
||||||
const val PREFS_LANGUAGE_DEFAULT = "system"
|
const val ROW_PACKAGE_NAME = "package_name"
|
||||||
|
const val ROW_NAME = "name"
|
||||||
|
const val ROW_SUMMARY = "summary"
|
||||||
|
const val ROW_DESCRIPTION = "description"
|
||||||
|
const val ROW_ADDED = "added"
|
||||||
|
const val ROW_UPDATED = "updated"
|
||||||
|
const val ROW_VERSION_CODE = "version_code"
|
||||||
|
const val ROW_SIGNATURES = "signatures"
|
||||||
|
const val ROW_COMPATIBLE = "compatible"
|
||||||
|
const val ROW_DATA = "data"
|
||||||
|
const val ROW_DATA_ITEM = "data_item"
|
||||||
|
const val ROW_VERSION = "version"
|
||||||
|
const val ROW_SIGNATURE = "signature"
|
||||||
|
const val ROW_ID = "_id"
|
||||||
|
const val ROW_ENABLED = "enabled"
|
||||||
|
const val ROW_DELETED = "deleted"
|
||||||
|
const val ROW_CAN_UPDATE = "can_update"
|
||||||
|
const val ROW_MATCH_RANK = "match_rank"
|
||||||
|
const val ROW_REPOSITORY_NAME = "repository"
|
||||||
|
const val ROW_PRODUCT_NAME = "product"
|
||||||
|
const val ROW_CATEGORY_NAME = "category"
|
||||||
|
const val ROW_INSTALLED_NAME = "memory_installed"
|
||||||
|
const val ROW_LOCK_NAME = "memory_lock"
|
||||||
|
|
||||||
const val JOB_ID_SYNC = 1
|
const val JOB_ID_SYNC = 1
|
||||||
}
|
|
||||||
|
const val PREFS_LANGUAGE = "languages"
|
||||||
|
const val PREFS_LANGUAGE_DEFAULT = "system"
|
||||||
|
@ -5,12 +5,13 @@ import android.app.Application
|
|||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.os.BatteryManager
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import com.looker.droidify.content.Cache
|
import com.looker.droidify.content.Cache
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.content.ProductPreferences
|
import com.looker.droidify.content.ProductPreferences
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.DatabaseX
|
||||||
import com.looker.droidify.index.RepositoryUpdater
|
import com.looker.droidify.index.RepositoryUpdater
|
||||||
import com.looker.droidify.network.CoilDownloader
|
import com.looker.droidify.network.CoilDownloader
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.droidify.network.Downloader
|
||||||
@ -21,27 +22,30 @@ import com.looker.droidify.utility.Utils.toInstalledItem
|
|||||||
import com.looker.droidify.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class MainApplication : Application(), ImageLoaderFactory {
|
class MainApplication : Application(), ImageLoaderFactory {
|
||||||
|
|
||||||
|
lateinit var db: DatabaseX
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val databaseUpdated = Database.init(this)
|
db = DatabaseX.getInstance(applicationContext)
|
||||||
Preferences.init(this)
|
Preferences.init(this)
|
||||||
ProductPreferences.init(this)
|
ProductPreferences.init(this)
|
||||||
RepositoryUpdater.init()
|
RepositoryUpdater.init(this)
|
||||||
listenApplications()
|
listenApplications()
|
||||||
listenPreferences()
|
listenPreferences()
|
||||||
|
|
||||||
if (databaseUpdated) {
|
/*if (databaseUpdated) {
|
||||||
forceSyncAll()
|
forceSyncAll()
|
||||||
}
|
}*/
|
||||||
|
|
||||||
Cache.cleanup(this)
|
Cache.cleanup(this)
|
||||||
updateSyncJob(false)
|
updateSyncJob(false)
|
||||||
@ -66,9 +70,9 @@ class MainApplication : Application(), ImageLoaderFactory {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (packageInfo != null) {
|
if (packageInfo != null) {
|
||||||
Database.InstalledAdapter.put(packageInfo.toInstalledItem())
|
db.installedDao.put(packageInfo.toInstalledItem())
|
||||||
} else {
|
} else {
|
||||||
Database.InstalledAdapter.delete(packageName)
|
db.installedDao.delete(packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +86,7 @@ class MainApplication : Application(), ImageLoaderFactory {
|
|||||||
val installedItems =
|
val installedItems =
|
||||||
packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag)
|
packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag)
|
||||||
.map { it.toInstalledItem() }
|
.map { it.toInstalledItem() }
|
||||||
Database.InstalledAdapter.putAll(installedItems)
|
db.installedDao.put(*installedItems.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenPreferences() {
|
private fun listenPreferences() {
|
||||||
@ -125,38 +129,65 @@ class MainApplication : Application(), ImageLoaderFactory {
|
|||||||
|
|
||||||
private fun updateSyncJob(force: Boolean) {
|
private fun updateSyncJob(force: Boolean) {
|
||||||
val jobScheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler
|
val jobScheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||||
val reschedule = force || !jobScheduler.allPendingJobs.any { it.id == Common.JOB_ID_SYNC }
|
val reschedule = force || !jobScheduler.allPendingJobs.any { it.id == JOB_ID_SYNC }
|
||||||
if (reschedule) {
|
if (reschedule) {
|
||||||
val autoSync = Preferences[Preferences.Key.AutoSync]
|
val autoSync = Preferences[Preferences.Key.AutoSync]
|
||||||
when (autoSync) {
|
when (autoSync) {
|
||||||
Preferences.AutoSync.Never -> {
|
is Preferences.AutoSync.Never -> {
|
||||||
jobScheduler.cancel(Common.JOB_ID_SYNC)
|
jobScheduler.cancel(JOB_ID_SYNC)
|
||||||
}
|
}
|
||||||
Preferences.AutoSync.Wifi, Preferences.AutoSync.Always -> {
|
is Preferences.AutoSync.Wifi -> {
|
||||||
val period = 12 * 60 * 60 * 1000L // 12 hours
|
autoSync(
|
||||||
val wifiOnly = autoSync == Preferences.AutoSync.Wifi
|
jobScheduler = jobScheduler,
|
||||||
jobScheduler.schedule(JobInfo
|
connectionType = JobInfo.NETWORK_TYPE_UNMETERED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Preferences.AutoSync.WifiBattery -> {
|
||||||
|
if (isCharging(this)) {
|
||||||
|
autoSync(
|
||||||
|
jobScheduler = jobScheduler,
|
||||||
|
connectionType = JobInfo.NETWORK_TYPE_UNMETERED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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(
|
.Builder(
|
||||||
Common.JOB_ID_SYNC,
|
JOB_ID_SYNC,
|
||||||
ComponentName(this, SyncService.Job::class.java)
|
ComponentName(this, SyncService.Job::class.java)
|
||||||
)
|
)
|
||||||
.setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY)
|
.setRequiredNetworkType(connectionType)
|
||||||
.apply {
|
.apply {
|
||||||
if (Android.sdk(26)) {
|
if (Android.sdk(26)) {
|
||||||
setRequiresBatteryNotLow(true)
|
setRequiresBatteryNotLow(true)
|
||||||
setRequiresStorageNotLow(true)
|
setRequiresStorageNotLow(true)
|
||||||
}
|
}
|
||||||
if (Android.sdk(24)) {
|
if (Android.sdk(24)) setPeriodic(period, JobInfo.getMinFlexMillis())
|
||||||
setPeriodic(period, JobInfo.getMinFlexMillis())
|
else setPeriodic(period)
|
||||||
} else {
|
|
||||||
setPeriodic(period)
|
|
||||||
}
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.build())
|
|
||||||
Unit
|
private fun isCharging(context: Context): Boolean {
|
||||||
}
|
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
}::class.java
|
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() {
|
private fun updateProxy() {
|
||||||
@ -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
|
Downloader.proxy = proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun forceSyncAll() {
|
private fun forceSyncAll() {
|
||||||
Database.RepositoryAdapter.getAll(null).forEach {
|
db.repositoryDao.all.mapNotNull { it.trueData }.forEach {
|
||||||
if (it.lastModified.isNotEmpty() || it.entityTag.isNotEmpty()) {
|
if (it.lastModified.isNotEmpty() || it.entityTag.isNotEmpty()) {
|
||||||
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
|
db.repositoryDao.put(it.copy(lastModified = "", entityTag = ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||||
|
@ -3,8 +3,8 @@ package com.looker.droidify.content
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import com.looker.droidify.Common.PREFS_LANGUAGE
|
import com.looker.droidify.PREFS_LANGUAGE
|
||||||
import com.looker.droidify.Common.PREFS_LANGUAGE_DEFAULT
|
import com.looker.droidify.PREFS_LANGUAGE_DEFAULT
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
@ -18,8 +18,8 @@ import java.net.Proxy
|
|||||||
object Preferences {
|
object Preferences {
|
||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
private val _subject = MutableSharedFlow<Key<*>>()
|
private val mutableSubject = MutableSharedFlow<Key<*>>()
|
||||||
val subject = _subject.asSharedFlow()
|
val subject = mutableSubject.asSharedFlow()
|
||||||
|
|
||||||
private val keys = sequenceOf(
|
private val keys = sequenceOf(
|
||||||
Key.Language,
|
Key.Language,
|
||||||
@ -39,12 +39,14 @@ object Preferences {
|
|||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
preferences =
|
preferences =
|
||||||
context.getSharedPreferences("${context.packageName}_preferences",
|
context.getSharedPreferences(
|
||||||
Context.MODE_PRIVATE)
|
"${context.packageName}_preferences",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
preferences.registerOnSharedPreferenceChangeListener { _, keyString ->
|
preferences.registerOnSharedPreferenceChangeListener { _, keyString ->
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
keys[keyString]?.let {
|
keys[keyString]?.let {
|
||||||
_subject.emit(it)
|
mutableSubject.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,10 +169,11 @@ object Preferences {
|
|||||||
|
|
||||||
sealed class AutoSync(override val valueString: String) : Enumeration<AutoSync> {
|
sealed class AutoSync(override val valueString: String) : Enumeration<AutoSync> {
|
||||||
override val values: List<AutoSync>
|
override val values: List<AutoSync>
|
||||||
get() = listOf(Never, Wifi, Always)
|
get() = listOf(Never, Wifi, WifiBattery, Always)
|
||||||
|
|
||||||
object Never : AutoSync("never")
|
object Never : AutoSync("never")
|
||||||
object Wifi : AutoSync("wifi")
|
object Wifi : AutoSync("wifi")
|
||||||
|
object WifiBattery : AutoSync("wifi-battery")
|
||||||
object Always : AutoSync("always")
|
object Always : AutoSync("always")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ package com.looker.droidify.content
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.DatabaseX
|
||||||
|
import com.looker.droidify.database.Lock
|
||||||
import com.looker.droidify.entity.ProductPreference
|
import com.looker.droidify.entity.ProductPreference
|
||||||
import com.looker.droidify.utility.extension.json.Json
|
import com.looker.droidify.utility.extension.json.Json
|
||||||
import com.looker.droidify.utility.extension.json.parseDictionary
|
import com.looker.droidify.utility.extension.json.parseDictionary
|
||||||
@ -21,17 +22,30 @@ object ProductPreferences {
|
|||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
private val mutableSubject = MutableSharedFlow<Pair<String, Long?>>()
|
private val mutableSubject = MutableSharedFlow<Pair<String, Long?>>()
|
||||||
private val subject = mutableSubject.asSharedFlow()
|
private val subject = mutableSubject.asSharedFlow()
|
||||||
|
lateinit var db: DatabaseX
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
|
db = DatabaseX.getInstance(context)
|
||||||
preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE)
|
preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE)
|
||||||
Database.LockAdapter.putAll(preferences.all.keys
|
db.lockDao.insert(*preferences.all.keys
|
||||||
.mapNotNull { packageName ->
|
.mapNotNull { pName ->
|
||||||
this[packageName].databaseVersionCode?.let { Pair(packageName, it) }
|
this[pName].databaseVersionCode?.let {
|
||||||
})
|
Lock().apply {
|
||||||
|
package_name = pName
|
||||||
|
version_code = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
)
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
subject.collect { (packageName, versionCode) ->
|
subject.collect { (packageName, versionCode) ->
|
||||||
if (versionCode != null) Database.LockAdapter.put(Pair(packageName, versionCode))
|
if (versionCode != null) db.lockDao.insert(Lock().apply {
|
||||||
else Database.LockAdapter.delete(packageName)
|
package_name = packageName
|
||||||
|
version_code = versionCode
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else db.lockDao.delete(packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,9 +87,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
|
|
||||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
|
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
|
||||||
val request = activeRequests[id]!!.request
|
val request = activeRequests[id]!!.request
|
||||||
|
val db = DatabaseX.getInstance(requireContext())
|
||||||
return QueryLoader(requireContext()) {
|
return QueryLoader(requireContext()) {
|
||||||
when (request) {
|
when (request) {
|
||||||
is Request.ProductsAvailable -> Database.ProductAdapter
|
is Request.ProductsAvailable -> db.productDao
|
||||||
.query(
|
.query(
|
||||||
installed = false,
|
installed = false,
|
||||||
updates = false,
|
updates = false,
|
||||||
@ -98,7 +99,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it
|
signal = it
|
||||||
)
|
)
|
||||||
is Request.ProductsInstalled -> Database.ProductAdapter
|
is Request.ProductsInstalled -> db.productDao
|
||||||
.query(
|
.query(
|
||||||
installed = true,
|
installed = true,
|
||||||
updates = false,
|
updates = false,
|
||||||
@ -107,7 +108,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it
|
signal = it
|
||||||
)
|
)
|
||||||
is Request.ProductsUpdates -> Database.ProductAdapter
|
is Request.ProductsUpdates -> db.productDao
|
||||||
.query(
|
.query(
|
||||||
installed = true,
|
installed = true,
|
||||||
updates = true,
|
updates = true,
|
||||||
@ -116,7 +117,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it
|
signal = it
|
||||||
)
|
)
|
||||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
is Request.Repositories -> db.repositoryDao.allCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,272 @@
|
|||||||
package com.looker.droidify.database
|
package com.looker.droidify.database
|
||||||
|
|
||||||
import android.database.SQLException
|
import android.database.Cursor
|
||||||
|
import android.os.CancellationSignal
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
|
import com.looker.droidify.*
|
||||||
|
import com.looker.droidify.entity.ProductItem
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface RepositoryDao {
|
interface BaseDao<T> {
|
||||||
@Insert
|
@Insert
|
||||||
@Throws(SQLException::class)
|
fun insert(vararg product: T)
|
||||||
fun insert(vararg repository: Repository)
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertReplace(vararg product: T)
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun update(vararg repository: Repository?)
|
fun update(vararg obj: T): Int
|
||||||
|
|
||||||
fun put(repository: Repository) {
|
@Delete
|
||||||
if (repository.id >= 0L) update(repository) else insert(repository)
|
fun delete(obj: T)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RepositoryDao : BaseDao<Repository> {
|
||||||
|
@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")
|
@Query("SELECT * FROM repository WHERE _id = :id and deleted == 0")
|
||||||
fun get(id: Long): Repository?
|
fun get(id: Long): Repository?
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
|
||||||
|
val allCursor: Cursor
|
||||||
|
|
||||||
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
|
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
|
||||||
val all: List<Repository>
|
val all: List<Repository>
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
|
||||||
|
val allFlowable: Flowable<List<Repository>>
|
||||||
|
|
||||||
@get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC")
|
@get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC")
|
||||||
val allDisabledDeleted: List<Repository.IdAndDeleted>
|
val allDisabledDeleted: List<Repository.IdAndDeleted>
|
||||||
|
|
||||||
@Delete
|
|
||||||
fun delete(repository: Repository)
|
|
||||||
|
|
||||||
@Query("DELETE FROM repository WHERE _id = :id")
|
@Query("DELETE FROM repository WHERE _id = :id")
|
||||||
fun deleteById(vararg id: Long): Int
|
fun deleteById(vararg id: Long): Int
|
||||||
|
|
||||||
|
// TODO optimize
|
||||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun markAsDeleted(id: Long) {
|
fun markAsDeleted(id: Long) {
|
||||||
update(get(id).apply { this?.deleted = 1 })
|
get(id).apply { this?.deleted = true }?.let { update(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ProductDao {
|
interface ProductDao : BaseDao<Product> {
|
||||||
@Query("SELECT COUNT(*) FROM product WHERE repository_id = :id")
|
@Query("SELECT COUNT(*) FROM product WHERE repository_id = :id")
|
||||||
fun countForRepository(id: Long): Long
|
fun countForRepository(id: Long): Long
|
||||||
|
|
||||||
@Query("SELECT * FROM product WHERE package_name = :packageName")
|
@Query("SELECT * FROM product WHERE package_name = :packageName")
|
||||||
fun get(packageName: String): Product?
|
fun get(packageName: String): List<Product?>
|
||||||
|
|
||||||
@Query("DELETE FROM product WHERE repository_id = :id")
|
@Query("DELETE FROM product WHERE repository_id = :id")
|
||||||
fun deleteById(vararg id: Long): Int
|
fun deleteById(vararg id: Long): Int
|
||||||
|
|
||||||
|
@RawQuery
|
||||||
|
fun query(
|
||||||
|
query: SupportSQLiteQuery
|
||||||
|
): Cursor
|
||||||
|
|
||||||
|
// TODO optimize and simplify
|
||||||
|
@Transaction
|
||||||
|
fun query(
|
||||||
|
installed: Boolean, updates: Boolean, searchQuery: String,
|
||||||
|
section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?
|
||||||
|
): Cursor {
|
||||||
|
val builder = QueryBuilder()
|
||||||
|
|
||||||
|
val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND
|
||||||
|
product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND
|
||||||
|
product.${ROW_SIGNATURES} != ''"""
|
||||||
|
|
||||||
|
builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID},
|
||||||
|
product.${ROW_PACKAGE_NAME}, product.${ROW_NAME},
|
||||||
|
product.${ROW_SUMMARY}, installed.${ROW_VERSION},
|
||||||
|
(COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND
|
||||||
|
product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} >
|
||||||
|
COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
|
||||||
|
AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE},
|
||||||
|
product.${ROW_DATA_ITEM},"""
|
||||||
|
|
||||||
|
if (searchQuery.isNotEmpty()) {
|
||||||
|
builder += """(((product.${ROW_NAME} LIKE ? OR
|
||||||
|
product.${ROW_SUMMARY} LIKE ?) * 7) |
|
||||||
|
((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) |
|
||||||
|
(product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},"""
|
||||||
|
builder %= List(4) { "%$searchQuery%" }
|
||||||
|
} else {
|
||||||
|
builder += "0 AS ${ROW_MATCH_RANK},"
|
||||||
|
}
|
||||||
|
|
||||||
|
builder += """MAX((product.${ROW_COMPATIBLE} AND
|
||||||
|
(installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
|
||||||
|
PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product"""
|
||||||
|
builder += """JOIN $ROW_REPOSITORY_NAME AS repository
|
||||||
|
ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}"""
|
||||||
|
builder += """LEFT JOIN $ROW_LOCK_NAME AS lock
|
||||||
|
ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}"""
|
||||||
|
|
||||||
|
if (!installed && !updates) {
|
||||||
|
builder += "LEFT"
|
||||||
|
}
|
||||||
|
builder += """JOIN $ROW_INSTALLED_NAME AS installed
|
||||||
|
ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}"""
|
||||||
|
|
||||||
|
if (section is ProductItem.Section.Category) {
|
||||||
|
builder += """JOIN $ROW_CATEGORY_NAME AS category
|
||||||
|
ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
builder += """WHERE repository.${ROW_ENABLED} != 0 AND
|
||||||
|
repository.${ROW_DELETED} == 0"""
|
||||||
|
|
||||||
|
if (section is ProductItem.Section.Category) {
|
||||||
|
builder += "AND category.${ROW_NAME} = ?"
|
||||||
|
builder %= section.name
|
||||||
|
} else if (section is ProductItem.Section.Repository) {
|
||||||
|
builder += "AND product.${ROW_REPOSITORY_ID} = ?"
|
||||||
|
builder %= section.id.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.isNotEmpty()) {
|
||||||
|
builder += """AND $ROW_MATCH_RANK > 0"""
|
||||||
|
}
|
||||||
|
|
||||||
|
builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1"
|
||||||
|
|
||||||
|
if (updates) {
|
||||||
|
builder += "AND $ROW_CAN_UPDATE"
|
||||||
|
}
|
||||||
|
builder += "ORDER BY"
|
||||||
|
|
||||||
|
if (searchQuery.isNotEmpty()) {
|
||||||
|
builder += """$ROW_MATCH_RANK DESC,"""
|
||||||
|
}
|
||||||
|
|
||||||
|
when (order) {
|
||||||
|
ProductItem.Order.NAME -> Unit
|
||||||
|
ProductItem.Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC,"
|
||||||
|
ProductItem.Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC,"
|
||||||
|
}::class
|
||||||
|
builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC"
|
||||||
|
|
||||||
|
return query(SimpleSQLiteQuery(builder.build()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@RawQuery
|
||||||
|
fun queryList(
|
||||||
|
query: SupportSQLiteQuery
|
||||||
|
): List<Product>
|
||||||
|
|
||||||
|
// TODO optimize and simplify
|
||||||
|
@Transaction
|
||||||
|
fun queryList(
|
||||||
|
installed: Boolean, updates: Boolean, searchQuery: String,
|
||||||
|
section: ProductItem.Section, order: ProductItem.Order
|
||||||
|
): List<Product> {
|
||||||
|
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
|
@Dao
|
||||||
interface CategoryDao {
|
interface CategoryDao : BaseDao<Category> {
|
||||||
@Query(
|
@get:Query(
|
||||||
"""SELECT DISTINCT category.name
|
"""SELECT DISTINCT category.name
|
||||||
FROM category AS category
|
FROM category AS category
|
||||||
JOIN repository AS repository
|
JOIN repository AS repository
|
||||||
@ -59,31 +274,85 @@ interface CategoryDao {
|
|||||||
WHERE repository.enabled != 0 AND
|
WHERE repository.enabled != 0 AND
|
||||||
repository.deleted == 0"""
|
repository.deleted == 0"""
|
||||||
)
|
)
|
||||||
fun getAll(): List<String>
|
val allNames: List<String>
|
||||||
|
|
||||||
@Query("DELETE FROM category WHERE repository_id = :id")
|
@Query("DELETE FROM category WHERE repository_id = :id")
|
||||||
fun deleteById(vararg id: Long): Int
|
fun deleteById(vararg id: Long): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface InstalledDao {
|
interface InstalledDao : BaseDao<Installed> {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
fun put(vararg isntalled: com.looker.droidify.entity.InstalledItem) {
|
||||||
@Throws(SQLException::class)
|
isntalled.forEach {
|
||||||
fun insert(vararg installed: Installed)
|
insertReplace(Installed(it.packageName).apply {
|
||||||
|
version = it.version
|
||||||
|
version_code = it.versionCode
|
||||||
|
signature = it.signature
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM installed WHERE package_name = :packageName")
|
@Query("SELECT * FROM memory_installed WHERE package_name = :packageName")
|
||||||
fun get(packageName: String): Installed?
|
fun get(packageName: String): Cursor
|
||||||
|
|
||||||
@Query("DELETE FROM installed WHERE package_name = :packageName")
|
@Query("DELETE FROM memory_installed WHERE package_name = :packageName")
|
||||||
fun delete(packageName: String)
|
fun delete(packageName: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface LockDao {
|
interface LockDao : BaseDao<Lock> {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Query("DELETE FROM memory_lock WHERE package_name = :packageName")
|
||||||
@Throws(SQLException::class)
|
|
||||||
fun insert(vararg lock: Lock)
|
|
||||||
|
|
||||||
@Query("DELETE FROM lock WHERE package_name = :packageName")
|
|
||||||
fun delete(packageName: String)
|
fun delete(packageName: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ProductTempDao : BaseDao<ProductTemp> {
|
||||||
|
@get:Query("SELECT * FROM temporary_product")
|
||||||
|
val all: Array<ProductTemp>
|
||||||
|
|
||||||
|
@Query("DELETE FROM temporary_product")
|
||||||
|
fun emptyTable()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertCategory(vararg product: CategoryTemp)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun putTemporary(products: List<com.looker.droidify.entity.Product>) {
|
||||||
|
products.forEach {
|
||||||
|
val signatures = it.signatures.joinToString { ".$it" }
|
||||||
|
.let { if (it.isNotEmpty()) "$it." else "" }
|
||||||
|
insert(it.let {
|
||||||
|
ProductTemp().apply {
|
||||||
|
repository_id = it.repositoryId
|
||||||
|
package_name = it.packageName
|
||||||
|
name = it.name
|
||||||
|
summary = it.summary
|
||||||
|
description = it.description
|
||||||
|
added = it.added
|
||||||
|
updated = it.updated
|
||||||
|
version_code = it.versionCode
|
||||||
|
this.signatures = signatures
|
||||||
|
compatible = if (it.compatible) 1 else 0
|
||||||
|
data = it
|
||||||
|
data_item = it.item()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it.categories.forEach { category ->
|
||||||
|
insertCategory(CategoryTemp().apply {
|
||||||
|
repository_id = it.repositoryId
|
||||||
|
package_name = it.packageName
|
||||||
|
name = category
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CategoryTempDao : BaseDao<CategoryTemp> {
|
||||||
|
@get:Query("SELECT * FROM temporary_category")
|
||||||
|
val all: Array<CategoryTemp>
|
||||||
|
|
||||||
|
@Query("DELETE FROM temporary_category")
|
||||||
|
fun emptyTable()
|
||||||
|
}
|
@ -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<String, String>?
|
|
||||||
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<Subject, MutableSet<() -> 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<Unit> {
|
|
||||||
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<String>? = null,
|
|
||||||
selection: Pair<String, Array<String>>? = 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 <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
|
|
||||||
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray {
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) }
|
|
||||||
return outputStream.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Repository> {
|
|
||||||
return db.query(
|
|
||||||
Schema.Repository.name,
|
|
||||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
|
||||||
signal = signal
|
|
||||||
).use { it.asSequence().map(::transform).toList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done Pair<Long,Int> instead
|
|
||||||
// MAYBE signal has to be considered
|
|
||||||
fun getAllDisabledDeleted(signal: CancellationSignal?): Set<Pair<Long, Boolean>> {
|
|
||||||
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<Pair<Long, Boolean>>) {
|
|
||||||
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<Product> {
|
|
||||||
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<String> {
|
|
||||||
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<InstalledItem>) {
|
|
||||||
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<String, Long>, 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<String, Long>) = put(lock, true)
|
|
||||||
|
|
||||||
// Done in insert (Lock object instead of pair)
|
|
||||||
fun putAll(locks: List<Pair<String, Long>>) {
|
|
||||||
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<Product>) {
|
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,12 +5,15 @@ import androidx.room.Database
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import com.looker.droidify.entity.Repository.Companion.defaultRepositories
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
Repository::class,
|
Repository::class,
|
||||||
Product::class,
|
Product::class,
|
||||||
|
ProductTemp::class,
|
||||||
Category::class,
|
Category::class,
|
||||||
|
CategoryTemp::class,
|
||||||
Installed::class,
|
Installed::class,
|
||||||
Lock::class
|
Lock::class
|
||||||
], version = 1
|
], version = 1
|
||||||
@ -19,7 +22,9 @@ import androidx.room.TypeConverters
|
|||||||
abstract class DatabaseX : RoomDatabase() {
|
abstract class DatabaseX : RoomDatabase() {
|
||||||
abstract val repositoryDao: RepositoryDao
|
abstract val repositoryDao: RepositoryDao
|
||||||
abstract val productDao: ProductDao
|
abstract val productDao: ProductDao
|
||||||
|
abstract val productTempDao: ProductTempDao
|
||||||
abstract val categoryDao: CategoryDao
|
abstract val categoryDao: CategoryDao
|
||||||
|
abstract val categoryTempDao: CategoryTempDao
|
||||||
abstract val installedDao: InstalledDao
|
abstract val installedDao: InstalledDao
|
||||||
abstract val lockDao: LockDao
|
abstract val lockDao: LockDao
|
||||||
|
|
||||||
@ -39,6 +44,11 @@ abstract class DatabaseX : RoomDatabase() {
|
|||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.build()
|
.build()
|
||||||
|
INSTANCE?.let { instance ->
|
||||||
|
if (instance.repositoryDao.count == 0) defaultRepositories.forEach {
|
||||||
|
instance.repositoryDao.put(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return INSTANCE!!
|
return INSTANCE!!
|
||||||
}
|
}
|
||||||
@ -46,6 +56,7 @@ abstract class DatabaseX : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cleanUp(pairs: Set<Pair<Long, Boolean>>) {
|
fun cleanUp(pairs: Set<Pair<Long, Boolean>>) {
|
||||||
|
runInTransaction {
|
||||||
val result = pairs.windowed(10, 10, true).map {
|
val result = pairs.windowed(10, 10, true).map {
|
||||||
val ids = it.map { it.first }.toLongArray()
|
val ids = it.map { it.first }.toLongArray()
|
||||||
val productsCount = productDao.deleteById(*ids)
|
val productsCount = productDao.deleteById(*ids)
|
||||||
@ -54,9 +65,24 @@ abstract class DatabaseX : RoomDatabase() {
|
|||||||
repositoryDao.deleteById(*deleteIds)
|
repositoryDao.deleteById(*deleteIds)
|
||||||
productsCount != 0 || categoriesCount != 0
|
productsCount != 0 || categoriesCount != 0
|
||||||
}
|
}
|
||||||
// Use live objects and observers instead
|
}
|
||||||
|
// TODO Use live objects and observers instead
|
||||||
/*if (result.any { it }) {
|
/*if (result.any { it }) {
|
||||||
com.looker.droidify.database.Database.notifyChanged(com.looker.droidify.database.Database.Subject.Products)
|
com.looker.droidify.database.Database.notifyChanged(com.looker.droidify.database.Database.Subject.Products)
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun finishTemporary(repository: com.looker.droidify.entity.Repository, success: Boolean) {
|
||||||
|
runInTransaction {
|
||||||
|
if (success) {
|
||||||
|
productDao.deleteById(repository.id)
|
||||||
|
categoryDao.deleteById(repository.id)
|
||||||
|
productDao.insert(*(productTempDao.all))
|
||||||
|
categoryDao.insert(*(categoryTempDao.all))
|
||||||
|
repositoryDao.put(repository)
|
||||||
|
}
|
||||||
|
productTempDao.emptyTable()
|
||||||
|
categoryTempDao.emptyTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -33,6 +33,8 @@ class QueryBuilder {
|
|||||||
this.arguments += arguments
|
this.arguments += arguments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build() = builder.toString()
|
||||||
|
|
||||||
fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor {
|
fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor {
|
||||||
val query = builder.toString()
|
val query = builder.toString()
|
||||||
val arguments = arguments.toTypedArray()
|
val arguments = arguments.toTypedArray()
|
||||||
|
@ -4,11 +4,10 @@ import androidx.room.ColumnInfo
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.looker.droidify.database.Database.jsonGenerate
|
|
||||||
import com.looker.droidify.database.Database.jsonParse
|
|
||||||
import com.looker.droidify.entity.Product
|
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
|
import com.looker.droidify.utility.jsonGenerate
|
||||||
|
import com.looker.droidify.utility.jsonParse
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class Repository {
|
class Repository {
|
||||||
@ -17,63 +16,72 @@ class Repository {
|
|||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
|
|
||||||
var enabled = 0
|
var enabled = 0
|
||||||
var deleted = 0
|
var deleted = false
|
||||||
|
|
||||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||||
var data: Repository? = null
|
var data: Repository? = null
|
||||||
|
|
||||||
|
val trueData: Repository?
|
||||||
|
get() = data?.copy(id = id)
|
||||||
|
|
||||||
class IdAndDeleted {
|
class IdAndDeleted {
|
||||||
@ColumnInfo(name = "_id")
|
@ColumnInfo(name = "_id")
|
||||||
var id = 0L
|
var id = 0L
|
||||||
|
|
||||||
var deleted = 0
|
var deleted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(primaryKeys = ["repository_id", "package_name"])
|
@Entity(tableName = "product", primaryKeys = ["repository_id", "package_name"])
|
||||||
class Product {
|
open class Product {
|
||||||
var repository_id: Long = 0
|
var repository_id = 0L
|
||||||
var package_name = ""
|
var package_name = ""
|
||||||
|
|
||||||
var name = ""
|
var name = ""
|
||||||
var summary = ""
|
var summary = ""
|
||||||
var description = ""
|
var description = ""
|
||||||
var added = 0
|
var added = 0L
|
||||||
var updated = 0
|
var updated = 0L
|
||||||
var version_code = 0
|
var version_code = 0L
|
||||||
var signatures = ""
|
var signatures = ""
|
||||||
var compatible = 0
|
var compatible = 0
|
||||||
|
|
||||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||||
var data: Product? = null
|
var data: com.looker.droidify.entity.Product? = null
|
||||||
|
|
||||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||||
var data_item: ProductItem? = null
|
var data_item: ProductItem? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(primaryKeys = ["repository_id", "package_name", "name"])
|
@Entity(tableName = "temporary_product")
|
||||||
class Category {
|
class ProductTemp : Product()
|
||||||
|
|
||||||
|
@Entity(tableName = "category", primaryKeys = ["repository_id", "package_name", "name"])
|
||||||
|
open class Category {
|
||||||
var repository_id: Long = 0
|
var repository_id: Long = 0
|
||||||
var package_name = ""
|
var package_name = ""
|
||||||
var name = ""
|
var name = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "temporary_category")
|
||||||
class Installed {
|
class CategoryTemp : Category()
|
||||||
|
|
||||||
|
@Entity(tableName = "memory_installed")
|
||||||
|
class Installed(pName: String = "") {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var package_name = ""
|
var package_name = pName
|
||||||
|
|
||||||
var version = ""
|
var version = ""
|
||||||
var version_code = 0
|
var version_code = 0L
|
||||||
var signature = ""
|
var signature = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "memory_lock")
|
||||||
class Lock {
|
class Lock {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var package_name = ""
|
var package_name = ""
|
||||||
|
|
||||||
var version_code = 0
|
var version_code = 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@ -87,11 +95,12 @@ object Converters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun toProduct(byteArray: ByteArray) = byteArray.jsonParse { Product.deserialize(it) }
|
fun toProduct(byteArray: ByteArray) =
|
||||||
|
byteArray.jsonParse { com.looker.droidify.entity.Product.deserialize(it) }
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun toByteArray(product: Product) = jsonGenerate(product::serialize)
|
fun toByteArray(product: com.looker.droidify.entity.Product) = jsonGenerate(product::serialize)
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -153,9 +153,9 @@ object IndexV1Parser {
|
|||||||
it.string("openCollective") -> donates += Product.Donate.OpenCollective(
|
it.string("openCollective") -> donates += Product.Donate.OpenCollective(
|
||||||
valueAsString
|
valueAsString
|
||||||
)
|
)
|
||||||
it.dictionary("localized") -> forEachKey { it ->
|
it.dictionary("localized") -> forEachKey { keyToken ->
|
||||||
if (it.token == JsonToken.START_OBJECT) {
|
if (keyToken.token == JsonToken.START_OBJECT) {
|
||||||
val locale = it.key
|
val locale = keyToken.key
|
||||||
var name = ""
|
var name = ""
|
||||||
var summary = ""
|
var summary = ""
|
||||||
var description = ""
|
var description = ""
|
||||||
|
@ -3,7 +3,7 @@ package com.looker.droidify.index
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.looker.droidify.content.Cache
|
import com.looker.droidify.content.Cache
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.DatabaseX
|
||||||
import com.looker.droidify.entity.Product
|
import com.looker.droidify.entity.Product
|
||||||
import com.looker.droidify.entity.Release
|
import com.looker.droidify.entity.Release
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
@ -59,29 +59,28 @@ object RepositoryUpdater {
|
|||||||
|
|
||||||
private val updaterLock = Any()
|
private val updaterLock = Any()
|
||||||
private val cleanupLock = Any()
|
private val cleanupLock = Any()
|
||||||
|
lateinit var db: DatabaseX
|
||||||
|
|
||||||
fun init() {
|
fun init(context: Context) {
|
||||||
|
db = DatabaseX.getInstance(context)
|
||||||
var lastDisabled = setOf<Long>()
|
var lastDisabled = setOf<Long>()
|
||||||
Observable.just(Unit)
|
Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
//.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle {
|
.flatMapSingle {
|
||||||
RxUtils.querySingle {
|
RxUtils.querySingle {
|
||||||
Database.RepositoryAdapter.getAllDisabledDeleted(
|
db.repositoryDao.allDisabledDeleted
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.forEach { it ->
|
.forEach { it ->
|
||||||
val newDisabled = it.asSequence().filter { !it.second }.map { it.first }.toSet()
|
val newDisabled = it.asSequence().filter { !it.deleted }.map { it.id }.toSet()
|
||||||
val disabled = newDisabled - lastDisabled
|
val disabled = newDisabled - lastDisabled
|
||||||
lastDisabled = newDisabled
|
lastDisabled = newDisabled
|
||||||
val deleted = it.asSequence().filter { it.second }.map { it.first }.toSet()
|
val deleted = it.asSequence().filter { it.deleted }.map { it.id }.toSet()
|
||||||
if (disabled.isNotEmpty() || deleted.isNotEmpty()) {
|
if (disabled.isNotEmpty() || deleted.isNotEmpty()) {
|
||||||
val pairs = (disabled.asSequence().map { Pair(it, false) } +
|
val pairs = (disabled.asSequence().map { Pair(it, false) } +
|
||||||
deleted.asSequence().map { Pair(it, true) }).toSet()
|
deleted.asSequence().map { Pair(it, true) }).toSet()
|
||||||
synchronized(cleanupLock) { Database.RepositoryAdapter.cleanup(pairs) }
|
synchronized(cleanupLock) { db.cleanUp(pairs) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,12 +192,14 @@ object RepositoryUpdater {
|
|||||||
file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit,
|
file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
var rollback = true
|
var rollback = true
|
||||||
|
val db = DatabaseX.getInstance(context)
|
||||||
return synchronized(updaterLock) {
|
return synchronized(updaterLock) {
|
||||||
try {
|
try {
|
||||||
val jarFile = JarFile(file, true)
|
val jarFile = JarFile(file, true)
|
||||||
val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry
|
val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry
|
||||||
val total = indexEntry.size
|
val total = indexEntry.size
|
||||||
Database.UpdaterAdapter.createTemporaryTable()
|
db.productTempDao.emptyTable()
|
||||||
|
db.categoryTempDao.emptyTable()
|
||||||
val features = context.packageManager.systemAvailableFeatures
|
val features = context.packageManager.systemAvailableFeatures
|
||||||
.asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen")
|
.asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen")
|
||||||
|
|
||||||
@ -231,7 +232,7 @@ object RepositoryUpdater {
|
|||||||
}
|
}
|
||||||
products += transformProduct(product, features, unstable)
|
products += transformProduct(product, features, unstable)
|
||||||
if (products.size >= 50) {
|
if (products.size >= 50) {
|
||||||
Database.UpdaterAdapter.putTemporary(products)
|
db.productTempDao.putTemporary(products)
|
||||||
products.clear()
|
products.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,7 +250,7 @@ object RepositoryUpdater {
|
|||||||
throw InterruptedException()
|
throw InterruptedException()
|
||||||
}
|
}
|
||||||
if (products.isNotEmpty()) {
|
if (products.isNotEmpty()) {
|
||||||
Database.UpdaterAdapter.putTemporary(products)
|
db.productTempDao.putTemporary(products)
|
||||||
products.clear()
|
products.clear()
|
||||||
}
|
}
|
||||||
Pair(changedRepository, certificateFromIndex)
|
Pair(changedRepository, certificateFromIndex)
|
||||||
@ -334,7 +335,7 @@ object RepositoryUpdater {
|
|||||||
progress.toLong(),
|
progress.toLong(),
|
||||||
totalCount.toLong()
|
totalCount.toLong()
|
||||||
)
|
)
|
||||||
Database.UpdaterAdapter.putTemporary(products
|
db.productTempDao.putTemporary(products
|
||||||
.map { transformProduct(it, features, unstable) })
|
.map { transformProduct(it, features, unstable) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -407,7 +408,7 @@ object RepositoryUpdater {
|
|||||||
}
|
}
|
||||||
callback(Stage.COMMIT, 0, null)
|
callback(Stage.COMMIT, 0, null)
|
||||||
synchronized(cleanupLock) {
|
synchronized(cleanupLock) {
|
||||||
Database.UpdaterAdapter.finishTemporary(
|
db.finishTemporary(
|
||||||
commitRepository,
|
commitRepository,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@ -423,7 +424,7 @@ object RepositoryUpdater {
|
|||||||
} finally {
|
} finally {
|
||||||
file.delete()
|
file.delete()
|
||||||
if (rollback) {
|
if (rollback) {
|
||||||
Database.UpdaterAdapter.finishTemporary(repository, false)
|
db.finishTemporary(repository, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@ import android.net.Uri
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.looker.droidify.Common.NOTIFICATION_CHANNEL_INSTALLER
|
import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER
|
||||||
import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER
|
import com.looker.droidify.NOTIFICATION_ID_INSTALLER
|
||||||
import com.looker.droidify.MainActivity
|
import com.looker.droidify.MainActivity
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.utility.Utils
|
import com.looker.droidify.utility.Utils
|
||||||
|
@ -17,7 +17,6 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.EditRepositoryBinding
|
import com.looker.droidify.databinding.EditRepositoryBinding
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.droidify.network.Downloader
|
||||||
@ -154,7 +153,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
val repository = repositoryId?.let(Database.RepositoryAdapter::get)
|
val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.trueData }
|
||||||
if (repository == null) {
|
if (repository == null) {
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
@ -233,7 +232,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val list = Database.RepositoryAdapter.getAll(null)
|
val list = screenActivity.db.repositoryDao.all.mapNotNull { it.trueData }
|
||||||
takenAddresses = list.asSequence().filter { it.id != repositoryId }
|
takenAddresses = list.asSequence().filter { it.id != repositoryId }
|
||||||
.flatMap { (it.mirrors + it.address).asSequence() }
|
.flatMap { (it.mirrors + it.address).asSequence() }
|
||||||
.map { it.withoutKnownPath }.toSet()
|
.map { it.withoutKnownPath }.toSet()
|
||||||
@ -449,10 +448,10 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager)
|
MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager)
|
||||||
invalidateState()
|
invalidateState()
|
||||||
} else {
|
} else {
|
||||||
val repository = repositoryId?.let(Database.RepositoryAdapter::get)
|
val repository = repositoryId?.let { screenActivity.db.repositoryDao.get(it)?.trueData }
|
||||||
?.edit(address, fingerprint, authentication)
|
?.edit(address, fingerprint, authentication)
|
||||||
?: Repository.newRepository(address, fingerprint, authentication)
|
?: Repository.newRepository(address, fingerprint, authentication)
|
||||||
val changedRepository = Database.RepositoryAdapter.put(repository)
|
val changedRepository = screenActivity.db.repositoryDao.put(repository)
|
||||||
if (repositoryId == null && changedRepository.enabled) {
|
if (repositoryId == null && changedRepository.enabled) {
|
||||||
binder.sync(changedRepository)
|
binder.sync(changedRepository)
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ import com.google.android.material.card.MaterialCardView
|
|||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textview.MaterialTextView
|
import com.google.android.material.textview.MaterialTextView
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.utility.extension.resources.clear
|
import com.looker.droidify.utility.extension.resources.clear
|
||||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||||
import com.looker.droidify.utility.extension.resources.inflate
|
import com.looker.droidify.utility.extension.resources.inflate
|
||||||
|
import com.looker.droidify.utility.getRepository
|
||||||
import com.looker.droidify.widget.CursorRecyclerAdapter
|
import com.looker.droidify.widget.CursorRecyclerAdapter
|
||||||
|
|
||||||
class RepositoriesAdapter(
|
class RepositoriesAdapter(
|
||||||
@ -45,7 +45,7 @@ class RepositoriesAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getRepository(position: Int): Repository {
|
private fun getRepository(position: Int): Repository {
|
||||||
return Database.RepositoryAdapter.transform(moveTo(position))
|
return moveTo(position).getRepository()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
|
@ -12,7 +12,6 @@ import androidx.appcompat.widget.LinearLayoutCompat
|
|||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.TitleTextItemBinding
|
import com.looker.droidify.databinding.TitleTextItemBinding
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
@ -99,7 +98,7 @@ class RepositoryFragment() : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRepositoryView() {
|
private fun updateRepositoryView() {
|
||||||
val repository = Database.RepositoryAdapter.get(repositoryId)
|
val repository = screenActivity.db.repositoryDao.get(repositoryId)?.trueData
|
||||||
val layout = layout!!
|
val layout = layout!!
|
||||||
layout.removeAllViews()
|
layout.removeAllViews()
|
||||||
if (repository == null) {
|
if (repository == null) {
|
||||||
@ -125,7 +124,7 @@ class RepositoryFragment() : ScreenFragment() {
|
|||||||
if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) {
|
if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) {
|
||||||
layout.addTitleText(
|
layout.addTitleText(
|
||||||
R.string.number_of_applications,
|
R.string.number_of_applications,
|
||||||
Database.ProductAdapter.getCount(repository.id).toString()
|
screenActivity.db.productDao.countForRepository(repository.id).toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -11,6 +11,7 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.circularreveal.CircularRevealFrameLayout
|
import com.google.android.material.circularreveal.CircularRevealFrameLayout
|
||||||
|
import com.looker.droidify.MainApplication
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
@ -26,6 +27,9 @@ abstract class ScreenActivity : AppCompatActivity() {
|
|||||||
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val db
|
||||||
|
get() = (application as MainApplication).db
|
||||||
|
|
||||||
sealed class SpecialIntent {
|
sealed class SpecialIntent {
|
||||||
object Updates : SpecialIntent()
|
object Updates : SpecialIntent()
|
||||||
class Install(val packageName: String?, val status: Int?, val promptIntent: Intent?) : SpecialIntent()
|
class Install(val packageName: String?, val status: Int?, val promptIntent: Intent?) : SpecialIntent()
|
||||||
|
@ -19,7 +19,7 @@ import androidx.viewpager2.widget.ViewPager2
|
|||||||
import coil.load
|
import coil.load
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.DatabaseX
|
||||||
import com.looker.droidify.entity.Product
|
import com.looker.droidify.entity.Product
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.graphics.PaddingDrawable
|
import com.looker.droidify.graphics.PaddingDrawable
|
||||||
@ -68,6 +68,7 @@ class ScreenshotsFragment() : DialogFragment() {
|
|||||||
|
|
||||||
val window = dialog.window
|
val window = dialog.window
|
||||||
val decorView = window?.decorView
|
val decorView = window?.decorView
|
||||||
|
val db = DatabaseX.getInstance(requireContext())
|
||||||
|
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
@ -132,13 +133,17 @@ class ScreenshotsFragment() : DialogFragment() {
|
|||||||
|
|
||||||
var restored = false
|
var restored = false
|
||||||
productDisposable = Observable.just(Unit)
|
productDisposable = Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Products))
|
//.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
|
.flatMapSingle {
|
||||||
|
RxUtils.querySingle {
|
||||||
|
db.productDao.get(packageName).mapNotNull { it?.data }
|
||||||
|
}
|
||||||
|
}
|
||||||
.map { it ->
|
.map { it ->
|
||||||
Pair(
|
Pair(
|
||||||
it.find { it.repositoryId == repositoryId },
|
it.find { it.repositoryId == repositoryId },
|
||||||
Database.RepositoryAdapter.get(repositoryId)
|
db.repositoryDao.get(repositoryId)?.trueData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -33,7 +33,6 @@ import com.looker.droidify.utility.Utils.languagesList
|
|||||||
import com.looker.droidify.utility.Utils.translateLocale
|
import com.looker.droidify.utility.Utils.translateLocale
|
||||||
import com.looker.droidify.utility.extension.resources.*
|
import com.looker.droidify.utility.extension.resources.*
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SettingsFragment : ScreenFragment() {
|
class SettingsFragment : ScreenFragment() {
|
||||||
@ -105,6 +104,7 @@ class SettingsFragment : ScreenFragment() {
|
|||||||
when (it) {
|
when (it) {
|
||||||
Preferences.AutoSync.Never -> getString(R.string.never)
|
Preferences.AutoSync.Never -> getString(R.string.never)
|
||||||
Preferences.AutoSync.Wifi -> getString(R.string.only_on_wifi)
|
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)
|
Preferences.AutoSync.Always -> getString(R.string.always)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||||||
import com.google.android.material.textview.MaterialTextView
|
import com.google.android.material.textview.MaterialTextView
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
import com.looker.droidify.databinding.TabsToolbarBinding
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
@ -235,9 +234,9 @@ class TabsFragment : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
categoriesDisposable = Observable.just(Unit)
|
categoriesDisposable = Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Products))
|
//.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } }
|
.flatMapSingle { RxUtils.querySingle { screenActivity.db.categoryDao.allNames } }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
setSectionsAndUpdate(
|
setSectionsAndUpdate(
|
||||||
@ -246,9 +245,9 @@ class TabsFragment : ScreenFragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
repositoriesDisposable = Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
//.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
|
.flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.trueData } } }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { it ->
|
.subscribe { it ->
|
||||||
setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
|
setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
|
||||||
|
@ -7,12 +7,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.droidify.*
|
||||||
import com.looker.droidify.Common.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.content.Cache
|
import com.looker.droidify.content.Cache
|
||||||
import com.looker.droidify.entity.Release
|
import com.looker.droidify.entity.Release
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
|
@ -12,15 +12,9 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.droidify.*
|
||||||
import com.looker.droidify.Common.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.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.DatabaseX
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.index.RepositoryUpdater
|
import com.looker.droidify.index.RepositoryUpdater
|
||||||
@ -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.android.notificationManager
|
||||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||||
import com.looker.droidify.utility.extension.text.formatSize
|
import com.looker.droidify.utility.extension.text.formatSize
|
||||||
|
import com.looker.droidify.utility.getProductItem
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
@ -104,7 +99,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sync(request: SyncRequest) {
|
fun sync(request: SyncRequest) {
|
||||||
val ids = Database.RepositoryAdapter.getAll(null)
|
val ids = db.repositoryDao.all.mapNotNull { it.trueData }
|
||||||
.asSequence().filter { it.enabled }.map { it.id }.toList()
|
.asSequence().filter { it.enabled }.map { it.id }.toList()
|
||||||
sync(ids, request)
|
sync(ids, request)
|
||||||
}
|
}
|
||||||
@ -130,7 +125,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setEnabled(repository: Repository, enabled: Boolean): Boolean {
|
fun setEnabled(repository: Repository, enabled: Boolean): Boolean {
|
||||||
Database.RepositoryAdapter.put(repository.enable(enabled))
|
db.repositoryDao.put(repository.enable(enabled))
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) {
|
if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) {
|
||||||
tasks += Task(repository.id, true)
|
tasks += Task(repository.id, true)
|
||||||
@ -149,10 +144,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRepository(repositoryId: Long): Boolean {
|
fun deleteRepository(repositoryId: Long): Boolean {
|
||||||
val repository = Database.RepositoryAdapter.get(repositoryId)
|
val repository = db.repositoryDao.get(repositoryId)?.trueData
|
||||||
return repository != null && run {
|
return repository != null && run {
|
||||||
setEnabled(repository, false)
|
setEnabled(repository, false)
|
||||||
Database.RepositoryAdapter.markAsDeleted(repository.id)
|
db.repositoryDao.markAsDeleted(repository.id)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,10 +155,12 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
|
|
||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
override fun onBind(intent: Intent): Binder = binder
|
override fun onBind(intent: Intent): Binder = binder
|
||||||
|
lateinit var db: DatabaseX
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
db = DatabaseX.getInstance(applicationContext)
|
||||||
if (Android.sdk(26)) {
|
if (Android.sdk(26)) {
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
NOTIFICATION_CHANNEL_SYNCING,
|
NOTIFICATION_CHANNEL_SYNCING,
|
||||||
@ -337,7 +334,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
if (currentTask == null) {
|
if (currentTask == null) {
|
||||||
if (tasks.isNotEmpty()) {
|
if (tasks.isNotEmpty()) {
|
||||||
val task = tasks.removeAt(0)
|
val task = tasks.removeAt(0)
|
||||||
val repository = Database.RepositoryAdapter.get(task.repositoryId)
|
val repository = db.repositoryDao.get(task.repositoryId)?.trueData
|
||||||
if (repository != null && repository.enabled) {
|
if (repository != null && repository.enabled) {
|
||||||
val lastStarted = started
|
val lastStarted = started
|
||||||
val newStarted =
|
val newStarted =
|
||||||
@ -382,7 +379,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
} else if (started != Started.NO) {
|
} else if (started != Started.NO) {
|
||||||
val disposable = RxUtils
|
val disposable = RxUtils
|
||||||
.querySingle { it ->
|
.querySingle { it ->
|
||||||
Database.ProductAdapter
|
db.productDao
|
||||||
.query(
|
.query(
|
||||||
installed = true,
|
installed = true,
|
||||||
updates = true,
|
updates = true,
|
||||||
@ -392,7 +389,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
signal = it
|
signal = it
|
||||||
)
|
)
|
||||||
.use {
|
.use {
|
||||||
it.asSequence().map(Database.ProductAdapter::transformItem)
|
it.asSequence().map { it.getProductItem() }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,12 @@ package com.looker.droidify.ui.activities
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.loader.app.LoaderManager
|
|
||||||
import androidx.loader.content.Loader
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
@ -20,11 +17,10 @@ import androidx.navigation.ui.setupWithNavController
|
|||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.droidify.BuildConfig
|
||||||
import com.looker.droidify.ContextWrapperX
|
import com.looker.droidify.ContextWrapperX
|
||||||
|
import com.looker.droidify.MainApplication
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.database.QueryLoader
|
|
||||||
import com.looker.droidify.databinding.ActivityMainXBinding
|
import com.looker.droidify.databinding.ActivityMainXBinding
|
||||||
import com.looker.droidify.installer.AppInstaller
|
import com.looker.droidify.installer.AppInstaller
|
||||||
import com.looker.droidify.screen.*
|
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 com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor> {
|
class MainActivityX : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
||||||
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||||
@ -67,6 +63,9 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val db
|
||||||
|
get() = (application as MainApplication).db
|
||||||
|
|
||||||
lateinit var cursorOwner: CursorOwner
|
lateinit var cursorOwner: CursorOwner
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@ -230,79 +229,4 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor>
|
|||||||
}
|
}
|
||||||
syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment)
|
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<Cursor> {
|
|
||||||
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<Cursor>, 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<Cursor>) = onLoadFinished(loader, null)
|
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,13 @@ import com.google.android.material.progressindicator.CircularProgressIndicator
|
|||||||
import com.google.android.material.textview.MaterialTextView
|
import com.google.android.material.textview.MaterialTextView
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.network.CoilDownloader
|
import com.looker.droidify.network.CoilDownloader
|
||||||
import com.looker.droidify.utility.Utils
|
import com.looker.droidify.utility.Utils
|
||||||
import com.looker.droidify.utility.extension.resources.*
|
import com.looker.droidify.utility.extension.resources.*
|
||||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||||
|
import com.looker.droidify.utility.getProductItem
|
||||||
import com.looker.droidify.widget.CursorRecyclerAdapter
|
import com.looker.droidify.widget.CursorRecyclerAdapter
|
||||||
|
|
||||||
class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
|
class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
|
||||||
@ -113,7 +113,7 @@ class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getProductItem(position: Int): ProductItem {
|
private fun getProductItem(position: Int): ProductItem {
|
||||||
return Database.ProductAdapter.transformItem(moveTo(position))
|
return moveTo(position).getProductItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
|
@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.ProductPreferences
|
import com.looker.droidify.content.ProductPreferences
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.entity.*
|
import com.looker.droidify.entity.*
|
||||||
import com.looker.droidify.installer.AppInstaller
|
import com.looker.droidify.installer.AppInstaller
|
||||||
import com.looker.droidify.screen.MessageDialog
|
import com.looker.droidify.screen.MessageDialog
|
||||||
@ -32,15 +31,17 @@ import com.looker.droidify.utility.Utils.rootInstallerEnabled
|
|||||||
import com.looker.droidify.utility.Utils.startUpdate
|
import com.looker.droidify.utility.Utils.startUpdate
|
||||||
import com.looker.droidify.utility.extension.android.*
|
import com.looker.droidify.utility.extension.android.*
|
||||||
import com.looker.droidify.utility.extension.text.trimAfter
|
import com.looker.droidify.utility.extension.text.trimAfter
|
||||||
|
import com.looker.droidify.utility.getInstalledItem
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||||
companion object {
|
companion object {
|
||||||
@ -129,12 +130,16 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
|
|
||||||
var first = true
|
var first = true
|
||||||
productDisposable = Observable.just(Unit)
|
productDisposable = Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Products))
|
//.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
|
.flatMapSingle {
|
||||||
|
RxUtils.querySingle {
|
||||||
|
screenActivity.db.productDao.get(packageName).mapNotNull { it?.data }
|
||||||
|
}
|
||||||
|
}
|
||||||
.flatMapSingle { products ->
|
.flatMapSingle { products ->
|
||||||
RxUtils
|
RxUtils
|
||||||
.querySingle { Database.RepositoryAdapter.getAll(it) }
|
.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.trueData } }
|
||||||
.map { it ->
|
.map { it ->
|
||||||
it.asSequence().map { Pair(it.id, it) }.toMap()
|
it.asSequence().map { Pair(it.id, it) }.toMap()
|
||||||
.let {
|
.let {
|
||||||
@ -151,7 +156,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
}
|
}
|
||||||
.flatMapSingle { products ->
|
.flatMapSingle { products ->
|
||||||
RxUtils
|
RxUtils
|
||||||
.querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) }
|
.querySingle { Nullable(screenActivity.db.installedDao.get(packageName).getInstalledItem()) }
|
||||||
.map { Pair(products, it) }
|
.map { Pair(products, it) }
|
||||||
}
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@ -452,15 +457,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
} else Unit
|
} else Unit
|
||||||
}
|
}
|
||||||
AppDetailAdapter.Action.SHARE -> {
|
AppDetailAdapter.Action.SHARE -> {
|
||||||
val sendIntent: Intent = Intent().apply {
|
shareIntent(packageName, products[0].first.name)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}::class
|
}::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) {
|
override fun onPreferenceChanged(preference: ProductPreference) {
|
||||||
lifecycleScope.launch { updateButtons(preference) }
|
lifecycleScope.launch { updateButtons(preference) }
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.entity.ProductItem
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.screen.BaseFragment
|
import com.looker.droidify.screen.BaseFragment
|
||||||
import com.looker.droidify.ui.adapters.AppListAdapter
|
import com.looker.droidify.ui.adapters.AppListAdapter
|
||||||
@ -78,10 +77,10 @@ class AppListFragment() : BaseFragment(), CursorOwner.Callback {
|
|||||||
|
|
||||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
repositoriesDisposable = Observable.just(Unit)
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
//.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
|
.flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all.mapNotNull { it.trueData } } }
|
||||||
.map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() }
|
.map { it.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it }
|
.subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it }
|
||||||
}
|
}
|
||||||
|
@ -5,34 +5,32 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.FragmentExploreXBinding
|
import com.looker.droidify.databinding.FragmentExploreXBinding
|
||||||
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.ui.adapters.AppListAdapter
|
import com.looker.droidify.ui.adapters.AppListAdapter
|
||||||
import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX
|
import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX
|
||||||
import com.looker.droidify.utility.RxUtils
|
import com.looker.droidify.utility.RxUtils
|
||||||
import com.looker.droidify.widget.RecyclerFastScroller
|
import com.looker.droidify.widget.RecyclerFastScroller
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
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 io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
|
class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
|
||||||
|
|
||||||
override val viewModel: MainNavFragmentViewModelX by viewModels()
|
override lateinit var viewModel: MainNavFragmentViewModelX
|
||||||
private lateinit var binding: FragmentExploreXBinding
|
private lateinit var binding: FragmentExploreXBinding
|
||||||
|
|
||||||
override val source = Source.AVAILABLE
|
override val source = Source.AVAILABLE
|
||||||
|
|
||||||
private var repositoriesDisposable: Disposable? = null
|
private var repositories: Map<Long, Repository> = mapOf()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -42,6 +40,9 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = FragmentExploreXBinding.inflate(inflater, container, false)
|
binding = FragmentExploreXBinding.inflate(inflater, container, false)
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
|
val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db)
|
||||||
|
viewModel = ViewModelProvider(this, viewModelFactory)
|
||||||
|
.get(MainNavFragmentViewModelX::class.java)
|
||||||
|
|
||||||
binding.recyclerView.apply {
|
binding.recyclerView.apply {
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
@ -58,22 +59,13 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
mainActivityX.attachCursorOwner(this, viewModel.request(source))
|
viewModel.fillList(source)
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
viewModel.db.repositoryDao.allFlowable
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
|
||||||
.observeOn(Schedulers.io())
|
.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() }
|
.map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it }
|
.subscribe { repositories = it }
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
mainActivityX.detachCursorOwner(this)
|
|
||||||
repositoriesDisposable?.dispose()
|
|
||||||
repositoriesDisposable = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||||
|
@ -5,34 +5,36 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.CursorOwner
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.FragmentInstalledXBinding
|
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.ui.viewmodels.MainNavFragmentViewModelX
|
||||||
import com.looker.droidify.utility.RxUtils
|
import com.looker.droidify.utility.RxUtils
|
||||||
import com.looker.droidify.widget.RecyclerFastScroller
|
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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
|
class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
|
||||||
|
|
||||||
override val viewModel: MainNavFragmentViewModelX by viewModels()
|
override lateinit var viewModel: MainNavFragmentViewModelX
|
||||||
private lateinit var binding: FragmentInstalledXBinding
|
private lateinit var binding: FragmentInstalledXBinding
|
||||||
|
|
||||||
|
private val installedItemAdapter = ItemAdapter<VAppItem>()
|
||||||
|
private var installedFastAdapter: FastAdapter<VAppItem>? = null
|
||||||
|
private val updatedItemAdapter = ItemAdapter<HAppItem>()
|
||||||
|
private var updatedFastAdapter: FastAdapter<HAppItem>? = null
|
||||||
|
|
||||||
override val source = Source.INSTALLED
|
override val source = Source.INSTALLED
|
||||||
|
|
||||||
private var repositoriesDisposable: Disposable? = null
|
private var repositories: Map<Long, Repository> = mapOf()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -42,12 +44,26 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = FragmentInstalledXBinding.inflate(inflater, container, false)
|
binding = FragmentInstalledXBinding.inflate(inflater, container, false)
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
|
val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db)
|
||||||
|
viewModel = ViewModelProvider(this, viewModelFactory)
|
||||||
|
.get(MainNavFragmentViewModelX::class.java)
|
||||||
|
|
||||||
binding.recyclerView.apply {
|
installedFastAdapter = FastAdapter.with(installedItemAdapter)
|
||||||
layoutManager = LinearLayoutManager(context)
|
installedFastAdapter?.setHasStableIds(true)
|
||||||
|
binding.installedRecycler.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
isMotionEventSplittingEnabled = false
|
isMotionEventSplittingEnabled = false
|
||||||
isVerticalScrollBarEnabled = 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)
|
RecyclerFastScroller(this)
|
||||||
}
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -56,28 +72,26 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
mainActivityX.attachCursorOwner(this, viewModel.request(source))
|
viewModel.fillList(source)
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
viewModel.db.repositoryDao.allFlowable
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
|
||||||
.observeOn(Schedulers.io())
|
.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() }
|
.map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it }
|
.subscribe { repositories = it }
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
mainActivityX.detachCursorOwner(this)
|
|
||||||
repositoriesDisposable?.dispose()
|
|
||||||
repositoriesDisposable = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||||
// TODO create app list out of cursor and use those on the different RecycleViews
|
// TODO get a list instead of the cursor
|
||||||
(binding.recyclerView.adapter as? AppListAdapter)?.apply {
|
// TODO use LiveData and observers instead of listeners
|
||||||
this.cursor = cursor
|
val appItemList: List<ProductItem> = listOf()
|
||||||
|
installedItemAdapter.set(appItemList
|
||||||
|
.map { VAppItem(it, repositories[it.repositoryId]) }
|
||||||
|
)
|
||||||
|
updatedItemAdapter.set(appItemList.filter { it.canUpdate }
|
||||||
|
.map { HAppItem(it, repositories[it.repositoryId]) }
|
||||||
|
)
|
||||||
|
/*
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
emptyText = when {
|
emptyText = when {
|
||||||
@ -88,6 +102,6 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,34 +5,36 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.CursorOwner
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.databinding.FragmentLatestXBinding
|
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.ui.viewmodels.MainNavFragmentViewModelX
|
||||||
import com.looker.droidify.utility.RxUtils
|
import com.looker.droidify.utility.RxUtils
|
||||||
import com.looker.droidify.widget.RecyclerFastScroller
|
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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
|
class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
|
||||||
|
|
||||||
override val viewModel: MainNavFragmentViewModelX by viewModels()
|
override lateinit var viewModel: MainNavFragmentViewModelX
|
||||||
private lateinit var binding: FragmentLatestXBinding
|
private lateinit var binding: FragmentLatestXBinding
|
||||||
|
|
||||||
override val source = Source.UPDATES
|
private val updatedItemAdapter = ItemAdapter<VAppItem>()
|
||||||
|
private var updatedFastAdapter: FastAdapter<VAppItem>? = null
|
||||||
|
private val newItemAdapter = ItemAdapter<HAppItem>()
|
||||||
|
private var newFastAdapter: FastAdapter<HAppItem>? = null
|
||||||
|
|
||||||
private var repositoriesDisposable: Disposable? = null
|
override val source = Source.AVAILABLE
|
||||||
|
|
||||||
|
private var repositories: Map<Long, Repository> = mapOf()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -42,13 +44,26 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = FragmentLatestXBinding.inflate(inflater, container, false)
|
binding = FragmentLatestXBinding.inflate(inflater, container, false)
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
|
val viewModelFactory = MainNavFragmentViewModelX.Factory(mainActivityX.db)
|
||||||
|
viewModel = ViewModelProvider(this, viewModelFactory)
|
||||||
|
.get(MainNavFragmentViewModelX::class.java)
|
||||||
|
|
||||||
binding.recyclerView.apply {
|
updatedFastAdapter = FastAdapter.with(updatedItemAdapter)
|
||||||
id = android.R.id.list
|
updatedFastAdapter?.setHasStableIds(true)
|
||||||
layoutManager = LinearLayoutManager(context)
|
binding.updatedRecycler.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
isMotionEventSplittingEnabled = false
|
isMotionEventSplittingEnabled = false
|
||||||
isVerticalScrollBarEnabled = 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)
|
RecyclerFastScroller(this)
|
||||||
}
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -57,28 +72,26 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
mainActivityX.attachCursorOwner(this, viewModel.request(source))
|
viewModel.fillList(source)
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
viewModel.db.repositoryDao.allFlowable
|
||||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
|
||||||
.observeOn(Schedulers.io())
|
.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() }
|
.map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it }
|
.subscribe { repositories = it }
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
mainActivityX.detachCursorOwner(this)
|
|
||||||
repositoriesDisposable?.dispose()
|
|
||||||
repositoriesDisposable = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||||
// TODO create app list out of cursor and use those on the different RecycleViews
|
// TODO get a list instead of the cursor
|
||||||
(binding.recyclerView.adapter as? AppListAdapter)?.apply {
|
// TODO use LiveData and observers instead of listeners
|
||||||
this.cursor = cursor
|
val appItemList: List<ProductItem> = 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 {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
emptyText = when {
|
emptyText = when {
|
||||||
@ -89,6 +102,6 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX
|
|||||||
abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
|
abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
|
||||||
val mainActivityX: MainActivityX
|
val mainActivityX: MainActivityX
|
||||||
get() = requireActivity() as MainActivityX
|
get() = requireActivity() as MainActivityX
|
||||||
abstract val viewModel: MainNavFragmentViewModelX
|
abstract var viewModel: MainNavFragmentViewModelX
|
||||||
abstract val source: Source
|
abstract val source: Source
|
||||||
|
|
||||||
open fun onBackPressed(): Boolean = false
|
open fun onBackPressed(): Boolean = false
|
||||||
@ -18,7 +18,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
|
|||||||
internal fun setSearchQuery(searchQuery: String) {
|
internal fun setSearchQuery(searchQuery: String) {
|
||||||
viewModel.setSearchQuery(searchQuery) {
|
viewModel.setSearchQuery(searchQuery) {
|
||||||
if (view != null) {
|
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) {
|
internal fun setSection(section: ProductItem.Section) {
|
||||||
viewModel.setSection(section) {
|
viewModel.setSection(section) {
|
||||||
if (view != null) {
|
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) {
|
internal fun setOrder(order: ProductItem.Order) {
|
||||||
viewModel.setOrder(order) {
|
viewModel.setOrder(order) {
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
mainActivityX.attachCursorOwner(this, viewModel.request(source))
|
viewModel.fillList(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,3 +45,36 @@ enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean
|
|||||||
INSTALLED(R.string.installed, false, true),
|
INSTALLED(R.string.installed, false, true),
|
||||||
UPDATES(R.string.updates, false, false)
|
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
|
||||||
|
}
|
||||||
|
}
|
42
src/main/kotlin/com/looker/droidify/ui/items/HAppItem.kt
Normal file
42
src/main/kotlin/com/looker/droidify/ui/items/HAppItem.kt
Normal file
@ -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<ItemAppHorizXBinding>() {
|
||||||
|
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<Any>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
79
src/main/kotlin/com/looker/droidify/ui/items/VAppItem.kt
Normal file
79
src/main/kotlin/com/looker/droidify/ui/items/VAppItem.kt
Normal file
@ -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<ItemAppVerticalXBinding>() {
|
||||||
|
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<Any>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,8 @@ package com.looker.droidify.ui.viewmodels
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
import com.looker.droidify.ui.activities.MainActivityX
|
|
||||||
|
|
||||||
class MainActivityViewModelX() : ViewModel() {
|
class MainActivityViewModelX : ViewModel() {
|
||||||
|
|
||||||
val activeRequests = mutableMapOf<Int, CursorOwner.ActiveRequest>()
|
val activeRequests = mutableMapOf<Int, CursorOwner.ActiveRequest>()
|
||||||
}
|
}
|
@ -1,15 +1,23 @@
|
|||||||
package com.looker.droidify.ui.viewmodels
|
package com.looker.droidify.ui.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
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.entity.ProductItem
|
||||||
|
import com.looker.droidify.ui.fragments.Request
|
||||||
import com.looker.droidify.ui.fragments.Source
|
import com.looker.droidify.ui.fragments.Source
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainNavFragmentViewModelX : ViewModel() {
|
class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() {
|
||||||
|
|
||||||
private val _order = MutableStateFlow(ProductItem.Order.LAST_UPDATE)
|
private val _order = MutableStateFlow(ProductItem.Order.LAST_UPDATE)
|
||||||
private val _sections = MutableStateFlow<ProductItem.Section>(ProductItem.Section.All)
|
private val _sections = MutableStateFlow<ProductItem.Section>(ProductItem.Section.All)
|
||||||
@ -32,7 +40,7 @@ class MainNavFragmentViewModelX : ViewModel() {
|
|||||||
started = SharingStarted.WhileSubscribed(5000)
|
started = SharingStarted.WhileSubscribed(5000)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun request(source: Source): CursorOwner.Request {
|
fun request(source: Source): Request {
|
||||||
var mSearchQuery = ""
|
var mSearchQuery = ""
|
||||||
var mSections: ProductItem.Section = ProductItem.Section.All
|
var mSections: ProductItem.Section = ProductItem.Section.All
|
||||||
var mOrder: ProductItem.Order = ProductItem.Order.NAME
|
var mOrder: ProductItem.Order = ProductItem.Order.NAME
|
||||||
@ -42,17 +50,17 @@ class MainNavFragmentViewModelX : ViewModel() {
|
|||||||
launch { order.collect { if (source.order) mOrder = it } }
|
launch { order.collect { if (source.order) mOrder = it } }
|
||||||
}
|
}
|
||||||
return when (source) {
|
return when (source) {
|
||||||
Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(
|
Source.AVAILABLE -> Request.ProductsAvailable(
|
||||||
mSearchQuery,
|
mSearchQuery,
|
||||||
mSections,
|
mSections,
|
||||||
mOrder
|
mOrder
|
||||||
)
|
)
|
||||||
Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(
|
Source.INSTALLED -> Request.ProductsInstalled(
|
||||||
mSearchQuery,
|
mSearchQuery,
|
||||||
mSections,
|
mSections,
|
||||||
mOrder
|
mOrder
|
||||||
)
|
)
|
||||||
Source.UPDATES -> CursorOwner.Request.ProductsUpdates(
|
Source.UPDATES -> Request.ProductsUpdates(
|
||||||
mSearchQuery,
|
mSearchQuery,
|
||||||
mSections,
|
mSections,
|
||||||
mOrder
|
mOrder
|
||||||
@ -60,6 +68,46 @@ class MainNavFragmentViewModelX : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var productsList = MediatorLiveData<MutableList<Product>>()
|
||||||
|
|
||||||
|
fun fillList(source: Source) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
productsList.value = query(request(source))?.toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun query(request: Request): List<Product>? {
|
||||||
|
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) {
|
fun setSection(newSection: ProductItem.Section, perform: () -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (newSection != sections.value) {
|
if (newSection != sections.value) {
|
||||||
@ -86,4 +134,14 @@ class MainNavFragmentViewModelX : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(val db: DatabaseX) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(MainNavFragmentViewModelX::class.java)) {
|
||||||
|
return MainNavFragmentViewModelX(db) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -7,26 +7,32 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.Signature
|
import android.content.pm.Signature
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.database.Cursor
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.looker.droidify.BuildConfig
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.looker.droidify.Common.PREFS_LANGUAGE_DEFAULT
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.*
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.entity.InstalledItem
|
import com.looker.droidify.entity.InstalledItem
|
||||||
import com.looker.droidify.entity.Product
|
import com.looker.droidify.entity.Product
|
||||||
|
import com.looker.droidify.entity.ProductItem
|
||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.DownloadService
|
import com.looker.droidify.service.DownloadService
|
||||||
import com.looker.droidify.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import com.looker.droidify.utility.extension.android.singleSignature
|
import com.looker.droidify.utility.extension.android.singleSignature
|
||||||
import com.looker.droidify.utility.extension.android.versionCodeCompat
|
import com.looker.droidify.utility.extension.android.versionCodeCompat
|
||||||
|
import com.looker.droidify.utility.extension.json.Json
|
||||||
|
import com.looker.droidify.utility.extension.json.parseDictionary
|
||||||
|
import com.looker.droidify.utility.extension.json.writeDictionary
|
||||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableCompat
|
import com.looker.droidify.utility.extension.resources.getDrawableCompat
|
||||||
import com.looker.droidify.utility.extension.text.hex
|
import com.looker.droidify.utility.extension.text.hex
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.CertificateEncodingException
|
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 <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
|
||||||
|
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) }
|
||||||
|
return outputStream.toByteArray()
|
||||||
|
}
|
@ -130,7 +130,7 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/modeBar">
|
app:layout_constraintTop_toBottomOf="@id/modeBar">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/installedRecycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
|
@ -44,12 +44,12 @@
|
|||||||
android:layout_marginHorizontal="12dp"
|
android:layout_marginHorizontal="12dp"
|
||||||
android:layout_marginVertical="14dp"
|
android:layout_marginVertical="14dp"
|
||||||
android:text="@string/new_applications"
|
android:text="@string/new_applications"
|
||||||
app:layout_constraintBottom_toTopOf="@id/updatedRecycler"
|
app:layout_constraintBottom_toTopOf="@id/newRecycler"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/updatedRecycler"
|
android:id="@+id/newRecycler"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
@ -112,7 +112,7 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/modeBar">
|
app:layout_constraintTop_toBottomOf="@id/modeBar">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/updatedRecycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
|
@ -23,23 +23,38 @@
|
|||||||
android:elevation="8dp"
|
android:elevation="8dp"
|
||||||
android:src="@drawable/ic_application_default"
|
android:src="@drawable/ic_application_default"
|
||||||
android:tint="?colorPrimary"
|
android:tint="?colorPrimary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/label"
|
app:layout_constraintBottom_toTopOf="@id/name"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
android:id="@+id/label"
|
android:id="@+id/name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="2dp"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/version"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/icon" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/version"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="2dp"
|
android:layout_marginHorizontal="2dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/icon" />
|
app:layout_constraintTop_toBottomOf="@id/name" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
</layout>
|
@ -81,4 +81,9 @@
|
|||||||
<string name="link_copied_to_clipboard">Odkaz zkopírován do schránky</string>
|
<string name="link_copied_to_clipboard">Odkaz zkopírován do schránky</string>
|
||||||
<string name="install_types">Typy instalací</string>
|
<string name="install_types">Typy instalací</string>
|
||||||
<string name="links">Odkazy</string>
|
<string name="links">Odkazy</string>
|
||||||
|
<plurals name="new_updates_DESC_FORMAT">
|
||||||
|
<item quantity="one">%d aplikace má novou verzi.</item>
|
||||||
|
<item quantity="few"></item>
|
||||||
|
<item quantity="other"></item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
@ -177,4 +177,5 @@
|
|||||||
<string name="update_all">Actualizar todos</string>
|
<string name="update_all">Actualizar todos</string>
|
||||||
<string name="installed_applications">Aplicaciones instaladas</string>
|
<string name="installed_applications">Aplicaciones instaladas</string>
|
||||||
<string name="new_applications">Aplicaciones nuevas</string>
|
<string name="new_applications">Aplicaciones nuevas</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Solo en Wi-Fi y Conectado</string>
|
||||||
</resources>
|
</resources>
|
@ -177,4 +177,5 @@
|
|||||||
<string name="ignore_all_updates">نادیده گرفتن تمام نسخههای جدید</string>
|
<string name="ignore_all_updates">نادیده گرفتن تمام نسخههای جدید</string>
|
||||||
<string name="http_error_DESC">پاسخ نادرست سرور.</string>
|
<string name="http_error_DESC">پاسخ نادرست سرور.</string>
|
||||||
<string name="incompatible_with_FORMAT">ناسازگار با %s</string>
|
<string name="incompatible_with_FORMAT">ناسازگار با %s</string>
|
||||||
|
<string name="only_on_wifi_and_battery">فقط در وایفای و به برق وصلشده</string>
|
||||||
</resources>
|
</resources>
|
@ -8,56 +8,56 @@
|
|||||||
<string name="amoled">Musta</string>
|
<string name="amoled">Musta</string>
|
||||||
<string name="anti_features">Anti-ominaisuudet</string>
|
<string name="anti_features">Anti-ominaisuudet</string>
|
||||||
<string name="application">Sovellus</string>
|
<string name="application">Sovellus</string>
|
||||||
<string name="action_failed">Toiminta epäonnistui</string>
|
<string name="action_failed">Toiminto epäonnistui</string>
|
||||||
<string name="add_repository">Lisää arkisto</string>
|
<string name="add_repository">Lisää tietovarasto</string>
|
||||||
<string name="application_not_found">Tätä sovellusta ei löytynyt</string>
|
<string name="application_not_found">Tätä sovellusta ei löytynyt</string>
|
||||||
<string name="author_email">Tekijän sähköposti</string>
|
<string name="author_email">Kehittäjän sähköposti</string>
|
||||||
<string name="author_website">Kirjoittajan verkkosivusto</string>
|
<string name="author_website">Kehittäjän verkkosivusto</string>
|
||||||
<string name="available">Saatavilla</string>
|
<string name="available">Saatavilla</string>
|
||||||
<string name="bug_tracker">Vikojen jäljitin</string>
|
<string name="bug_tracker">Vikojen jäljitin</string>
|
||||||
<string name="delete">Poista</string>
|
<string name="delete">Poista</string>
|
||||||
<string name="dark">Pimeä</string>
|
<string name="dark">Tumma</string>
|
||||||
<string name="could_not_validate_FORMAT">Ei voitu validoida %s</string>
|
<string name="could_not_validate_FORMAT">Ei voitu validoida kohdetta %s</string>
|
||||||
<string name="compiled_for_debugging">Käännetty virheenkorjausta varten</string>
|
<string name="compiled_for_debugging">Käännetty virheenkorjausta varten</string>
|
||||||
<string name="confirmation">Vahvistus</string>
|
<string name="confirmation">Vahvistus</string>
|
||||||
<string name="connecting">Yhdistetään…</string>
|
<string name="connecting">Yhdistetään…</string>
|
||||||
<string name="contains_non_free_media">Sisältää ei-vapaata mediaa</string>
|
<string name="contains_non_free_media">Sisältää ei-vapaata mediaa</string>
|
||||||
<string name="could_not_download_FORMAT">Ei voitu ladata %s</string>
|
<string name="could_not_download_FORMAT">Ei voitu ladata kohdetta %s</string>
|
||||||
<string name="could_not_sync_FORMAT">Ei voitu synkronoida %s</string>
|
<string name="could_not_sync_FORMAT">Ei voitu synkronoida kohdetta %s</string>
|
||||||
<string name="credits">Opintopisteet</string>
|
<string name="credits">Tekijät</string>
|
||||||
<string name="cancel">Peruuta</string>
|
<string name="cancel">Peruuta</string>
|
||||||
<string name="cant_edit_sync_DESC">Arkistoa ei voi muokata, koska se synkronoidaan juuri nyt.</string>
|
<string name="cant_edit_sync_DESC">Tietovarastoa ei voida muokata, koska sen synkronointi on käynnissä.</string>
|
||||||
<string name="changelog">Muutosloki</string>
|
<string name="changelog">Muutosloki</string>
|
||||||
<string name="changes">Muutokset</string>
|
<string name="changes">Muutokset</string>
|
||||||
<string name="checking_repository">Tarkistetaan tietovarastoa…</string>
|
<string name="checking_repository">Tarkistetaan tietovarastoa…</string>
|
||||||
<string name="fingerprint">Sormenjälki</string>
|
<string name="fingerprint">Sormenjälki</string>
|
||||||
<string name="file_format_error_DESC">Väärä tiedostomuoto.</string>
|
<string name="file_format_error_DESC">Väärä tiedostomuoto.</string>
|
||||||
<string name="edit_repository">Muokkaa arkistoa</string>
|
<string name="edit_repository">Muokkaa tietovarastoa</string>
|
||||||
<string name="downloading_FORMAT">Ladataan %s…</string>
|
<string name="downloading_FORMAT">Ladataan %s…</string>
|
||||||
<string name="downloading">Lataaminen</string>
|
<string name="downloading">Ladataan</string>
|
||||||
<string name="downloaded_FORMAT">Ladattu %s</string>
|
<string name="downloaded_FORMAT">Kohde %s ladattu</string>
|
||||||
<string name="donate">Lahjoita</string>
|
<string name="donate">Lahjoita</string>
|
||||||
<string name="details">Tiedot</string>
|
<string name="details">Tiedot</string>
|
||||||
<string name="description">Description</string>
|
<string name="description">Kuvaus</string>
|
||||||
<string name="delete_repository_DESC">Poista arkisto\?</string>
|
<string name="delete_repository_DESC">Poistetaanko tietovarasto\?</string>
|
||||||
<string name="has_advertising">On mainontaa</string>
|
<string name="has_advertising">Sisältää mainontaa</string>
|
||||||
<string name="has_non_free_dependencies">Ei-vapaita riippuvuuksia</string>
|
<string name="has_non_free_dependencies">Ei-vapaita riippuvuuksia</string>
|
||||||
<string name="has_security_vulnerabilities">On tietoturva-aukkoja</string>
|
<string name="has_security_vulnerabilities">Tietoturvassa on haavoittuvuuksia</string>
|
||||||
<string name="http_error_DESC">Virheellinen palvelinvastaus.</string>
|
<string name="http_error_DESC">Virheellinen palvelimen vastaus.</string>
|
||||||
<string name="http_proxy">HTTP-välityspalvelin</string>
|
<string name="http_proxy">HTTP-välityspalvelin</string>
|
||||||
<string name="ignore_all_updates">Jätä kaikki uudet versiot huomiotta</string>
|
<string name="ignore_all_updates">Älä huomioi uusia versioita</string>
|
||||||
<string name="ignore_this_update">Jätä tämä versio huomiotta</string>
|
<string name="ignore_this_update">Jätä tämä versio huomiotta</string>
|
||||||
<string name="incompatible_api_DESC_FORMAT">Sinun %1$s (API-versio %2$d) ei ole tuettu. %3$s</string>
|
<string name="incompatible_api_DESC_FORMAT">Sinun %1$s (API-versio %2$d) ei ole tuettu. %3$s</string>
|
||||||
<string name="incompatible_api_max_DESC_FORMAT">Suurin API-versio on %d.</string>
|
<string name="incompatible_api_max_DESC_FORMAT">Suurin API-versio on %d.</string>
|
||||||
<string name="incompatible_api_min_DESC_FORMAT">Vähimmäis API-versio on %d.</string>
|
<string name="incompatible_api_min_DESC_FORMAT">Varhaisin API-versio on %d.</string>
|
||||||
<string name="incompatible_features_DESC">Puuttuvat ominaisuudet.</string>
|
<string name="incompatible_features_DESC">Puuttuvat ominaisuudet.</string>
|
||||||
<string name="incompatible_older_DESC">Tämä versio on vanhempi kuin laitteeseesi asennettu versio. Poista se ensin.</string>
|
<string name="incompatible_older_DESC">Tämä versio on vanhempi kuin laitteeseesi asennettu versio. Poista se ensin.</string>
|
||||||
<string name="incompatible_version">Yhteensopimaton versio</string>
|
<string name="incompatible_version">Yhteensopimaton versio</string>
|
||||||
<string name="incompatible_versions">Yhteensopimattomat versiot</string>
|
<string name="incompatible_versions">Yhteensopimattomat versiot</string>
|
||||||
<string name="incompatible_versions_summary">Näytä sovellusversiot, jotka eivät ole yhteensopivia laitteen kanssa</string>
|
<string name="incompatible_versions_summary">Näytä sovellusversiot, jotka eivät ole yhteensopivia laitteen kanssa</string>
|
||||||
<string name="incompatible_with_FORMAT">Yhteensopimaton %s</string>
|
<string name="incompatible_with_FORMAT">Yhteensopimaton kohteen %s kanssa</string>
|
||||||
<string name="install">Asenna</string>
|
<string name="install">Asenna</string>
|
||||||
<string name="install_types">Asennuksen tyypit</string>
|
<string name="install_types">Asennustyypit</string>
|
||||||
<string name="installed">Asennettu</string>
|
<string name="installed">Asennettu</string>
|
||||||
<string name="integrity_check_error_DESC">Ei voitu tarkistaa eheyttä.</string>
|
<string name="integrity_check_error_DESC">Ei voitu tarkistaa eheyttä.</string>
|
||||||
<string name="invalid_address">Virheellinen osoite</string>
|
<string name="invalid_address">Virheellinen osoite</string>
|
||||||
@ -66,74 +66,74 @@
|
|||||||
<string name="invalid_permissions_error_DESC">Virheelliset käyttöoikeudet.</string>
|
<string name="invalid_permissions_error_DESC">Virheelliset käyttöoikeudet.</string>
|
||||||
<string name="invalid_signature_error_DESC">Virheellinen allekirjoitus.</string>
|
<string name="invalid_signature_error_DESC">Virheellinen allekirjoitus.</string>
|
||||||
<string name="invalid_username_format">Virheellinen käyttäjänimen muoto</string>
|
<string name="invalid_username_format">Virheellinen käyttäjänimen muoto</string>
|
||||||
<string name="launch">Laukaisu</string>
|
<string name="launch">Avaa</string>
|
||||||
<string name="license">Lisenssi</string>
|
<string name="license">Lisenssi</string>
|
||||||
<string name="license_FORMAT">%s -lisenssi</string>
|
<string name="license_FORMAT">%s -lisenssi</string>
|
||||||
<string name="light">Valo</string>
|
<string name="light">Vaalea</string>
|
||||||
<string name="link_copied_to_clipboard">Linkki kopioitu leikepöydälle</string>
|
<string name="link_copied_to_clipboard">Linkki kopioitu leikepöydälle</string>
|
||||||
<string name="links">Linkit</string>
|
<string name="links">Linkit</string>
|
||||||
<string name="list_animation">Lista-animaatiot</string>
|
<string name="list_animation">Lista-animaatiot</string>
|
||||||
<string name="list_animation_description">Näytä listan animaatio pääsivulla</string>
|
<string name="list_animation_description">Näytä listan animaatio pääsivulla</string>
|
||||||
<string name="merging_FORMAT">Yhdistäminen %s</string>
|
<string name="merging_FORMAT">Yhdistäminen %s</string>
|
||||||
<string name="name">Nimi</string>
|
<string name="name">Nimi</string>
|
||||||
<string name="network_error_DESC">Verkko virhe.</string>
|
<string name="network_error_DESC">Verkkovirhe.</string>
|
||||||
<string name="never">Koskaan</string>
|
<string name="never">Ei koskaan</string>
|
||||||
<string name="new_updates_available">Sovellusten uudet versiot saatavilla</string>
|
<string name="new_updates_available">Päivityksiä saatavilla</string>
|
||||||
<plurals name="new_updates_DESC_FORMAT">
|
<plurals name="new_updates_DESC_FORMAT">
|
||||||
<item quantity="one">%d -sovelluksella on uusi versio.</item>
|
<item quantity="one">Uusi versio on saatavilla yhteen sovellukseen.</item>
|
||||||
<item quantity="other">%d -sovelluksia, joilla on uusia versioita.</item>
|
<item quantity="other">Uusi versio saatavilla %d sovellukseen.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="no_applications_available">Ei käytettävissä olevia sovelluksia</string>
|
<string name="no_applications_available">Ei sovelluksia saatavilla</string>
|
||||||
<string name="no_applications_installed">Ei asennettuja sovelluksia</string>
|
<string name="no_applications_installed">Ei asennettuja sovelluksia</string>
|
||||||
<string name="no_description_available_DESC">Kuvausta ei ole saatavilla.</string>
|
<string name="no_description_available_DESC">Kuvausta ei ole saatavilla.</string>
|
||||||
<string name="no_matching_applications_found">Ei löytynyt tällaisia sovelluksia</string>
|
<string name="no_matching_applications_found">Haulla ei löytynyt sovelluksia</string>
|
||||||
<string name="provided_by_FORMAT">Toimittanut %s</string>
|
<string name="provided_by_FORMAT">Toimittanut %s</string>
|
||||||
<string name="proxy_host">Proxy-isäntä</string>
|
<string name="proxy_host">Välityspalvelimen osoite</string>
|
||||||
<string name="proxy_port">Proxy portti</string>
|
<string name="proxy_port">Välityspalvelimen portti</string>
|
||||||
<string name="proxy_type">Proxy tyyppi</string>
|
<string name="proxy_type">Välityspalvelimen tyyppi</string>
|
||||||
<string name="recently_updated">Äskettäin päivitetty</string>
|
<string name="recently_updated">Äskettäin päivitetty</string>
|
||||||
<string name="repositories">Tietovarastot</string>
|
<string name="repositories">Tietovarastot</string>
|
||||||
<string name="repository">Varasto</string>
|
<string name="repository">Tietovarasto</string>
|
||||||
<string name="requires_FORMAT">Vaatii %s</string>
|
<string name="requires_FORMAT">Vaatii %s</string>
|
||||||
<string name="root_permission">Hiljainen asennus</string>
|
<string name="root_permission">Hiljainen asennus</string>
|
||||||
<string name="root_permission_description">Salli pääkäyttäjän oikeudet hiljaisiin asennuksiin</string>
|
<string name="root_permission_description">Salli pääkäyttäjän oikeudet hiljaisiin asennuksiin</string>
|
||||||
<string name="save">Tallenna</string>
|
<string name="save">Tallenna</string>
|
||||||
<string name="saving_details">Tallennetaan tietoja…</string>
|
<string name="saving_details">Tallennetaan tietoja…</string>
|
||||||
<string name="screenshots">Kuvakaappaukset</string>
|
<string name="screenshots">Kuvakaappaukset</string>
|
||||||
<string name="only_on_wifi">Vain Wi-Fi</string>
|
<string name="only_on_wifi">Vain Wi-Fi-yhteydellä</string>
|
||||||
<string name="open_DESC_FORMAT">Avaa %s\?</string>
|
<string name="open_DESC_FORMAT">Avataanko %s\?</string>
|
||||||
<string name="other">Muut</string>
|
<string name="other">Muut</string>
|
||||||
<string name="parsing_index_error_DESC">Indeksitiedostoa ei voitu analysoida.</string>
|
<string name="parsing_index_error_DESC">Indeksitiedostoa ei voitu analysoida.</string>
|
||||||
<string name="password">Salasana</string>
|
<string name="password">Salasana</string>
|
||||||
<string name="password_missing">Salasana puuttuu</string>
|
<string name="password_missing">Salasana puuttuu</string>
|
||||||
<string name="permissions">Luvat</string>
|
<string name="permissions">Käyttöoikeudet</string>
|
||||||
<string name="plus_more_FORMAT">+%d lisää</string>
|
<string name="plus_more_FORMAT">+%d lisää</string>
|
||||||
<string name="settings">Asetukset</string>
|
<string name="settings">Asetukset</string>
|
||||||
<string name="processing_FORMAT">Käsitellään %1$s…</string>
|
<string name="processing_FORMAT">Käsitellään kohdetta %1$s…</string>
|
||||||
<string name="project_website">Hankkeen verkkosivusto</string>
|
<string name="project_website">Verkkosivusto</string>
|
||||||
<string name="promotes_non_free_network_services">Edistää muita kuin ilmaisia verkkopalveluja</string>
|
<string name="promotes_non_free_network_services">Edistää ei-vapaita verkkopalveluja</string>
|
||||||
<string name="search">Haku</string>
|
<string name="search">Haku</string>
|
||||||
<string name="select_mirror">Valitse peili</string>
|
<string name="select_mirror">Valitse peilipalvelin</string>
|
||||||
<string name="no_proxy">Ei proxy</string>
|
<string name="no_proxy">Ei välityspalvelinta</string>
|
||||||
<string name="notify_about_updates">Ilmoita sovellusten uusista versioista</string>
|
<string name="notify_about_updates">Ilmoita sovellusten uusista versioista</string>
|
||||||
<string name="notify_about_updates_summary">Näytä ilmoitus, kun uusia versioita on saatavilla</string>
|
<string name="notify_about_updates_summary">Näytä ilmoitus, kun uusia versioita on saatavilla</string>
|
||||||
<string name="only_compatible_with_FORMAT">Yhteensopiva vain %s</string>
|
<string name="only_compatible_with_FORMAT">Yhteensopiva vain arkkitehtuurilla %s</string>
|
||||||
<string name="share">Osake</string>
|
<string name="share">Jaa</string>
|
||||||
<string name="show_more">Näytä lisää</string>
|
<string name="show_more">Näytä lisää</string>
|
||||||
<string name="show_older_versions">Näytä vanhemmat versiot</string>
|
<string name="show_older_versions">Näytä vanhemmat versiot</string>
|
||||||
<string name="ok">OKEI</string>
|
<string name="ok">OK</string>
|
||||||
<string name="tracks_or_reports_your_activity">Seuraa toimintaasi tai raportoi siitä</string>
|
<string name="tracks_or_reports_your_activity">Seuraa tai raportoi toimintaasi</string>
|
||||||
<string name="uninstall">Poista asennus</string>
|
<string name="uninstall">Poista asennus</string>
|
||||||
<string name="unknown">Tuntematon</string>
|
<string name="unknown">Tuntematon</string>
|
||||||
<string name="unknown_error_DESC">Tuntematon virhe.</string>
|
<string name="unknown_error_DESC">Tuntematon virhe.</string>
|
||||||
<string name="unknown_FORMAT">Tuntematon: %s</string>
|
<string name="unknown_FORMAT">Tuntematon: %s</string>
|
||||||
<string name="unsigned">Merkitsemätön</string>
|
<string name="unsigned">Allekirjoittamaton</string>
|
||||||
<string name="unstable_updates">Epävakaat päivitykset</string>
|
<string name="unstable_updates">Epävakaat päivitykset</string>
|
||||||
<string name="unverified">Vahvistamaton</string>
|
<string name="unverified">Vahvistamaton</string>
|
||||||
<string name="upstream_source_code_is_not_free">Alkuperäinen lähdekoodi ei ole vapaa</string>
|
<string name="upstream_source_code_is_not_free">Alkuperäinen lähdekoodi ei ole vapaa</string>
|
||||||
<string name="website">Verkkosivut</string>
|
<string name="website">Verkkosivut</string>
|
||||||
<string name="whats_new">Mitä uutta</string>
|
<string name="whats_new">Mitä uutta</string>
|
||||||
<string name="waiting_to_start_download">Odotan latauksen aloittamista…</string>
|
<string name="waiting_to_start_download">Odotetaan latauksen aloittamista…</string>
|
||||||
<string name="username_missing">Käyttäjätunnus puuttuu</string>
|
<string name="username_missing">Käyttäjätunnus puuttuu</string>
|
||||||
<string name="username">Käyttäjänimi</string>
|
<string name="username">Käyttäjänimi</string>
|
||||||
<string name="signature_FORMAT">Allekirjoitus %s</string>
|
<string name="signature_FORMAT">Allekirjoitus %s</string>
|
||||||
@ -141,12 +141,12 @@
|
|||||||
<string name="size">Koko</string>
|
<string name="size">Koko</string>
|
||||||
<string name="skip">Ohita</string>
|
<string name="skip">Ohita</string>
|
||||||
<string name="socks_proxy">SOCKS-välityspalvelin</string>
|
<string name="socks_proxy">SOCKS-välityspalvelin</string>
|
||||||
<string name="sorting_order">Lajittelu järjestys</string>
|
<string name="sorting_order">Lajittelujärjestys</string>
|
||||||
<string name="source_code">Lähdekoodi</string>
|
<string name="source_code">Lähdekoodi</string>
|
||||||
<string name="source_code_no_longer_available">Lähdekoodi ei enää saatavilla</string>
|
<string name="source_code_no_longer_available">Lähdekoodi ei enää saatavilla</string>
|
||||||
<string name="suggested">Suositeltu</string>
|
<string name="suggested">Suositeltu</string>
|
||||||
<string name="sync_repositories">Synkronoi arkistot</string>
|
<string name="sync_repositories">Synkronoi tietovarastot</string>
|
||||||
<string name="sync_repositories_automatically">Synkronoi arkistot automaattisesti</string>
|
<string name="sync_repositories_automatically">Synkronoi tietovarastot automaattisesti</string>
|
||||||
<string name="syncing">Synkronointi</string>
|
<string name="syncing">Synkronointi</string>
|
||||||
<string name="syncing_FORMAT">Synkronoidaan %s…</string>
|
<string name="syncing_FORMAT">Synkronoidaan %s…</string>
|
||||||
<string name="themes">Teemat</string>
|
<string name="themes">Teemat</string>
|
||||||
@ -157,13 +157,13 @@
|
|||||||
<string name="update">Päivitys</string>
|
<string name="update">Päivitys</string>
|
||||||
<string name="updates">Päivitykset</string>
|
<string name="updates">Päivitykset</string>
|
||||||
<string name="promotes_non_free_software">Edistää ei-vapaita ohjelmia</string>
|
<string name="promotes_non_free_software">Edistää ei-vapaita ohjelmia</string>
|
||||||
<string name="repository_not_used_DESC">Tätä arkistoa ei ole vielä käytetty. Ota se käyttöön nähdäksesi siinä olevat sovellukset.</string>
|
<string name="repository_not_used_DESC">Tätä tietovarastoa ei ole vielä käytetty. Ota se käyttöön nähdäksesi siinä olevat sovellukset.</string>
|
||||||
<string name="proxy">Proxy</string>
|
<string name="proxy">Välityspalvelin</string>
|
||||||
<string name="repository_unsigned_DESC">Allekirjoittamaton. Sovellusluetteloa ei voitu tarkistaa. Ole varovainen ladatessasi sovelluksia allekirjoittamattomista arkistoista.</string>
|
<string name="repository_unsigned_DESC">Allekirjoittamaton. Sovellusluetteloa ei voitu varmistaa. Ole varovainen ladatessasi sovelluksia allekirjoittamattomista tietovarastoista.</string>
|
||||||
<string name="version_FORMAT">Versio %s</string>
|
<string name="version_FORMAT">Versio %s</string>
|
||||||
<string name="validation_index_error_DESC">Indeksiä ei voitu validoida.</string>
|
<string name="validation_index_error_DESC">Indeksiä ei voitu validoida.</string>
|
||||||
<string name="unstable_updates_summary">Ehdota epävakaiden versioiden asentamista</string>
|
<string name="unstable_updates_summary">Ehdota epävakaiden versioiden asentamista</string>
|
||||||
<string name="incompatible_signature_DESC">Tämä versio on allekirjoitettu eri varmenteella kuin laitteeseesi asennettu varmenne. Poista se ensin.</string>
|
<string name="incompatible_signature_DESC">Tämä versio on allekirjoitettu eri varmenteella kuin laitteellesi asennettu versio. Poista se ensin.</string>
|
||||||
<string name="incompatible_platforms_DESC_FORMAT">Alustasi %1$s ei ole tuettu. Tuetut alustat: %2$s.</string>
|
<string name="incompatible_platforms_DESC_FORMAT">Alustasi %1$s ei ole tuettu. Tuetut alustat: %2$s.</string>
|
||||||
<string name="versions">Versiot</string>
|
<string name="versions">Versiot</string>
|
||||||
<string name="version">Versio</string>
|
<string name="version">Versio</string>
|
||||||
|
@ -177,4 +177,5 @@
|
|||||||
<string name="latest">Nouveautés</string>
|
<string name="latest">Nouveautés</string>
|
||||||
<string name="new_applications">Nouvelles applications</string>
|
<string name="new_applications">Nouvelles applications</string>
|
||||||
<string name="update_all">Tout mettre à jour</string>
|
<string name="update_all">Tout mettre à jour</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Uniquement en Wi-Fi et branché</string>
|
||||||
</resources>
|
</resources>
|
180
src/main/res/values-in/strings.xml
Normal file
180
src/main/res/values-in/strings.xml
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="bug_tracker">Pelacak bug</string>
|
||||||
|
<string name="incompatible_platforms_DESC_FORMAT">Platform %1$s Anda tidak didukung. Platform yang didukung: %2$s.</string>
|
||||||
|
<string name="all_applications">Semua aplikasi</string>
|
||||||
|
<string name="delete">Hapus</string>
|
||||||
|
<string name="anti_features">Anti-fitur</string>
|
||||||
|
<string name="author_website">Situs pembuat</string>
|
||||||
|
<string name="available">Tersedia</string>
|
||||||
|
<string name="cancel">Batal</string>
|
||||||
|
<string name="cant_edit_sync_DESC">Tidak dapat mengedit repositori karena sedang mensinkronisasi.</string>
|
||||||
|
<string name="changelog">Daftar perubahan</string>
|
||||||
|
<string name="changes">Perubahan</string>
|
||||||
|
<string name="checking_repository">Memeriksa repositori…</string>
|
||||||
|
<string name="connecting">Menghubungkan…</string>
|
||||||
|
<string name="contains_non_free_media">Mengandung media non-bebas</string>
|
||||||
|
<string name="could_not_sync_FORMAT">Tidak dapat mensinkronisasi %s</string>
|
||||||
|
<string name="could_not_validate_FORMAT">Tidak dapat memvalidasi %s</string>
|
||||||
|
<string name="credits">Kredit</string>
|
||||||
|
<string name="description">Deskripsi</string>
|
||||||
|
<string name="details">Detail</string>
|
||||||
|
<string name="donate">Donasi</string>
|
||||||
|
<string name="downloaded_FORMAT">%s telah diunduh</string>
|
||||||
|
<string name="downloading_FORMAT">Mengunduh %s…</string>
|
||||||
|
<string name="edit_repository">Ubah repositori</string>
|
||||||
|
<string name="file_format_error_DESC">Format berkas tidak valid.</string>
|
||||||
|
<string name="fingerprint">Sidik</string>
|
||||||
|
<string name="has_advertising">Ada iklan</string>
|
||||||
|
<string name="has_security_vulnerabilities">Ada celah keamanan</string>
|
||||||
|
<string name="http_error_DESC">Respon server tidak valid.</string>
|
||||||
|
<string name="ignore_all_updates">Abaikan semua versi baru</string>
|
||||||
|
<string name="ignore_this_update">Abaikan versi ini</string>
|
||||||
|
<string name="incompatible_api_DESC_FORMAT">%1$s Anda (versi API %2$d) tidak didukung. %3$s</string>
|
||||||
|
<string name="incompatible_api_max_DESC_FORMAT">Versi API maksimal %d.</string>
|
||||||
|
<string name="incompatible_api_min_DESC_FORMAT">Versi API minimal %d.</string>
|
||||||
|
<string name="incompatible_features_DESC">Fitur yang hilang.</string>
|
||||||
|
<string name="incompatible_older_DESC">Versi ini lebih tua dari yang ter-install di perangkat Anda. Uninstall terlebih dahulu.</string>
|
||||||
|
<string name="incompatible_version">Versi yang tidak cocok</string>
|
||||||
|
<string name="incompatible_versions">Versi yang tidak cocok</string>
|
||||||
|
<string name="install_types">Tipe Instalasi</string>
|
||||||
|
<string name="installed">Ter-install</string>
|
||||||
|
<string name="integrity_check_error_DESC">Tidak dapat mengecek integritas.</string>
|
||||||
|
<string name="invalid_address">Alamat tidak valid</string>
|
||||||
|
<string name="list_animation_description">Perlihatkan animasi list di halaman utama</string>
|
||||||
|
<string name="merging_FORMAT">Menggabungkan %s</string>
|
||||||
|
<string name="name">Nama</string>
|
||||||
|
<string name="never">Tidak pernah</string>
|
||||||
|
<string name="no_proxy">Tanpa proxy</string>
|
||||||
|
<string name="notify_about_updates">Beritahu tentang versi baru aplikasi</string>
|
||||||
|
<string name="only_compatible_with_FORMAT">Hanya kompatibel dengan %s</string>
|
||||||
|
<string name="only_on_wifi">Hanya di jaringan Wi-Fi</string>
|
||||||
|
<string name="password">Sandi</string>
|
||||||
|
<string name="settings">Pengaturan</string>
|
||||||
|
<string name="project_website">Situs projek</string>
|
||||||
|
<string name="provided_by_FORMAT">Disediakan oleh %s</string>
|
||||||
|
<string name="sync_repositories_automatically">Sinkronisasi repositori otomatis</string>
|
||||||
|
<string name="syncing">Mensinkronisasi</string>
|
||||||
|
<string name="system">Sistem</string>
|
||||||
|
<string name="tap_to_install_DESC">Tap untuk menginstall.</string>
|
||||||
|
<string name="target">Target</string>
|
||||||
|
<string name="theme">Tema</string>
|
||||||
|
<string name="themes">Tema</string>
|
||||||
|
<string name="tracks_or_reports_your_activity">Merekam atau melaporkan aktivitas Anda</string>
|
||||||
|
<string name="uninstall">Uninstall</string>
|
||||||
|
<string name="unknown_error_DESC">Galat.</string>
|
||||||
|
<string name="unstable_updates_summary">Sarankan menginstall versi tidak stabil</string>
|
||||||
|
<string name="update">Pembaruan</string>
|
||||||
|
<string name="updates">Pembaruan</string>
|
||||||
|
<string name="waiting_to_start_download">Menunggu mulai unduhan…</string>
|
||||||
|
<string name="explore">Jelajahi</string>
|
||||||
|
<string name="update_all">Perbaharui semua</string>
|
||||||
|
<string name="sort_filter">Urutkan & Saring</string>
|
||||||
|
<string name="action_failed">Aksi gagal</string>
|
||||||
|
<string name="add_repository">Tambah repositori</string>
|
||||||
|
<string name="all_applications_up_to_date">Semua aplikasi Anda terbaharukan</string>
|
||||||
|
<string name="always">Selalu</string>
|
||||||
|
<string name="author_email">E-mail pembuat</string>
|
||||||
|
<string name="compiled_for_debugging">Dikompilasi untuk debugging</string>
|
||||||
|
<string name="address">Alamat</string>
|
||||||
|
<string name="already_exists">Sudah ada</string>
|
||||||
|
<string name="amoled">Hitam</string>
|
||||||
|
<string name="application_not_found">Tidak dapat menemukan aplikasi itu</string>
|
||||||
|
<string name="application">Aplikasi</string>
|
||||||
|
<string name="confirmation">Konfirmasi</string>
|
||||||
|
<string name="could_not_download_FORMAT">Tidak dapat mengunduh %s</string>
|
||||||
|
<string name="delete_repository_DESC">Hapus repositori\?</string>
|
||||||
|
<string name="has_non_free_dependencies">Ada dependensi non-bebas</string>
|
||||||
|
<string name="incompatible_signature_DESC">Versi ini ditandatangani dengan sertifikat yang berbeda dari yang ter-install di perangkat Anda. Uninstall terlebih dahulu.</string>
|
||||||
|
<string name="install">Install</string>
|
||||||
|
<string name="no_applications_installed">Tidak ada aplikasi yang ter-install</string>
|
||||||
|
<string name="dark">Gelap</string>
|
||||||
|
<string name="downloading">Mengunduh</string>
|
||||||
|
<string name="http_proxy">HTTP proxy</string>
|
||||||
|
<string name="incompatible_with_FORMAT">Tidak cocok dengan %s</string>
|
||||||
|
<string name="incompatible_versions_summary">Perlihatkan versi aplikasi yang tidak cocok dengan perangkat</string>
|
||||||
|
<string name="invalid_fingerprint_format">Format sidik tidak valid</string>
|
||||||
|
<string name="parsing_index_error_DESC">Tidak dapat mengurai berkas indeks.</string>
|
||||||
|
<string name="invalid_metadata_error_DESC">Metadata tidak valid.</string>
|
||||||
|
<string name="invalid_permissions_error_DESC">Perizinan tidak valid.</string>
|
||||||
|
<string name="processing_FORMAT">Memproses %1$s…</string>
|
||||||
|
<string name="invalid_signature_error_DESC">Tandatangan tidak valid.</string>
|
||||||
|
<string name="license_FORMAT">Lisensi %s</string>
|
||||||
|
<string name="list_animation">Animasi List</string>
|
||||||
|
<plurals name="new_updates_DESC_FORMAT">
|
||||||
|
<item quantity="other">%d aplikasi dengan versi baru.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="invalid_username_format">Format username tidak valid</string>
|
||||||
|
<string name="launch">Jalankan</string>
|
||||||
|
<string name="license">Lisensi</string>
|
||||||
|
<string name="link_copied_to_clipboard">Tautan disalin ke clipboard</string>
|
||||||
|
<string name="links">Tautan</string>
|
||||||
|
<string name="light">Terang</string>
|
||||||
|
<string name="network_error_DESC">Galat jaringan.</string>
|
||||||
|
<string name="new_updates_available">Versi aplikasi baru tersedia</string>
|
||||||
|
<string name="no_applications_available">Tidak ada aplikasi yang tersedia</string>
|
||||||
|
<string name="notify_about_updates_summary">Perlihatkan notifikasi saat versi baru tersedia</string>
|
||||||
|
<string name="number_of_applications">Jumlah aplikasi</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="password_missing">Sandi tidak ditemukan</string>
|
||||||
|
<string name="permissions">Perizinan</string>
|
||||||
|
<string name="socks_proxy">Proxy SOCKS</string>
|
||||||
|
<string name="no_description_available_DESC">Tidak ada deskripsi.</string>
|
||||||
|
<string name="no_matching_applications_found">Tidak dapat menemukan aplikasi tersebut</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Hanya di jaringan Wi-Fi dan terhubung ke listrik</string>
|
||||||
|
<string name="open_DESC_FORMAT">Buka %s\?</string>
|
||||||
|
<string name="other">Lainnya</string>
|
||||||
|
<string name="plus_more_FORMAT">+%d lebih banyak</string>
|
||||||
|
<string name="proxy_port">Port proxy</string>
|
||||||
|
<string name="repository">Repositori</string>
|
||||||
|
<string name="promotes_non_free_software">Mempromosikan software non-bebas</string>
|
||||||
|
<string name="proxy">Proxy</string>
|
||||||
|
<string name="promotes_non_free_network_services">Mempromosikan situs non-bebas</string>
|
||||||
|
<string name="proxy_type">Jenis proxy</string>
|
||||||
|
<string name="proxy_host">Host proxy</string>
|
||||||
|
<string name="show_older_versions">Perlihatkan versi yang lebih tua</string>
|
||||||
|
<string name="recently_updated">Diperbaharui akhir-akhir ini</string>
|
||||||
|
<string name="repositories">Repositori</string>
|
||||||
|
<string name="root_permission_description">Perbolehkan perizinan root untuk instalasi senyap</string>
|
||||||
|
<string name="repository_unsigned_DESC">Tidak ditandatangani. Tidak dapat memverifikasi daftar aplikasi. Hati-hati mengunduh aplikasi dari repositori yang tidak ditandatangani.</string>
|
||||||
|
<string name="saving_details">Menyimpan detail…</string>
|
||||||
|
<string name="select_mirror">Pilih mirror</string>
|
||||||
|
<string name="sorting_order">Pengurutan</string>
|
||||||
|
<string name="source_code">Kode sumber</string>
|
||||||
|
<string name="username">Username</string>
|
||||||
|
<string name="repository_not_used_DESC">Repositori ini belum dipakai. Aktifkan untuk melihat aplikasi di dalamnya.</string>
|
||||||
|
<string name="requires_FORMAT">Membutuhkan %s</string>
|
||||||
|
<string name="save">Simpan</string>
|
||||||
|
<string name="search">Cari</string>
|
||||||
|
<string name="root_permission">Instalasi Senyap</string>
|
||||||
|
<string name="screenshots">Tangkapan layar</string>
|
||||||
|
<string name="skip">Lewati</string>
|
||||||
|
<string name="source_code_no_longer_available">Kode sumber sudah tidak tersedia</string>
|
||||||
|
<string name="suggested">Disarankan</string>
|
||||||
|
<string name="share">Bagikan</string>
|
||||||
|
<string name="show_more">Perlihatkan lebih banyak</string>
|
||||||
|
<string name="signature_FORMAT">Tandatangan %s</string>
|
||||||
|
<string name="signed_using_unsafe_algorithm">Ditandatangani dengan algoritma yang tidak aman</string>
|
||||||
|
<string name="size">Ukuran</string>
|
||||||
|
<string name="version">Versi</string>
|
||||||
|
<string name="sync_repositories">Sinkronisasi repositori</string>
|
||||||
|
<string name="syncing_FORMAT">Mensinkronisasi %s…</string>
|
||||||
|
<string name="unsigned">Tidak ditandatangani</string>
|
||||||
|
<string name="unstable_updates">Pembaruan tidak stabil</string>
|
||||||
|
<string name="upstream_source_code_is_not_free">Kode sumber tidak bebas</string>
|
||||||
|
<string name="unverified">Tidak terverifikasi</string>
|
||||||
|
<string name="validation_index_error_DESC">Indeks tidak dapat divalidasi.</string>
|
||||||
|
<string name="unknown">Tidak diketahui</string>
|
||||||
|
<string name="unknown_FORMAT">Tidak diketahui: %s</string>
|
||||||
|
<string name="username_missing">Username tidak ada</string>
|
||||||
|
<string name="versions">Versi</string>
|
||||||
|
<string name="show_less">Perlihatkan Lebih Sedikit</string>
|
||||||
|
<string name="latest">Terbaru</string>
|
||||||
|
<string name="version_FORMAT">Versi %s</string>
|
||||||
|
<string name="website">Situs</string>
|
||||||
|
<string name="prefs_language_title">Bahasa</string>
|
||||||
|
<string name="prefs_personalization">Personalisasi</string>
|
||||||
|
<string name="whats_new">Apa yang Baru</string>
|
||||||
|
<string name="installed_applications">Aplikasi ter-install</string>
|
||||||
|
<string name="new_applications">Aplikasi baru</string>
|
||||||
|
</resources>
|
@ -45,7 +45,7 @@
|
|||||||
<string name="has_security_vulnerabilities">Contiene vulnerabilità</string>
|
<string name="has_security_vulnerabilities">Contiene vulnerabilità</string>
|
||||||
<string name="http_error_DESC">Risposta del server non valida.</string>
|
<string name="http_error_DESC">Risposta del server non valida.</string>
|
||||||
<string name="http_proxy">Proxy HTTP</string>
|
<string name="http_proxy">Proxy HTTP</string>
|
||||||
<string name="ignore_all_updates">Ignora tuttie le nuove versioni</string>
|
<string name="ignore_all_updates">Ignora tutte le nuove versioni</string>
|
||||||
<string name="ignore_this_update">Ignora questa versione</string>
|
<string name="ignore_this_update">Ignora questa versione</string>
|
||||||
<string name="incompatible_api_DESC_FORMAT">Il tuo %1$s (API version %2$d) non è supportato. %3$s</string>
|
<string name="incompatible_api_DESC_FORMAT">Il tuo %1$s (API version %2$d) non è supportato. %3$s</string>
|
||||||
<string name="incompatible_api_max_DESC_FORMAT">La versione massima API è %d.</string>
|
<string name="incompatible_api_max_DESC_FORMAT">La versione massima API è %d.</string>
|
||||||
@ -180,4 +180,5 @@
|
|||||||
<string name="latest">Più recenti</string>
|
<string name="latest">Più recenti</string>
|
||||||
<string name="sort_filter">Ordina e filtra</string>
|
<string name="sort_filter">Ordina e filtra</string>
|
||||||
<string name="new_applications">Nuove applicazioni</string>
|
<string name="new_applications">Nuove applicazioni</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Solo su Wi-Fi e Plugged-In</string>
|
||||||
</resources>
|
</resources>
|
173
src/main/res/values-ja/strings.xml
Normal file
173
src/main/res/values-ja/strings.xml
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="author_website">作者のウェブサイト</string>
|
||||||
|
<string name="action_failed">操作に失敗しました</string>
|
||||||
|
<string name="add_repository">リポジトリ追加</string>
|
||||||
|
<string name="all_applications_up_to_date">すべてのアプリが最新です</string>
|
||||||
|
<string name="already_exists">既に存在しています</string>
|
||||||
|
<string name="always">常に</string>
|
||||||
|
<string name="amoled">ブラック</string>
|
||||||
|
<string name="anti_features">好ましくない可能性のある機能</string>
|
||||||
|
<string name="application">アプリ</string>
|
||||||
|
<string name="available">利用可能</string>
|
||||||
|
<string name="bug_tracker">バグトラッカー</string>
|
||||||
|
<string name="cant_edit_sync_DESC">現在同期中のため,リポジトリを編集できません。</string>
|
||||||
|
<string name="changes">変更内容</string>
|
||||||
|
<string name="confirmation">確認</string>
|
||||||
|
<string name="connecting">接続中…</string>
|
||||||
|
<string name="contains_non_free_media">非フリーのメディアを含みます</string>
|
||||||
|
<string name="could_not_download_FORMAT">%s をダウンロードできませんでした</string>
|
||||||
|
<string name="could_not_sync_FORMAT">%s を同期できませんでした</string>
|
||||||
|
<string name="could_not_validate_FORMAT">%s を確認できませんでした</string>
|
||||||
|
<string name="credits">クレジット</string>
|
||||||
|
<string name="dark">ダーク</string>
|
||||||
|
<string name="delete_repository_DESC">リポジトリを削除しますか\?</string>
|
||||||
|
<string name="description">説明</string>
|
||||||
|
<string name="details">詳細</string>
|
||||||
|
<string name="downloaded_FORMAT">%s をダウンロードしました</string>
|
||||||
|
<string name="downloading">ダウンロード中</string>
|
||||||
|
<string name="downloading_FORMAT">%s をダウンロード中…</string>
|
||||||
|
<string name="file_format_error_DESC">無効なファイル形式です。</string>
|
||||||
|
<string name="fingerprint">電子指紋</string>
|
||||||
|
<string name="has_non_free_dependencies">非フリーな依存関係を含みます</string>
|
||||||
|
<string name="has_security_vulnerabilities">セキュリティ上の危険性を含みます</string>
|
||||||
|
<string name="http_error_DESC">無効なサーバー応答です。</string>
|
||||||
|
<string name="http_proxy">HTTPプロキシ</string>
|
||||||
|
<string name="incompatible_features_DESC">不足している機能。</string>
|
||||||
|
<string name="incompatible_api_max_DESC_FORMAT">最大のAPIバージョンは %d です。</string>
|
||||||
|
<string name="incompatible_api_min_DESC_FORMAT">最小のAPIバージョンは %d です。</string>
|
||||||
|
<string name="incompatible_version">互換性のないバージョン</string>
|
||||||
|
<string name="incompatible_versions">互換性のないバージョン</string>
|
||||||
|
<string name="install">インストール</string>
|
||||||
|
<string name="invalid_address">無効なアドレス</string>
|
||||||
|
<string name="invalid_fingerprint_format">無効な電子指紋形式</string>
|
||||||
|
<string name="invalid_permissions_error_DESC">無効なパーミッションです。</string>
|
||||||
|
<string name="invalid_metadata_error_DESC">無効なメタデータです。</string>
|
||||||
|
<string name="invalid_signature_error_DESC">無効な署名です。</string>
|
||||||
|
<string name="invalid_username_format">無効なユーザー名形式</string>
|
||||||
|
<string name="license">ライセンス</string>
|
||||||
|
<string name="light">ライト</string>
|
||||||
|
<string name="links">リンク</string>
|
||||||
|
<string name="list_animation">リストアニメーション</string>
|
||||||
|
<string name="list_animation_description">メインページにリストアニメーションを表示</string>
|
||||||
|
<string name="merging_FORMAT">%s をマージ</string>
|
||||||
|
<string name="name">名前</string>
|
||||||
|
<string name="network_error_DESC">ネットワーク エラー。</string>
|
||||||
|
<string name="never">なし</string>
|
||||||
|
<string name="new_updates_available">アプリの新バージョンが利用可能</string>
|
||||||
|
<string name="no_applications_available">利用可能なアプリはありません</string>
|
||||||
|
<string name="no_applications_installed">インストール済みのアプリはありません</string>
|
||||||
|
<string name="no_description_available_DESC">説明がありません。</string>
|
||||||
|
<string name="no_proxy">プロキシなし</string>
|
||||||
|
<string name="notify_about_updates">アプリの新バージョンを通知</string>
|
||||||
|
<string name="number_of_applications">アプリの数</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="only_on_wifi">Wi-Fiのみ</string>
|
||||||
|
<string name="open_DESC_FORMAT">%s を開きますか\?</string>
|
||||||
|
<string name="other">その他</string>
|
||||||
|
<string name="parsing_index_error_DESC">インデックスファイルを解析できませんでした。</string>
|
||||||
|
<string name="password">パスワード</string>
|
||||||
|
<string name="password_missing">パスワードがありません</string>
|
||||||
|
<string name="plus_more_FORMAT">+ %d 詳細</string>
|
||||||
|
<string name="processing_FORMAT">%1$s を処理中…</string>
|
||||||
|
<string name="promotes_non_free_network_services">非フリーなネットワークサービスを推奨</string>
|
||||||
|
<string name="promotes_non_free_software">非フリーなソフトウェアを推奨</string>
|
||||||
|
<string name="provided_by_FORMAT">%s 提供</string>
|
||||||
|
<string name="proxy_host">プロキシホスト</string>
|
||||||
|
<string name="proxy_port">プロキシポート</string>
|
||||||
|
<string name="proxy_type">プロキシタイプ</string>
|
||||||
|
<string name="recently_updated">新規更新</string>
|
||||||
|
<string name="repositories">リポジトリ</string>
|
||||||
|
<string name="address">アドレス</string>
|
||||||
|
<string name="all_applications">すべてのアプリ</string>
|
||||||
|
<string name="application_not_found">アプリを発見できませんでした</string>
|
||||||
|
<string name="cancel">キャンセル</string>
|
||||||
|
<string name="edit_repository">リポジトリを編集</string>
|
||||||
|
<string name="author_email">作者のメールアドレス</string>
|
||||||
|
<string name="changelog">更新履歴</string>
|
||||||
|
<string name="checking_repository">リポジトリのチェック中です…</string>
|
||||||
|
<string name="delete">削除</string>
|
||||||
|
<string name="donate">寄付</string>
|
||||||
|
<string name="compiled_for_debugging">デバッグ用にコンパイル済み</string>
|
||||||
|
<string name="has_advertising">広告を含みます</string>
|
||||||
|
<string name="ignore_this_update">このバージョンを無視</string>
|
||||||
|
<string name="ignore_all_updates">すべての新バージョンを無視</string>
|
||||||
|
<string name="incompatible_older_DESC">これはあなたのデバイスにインストールされているバージョンより古いバージョンです。先にアンインストールしてください。</string>
|
||||||
|
<string name="incompatible_api_DESC_FORMAT">お使いの %1$s (APIバージョン %2$d) はサポートされていません。%3$s</string>
|
||||||
|
<string name="installed">インストール済み</string>
|
||||||
|
<string name="integrity_check_error_DESC">整合性を確認できませんでした。</string>
|
||||||
|
<string name="incompatible_platforms_DESC_FORMAT">あなたの %1$s プラットフォームはサポート外です。サポートされているプラットフォーム: %2$s 。</string>
|
||||||
|
<string name="incompatible_signature_DESC">このバージョンは,デバイスにインストールされているアプリの証明書とは異なる証明書で署名されています。先にアンインストールしてください。</string>
|
||||||
|
<string name="incompatible_versions_summary">デバイスと互換性のないアプリケーションのバージョンを表示</string>
|
||||||
|
<string name="incompatible_with_FORMAT">%s とは互換性がありません</string>
|
||||||
|
<string name="link_copied_to_clipboard">リンクがクリップボードにコピーされました</string>
|
||||||
|
<plurals name="new_updates_DESC_FORMAT">
|
||||||
|
<item quantity="other">%d 個のアプリで新バージョンが利用可能です。</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="notify_about_updates_summary">新しいバージョンが利用可能なときに通知を表示します</string>
|
||||||
|
<string name="permissions">権限</string>
|
||||||
|
<string name="install_types">インストールの種類</string>
|
||||||
|
<string name="license_FORMAT">%s ライセンス</string>
|
||||||
|
<string name="launch">起動</string>
|
||||||
|
<string name="only_compatible_with_FORMAT">%s とのみ互換性があります</string>
|
||||||
|
<string name="no_matching_applications_found">そのようなアプリは見つかりませんでした</string>
|
||||||
|
<string name="settings">設定</string>
|
||||||
|
<string name="project_website">プロジェクトのウェブサイト</string>
|
||||||
|
<string name="proxy">プロキシ</string>
|
||||||
|
<string name="repository">リポジトリ</string>
|
||||||
|
<string name="repository_not_used_DESC">このリポジトリは未だ使用されていません。オンにすると、アプリケーションを見ることができます。</string>
|
||||||
|
<string name="syncing">同期中</string>
|
||||||
|
<string name="unknown">不明</string>
|
||||||
|
<string name="unknown_error_DESC">不明なエラーです。</string>
|
||||||
|
<string name="username">ユーザーネーム</string>
|
||||||
|
<string name="upstream_source_code_is_not_free">上流のソースコードは非フリーです</string>
|
||||||
|
<string name="installed_applications">インストール済みのアプリケーション</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Wi-Fi接続時と充電時のみ</string>
|
||||||
|
<string name="root_permission">サイレントインストール</string>
|
||||||
|
<string name="show_older_versions">古いバージョンを見る</string>
|
||||||
|
<string name="signed_using_unsafe_algorithm">安全ではないアルゴリズムで署名されています</string>
|
||||||
|
<string name="size">サイズ</string>
|
||||||
|
<string name="skip">スキップ</string>
|
||||||
|
<string name="sorting_order">並び変え</string>
|
||||||
|
<string name="source_code">ソースコード</string>
|
||||||
|
<string name="source_code_no_longer_available">ソースコードは使用できません</string>
|
||||||
|
<string name="suggested">提案</string>
|
||||||
|
<string name="sync_repositories">リポジトリを同期</string>
|
||||||
|
<string name="sync_repositories_automatically">自動的にリポジトリを同期</string>
|
||||||
|
<string name="system">システム</string>
|
||||||
|
<string name="tap_to_install_DESC">タップしてインストールします。</string>
|
||||||
|
<string name="tracks_or_reports_your_activity">あなたのアクティビティを追跡、報告します</string>
|
||||||
|
<string name="uninstall">アンインストール</string>
|
||||||
|
<string name="unstable_updates">不安定なアップデート</string>
|
||||||
|
<string name="website">ウェブサイト</string>
|
||||||
|
<string name="prefs_language_title">言語</string>
|
||||||
|
<string name="show_less">表示を減らす</string>
|
||||||
|
<string name="latest">最新</string>
|
||||||
|
<string name="update_all">全てをアップデート</string>
|
||||||
|
<string name="new_applications">新しいアプリケーション</string>
|
||||||
|
<string name="root_permission_description">サイレントインストールのためにスーパーユーザー権限を許可</string>
|
||||||
|
<string name="share">共有</string>
|
||||||
|
<string name="show_more">もっと見る</string>
|
||||||
|
<string name="update">アップデート</string>
|
||||||
|
<string name="unstable_updates_summary">不安定なバージョンのインストールを提案する</string>
|
||||||
|
<string name="version">バージョン</string>
|
||||||
|
<string name="waiting_to_start_download">ダウンロード開始を待っています…</string>
|
||||||
|
<string name="save">保存</string>
|
||||||
|
<string name="theme">テーマ</string>
|
||||||
|
<string name="screenshots">スクリーンショット</string>
|
||||||
|
<string name="select_mirror">ミラーを選択</string>
|
||||||
|
<string name="search">検索</string>
|
||||||
|
<string name="validation_index_error_DESC">インデックスが検証できません。</string>
|
||||||
|
<string name="socks_proxy">SOCKSプロキシ</string>
|
||||||
|
<string name="username_missing">ユーザーネームがありません</string>
|
||||||
|
<string name="sort_filter">並べ替えとフィルター</string>
|
||||||
|
<string name="saving_details">詳細を保存しています…</string>
|
||||||
|
<string name="signature_FORMAT">署名 %s</string>
|
||||||
|
<string name="syncing_FORMAT">%sを同期中…</string>
|
||||||
|
<string name="unsigned">署名なし</string>
|
||||||
|
<string name="updates">アップデート</string>
|
||||||
|
<string name="whats_new">新機能</string>
|
||||||
|
<string name="versions">バージョン</string>
|
||||||
|
<string name="unverified">未確認</string>
|
||||||
|
<string name="themes">テーマ</string>
|
||||||
|
</resources>
|
@ -2,18 +2,18 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="already_exists">Finnes allerede</string>
|
<string name="already_exists">Finnes allerede</string>
|
||||||
<string name="always">Alltid</string>
|
<string name="always">Alltid</string>
|
||||||
<string name="amoled">Svart</string>
|
<string name="amoled">Amoled</string>
|
||||||
<string name="author_email">Forfatter e-post</string>
|
<string name="author_email">Utviklerens e-postadresse</string>
|
||||||
<string name="author_website">Utviklerens nettside</string>
|
<string name="author_website">Utviklerens nettside</string>
|
||||||
<string name="changelog">Endringslogg</string>
|
<string name="changelog">Endringslogg</string>
|
||||||
<string name="cant_edit_sync_DESC">Kan ikke redigere pakkebrønnen siden den synkroniseres akkurat nå.</string>
|
<string name="cant_edit_sync_DESC">Kan ikke redigere pakkebrønnen siden den synkroniseres akkurat nå.</string>
|
||||||
<string name="checking_repository">Kontrollerer depotet…</string>
|
<string name="checking_repository">Sjekker pakkebrønn</string>
|
||||||
<string name="compiled_for_debugging">Kompilert for avlusing</string>
|
<string name="compiled_for_debugging">Kompilert for avlusing</string>
|
||||||
<string name="confirmation">Bekreftelse</string>
|
<string name="confirmation">Bekreftelse</string>
|
||||||
<string name="could_not_download_FORMAT">Kunne ikke laste ned %s</string>
|
<string name="could_not_download_FORMAT">Kunne ikke laste ned %s</string>
|
||||||
<string name="connecting">Kobler til…</string>
|
<string name="connecting">Kobler til</string>
|
||||||
<string name="contains_non_free_media">Inneholder ufri media</string>
|
<string name="contains_non_free_media">Inneholder ufri media</string>
|
||||||
<string name="delete_repository_DESC">Slett depotet\?</string>
|
<string name="delete_repository_DESC">Vil du slette pakkebrønnen\?</string>
|
||||||
<string name="downloaded_FORMAT">Lastet ned %s</string>
|
<string name="downloaded_FORMAT">Lastet ned %s</string>
|
||||||
<string name="downloading">Laster ned</string>
|
<string name="downloading">Laster ned</string>
|
||||||
<string name="fingerprint">Fingeravtrykk</string>
|
<string name="fingerprint">Fingeravtrykk</string>
|
||||||
@ -36,13 +36,13 @@
|
|||||||
<string name="license_FORMAT">%s-lisens</string>
|
<string name="license_FORMAT">%s-lisens</string>
|
||||||
<string name="link_copied_to_clipboard">Lenke kopiert til utklippstavle</string>
|
<string name="link_copied_to_clipboard">Lenke kopiert til utklippstavle</string>
|
||||||
<string name="list_animation">Listeanimasjoner</string>
|
<string name="list_animation">Listeanimasjoner</string>
|
||||||
<string name="list_animation_description">Vis liste animasjon på hovedsiden</string>
|
<string name="list_animation_description">Skru på listeanimasjoner på hovedsiden</string>
|
||||||
<string name="new_updates_available">Nye versjoner av programmer tilgjengelig</string>
|
<string name="new_updates_available">Nye versjoner av programmer tilgjengelig</string>
|
||||||
<string name="no_applications_available">Ingen programmer tilgjengelig</string>
|
<string name="no_applications_available">Ingen programmer tilgjengelige</string>
|
||||||
<string name="no_applications_installed">Ingen programmer installert</string>
|
<string name="no_applications_installed">Ingen programmer installert</string>
|
||||||
<string name="no_description_available_DESC">Ingen beskrivelse tilgjengelig.</string>
|
<string name="no_description_available_DESC">Ingen beskrivelse tilgjengelig.</string>
|
||||||
<string name="open_DESC_FORMAT">Åpne %s\?</string>
|
<string name="open_DESC_FORMAT">Åpne %s\?</string>
|
||||||
<string name="no_matching_applications_found">Fant ingen slike programmer</string>
|
<string name="no_matching_applications_found">Fant ingen samsvarende programmer</string>
|
||||||
<string name="notify_about_updates">Gi merknad om nye versjoner av programmer</string>
|
<string name="notify_about_updates">Gi merknad om nye versjoner av programmer</string>
|
||||||
<string name="number_of_applications">Antall programmer</string>
|
<string name="number_of_applications">Antall programmer</string>
|
||||||
<string name="only_compatible_with_FORMAT">Kun kompatibelt med %s</string>
|
<string name="only_compatible_with_FORMAT">Kun kompatibelt med %s</string>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
<string name="permissions">Tilganger</string>
|
<string name="permissions">Tilganger</string>
|
||||||
<string name="plus_more_FORMAT">+%d til</string>
|
<string name="plus_more_FORMAT">+%d til</string>
|
||||||
<string name="settings">Innstillinger</string>
|
<string name="settings">Innstillinger</string>
|
||||||
<string name="processing_FORMAT">Behandler %1$s…</string>
|
<string name="processing_FORMAT">Behandler %1$s</string>
|
||||||
<string name="promotes_non_free_network_services">Promoterer ufrie nettverkstjenester</string>
|
<string name="promotes_non_free_network_services">Promoterer ufrie nettverkstjenester</string>
|
||||||
<string name="provided_by_FORMAT">Tilbudt av %s</string>
|
<string name="provided_by_FORMAT">Tilbudt av %s</string>
|
||||||
<string name="proxy_host">Mellomtjenervert</string>
|
<string name="proxy_host">Mellomtjenervert</string>
|
||||||
@ -94,12 +94,12 @@
|
|||||||
<string name="version_FORMAT">Versjon %s</string>
|
<string name="version_FORMAT">Versjon %s</string>
|
||||||
<string name="waiting_to_start_download">Venter på å laste ned…</string>
|
<string name="waiting_to_start_download">Venter på å laste ned…</string>
|
||||||
<string name="action_failed">Handlingen mislyktes</string>
|
<string name="action_failed">Handlingen mislyktes</string>
|
||||||
<string name="all_applications_up_to_date">Alle programmene dine er av nyeste dato</string>
|
<string name="all_applications_up_to_date">Alt er av nyeste dato</string>
|
||||||
<string name="launch">Kjør</string>
|
<string name="launch">Kjør</string>
|
||||||
<string name="notify_about_updates_summary">Vis en merknad når nye versjoner er tilgjengelig</string>
|
<string name="notify_about_updates_summary">Vis en merknad når nye versjoner er tilgjengelig</string>
|
||||||
<string name="add_repository">Legg til pakkebrønn</string>
|
<string name="add_repository">Legg til pakkebrønn</string>
|
||||||
<string name="credits">Bidragsytere</string>
|
<string name="credits">Bidragsytere</string>
|
||||||
<string name="downloading_FORMAT">Nedlasting %s…</string>
|
<string name="downloading_FORMAT">Laster ned %s</string>
|
||||||
<string name="network_error_DESC">Nettverksfeil.</string>
|
<string name="network_error_DESC">Nettverksfeil.</string>
|
||||||
<string name="no_proxy">Ingen mellomtjener</string>
|
<string name="no_proxy">Ingen mellomtjener</string>
|
||||||
<string name="only_on_wifi">Kun på Wi-Fi</string>
|
<string name="only_on_wifi">Kun på Wi-Fi</string>
|
||||||
@ -108,14 +108,14 @@
|
|||||||
<string name="versions">Versjoner</string>
|
<string name="versions">Versjoner</string>
|
||||||
<string name="details">Detaljer</string>
|
<string name="details">Detaljer</string>
|
||||||
<string name="incompatible_features_DESC">Manglende funksjoner.</string>
|
<string name="incompatible_features_DESC">Manglende funksjoner.</string>
|
||||||
<string name="application_not_found">Finner ikke det programmet</string>
|
<string name="application_not_found">Fant ikke programmet</string>
|
||||||
<string name="changes">Endringer</string>
|
<string name="changes">Endringer</string>
|
||||||
<string name="could_not_sync_FORMAT">Kunne ikke synkronisere %s</string>
|
<string name="could_not_sync_FORMAT">Kunne ikke synkronisere %s</string>
|
||||||
<string name="dark">Mørk</string>
|
<string name="dark">Mørk</string>
|
||||||
<string name="description">Beskrivelse</string>
|
<string name="description">Beskrivelse</string>
|
||||||
<string name="donate">Doner</string>
|
<string name="donate">Doner</string>
|
||||||
<string name="edit_repository">Rediger pakkebrønn</string>
|
<string name="edit_repository">Rediger pakkebrønn</string>
|
||||||
<string name="incompatible_versions_summary">Vis programversjoner som ikke er kompatible med enheten</string>
|
<string name="incompatible_versions_summary">Vis programversjoner som er ukompatible med enheten</string>
|
||||||
<string name="installed">Installert</string>
|
<string name="installed">Installert</string>
|
||||||
<string name="invalid_metadata_error_DESC">Ugyldig metadata.</string>
|
<string name="invalid_metadata_error_DESC">Ugyldig metadata.</string>
|
||||||
<plurals name="new_updates_DESC_FORMAT">
|
<plurals name="new_updates_DESC_FORMAT">
|
||||||
@ -166,9 +166,9 @@
|
|||||||
<string name="source_code_no_longer_available">Kildekoden er ikke lenger tilgjengelig</string>
|
<string name="source_code_no_longer_available">Kildekoden er ikke lenger tilgjengelig</string>
|
||||||
<string name="updates">Oppgraderinger</string>
|
<string name="updates">Oppgraderinger</string>
|
||||||
<string name="whats_new">Seneste</string>
|
<string name="whats_new">Seneste</string>
|
||||||
<string name="repository_not_used_DESC">Denne pakkebrønnen har ikke blitt brukt enda. Skru den på for å vise programmene i den.</string>
|
<string name="repository_not_used_DESC">Denne pakkebrønnen har ikke blitt brukt enda. Du må skru den på for å vise programmene den tilbyr.</string>
|
||||||
<string name="update">Oppgradering</string>
|
<string name="update">Oppgrader</string>
|
||||||
<string name="prefs_language_title">Språk</string>
|
<string name="prefs_language_title">Språ</string>
|
||||||
<string name="prefs_personalization">Personalisering</string>
|
<string name="prefs_personalization">Personalisering</string>
|
||||||
<string name="show_less">Vis mindre</string>
|
<string name="show_less">Vis mindre</string>
|
||||||
<string name="latest">Siste</string>
|
<string name="latest">Siste</string>
|
||||||
@ -177,4 +177,5 @@
|
|||||||
<string name="installed_applications">Installerte programmer</string>
|
<string name="installed_applications">Installerte programmer</string>
|
||||||
<string name="sort_filter">Sorter og filtrer</string>
|
<string name="sort_filter">Sorter og filtrer</string>
|
||||||
<string name="new_applications">Nye programmer</string>
|
<string name="new_applications">Nye programmer</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Kun på Wi-Fi tilkoblet lader</string>
|
||||||
</resources>
|
</resources>
|
@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="action_failed">A ação falhou</string>
|
<string name="action_failed">Ação falhou</string>
|
||||||
<string name="add_repository">Adicionar repositório</string>
|
<string name="add_repository">Adicionar repositório</string>
|
||||||
<string name="address">Endereço</string>
|
<string name="address">Endereço</string>
|
||||||
<string name="all_applications">Todos os aplicativos</string>
|
<string name="all_applications">Todos os aplicativos</string>
|
||||||
<string name="all_applications_up_to_date">Todos os seus aplicativos estão atualizados</string>
|
<string name="all_applications_up_to_date">Todos os teus aplicativos estão atualizados</string>
|
||||||
<string name="already_exists">Já existe</string>
|
<string name="already_exists">Já existe</string>
|
||||||
<string name="always">Sempre</string>
|
<string name="always">Sempre</string>
|
||||||
<string name="amoled">Escuro</string>
|
<string name="amoled">Preto</string>
|
||||||
<string name="anti_features">Características indesejadas</string>
|
<string name="anti_features">Anti-funções</string>
|
||||||
<string name="application">Aplicativo</string>
|
<string name="application">Aplicativo</string>
|
||||||
<string name="application_not_found">Não foi possível encontrar esse aplicativo</string>
|
<string name="application_not_found">Não foi possível encontrar esse aplicativo</string>
|
||||||
<string name="author_email">E-mail do autor</string>
|
<string name="author_email">E-mail do autor</string>
|
||||||
@ -180,4 +180,5 @@
|
|||||||
<string name="latest">Mais recente</string>
|
<string name="latest">Mais recente</string>
|
||||||
<string name="explore">Explorar</string>
|
<string name="explore">Explorar</string>
|
||||||
<string name="installed_applications">Aplicativos instalados</string>
|
<string name="installed_applications">Aplicativos instalados</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Apenas em Wi-Fi e Plugado</string>
|
||||||
</resources>
|
</resources>
|
@ -178,4 +178,7 @@
|
|||||||
<string name="explore">Исследуйте</string>
|
<string name="explore">Исследуйте</string>
|
||||||
<string name="update_all">Обновить все</string>
|
<string name="update_all">Обновить все</string>
|
||||||
<string name="installed_applications">Установленные приложения</string>
|
<string name="installed_applications">Установленные приложения</string>
|
||||||
|
<string name="latest">Последние</string>
|
||||||
|
<string name="sort_filter">Сортировать и фильтровать</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Только при Wi-Fi и подключении к сети</string>
|
||||||
</resources>
|
</resources>
|
@ -178,4 +178,5 @@
|
|||||||
<string name="latest">En yeni</string>
|
<string name="latest">En yeni</string>
|
||||||
<string name="update_all">Tümünü güncelle</string>
|
<string name="update_all">Tümünü güncelle</string>
|
||||||
<string name="explore">Keşfet</string>
|
<string name="explore">Keşfet</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Yalnızca Wi-Fi\'de ve Prize Takılı</string>
|
||||||
</resources>
|
</resources>
|
@ -177,4 +177,7 @@
|
|||||||
<string name="update_all">Оновити все</string>
|
<string name="update_all">Оновити все</string>
|
||||||
<string name="installed_applications">Встановлені додатки</string>
|
<string name="installed_applications">Встановлені додатки</string>
|
||||||
<string name="new_applications">Нові додатки</string>
|
<string name="new_applications">Нові додатки</string>
|
||||||
|
<string name="latest">Останні</string>
|
||||||
|
<string name="sort_filter">Сортувати та фільтрувати</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Лише при Wi-Fi і підключеному до мережі</string>
|
||||||
</resources>
|
</resources>
|
@ -179,4 +179,5 @@
|
|||||||
<string name="update_all">更新全部</string>
|
<string name="update_all">更新全部</string>
|
||||||
<string name="new_applications">新程序</string>
|
<string name="new_applications">新程序</string>
|
||||||
<string name="installed_applications">已安装的程序</string>
|
<string name="installed_applications">已安装的程序</string>
|
||||||
|
<string name="only_on_wifi_and_battery">仅在 Wi-Fi 和充电情况下</string>
|
||||||
</resources>
|
</resources>
|
@ -99,6 +99,7 @@
|
|||||||
<string name="ok">OK</string>
|
<string name="ok">OK</string>
|
||||||
<string name="only_compatible_with_FORMAT">Only compatible with %s</string>
|
<string name="only_compatible_with_FORMAT">Only compatible with %s</string>
|
||||||
<string name="only_on_wifi">Only on Wi-Fi</string>
|
<string name="only_on_wifi">Only on Wi-Fi</string>
|
||||||
|
<string name="only_on_wifi_and_battery">Only on Wi-Fi and Plugged-In</string>
|
||||||
<string name="open_DESC_FORMAT">Open %s?</string>
|
<string name="open_DESC_FORMAT">Open %s?</string>
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
<string name="parsing_index_error_DESC">Could not parse the index file.</string>
|
<string name="parsing_index_error_DESC">Could not parse the index file.</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user