mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-08-02 08:31:59 +00:00
Reformated all the code
This commit is contained in:
@@ -7,14 +7,19 @@ class MainActivity: ScreenActivity() {
|
||||
companion object {
|
||||
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
||||
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
const val EXTRA_CACHE_FILE_NAME = "${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
const val EXTRA_CACHE_FILE_NAME =
|
||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
}
|
||||
|
||||
override fun handleIntent(intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
|
||||
ACTION_INSTALL -> handleSpecialIntent(SpecialIntent.Install(intent.packageName,
|
||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)))
|
||||
ACTION_INSTALL -> handleSpecialIntent(
|
||||
SpecialIntent.Install(
|
||||
intent.packageName,
|
||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
)
|
||||
)
|
||||
else -> super.handleIntent(intent)
|
||||
}
|
||||
}
|
||||
|
@@ -4,14 +4,8 @@ import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.*
|
||||
import android.content.pm.PackageInfo
|
||||
import com.squareup.picasso.OkHttp3Downloader
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.looker.droidify.content.Cache
|
||||
import com.looker.droidify.content.Preferences
|
||||
import com.looker.droidify.content.ProductPreferences
|
||||
@@ -23,7 +17,11 @@ import com.looker.droidify.network.PicassoDownloader
|
||||
import com.looker.droidify.service.Connection
|
||||
import com.looker.droidify.service.SyncService
|
||||
import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.android.singleSignature
|
||||
import com.looker.droidify.utility.extension.android.versionCodeCompat
|
||||
import com.squareup.picasso.OkHttp3Downloader
|
||||
import com.squareup.picasso.Picasso
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
|
||||
@@ -48,8 +46,11 @@ class MainApplication: Application() {
|
||||
listenApplications()
|
||||
listenPreferences()
|
||||
|
||||
Picasso.setSingletonInstance(Picasso.Builder(this)
|
||||
.downloader(OkHttp3Downloader(PicassoDownloader.Factory(Cache.getImagesDir(this)))).build())
|
||||
Picasso.setSingletonInstance(
|
||||
Picasso.Builder(this)
|
||||
.downloader(OkHttp3Downloader(PicassoDownloader.Factory(Cache.getImagesDir(this))))
|
||||
.build()
|
||||
)
|
||||
|
||||
if (databaseUpdated) {
|
||||
forceSyncAll()
|
||||
@@ -62,13 +63,17 @@ class MainApplication: Application() {
|
||||
private fun listenApplications() {
|
||||
registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val packageName = intent.data?.let { if (it.scheme == "package") it.schemeSpecificPart else null }
|
||||
val packageName =
|
||||
intent.data?.let { if (it.scheme == "package") it.schemeSpecificPart else null }
|
||||
if (packageName != null) {
|
||||
when (intent.action.orEmpty()) {
|
||||
Intent.ACTION_PACKAGE_ADDED,
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
val packageInfo = try {
|
||||
packageManager.getPackageInfo(packageName, Android.PackageManager.signaturesFlag)
|
||||
packageManager.getPackageInfo(
|
||||
packageName,
|
||||
Android.PackageManager.signaturesFlag
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -86,7 +91,8 @@ class MainApplication: Application() {
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
val installedItems = packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag)
|
||||
val installedItems =
|
||||
packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag)
|
||||
.map { it.toInstalledItem() }
|
||||
Database.InstalledAdapter.putAll(installedItems)
|
||||
}
|
||||
@@ -127,7 +133,10 @@ class MainApplication: Application() {
|
||||
val period = 12 * 60 * 60 * 1000L // 12 hours
|
||||
val wifiOnly = autoSync == Preferences.AutoSync.Wifi
|
||||
jobScheduler.schedule(JobInfo
|
||||
.Builder(Common.JOB_ID_SYNC, ComponentName(this, SyncService.Job::class.java))
|
||||
.Builder(
|
||||
Common.JOB_ID_SYNC,
|
||||
ComponentName(this, SyncService.Job::class.java)
|
||||
)
|
||||
.setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY)
|
||||
.apply {
|
||||
if (Android.sdk(26)) {
|
||||
|
@@ -10,14 +10,17 @@ import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import android.system.Os
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
object Cache {
|
||||
private fun ensureCacheDir(context: Context, name: String): File {
|
||||
return File(context.cacheDir, name).apply { isDirectory || mkdirs() || throw RuntimeException() }
|
||||
return File(
|
||||
context.cacheDir,
|
||||
name
|
||||
).apply { isDirectory || mkdirs() || throw RuntimeException() }
|
||||
}
|
||||
|
||||
private fun applyOrMode(file: File, mode: Int) {
|
||||
@@ -60,8 +63,10 @@ object Cache {
|
||||
|
||||
fun getReleaseUri(context: Context, cacheFileName: String): Uri {
|
||||
val file = getReleaseFile(context, cacheFileName)
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PROVIDERS)
|
||||
val authority = packageInfo.providers.find { it.name == Provider::class.java.name }!!.authority
|
||||
val packageInfo =
|
||||
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PROVIDERS)
|
||||
val authority =
|
||||
packageInfo.providers.find { it.name == Provider::class.java.name }!!.authority
|
||||
return Uri.Builder().scheme("content").authority(authority)
|
||||
.encodedPath(subPath(context.cacheDir, file)).build()
|
||||
}
|
||||
@@ -71,7 +76,15 @@ object Cache {
|
||||
}
|
||||
|
||||
fun cleanup(context: Context) {
|
||||
thread { cleanup(context, Pair("images", 0), Pair("partial", 24), Pair("releases", 24), Pair("temporary", 1)) }
|
||||
thread {
|
||||
cleanup(
|
||||
context,
|
||||
Pair("images", 0),
|
||||
Pair("partial", 24),
|
||||
Pair("releases", 24),
|
||||
Pair("temporary", 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup(context: Context, vararg dirHours: Pair<String, Int>) {
|
||||
@@ -130,15 +143,20 @@ object Cache {
|
||||
|
||||
private fun getFileAndTypeForUri(uri: Uri): Pair<File, String> {
|
||||
return when (uri.pathSegments?.firstOrNull()) {
|
||||
"releases" -> Pair(File(context!!.cacheDir, uri.encodedPath!!), "application/vnd.android.package-archive")
|
||||
"releases" -> Pair(
|
||||
File(context!!.cacheDir, uri.encodedPath!!),
|
||||
"application/vnd.android.package-archive"
|
||||
)
|
||||
else -> throw SecurityException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
override fun query(uri: Uri, projection: Array<String>?,
|
||||
selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
|
||||
override fun query(
|
||||
uri: Uri, projection: Array<String>?,
|
||||
selection: String?, selectionArgs: Array<out String>?, sortOrder: String?
|
||||
): Cursor {
|
||||
val file = getFileAndTypeForUri(uri).first
|
||||
val columns = (projection ?: defaultColumns).mapNotNull {
|
||||
when (it) {
|
||||
@@ -150,15 +168,19 @@ object Cache {
|
||||
return MatrixCursor(columns.first.toTypedArray()).apply { addRow(columns.second.toTypedArray()) }
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? = getFileAndTypeForUri(uri).second
|
||||
override fun getType(uri: Uri): String = getFileAndTypeForUri(uri).second
|
||||
|
||||
private val unsupported: Nothing
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? = unsupported
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = unsupported
|
||||
override fun update(uri: Uri, contentValues: ContentValues?,
|
||||
selection: String?, selectionArgs: Array<out String>?): Int = unsupported
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri = unsupported
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
|
||||
unsupported
|
||||
|
||||
override fun update(
|
||||
uri: Uri, contentValues: ContentValues?,
|
||||
selection: String?, selectionArgs: Array<out String>?
|
||||
): Int = unsupported
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
val openMode = when (mode) {
|
||||
|
@@ -3,11 +3,11 @@ package com.looker.droidify.content
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.entity.ProductItem
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.net.Proxy
|
||||
|
||||
object Preferences {
|
||||
@@ -15,12 +15,19 @@ object Preferences {
|
||||
|
||||
private val subject = PublishSubject.create<Key<*>>()
|
||||
|
||||
private val keys = sequenceOf(Key.AutoSync, Key.IncompatibleVersions, Key.ProxyHost, Key.ProxyPort, Key.ProxyType,
|
||||
Key.SortOrder, Key.Theme, Key.UpdateNotify, Key.UpdateUnstable).map { Pair(it.name, it) }.toMap()
|
||||
private val keys = sequenceOf(
|
||||
Key.AutoSync, Key.IncompatibleVersions, Key.ProxyHost, Key.ProxyPort, Key.ProxyType,
|
||||
Key.SortOrder, Key.Theme, Key.UpdateNotify, Key.UpdateUnstable
|
||||
).map { Pair(it.name, it) }.toMap()
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE)
|
||||
preferences.registerOnSharedPreferenceChangeListener { _, keyString -> keys[keyString]?.let(subject::onNext) }
|
||||
preferences =
|
||||
context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE)
|
||||
preferences.registerOnSharedPreferenceChangeListener { _, keyString ->
|
||||
keys[keyString]?.let(
|
||||
subject::onNext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val observable: Observable<Key<*>>
|
||||
@@ -29,11 +36,20 @@ object Preferences {
|
||||
sealed class Value<T> {
|
||||
abstract val value: T
|
||||
|
||||
internal abstract fun get(preferences: SharedPreferences, key: String, defaultValue: Value<T>): T
|
||||
internal abstract fun get(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Value<T>
|
||||
): T
|
||||
|
||||
internal abstract fun set(preferences: SharedPreferences, key: String, value: T)
|
||||
|
||||
class BooleanValue(override val value: Boolean) : Value<Boolean>() {
|
||||
override fun get(preferences: SharedPreferences, key: String, defaultValue: Value<Boolean>): Boolean {
|
||||
override fun get(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Value<Boolean>
|
||||
): Boolean {
|
||||
return preferences.getBoolean(key, defaultValue.value)
|
||||
}
|
||||
|
||||
@@ -43,7 +59,11 @@ object Preferences {
|
||||
}
|
||||
|
||||
class IntValue(override val value: Int) : Value<Int>() {
|
||||
override fun get(preferences: SharedPreferences, key: String, defaultValue: Value<Int>): Int {
|
||||
override fun get(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Value<Int>
|
||||
): Int {
|
||||
return preferences.getInt(key, defaultValue.value)
|
||||
}
|
||||
|
||||
@@ -53,7 +73,11 @@ object Preferences {
|
||||
}
|
||||
|
||||
class StringValue(override val value: String) : Value<String>() {
|
||||
override fun get(preferences: SharedPreferences, key: String, defaultValue: Value<String>): String {
|
||||
override fun get(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Value<String>
|
||||
): String {
|
||||
return preferences.getString(key, defaultValue.value) ?: defaultValue.value
|
||||
}
|
||||
|
||||
@@ -63,9 +87,14 @@ object Preferences {
|
||||
}
|
||||
|
||||
class EnumerationValue<T : Enumeration<T>>(override val value: T) : Value<T>() {
|
||||
override fun get(preferences: SharedPreferences, key: String, defaultValue: Value<T>): T {
|
||||
override fun get(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Value<T>
|
||||
): T {
|
||||
val value = preferences.getString(key, defaultValue.value.valueString)
|
||||
return defaultValue.value.values.find { it.valueString == value } ?: defaultValue.value
|
||||
return defaultValue.value.values.find { it.valueString == value }
|
||||
?: defaultValue.value
|
||||
}
|
||||
|
||||
override fun set(preferences: SharedPreferences, key: String, value: T) {
|
||||
@@ -80,14 +109,33 @@ object Preferences {
|
||||
}
|
||||
|
||||
sealed class Key<T>(val name: String, val default: Value<T>) {
|
||||
object AutoSync: Key<Preferences.AutoSync>("auto_sync", Value.EnumerationValue(Preferences.AutoSync.Wifi))
|
||||
object IncompatibleVersions: Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
||||
object AutoSync : Key<Preferences.AutoSync>(
|
||||
"auto_sync",
|
||||
Value.EnumerationValue(Preferences.AutoSync.Wifi)
|
||||
)
|
||||
|
||||
object IncompatibleVersions :
|
||||
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
||||
|
||||
object ProxyHost : Key<String>("proxy_host", Value.StringValue("localhost"))
|
||||
object ProxyPort : Key<Int>("proxy_port", Value.IntValue(9050))
|
||||
object ProxyType: Key<Preferences.ProxyType>("proxy_type", Value.EnumerationValue(Preferences.ProxyType.Direct))
|
||||
object SortOrder: Key<Preferences.SortOrder>("sort_order", Value.EnumerationValue(Preferences.SortOrder.Update))
|
||||
object Theme: Key<Preferences.Theme>("theme", Value.EnumerationValue(if (Android.sdk(29))
|
||||
Preferences.Theme.System else Preferences.Theme.Light))
|
||||
object ProxyType : Key<Preferences.ProxyType>(
|
||||
"proxy_type",
|
||||
Value.EnumerationValue(Preferences.ProxyType.Direct)
|
||||
)
|
||||
|
||||
object SortOrder : Key<Preferences.SortOrder>(
|
||||
"sort_order",
|
||||
Value.EnumerationValue(Preferences.SortOrder.Update)
|
||||
)
|
||||
|
||||
object Theme : Key<Preferences.Theme>(
|
||||
"theme", Value.EnumerationValue(
|
||||
if (Android.sdk(29))
|
||||
Preferences.Theme.System else Preferences.Theme.Light
|
||||
)
|
||||
)
|
||||
|
||||
object UpdateNotify : Key<Boolean>("update_notify", Value.BooleanValue(true))
|
||||
object UpdateUnstable : Key<Boolean>("update_unstable", Value.BooleanValue(false))
|
||||
}
|
||||
@@ -101,7 +149,8 @@ object Preferences {
|
||||
object Always : AutoSync("always")
|
||||
}
|
||||
|
||||
sealed class ProxyType(override val valueString: String, val proxyType: Proxy.Type): Enumeration<ProxyType> {
|
||||
sealed class ProxyType(override val valueString: String, val proxyType: Proxy.Type) :
|
||||
Enumeration<ProxyType> {
|
||||
override val values: List<ProxyType>
|
||||
get() = listOf(Direct, Http, Socks)
|
||||
|
||||
@@ -110,7 +159,8 @@ object Preferences {
|
||||
object Socks : ProxyType("socks", Proxy.Type.SOCKS)
|
||||
}
|
||||
|
||||
sealed class SortOrder(override val valueString: String, val order: ProductItem.Order): Enumeration<SortOrder> {
|
||||
sealed class SortOrder(override val valueString: String, val order: ProductItem.Order) :
|
||||
Enumeration<SortOrder> {
|
||||
override val values: List<SortOrder>
|
||||
get() = listOf(Name, Added, Update)
|
||||
|
||||
|
@@ -2,11 +2,13 @@ package com.looker.droidify.content
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.entity.ProductPreference
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
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.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
@@ -18,7 +20,14 @@ object ProductPreferences {
|
||||
fun init(context: Context) {
|
||||
preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE)
|
||||
Database.LockAdapter.putAll(preferences.all.keys
|
||||
.mapNotNull { packageName -> this[packageName].databaseVersionCode?.let { Pair(packageName, it) } })
|
||||
.mapNotNull { packageName ->
|
||||
this[packageName].databaseVersionCode?.let {
|
||||
Pair(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
})
|
||||
subject
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe { (packageName, versionCode) ->
|
||||
@@ -54,10 +63,14 @@ object ProductPreferences {
|
||||
operator fun set(packageName: String, productPreference: ProductPreference) {
|
||||
val oldProductPreference = this[packageName]
|
||||
preferences.edit().putString(packageName, ByteArrayOutputStream()
|
||||
.apply { Json.factory.createGenerator(this).use { it.writeDictionary(productPreference::serialize) } }
|
||||
.apply {
|
||||
Json.factory.createGenerator(this)
|
||||
.use { it.writeDictionary(productPreference::serialize) }
|
||||
}
|
||||
.toByteArray().toString(Charset.defaultCharset())).apply()
|
||||
if (oldProductPreference.ignoreUpdates != productPreference.ignoreUpdates ||
|
||||
oldProductPreference.ignoreVersionCode != productPreference.ignoreVersionCode) {
|
||||
oldProductPreference.ignoreVersionCode != productPreference.ignoreVersionCode
|
||||
) {
|
||||
subject.onNext(Pair(packageName, productPreference.databaseVersionCode))
|
||||
}
|
||||
}
|
||||
|
@@ -11,20 +11,26 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
sealed class Request {
|
||||
internal abstract val id: Int
|
||||
|
||||
data class ProductsAvailable(val searchQuery: String, val section: ProductItem.Section,
|
||||
val order: ProductItem.Order): Request() {
|
||||
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() {
|
||||
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() {
|
||||
data class ProductsUpdates(
|
||||
val searchQuery: String, val section: ProductItem.Section,
|
||||
val order: ProductItem.Order
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 3
|
||||
}
|
||||
@@ -39,7 +45,11 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
fun onCursorData(request: Request, cursor: Cursor?)
|
||||
}
|
||||
|
||||
private data class ActiveRequest(val request: Request, val callback: Callback?, val cursor: Cursor?)
|
||||
private data class ActiveRequest(
|
||||
val request: Request,
|
||||
val callback: Callback?,
|
||||
val cursor: Cursor?
|
||||
)
|
||||
|
||||
init {
|
||||
retainInstance = true
|
||||
@@ -50,7 +60,8 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
fun attach(callback: Callback, request: Request) {
|
||||
val oldActiveRequest = activeRequests[request.id]
|
||||
if (oldActiveRequest?.callback != null &&
|
||||
oldActiveRequest.callback != callback && oldActiveRequest.cursor != null) {
|
||||
oldActiveRequest.callback != callback && oldActiveRequest.cursor != null
|
||||
) {
|
||||
oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null)
|
||||
}
|
||||
val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) {
|
||||
@@ -79,11 +90,32 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
return QueryLoader(requireContext()) {
|
||||
when (request) {
|
||||
is Request.ProductsAvailable -> Database.ProductAdapter
|
||||
.query(false, false, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = false,
|
||||
updates = false,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.ProductsInstalled -> Database.ProductAdapter
|
||||
.query(true, false, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = true,
|
||||
updates = false,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.ProductsUpdates -> Database.ProductAdapter
|
||||
.query(true, true, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
||||
}
|
||||
}
|
||||
|
@@ -8,13 +8,16 @@ import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.os.CancellationSignal
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
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.*
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
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 {
|
||||
@@ -49,8 +52,12 @@ object Database {
|
||||
}
|
||||
|
||||
val createIndexPairFormatted: Pair<String, String>?
|
||||
get() = createIndex?.let { Pair("CREATE INDEX ${innerName}_index ON $innerName ($it)",
|
||||
"CREATE INDEX ${name}_index ON $innerName ($it)") }
|
||||
get() = createIndex?.let {
|
||||
Pair(
|
||||
"CREATE INDEX ${innerName}_index ON $innerName ($it)",
|
||||
"CREATE INDEX ${name}_index ON $innerName ($it)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object Schema {
|
||||
@@ -161,8 +168,11 @@ object Database {
|
||||
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)
|
||||
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)
|
||||
@@ -174,7 +184,14 @@ object Database {
|
||||
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)
|
||||
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
|
||||
@@ -183,8 +200,10 @@ object Database {
|
||||
|
||||
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)))
|
||||
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
|
||||
}
|
||||
@@ -203,9 +222,15 @@ object Database {
|
||||
|
||||
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() }
|
||||
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 {
|
||||
@@ -224,8 +249,10 @@ object Database {
|
||||
}
|
||||
|
||||
private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) {
|
||||
val tables = db.query("sqlite_master", columns = arrayOf("name"),
|
||||
selection = Pair("type = ?", arrayOf("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 }
|
||||
@@ -247,7 +274,8 @@ object Database {
|
||||
|
||||
private val observers = mutableMapOf<Subject, MutableSet<() -> Unit>>()
|
||||
|
||||
private fun dataObservable(subject: Subject): (Boolean, () -> Unit) -> Unit = { register, observer ->
|
||||
private fun dataObservable(subject: Subject): (Boolean, () -> Unit) -> Unit =
|
||||
{ register, observer ->
|
||||
synchronized(observers) {
|
||||
val set = observers[subject] ?: run {
|
||||
val set = mutableSetOf<() -> Unit>()
|
||||
@@ -277,14 +305,35 @@ object Database {
|
||||
}
|
||||
}
|
||||
|
||||
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.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,
|
||||
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)
|
||||
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 {
|
||||
@@ -322,24 +371,41 @@ object Database {
|
||||
}
|
||||
|
||||
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())))
|
||||
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) }
|
||||
}
|
||||
|
||||
fun getAll(signal: CancellationSignal?): List<Repository> {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||
signal = signal).use { it.asSequence().map(::transform).toList() }
|
||||
signal = signal
|
||||
).use { it.asSequence().map(::transform).toList() }
|
||||
}
|
||||
|
||||
fun getAllDisabledDeleted(signal: CancellationSignal?): Set<Pair<Long, Boolean>> {
|
||||
return db.query(Schema.Repository.name,
|
||||
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() }
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
fun markAsDeleted(id: Long) {
|
||||
@@ -352,14 +418,22 @@ object Database {
|
||||
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 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)
|
||||
db.delete(
|
||||
Schema.Repository.name,
|
||||
"${Schema.Repository.ROW_ID} IN ($deleteIdsString)",
|
||||
null
|
||||
)
|
||||
}
|
||||
productsCount != 0 || categoriesCount != 0
|
||||
}
|
||||
@@ -369,33 +443,53 @@ object Database {
|
||||
}
|
||||
|
||||
fun query(signal: CancellationSignal?): Cursor {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||
signal = signal).observable(Subject.Repositories)
|
||||
signal = signal
|
||||
).observable(Subject.Repositories)
|
||||
}
|
||||
|
||||
fun transform(cursor: Cursor): Repository {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA))
|
||||
.jsonParse { Repository.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)), it) }
|
||||
.jsonParse {
|
||||
Repository.deserialize(
|
||||
cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ProductAdapter {
|
||||
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),
|
||||
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() }
|
||||
signal = signal
|
||||
).use { it.asSequence().map(::transform).toList() }
|
||||
}
|
||||
|
||||
fun getCount(repositoryId: Long): Int {
|
||||
return db.query(Schema.Product.name, columns = arrayOf("COUNT (*)"),
|
||||
selection = Pair("${Schema.Product.ROW_REPOSITORY_ID} = ?", arrayOf(repositoryId.toString())))
|
||||
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 }
|
||||
}
|
||||
|
||||
fun query(installed: Boolean, updates: Boolean, searchQuery: String,
|
||||
section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?): Cursor {
|
||||
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
|
||||
@@ -472,20 +566,29 @@ object Database {
|
||||
|
||||
private fun transform(cursor: Cursor): Product {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA))
|
||||
.jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it) }
|
||||
.jsonParse {
|
||||
Product.deserialize(
|
||||
cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun transformItem(cursor: Cursor): ProductItem {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM))
|
||||
.jsonParse { ProductItem.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
.jsonParse {
|
||||
ProductItem.deserialize(
|
||||
cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)).orEmpty(),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION))
|
||||
.orEmpty(),
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it) }
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,18 +603,24 @@ object Database {
|
||||
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() }
|
||||
return builder.query(db, signal).use {
|
||||
it.asSequence()
|
||||
.map { it.getString(it.getColumnIndex(Schema.Category.ROW_NAME)) }.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object InstalledAdapter {
|
||||
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),
|
||||
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) }
|
||||
signal = signal
|
||||
).use { it.firstOrNull()?.let(::transform) }
|
||||
}
|
||||
|
||||
private fun put(installedItem: InstalledItem, notify: Boolean) {
|
||||
@@ -540,17 +649,23 @@ object Database {
|
||||
}
|
||||
|
||||
fun delete(packageName: String) {
|
||||
val count = db.delete(Schema.Installed.name, "${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName))
|
||||
val count = db.delete(
|
||||
Schema.Installed.name,
|
||||
"${Schema.Installed.ROW_PACKAGE_NAME} = ?",
|
||||
arrayOf(packageName)
|
||||
)
|
||||
if (count > 0) {
|
||||
notifyChanged(Subject.Products)
|
||||
}
|
||||
}
|
||||
|
||||
private fun transform(cursor: Cursor): InstalledItem {
|
||||
return InstalledItem(cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_PACKAGE_NAME)),
|
||||
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)))
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_SIGNATURE))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +732,10 @@ object Database {
|
||||
put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize))
|
||||
})
|
||||
for (category in product.categories) {
|
||||
db.insertOrReplace(true, Schema.Category.temporaryName, ContentValues().apply {
|
||||
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)
|
||||
@@ -634,10 +752,14 @@ object Database {
|
||||
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.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)
|
||||
@@ -648,7 +770,11 @@ object Database {
|
||||
db.endTransaction()
|
||||
}
|
||||
if (success) {
|
||||
notifyChanged(Subject.Repositories, Subject.Repository(repository.id), Subject.Products)
|
||||
notifyChanged(
|
||||
Subject.Repositories,
|
||||
Subject.Repository(repository.id),
|
||||
Subject.Products
|
||||
)
|
||||
}
|
||||
} else {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||
|
@@ -5,8 +5,12 @@ import android.database.ContentObserver
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWrapper
|
||||
|
||||
class ObservableCursor(cursor: Cursor, private val observable: (register: Boolean,
|
||||
observer: () -> Unit) -> Unit): CursorWrapper(cursor) {
|
||||
class ObservableCursor(
|
||||
cursor: Cursor, private val observable: (
|
||||
register: Boolean,
|
||||
observer: () -> Unit
|
||||
) -> Unit
|
||||
) : CursorWrapper(cursor) {
|
||||
private var registered = false
|
||||
private val contentObservable = ContentObservable()
|
||||
|
||||
|
@@ -4,8 +4,8 @@ import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.CancellationSignal
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.asSequence
|
||||
import com.looker.droidify.utility.extension.text.debug
|
||||
|
||||
class QueryBuilder {
|
||||
companion object {
|
||||
|
@@ -1,3 +1,8 @@
|
||||
package com.looker.droidify.entity
|
||||
|
||||
class InstalledItem(val packageName: String, val version: String, val versionCode: Long, val signature: String)
|
||||
class InstalledItem(
|
||||
val packageName: String,
|
||||
val version: String,
|
||||
val versionCode: Long,
|
||||
val signature: String
|
||||
)
|
||||
|
@@ -4,14 +4,32 @@ import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||
|
||||
data class Product(val repositoryId: Long, val packageName: String, val name: String, val summary: String,
|
||||
val description: String, val whatsNew: String, val icon: String, val metadataIcon: String, val author: Author,
|
||||
val source: String, val changelog: String, val web: String, val tracker: String,
|
||||
val added: Long, val updated: Long, val suggestedVersionCode: Long,
|
||||
val categories: List<String>, val antiFeatures: List<String>, val licenses: List<String>,
|
||||
val donates: List<Donate>, val screenshots: List<Screenshot>, val releases: List<Release>) {
|
||||
data class Product(
|
||||
val repositoryId: Long,
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
val summary: String,
|
||||
val description: String,
|
||||
val whatsNew: String,
|
||||
val icon: String,
|
||||
val metadataIcon: String,
|
||||
val author: Author,
|
||||
val source: String,
|
||||
val changelog: String,
|
||||
val web: String,
|
||||
val tracker: String,
|
||||
val added: Long,
|
||||
val updated: Long,
|
||||
val suggestedVersionCode: Long,
|
||||
val categories: List<String>,
|
||||
val antiFeatures: List<String>,
|
||||
val licenses: List<String>,
|
||||
val donates: List<Donate>,
|
||||
val screenshots: List<Screenshot>,
|
||||
val releases: List<Release>
|
||||
) {
|
||||
data class Author(val name: String, val email: String, val web: String)
|
||||
|
||||
sealed class Donate {
|
||||
@@ -54,7 +72,19 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
get() = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct().toList()
|
||||
|
||||
fun item(): ProductItem {
|
||||
return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon, version, "", compatible, false, 0)
|
||||
return ProductItem(
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
icon,
|
||||
metadataIcon,
|
||||
version,
|
||||
"",
|
||||
compatible,
|
||||
false,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
fun canUpdate(installedItem: InstalledItem?): Boolean {
|
||||
@@ -128,9 +158,15 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> findSuggested(products: List<T>, installedItem: InstalledItem?, extract: (T) -> Product): T? {
|
||||
return products.maxWith(compareBy({ extract(it).compatible &&
|
||||
(installedItem == null || installedItem.signature in extract(it).signatures) }, { extract(it).versionCode }))
|
||||
fun <T> findSuggested(
|
||||
products: List<T>,
|
||||
installedItem: InstalledItem?,
|
||||
extract: (T) -> Product
|
||||
): T? {
|
||||
return products.maxWithOrNull(compareBy({
|
||||
extract(it).compatible &&
|
||||
(installedItem == null || installedItem.signature in extract(it).signatures)
|
||||
}, { extract(it).versionCode }))
|
||||
}
|
||||
|
||||
fun deserialize(repositoryId: Long, description: String, parser: JsonParser): Product {
|
||||
@@ -156,7 +192,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
var donates = emptyList<Donate>()
|
||||
var screenshots = emptyList<Screenshot>()
|
||||
var releases = emptyList<Release>()
|
||||
parser.forEachKey {
|
||||
parser.forEachKey { it ->
|
||||
when {
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> name = valueAsString
|
||||
@@ -201,7 +237,8 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
it.array("screenshots") -> screenshots = collectNotNull(JsonToken.START_OBJECT) {
|
||||
it.array("screenshots") -> screenshots =
|
||||
collectNotNull(JsonToken.START_OBJECT) {
|
||||
var locale = ""
|
||||
var type = ""
|
||||
var path = ""
|
||||
@@ -213,15 +250,38 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
Screenshot.Type.values().find { it.jsonName == type }?.let { Screenshot(locale, it, path) }
|
||||
Screenshot.Type.values().find { it.jsonName == type }
|
||||
?.let { Screenshot(locale, it, path) }
|
||||
}
|
||||
it.array("releases") -> releases = collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize)
|
||||
it.array("releases") -> releases =
|
||||
collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize)
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
return Product(repositoryId, packageName, name, summary, description, whatsNew, icon, metadataIcon,
|
||||
Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated,
|
||||
suggestedVersionCode, categories, antiFeatures, licenses, donates, screenshots, releases)
|
||||
return Product(
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
whatsNew,
|
||||
icon,
|
||||
metadataIcon,
|
||||
Author(authorName, authorEmail, authorWeb),
|
||||
source,
|
||||
changelog,
|
||||
web,
|
||||
tracker,
|
||||
added,
|
||||
updated,
|
||||
suggestedVersionCode,
|
||||
categories,
|
||||
antiFeatures,
|
||||
licenses,
|
||||
donates,
|
||||
screenshots,
|
||||
releases
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,14 +5,18 @@ import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.utility.KParcelable
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.json.forEachKey
|
||||
|
||||
data class ProductItem(val repositoryId: Long, val packageName: String, val name: String, val summary: String,
|
||||
data class ProductItem(
|
||||
val repositoryId: Long, val packageName: String, val name: String, val summary: String,
|
||||
val icon: String, val metadataIcon: String, val version: String, val installedVersion: String,
|
||||
val compatible: Boolean, val canUpdate: Boolean, val matchRank: Int) {
|
||||
val compatible: Boolean, val canUpdate: Boolean, val matchRank: Int
|
||||
) {
|
||||
sealed class Section : KParcelable {
|
||||
object All : Section() {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { All }
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator { All }
|
||||
}
|
||||
|
||||
data class Category(val name: String) : Section() {
|
||||
@@ -21,7 +25,9 @@ data class ProductItem(val repositoryId: Long, val packageName: String, val name
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator {
|
||||
val name = it.readString()!!
|
||||
Category(name)
|
||||
}
|
||||
@@ -35,7 +41,9 @@ data class ProductItem(val repositoryId: Long, val packageName: String, val name
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator {
|
||||
val id = it.readLong()
|
||||
val name = it.readString()!!
|
||||
Repository(id, name)
|
||||
@@ -58,9 +66,11 @@ data class ProductItem(val repositoryId: Long, val packageName: String, val name
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun deserialize(repositoryId: Long, packageName: String, name: String, summary: String,
|
||||
fun deserialize(
|
||||
repositoryId: Long, packageName: String, name: String, summary: String,
|
||||
installedVersion: String, compatible: Boolean, canUpdate: Boolean, matchRank: Int,
|
||||
parser: JsonParser): ProductItem {
|
||||
parser: JsonParser
|
||||
): ProductItem {
|
||||
var icon = ""
|
||||
var metadataIcon = ""
|
||||
var version = ""
|
||||
@@ -72,8 +82,10 @@ data class ProductItem(val repositoryId: Long, val packageName: String, val name
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon,
|
||||
version, installedVersion, compatible, canUpdate, matchRank)
|
||||
return ProductItem(
|
||||
repositoryId, packageName, name, summary, icon, metadataIcon,
|
||||
version, installedVersion, compatible, canUpdate, matchRank
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package com.looker.droidify.entity
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.json.forEachKey
|
||||
|
||||
data class ProductPreference(val ignoreUpdates: Boolean, val ignoreVersionCode: Long) {
|
||||
fun shouldIgnoreUpdate(versionCode: Long): Boolean {
|
||||
|
@@ -6,13 +6,31 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
|
||||
data class Release(val selected: Boolean, val version: String, val versionCode: Long,
|
||||
val added: Long, val size: Long, val minSdkVersion: Int, val targetSdkVersion: Int, val maxSdkVersion: Int,
|
||||
val source: String, val release: String, val hash: String, val hashType: String, val signature: String,
|
||||
val obbMain: String, val obbMainHash: String, val obbMainHashType: String,
|
||||
val obbPatch: String, val obbPatchHash: String, val obbPatchHashType: String,
|
||||
val permissions: List<String>, val features: List<String>, val platforms: List<String>,
|
||||
val incompatibilities: List<Incompatibility>) {
|
||||
data class Release(
|
||||
val selected: Boolean,
|
||||
val version: String,
|
||||
val versionCode: Long,
|
||||
val added: Long,
|
||||
val size: Long,
|
||||
val minSdkVersion: Int,
|
||||
val targetSdkVersion: Int,
|
||||
val maxSdkVersion: Int,
|
||||
val source: String,
|
||||
val release: String,
|
||||
val hash: String,
|
||||
val hashType: String,
|
||||
val signature: String,
|
||||
val obbMain: String,
|
||||
val obbMainHash: String,
|
||||
val obbMainHashType: String,
|
||||
val obbPatch: String,
|
||||
val obbPatchHash: String,
|
||||
val obbPatchHashType: String,
|
||||
val permissions: List<String>,
|
||||
val features: List<String>,
|
||||
val platforms: List<String>,
|
||||
val incompatibilities: List<Incompatibility>
|
||||
) {
|
||||
sealed class Incompatibility {
|
||||
object MinSdk : Incompatibility()
|
||||
object MaxSdk : Incompatibility()
|
||||
@@ -102,7 +120,7 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
|
||||
var features = emptyList<String>()
|
||||
var platforms = emptyList<String>()
|
||||
var incompatibilities = emptyList<Incompatibility>()
|
||||
parser.forEachKey {
|
||||
parser.forEachKey { it ->
|
||||
when {
|
||||
it.boolean("selected") -> selected = valueAsBoolean
|
||||
it.string("version") -> version = valueAsString
|
||||
@@ -126,7 +144,8 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
|
||||
it.array("permissions") -> permissions = collectNotNullStrings()
|
||||
it.array("features") -> features = collectNotNullStrings()
|
||||
it.array("platforms") -> platforms = collectNotNullStrings()
|
||||
it.array("incompatibilities") -> incompatibilities = collectNotNull(JsonToken.START_OBJECT) {
|
||||
it.array("incompatibilities") -> incompatibilities =
|
||||
collectNotNull(JsonToken.START_OBJECT) {
|
||||
var type = ""
|
||||
var feature = ""
|
||||
forEachKey {
|
||||
@@ -147,10 +166,31 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
return Release(selected, version, versionCode, added, size,
|
||||
minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature,
|
||||
obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType,
|
||||
permissions, features, platforms, incompatibilities)
|
||||
return Release(
|
||||
selected,
|
||||
version,
|
||||
versionCode,
|
||||
added,
|
||||
size,
|
||||
minSdkVersion,
|
||||
targetSdkVersion,
|
||||
maxSdkVersion,
|
||||
source,
|
||||
release,
|
||||
hash,
|
||||
hashType,
|
||||
signature,
|
||||
obbMain,
|
||||
obbMainHash,
|
||||
obbMainHashType,
|
||||
obbPatch,
|
||||
obbPatchHash,
|
||||
obbPatchHashType,
|
||||
permissions,
|
||||
features,
|
||||
platforms,
|
||||
incompatibilities
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,26 +2,39 @@ package com.looker.droidify.entity
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.json.collectNotNullStrings
|
||||
import com.looker.droidify.utility.extension.json.forEachKey
|
||||
import com.looker.droidify.utility.extension.json.writeArray
|
||||
import java.net.URL
|
||||
|
||||
data class Repository(val id: Long, val address: String, val mirrors: List<String>,
|
||||
data class Repository(
|
||||
val id: Long, val address: String, val mirrors: List<String>,
|
||||
val name: String, val description: String, val version: Int, val enabled: Boolean,
|
||||
val fingerprint: String, val lastModified: String, val entityTag: String,
|
||||
val updated: Long, val timestamp: Long, val authentication: String) {
|
||||
val updated: Long, val timestamp: Long, val authentication: String
|
||||
) {
|
||||
fun edit(address: String, fingerprint: String, authentication: String): Repository {
|
||||
val addressChanged = this.address != address
|
||||
val fingerprintChanged = this.fingerprint != fingerprint
|
||||
val changed = addressChanged || fingerprintChanged
|
||||
return copy(address = address, fingerprint = fingerprint, lastModified = if (changed) "" else lastModified,
|
||||
entityTag = if (changed) "" else entityTag, authentication = authentication)
|
||||
return copy(
|
||||
address = address,
|
||||
fingerprint = fingerprint,
|
||||
lastModified = if (changed) "" else lastModified,
|
||||
entityTag = if (changed) "" else entityTag,
|
||||
authentication = authentication
|
||||
)
|
||||
}
|
||||
|
||||
fun update(mirrors: List<String>, name: String, description: String, version: Int,
|
||||
lastModified: String, entityTag: String, timestamp: Long): Repository {
|
||||
return copy(mirrors = mirrors, name = name, description = description,
|
||||
fun update(
|
||||
mirrors: List<String>, name: String, description: String, version: Int,
|
||||
lastModified: String, entityTag: String, timestamp: Long
|
||||
): Repository {
|
||||
return copy(
|
||||
mirrors = mirrors, name = name, description = description,
|
||||
version = if (version >= 0) version else this.version, lastModified = lastModified,
|
||||
entityTag = entityTag, updated = System.currentTimeMillis(), timestamp = timestamp)
|
||||
entityTag = entityTag, updated = System.currentTimeMillis(), timestamp = timestamp
|
||||
)
|
||||
}
|
||||
|
||||
fun enable(enabled: Boolean): Repository {
|
||||
@@ -75,11 +88,17 @@ data class Repository(val id: Long, val address: String, val mirrors: List<Strin
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
return Repository(id, address, mirrors, name, description, version, enabled, fingerprint,
|
||||
lastModified, entityTag, updated, timestamp, authentication)
|
||||
return Repository(
|
||||
id, address, mirrors, name, description, version, enabled, fingerprint,
|
||||
lastModified, entityTag, updated, timestamp, authentication
|
||||
)
|
||||
}
|
||||
|
||||
fun newRepository(address: String, fingerprint: String, authentication: String): Repository {
|
||||
fun newRepository(
|
||||
address: String,
|
||||
fingerprint: String,
|
||||
authentication: String
|
||||
): Repository {
|
||||
val name = try {
|
||||
URL(address).let { "${it.host}${it.path}" }
|
||||
} catch (e: Exception) {
|
||||
@@ -88,37 +107,71 @@ data class Repository(val id: Long, val address: String, val mirrors: List<Strin
|
||||
return defaultRepository(address, name, "", 0, true, fingerprint, authentication)
|
||||
}
|
||||
|
||||
private fun defaultRepository(address: String, name: String, description: String,
|
||||
version: Int, enabled: Boolean, fingerprint: String, authentication: String): Repository {
|
||||
return Repository(-1, address, emptyList(), name, description, version, enabled,
|
||||
fingerprint, "", "", 0L, 0L, authentication)
|
||||
private fun defaultRepository(
|
||||
address: String, name: String, description: String,
|
||||
version: Int, enabled: Boolean, fingerprint: String, authentication: String
|
||||
): Repository {
|
||||
return Repository(
|
||||
-1, address, emptyList(), name, description, version, enabled,
|
||||
fingerprint, "", "", 0L, 0L, authentication
|
||||
)
|
||||
}
|
||||
|
||||
val defaultRepositories = listOf(run {
|
||||
defaultRepository("https://f-droid.org/repo", "F-Droid", "The official F-Droid Free Software repository. " +
|
||||
defaultRepository(
|
||||
"https://f-droid.org/repo",
|
||||
"F-Droid",
|
||||
"The official F-Droid Free Software repository. " +
|
||||
"Everything in this repository is always built from the source code.",
|
||||
21, true, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "")
|
||||
21,
|
||||
true,
|
||||
"43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB",
|
||||
""
|
||||
)
|
||||
}, run {
|
||||
defaultRepository("https://f-droid.org/archive", "F-Droid Archive", "The archive of the official F-Droid Free " +
|
||||
defaultRepository(
|
||||
"https://f-droid.org/archive",
|
||||
"F-Droid Archive",
|
||||
"The archive of the official F-Droid Free " +
|
||||
"Software repository. Apps here are old and can contain known vulnerabilities and security issues!",
|
||||
21, false, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "")
|
||||
21,
|
||||
false,
|
||||
"43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB",
|
||||
""
|
||||
)
|
||||
}, run {
|
||||
defaultRepository("https://guardianproject.info/fdroid/repo", "Guardian Project Official Releases", "The " +
|
||||
defaultRepository(
|
||||
"https://guardianproject.info/fdroid/repo",
|
||||
"Guardian Project Official Releases",
|
||||
"The " +
|
||||
"official repository of The Guardian Project apps for use with the F-Droid client. Applications in this " +
|
||||
"repository are official binaries built by the original application developers and signed by the same key as " +
|
||||
"the APKs that are released in the Google Play Store.",
|
||||
21, false, "B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135", "")
|
||||
21,
|
||||
false,
|
||||
"B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135",
|
||||
""
|
||||
)
|
||||
}, run {
|
||||
defaultRepository("https://guardianproject.info/fdroid/archive", "Guardian Project Archive", "The official " +
|
||||
defaultRepository(
|
||||
"https://guardianproject.info/fdroid/archive",
|
||||
"Guardian Project Archive",
|
||||
"The official " +
|
||||
"repository of The Guardian Project apps for use with the F-Droid client. This contains older versions of " +
|
||||
"applications from the main repository.", 21, false,
|
||||
"B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135", "")
|
||||
"applications from the main repository.",
|
||||
21,
|
||||
false,
|
||||
"B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135",
|
||||
""
|
||||
)
|
||||
}, run {
|
||||
defaultRepository("https://apt.izzysoft.de/fdroid/repo", "IzzyOnDroid F-Droid Repo", "This is a " +
|
||||
defaultRepository(
|
||||
"https://apt.izzysoft.de/fdroid/repo", "IzzyOnDroid F-Droid Repo", "This is a " +
|
||||
"repository of apps to be used with F-Droid the original application developers, taken from the resp. " +
|
||||
"repositories (mostly GitHub). At this moment I cannot give guarantees on regular updates for all of them, " +
|
||||
"though most are checked multiple times a week ", 21, true,
|
||||
"3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A", "")
|
||||
"3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A", ""
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package com.looker.droidify.graphics
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import kotlin.math.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class PaddingDrawable(drawable: Drawable, private val factor: Float) : DrawableWrapper(drawable) {
|
||||
override fun getIntrinsicWidth(): Int = (factor * super.getIntrinsicWidth()).roundToInt()
|
||||
@@ -13,7 +13,9 @@ class PaddingDrawable(drawable: Drawable, private val factor: Float): DrawableWr
|
||||
val height = (bounds.height() / factor).roundToInt()
|
||||
val left = (bounds.width() - width) / 2
|
||||
val top = (bounds.height() - height) / 2
|
||||
drawable.setBounds(bounds.left + left, bounds.top + top,
|
||||
bounds.left + left + width, bounds.top + top + height)
|
||||
drawable.setBounds(
|
||||
bounds.left + left, bounds.top + top,
|
||||
bounds.left + left + width, bounds.top + top + height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package com.looker.droidify.index
|
||||
|
||||
import com.looker.droidify.entity.Product
|
||||
import com.looker.droidify.entity.Release
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import org.xml.sax.Attributes
|
||||
import org.xml.sax.helpers.DefaultHandler
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.*
|
||||
|
||||
class IndexHandler(private val repositoryId: Long, private val callback: Callback): DefaultHandler() {
|
||||
class IndexHandler(private val repositoryId: Long, private val callback: Callback) :
|
||||
DefaultHandler() {
|
||||
companion object {
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
@@ -28,15 +28,23 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onRepository(mirrors: List<String>, name: String, description: String,
|
||||
certificate: String, version: Int, timestamp: Long)
|
||||
fun onRepository(
|
||||
mirrors: List<String>, name: String, description: String,
|
||||
certificate: String, version: Int, timestamp: Long
|
||||
)
|
||||
|
||||
fun onProduct(product: Product)
|
||||
}
|
||||
|
||||
internal object DonateComparator : Comparator<Product.Donate> {
|
||||
private val classes = listOf(Product.Donate.Regular::class, Product.Donate.Bitcoin::class,
|
||||
Product.Donate.Litecoin::class, Product.Donate.Flattr::class, Product.Donate.Liberapay::class,
|
||||
Product.Donate.OpenCollective::class)
|
||||
private val classes = listOf(
|
||||
Product.Donate.Regular::class,
|
||||
Product.Donate.Bitcoin::class,
|
||||
Product.Donate.Litecoin::class,
|
||||
Product.Donate.Flattr::class,
|
||||
Product.Donate.Liberapay::class,
|
||||
Product.Donate.OpenCollective::class
|
||||
)
|
||||
|
||||
override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int {
|
||||
val index1 = classes.indexOf(donate1::class)
|
||||
@@ -80,10 +88,30 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
val releases = mutableListOf<Release>()
|
||||
|
||||
fun build(): Product {
|
||||
return Product(repositoryId, packageName, name, summary, description, "", icon, "",
|
||||
Product.Author(authorName, authorEmail, ""), source, changelog, web, tracker, added, updated,
|
||||
suggestedVersionCode, categories.toList(), antiFeatures.toList(),
|
||||
licenses, donates.sortedWith(DonateComparator), emptyList(), releases)
|
||||
return Product(
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
"",
|
||||
icon,
|
||||
"",
|
||||
Product.Author(authorName, authorEmail, ""),
|
||||
source,
|
||||
changelog,
|
||||
web,
|
||||
tracker,
|
||||
added,
|
||||
updated,
|
||||
suggestedVersionCode,
|
||||
categories.toList(),
|
||||
antiFeatures.toList(),
|
||||
licenses,
|
||||
donates.sortedWith(DonateComparator),
|
||||
emptyList(),
|
||||
releases
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +140,31 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
val hashType = if (hash.isNotEmpty() && hashType.isEmpty()) "sha256" else hashType
|
||||
val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else ""
|
||||
val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else ""
|
||||
return Release(false, version, versionCode, added, size,
|
||||
minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature,
|
||||
obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType,
|
||||
permissions.toList(), features.toList(), platforms.toList(), emptyList())
|
||||
return Release(
|
||||
false,
|
||||
version,
|
||||
versionCode,
|
||||
added,
|
||||
size,
|
||||
minSdkVersion,
|
||||
targetSdkVersion,
|
||||
maxSdkVersion,
|
||||
source,
|
||||
release,
|
||||
hash,
|
||||
hashType,
|
||||
signature,
|
||||
obbMain,
|
||||
obbMainHash,
|
||||
obbMainHashType,
|
||||
obbPatch,
|
||||
obbPatchHash,
|
||||
obbPatchHashType,
|
||||
permissions.toList(),
|
||||
features.toList(),
|
||||
platforms.toList(),
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +177,12 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
private fun Attributes.get(localName: String): String = getValue("", localName).orEmpty()
|
||||
private fun String.cleanWhiteSpace(): String = replace("\\s".toRegex(), " ")
|
||||
|
||||
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
|
||||
override fun startElement(
|
||||
uri: String,
|
||||
localName: String,
|
||||
qName: String,
|
||||
attributes: Attributes
|
||||
) {
|
||||
super.startElement(uri, localName, qName, attributes)
|
||||
|
||||
val repositoryBuilder = repositoryBuilder
|
||||
@@ -144,7 +198,8 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
repositoryBuilder.description = attributes.get("description").cleanWhiteSpace()
|
||||
repositoryBuilder.certificate = attributes.get("pubkey")
|
||||
repositoryBuilder.version = attributes.get("version").toIntOrNull() ?: 0
|
||||
repositoryBuilder.timestamp = (attributes.get("timestamp").toLongOrNull() ?: 0L) * 1000L
|
||||
repositoryBuilder.timestamp =
|
||||
(attributes.get("timestamp").toLongOrNull() ?: 0L) * 1000L
|
||||
}
|
||||
}
|
||||
localName == "application" && productBuilder == null -> {
|
||||
@@ -186,8 +241,14 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
if (repositoryBuilder != null) {
|
||||
val mirrors = (listOf(repositoryBuilder.address) + repositoryBuilder.mirrors)
|
||||
.filter { it.isNotEmpty() }.distinct()
|
||||
callback.onRepository(mirrors, repositoryBuilder.name, repositoryBuilder.description,
|
||||
repositoryBuilder.certificate, repositoryBuilder.version, repositoryBuilder.timestamp)
|
||||
callback.onRepository(
|
||||
mirrors,
|
||||
repositoryBuilder.name,
|
||||
repositoryBuilder.description,
|
||||
repositoryBuilder.certificate,
|
||||
repositoryBuilder.version,
|
||||
repositoryBuilder.timestamp
|
||||
)
|
||||
this.repositoryBuilder = null
|
||||
}
|
||||
}
|
||||
@@ -213,7 +274,8 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
"added" -> releaseBuilder.added = content.parseDate()
|
||||
"size" -> releaseBuilder.size = content.toLongOrNull() ?: 0
|
||||
"sdkver" -> releaseBuilder.minSdkVersion = content.toIntOrNull() ?: 0
|
||||
"targetSdkVersion" -> releaseBuilder.targetSdkVersion = content.toIntOrNull() ?: 0
|
||||
"targetSdkVersion" -> releaseBuilder.targetSdkVersion =
|
||||
content.toIntOrNull() ?: 0
|
||||
"maxsdkver" -> releaseBuilder.maxSdkVersion = content.toIntOrNull() ?: 0
|
||||
"srcname" -> releaseBuilder.source = content
|
||||
"apkname" -> releaseBuilder.release = content
|
||||
@@ -223,9 +285,12 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
"obbMainFileSha256" -> releaseBuilder.obbMainHash = content
|
||||
"obbPatchFile" -> releaseBuilder.obbPatch = content
|
||||
"obbPatchFileSha256" -> releaseBuilder.obbPatchHash = content
|
||||
"permissions" -> releaseBuilder.permissions += content.split(',').filter { it.isNotEmpty() }
|
||||
"features" -> releaseBuilder.features += content.split(',').filter { it.isNotEmpty() }
|
||||
"nativecode" -> releaseBuilder.platforms += content.split(',').filter { it.isNotEmpty() }
|
||||
"permissions" -> releaseBuilder.permissions += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
"features" -> releaseBuilder.features += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
"nativecode" -> releaseBuilder.platforms += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
productBuilder != null -> {
|
||||
@@ -243,16 +308,22 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
"tracker" -> productBuilder.tracker = content
|
||||
"added" -> productBuilder.added = content.parseDate()
|
||||
"lastupdated" -> productBuilder.updated = content.parseDate()
|
||||
"marketvercode" -> productBuilder.suggestedVersionCode = content.toLongOrNull() ?: 0L
|
||||
"categories" -> productBuilder.categories += content.split(',').filter { it.isNotEmpty() }
|
||||
"antifeatures" -> productBuilder.antiFeatures += content.split(',').filter { it.isNotEmpty() }
|
||||
"license" -> productBuilder.licenses += content.split(',').filter { it.isNotEmpty() }
|
||||
"marketvercode" -> productBuilder.suggestedVersionCode =
|
||||
content.toLongOrNull() ?: 0L
|
||||
"categories" -> productBuilder.categories += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
"antifeatures" -> productBuilder.antiFeatures += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
"license" -> productBuilder.licenses += content.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
"donate" -> productBuilder.donates += Product.Donate.Regular(content)
|
||||
"bitcoin" -> productBuilder.donates += Product.Donate.Bitcoin(content)
|
||||
"litecoin" -> productBuilder.donates += Product.Donate.Litecoin(content)
|
||||
"flattr" -> productBuilder.donates += Product.Donate.Flattr(content)
|
||||
"liberapay" -> productBuilder.donates += Product.Donate.Liberapay(content)
|
||||
"openCollective" -> productBuilder.donates += Product.Donate.OpenCollective(content)
|
||||
"openCollective" -> productBuilder.donates += Product.Donate.OpenCollective(
|
||||
content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,11 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.entity.Product
|
||||
import com.looker.droidify.entity.Release
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.android.asSequence
|
||||
import com.looker.droidify.utility.extension.android.execWithResult
|
||||
import com.looker.droidify.utility.extension.json.Json
|
||||
import com.looker.droidify.utility.extension.json.collectNotNull
|
||||
import com.looker.droidify.utility.extension.json.writeDictionary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
@@ -25,7 +28,8 @@ class IndexMerger(file: File): Closeable {
|
||||
fun addProducts(products: List<Product>) {
|
||||
for (product in products) {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
Json.factory.createGenerator(outputStream).use { it.writeDictionary(product::serialize) }
|
||||
Json.factory.createGenerator(outputStream)
|
||||
.use { it.writeDictionary(product::serialize) }
|
||||
db.insert("product", null, ContentValues().apply {
|
||||
put("package_name", product.packageName)
|
||||
put("description", product.description)
|
||||
@@ -61,20 +65,30 @@ class IndexMerger(file: File): Closeable {
|
||||
|
||||
fun forEach(repositoryId: Long, windowSize: Int, callback: (List<Product>, Int) -> Unit) {
|
||||
closeTransaction()
|
||||
db.rawQuery("""SELECT product.description, product.data AS pd, releases.data AS rd FROM product
|
||||
LEFT JOIN releases ON product.package_name = releases.package_name""", null)
|
||||
?.use { it.asSequence().map {
|
||||
db.rawQuery(
|
||||
"""SELECT product.description, product.data AS pd, releases.data AS rd FROM product
|
||||
LEFT JOIN releases ON product.package_name = releases.package_name""", null
|
||||
)
|
||||
?.use { it ->
|
||||
it.asSequence().map {
|
||||
val description = it.getString(0)
|
||||
val product = Json.factory.createParser(it.getBlob(1)).use {
|
||||
it.nextToken()
|
||||
Product.deserialize(repositoryId, description, it)
|
||||
}
|
||||
val releases = it.getBlob(2)?.let { Json.factory.createParser(it).use {
|
||||
val releases = it.getBlob(2)?.let {
|
||||
Json.factory.createParser(it).use {
|
||||
it.nextToken()
|
||||
it.collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize)
|
||||
} }.orEmpty()
|
||||
it.collectNotNull(
|
||||
JsonToken.START_OBJECT,
|
||||
Release.Companion::deserialize
|
||||
)
|
||||
}
|
||||
}.orEmpty()
|
||||
product.copy(releases = releases)
|
||||
}.windowed(windowSize, windowSize, true).forEach { products -> callback(products, it.count) } }
|
||||
}.windowed(windowSize, windowSize, true)
|
||||
.forEach { products -> callback(products, it.count) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
@@ -4,31 +4,54 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.entity.Product
|
||||
import com.looker.droidify.entity.Release
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.json.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||
import java.io.InputStream
|
||||
|
||||
object IndexV1Parser {
|
||||
interface Callback {
|
||||
fun onRepository(mirrors: List<String>, name: String, description: String, version: Int, timestamp: Long)
|
||||
fun onRepository(
|
||||
mirrors: List<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
version: Int,
|
||||
timestamp: Long
|
||||
)
|
||||
|
||||
fun onProduct(product: Product)
|
||||
fun onReleases(packageName: String, releases: List<Release>)
|
||||
}
|
||||
|
||||
private class Screenshots(val phone: List<String>, val smallTablet: List<String>, val largeTablet: List<String>)
|
||||
private class Localized(val name: String, val summary: String, val description: String,
|
||||
val whatsNew: String, val metadataIcon: String, val screenshots: Screenshots?)
|
||||
private class Screenshots(
|
||||
val phone: List<String>,
|
||||
val smallTablet: List<String>,
|
||||
val largeTablet: List<String>
|
||||
)
|
||||
|
||||
private fun <T> Map<String, Localized>.getAndCall(key: String, callback: (String, Localized) -> T?): T? {
|
||||
private class Localized(
|
||||
val name: String, val summary: String, val description: String,
|
||||
val whatsNew: String, val metadataIcon: String, val screenshots: Screenshots?
|
||||
)
|
||||
|
||||
private fun <T> Map<String, Localized>.getAndCall(
|
||||
key: String,
|
||||
callback: (String, Localized) -> T?
|
||||
): T? {
|
||||
return this[key]?.let { callback(key, it) }
|
||||
}
|
||||
|
||||
private fun <T> Map<String, Localized>.find(callback: (String, Localized) -> T?): T? {
|
||||
return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall("en", callback)
|
||||
return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall(
|
||||
"en",
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
private fun Map<String, Localized>.findString(fallback: String, callback: (Localized) -> String): String {
|
||||
private fun Map<String, Localized>.findString(
|
||||
fallback: String,
|
||||
callback: (Localized) -> String
|
||||
): String {
|
||||
return (find { _, localized -> callback(localized).nullIfEmpty() } ?: fallback).trim()
|
||||
}
|
||||
|
||||
@@ -37,7 +60,7 @@ object IndexV1Parser {
|
||||
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
|
||||
jsonParser.illegal()
|
||||
} else {
|
||||
jsonParser.forEachKey {
|
||||
jsonParser.forEachKey { it ->
|
||||
when {
|
||||
it.dictionary("repo") -> {
|
||||
var address = ""
|
||||
@@ -57,7 +80,8 @@ object IndexV1Parser {
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
val realMirrors = ((if (address.isNotEmpty()) listOf(address) else emptyList()) + mirrors).distinct()
|
||||
val realMirrors =
|
||||
((if (address.isNotEmpty()) listOf(address) else emptyList()) + mirrors).distinct()
|
||||
callback.onRepository(realMirrors, name, description, version, timestamp)
|
||||
}
|
||||
it.array("apps") -> forEach(JsonToken.START_OBJECT) {
|
||||
@@ -100,7 +124,7 @@ object IndexV1Parser {
|
||||
val licenses = mutableListOf<String>()
|
||||
val donates = mutableListOf<Product.Donate>()
|
||||
val localizedMap = mutableMapOf<String, Localized>()
|
||||
forEachKey {
|
||||
forEachKey { it ->
|
||||
when {
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> nameFallback = valueAsString
|
||||
@@ -116,16 +140,20 @@ object IndexV1Parser {
|
||||
it.string("issueTracker") -> tracker = valueAsString
|
||||
it.number("added") -> added = valueAsLong
|
||||
it.number("lastUpdated") -> updated = valueAsLong
|
||||
it.string("suggestedVersionCode") -> suggestedVersionCode = valueAsString.toLongOrNull() ?: 0L
|
||||
it.string("suggestedVersionCode") -> suggestedVersionCode =
|
||||
valueAsString.toLongOrNull() ?: 0L
|
||||
it.array("categories") -> categories = collectDistinctNotEmptyStrings()
|
||||
it.array("antiFeatures") -> antiFeatures = collectDistinctNotEmptyStrings()
|
||||
it.string("license") -> licenses += valueAsString.split(',').filter { it.isNotEmpty() }
|
||||
it.string("license") -> licenses += valueAsString.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
it.string("donate") -> donates += Product.Donate.Regular(valueAsString)
|
||||
it.string("bitcoin") -> donates += Product.Donate.Bitcoin(valueAsString)
|
||||
it.string("flattrID") -> donates += Product.Donate.Flattr(valueAsString)
|
||||
it.string("liberapayID") -> donates += Product.Donate.Liberapay(valueAsString)
|
||||
it.string("openCollective") -> donates += Product.Donate.OpenCollective(valueAsString)
|
||||
it.dictionary("localized") -> forEachKey {
|
||||
it.string("openCollective") -> donates += Product.Donate.OpenCollective(
|
||||
valueAsString
|
||||
)
|
||||
it.dictionary("localized") -> forEachKey { it ->
|
||||
if (it.token == JsonToken.START_OBJECT) {
|
||||
val locale = it.key
|
||||
var name = ""
|
||||
@@ -143,16 +171,22 @@ object IndexV1Parser {
|
||||
it.string("description") -> description = valueAsString
|
||||
it.string("whatsNew") -> whatsNew = valueAsString
|
||||
it.string("icon") -> metadataIcon = valueAsString
|
||||
it.array("phoneScreenshots") -> phone = collectDistinctNotEmptyStrings()
|
||||
it.array("sevenInchScreenshots") -> smallTablet = collectDistinctNotEmptyStrings()
|
||||
it.array("tenInchScreenshots") -> largeTablet = collectDistinctNotEmptyStrings()
|
||||
it.array("phoneScreenshots") -> phone =
|
||||
collectDistinctNotEmptyStrings()
|
||||
it.array("sevenInchScreenshots") -> smallTablet =
|
||||
collectDistinctNotEmptyStrings()
|
||||
it.array("tenInchScreenshots") -> largeTablet =
|
||||
collectDistinctNotEmptyStrings()
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
val screenshots = if (sequenceOf(phone, smallTablet, largeTablet).any { it.isNotEmpty() })
|
||||
val screenshots =
|
||||
if (sequenceOf(phone, smallTablet, largeTablet).any { it.isNotEmpty() })
|
||||
Screenshots(phone, smallTablet, largeTablet) else null
|
||||
localizedMap[locale] = Localized(name, summary, description, whatsNew,
|
||||
metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), screenshots)
|
||||
localizedMap[locale] = Localized(
|
||||
name, summary, description, whatsNew,
|
||||
metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), screenshots
|
||||
)
|
||||
} else {
|
||||
skipChildren()
|
||||
}
|
||||
@@ -162,22 +196,58 @@ object IndexV1Parser {
|
||||
}
|
||||
val name = localizedMap.findString(nameFallback) { it.name }
|
||||
val summary = localizedMap.findString(summaryFallback) { it.summary }
|
||||
val description = localizedMap.findString(descriptionFallback) { it.description }.replace("\n", "<br/>")
|
||||
val description =
|
||||
localizedMap.findString(descriptionFallback) { it.description }.replace("\n", "<br/>")
|
||||
val whatsNew = localizedMap.findString("") { it.whatsNew }.replace("\n", "<br/>")
|
||||
val metadataIcon = localizedMap.findString("") { it.metadataIcon }
|
||||
val screenshotPairs = localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } }
|
||||
val screenshotPairs =
|
||||
localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } }
|
||||
val screenshots = screenshotPairs
|
||||
?.let { (key, screenshots) -> screenshots.phone.asSequence()
|
||||
?.let { (key, screenshots) ->
|
||||
screenshots.phone.asSequence()
|
||||
.map { Product.Screenshot(key, Product.Screenshot.Type.PHONE, it) } +
|
||||
screenshots.smallTablet.asSequence()
|
||||
.map { Product.Screenshot(key, Product.Screenshot.Type.SMALL_TABLET, it) } +
|
||||
.map {
|
||||
Product.Screenshot(
|
||||
key,
|
||||
Product.Screenshot.Type.SMALL_TABLET,
|
||||
it
|
||||
)
|
||||
} +
|
||||
screenshots.largeTablet.asSequence()
|
||||
.map { Product.Screenshot(key, Product.Screenshot.Type.LARGE_TABLET, it) } }
|
||||
.map {
|
||||
Product.Screenshot(
|
||||
key,
|
||||
Product.Screenshot.Type.LARGE_TABLET,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
.orEmpty().toList()
|
||||
return Product(repositoryId, packageName, name, summary, description, whatsNew, icon, metadataIcon,
|
||||
Product.Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated,
|
||||
suggestedVersionCode, categories, antiFeatures, licenses,
|
||||
donates.sortedWith(IndexHandler.DonateComparator), screenshots, emptyList())
|
||||
return Product(
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
whatsNew,
|
||||
icon,
|
||||
metadataIcon,
|
||||
Product.Author(authorName, authorEmail, authorWeb),
|
||||
source,
|
||||
changelog,
|
||||
web,
|
||||
tracker,
|
||||
added,
|
||||
updated,
|
||||
suggestedVersionCode,
|
||||
categories,
|
||||
antiFeatures,
|
||||
licenses,
|
||||
donates.sortedWith(IndexHandler.DonateComparator),
|
||||
screenshots,
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun JsonParser.parseRelease(): Release {
|
||||
@@ -225,13 +295,35 @@ object IndexV1Parser {
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
val hashType = if (hash.isNotEmpty() && hashTypeCandidate.isEmpty()) "sha256" else hashTypeCandidate
|
||||
val hashType =
|
||||
if (hash.isNotEmpty() && hashTypeCandidate.isEmpty()) "sha256" else hashTypeCandidate
|
||||
val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else ""
|
||||
val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else ""
|
||||
return Release(false, version, versionCode, added, size,
|
||||
minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature,
|
||||
obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType,
|
||||
permissions.toList(), features, platforms, emptyList())
|
||||
return Release(
|
||||
false,
|
||||
version,
|
||||
versionCode,
|
||||
added,
|
||||
size,
|
||||
minSdkVersion,
|
||||
targetSdkVersion,
|
||||
maxSdkVersion,
|
||||
source,
|
||||
release,
|
||||
hash,
|
||||
hashType,
|
||||
signature,
|
||||
obbMain,
|
||||
obbMainHash,
|
||||
obbMainHashType,
|
||||
obbPatch,
|
||||
obbPatchHash,
|
||||
obbPatchHashType,
|
||||
permissions.toList(),
|
||||
features,
|
||||
platforms,
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun JsonParser.collectPermissions(permissions: LinkedHashSet<String>, minSdk: Int) {
|
||||
|
@@ -2,9 +2,6 @@ package com.looker.droidify.index
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import com.looker.droidify.content.Cache
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.entity.Product
|
||||
@@ -14,12 +11,15 @@ import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.utility.ProgressInputStream
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.text.unhex
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.xml.sax.InputSource
|
||||
import java.io.File
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import javax.xml.parsers.SAXParserFactory
|
||||
@@ -29,7 +29,11 @@ object RepositoryUpdater {
|
||||
DOWNLOAD, PROCESS, MERGE, COMMIT
|
||||
}
|
||||
|
||||
private enum class IndexType(val jarName: String, val contentName: String, val certificateFromIndex: Boolean) {
|
||||
private enum class IndexType(
|
||||
val jarName: String,
|
||||
val contentName: String,
|
||||
val certificateFromIndex: Boolean
|
||||
) {
|
||||
INDEX("index.jar", "index.xml", true),
|
||||
INDEX_V1("index-v1.jar", "index-v1.json", false)
|
||||
}
|
||||
@@ -45,7 +49,10 @@ object RepositoryUpdater {
|
||||
this.errorType = errorType
|
||||
}
|
||||
|
||||
constructor(errorType: ErrorType, message: String, cause: Exception): super(message, cause) {
|
||||
constructor(errorType: ErrorType, message: String, cause: Exception) : super(
|
||||
message,
|
||||
cause
|
||||
) {
|
||||
this.errorType = errorType
|
||||
}
|
||||
}
|
||||
@@ -61,8 +68,14 @@ object RepositoryUpdater {
|
||||
Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAllDisabledDeleted(it) } }
|
||||
.forEach {
|
||||
.flatMapSingle {
|
||||
RxUtils.querySingle {
|
||||
Database.RepositoryAdapter.getAllDisabledDeleted(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
.forEach { it ->
|
||||
val newDisabled = it.asSequence().filter { !it.second }.map { it.first }.toSet()
|
||||
val disabled = newDisabled - lastDisabled
|
||||
lastDisabled = newDisabled
|
||||
@@ -79,13 +92,17 @@ object RepositoryUpdater {
|
||||
synchronized(updaterLock) { }
|
||||
}
|
||||
|
||||
fun update(repository: Repository, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Boolean> {
|
||||
fun update(
|
||||
repository: Repository, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
): Single<Boolean> {
|
||||
return update(repository, listOf(IndexType.INDEX_V1, IndexType.INDEX), unstable, callback)
|
||||
}
|
||||
|
||||
private fun update(repository: Repository, indexTypes: List<IndexType>, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Boolean> {
|
||||
private fun update(
|
||||
repository: Repository, indexTypes: List<IndexType>, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
): Single<Boolean> {
|
||||
val indexType = indexTypes[0]
|
||||
return downloadIndex(repository, indexType, callback)
|
||||
.flatMap { (result, file) ->
|
||||
@@ -97,41 +114,74 @@ object RepositoryUpdater {
|
||||
!result.success -> {
|
||||
file.delete()
|
||||
if (result.code == 404 && indexTypes.isNotEmpty()) {
|
||||
update(repository, indexTypes.subList(1, indexTypes.size), unstable, callback)
|
||||
update(
|
||||
repository,
|
||||
indexTypes.subList(1, indexTypes.size),
|
||||
unstable,
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
Single.error(UpdateException(ErrorType.HTTP, "Invalid response: HTTP ${result.code}"))
|
||||
Single.error(
|
||||
UpdateException(
|
||||
ErrorType.HTTP,
|
||||
"Invalid response: HTTP ${result.code}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
RxUtils.managedSingle { processFile(repository, indexType, unstable,
|
||||
file, result.lastModified, result.entityTag, callback) }
|
||||
RxUtils.managedSingle {
|
||||
processFile(
|
||||
repository, indexType, unstable,
|
||||
file, result.lastModified, result.entityTag, callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadIndex(repository: Repository, indexType: IndexType,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Pair<Downloader.Result, File>> {
|
||||
private fun downloadIndex(
|
||||
repository: Repository, indexType: IndexType,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
): Single<Pair<Downloader.Result, File>> {
|
||||
return Single.just(Unit)
|
||||
.map { Cache.getTemporaryFile(context) }
|
||||
.flatMap { file -> Downloader
|
||||
.download(Uri.parse(repository.address).buildUpon()
|
||||
.appendPath(indexType.jarName).build().toString(), file, repository.lastModified, repository.entityTag,
|
||||
repository.authentication) { read, total -> callback(Stage.DOWNLOAD, read, total) }
|
||||
.flatMap { file ->
|
||||
Downloader
|
||||
.download(
|
||||
Uri.parse(repository.address).buildUpon()
|
||||
.appendPath(indexType.jarName).build().toString(),
|
||||
file,
|
||||
repository.lastModified,
|
||||
repository.entityTag,
|
||||
repository.authentication
|
||||
) { read, total -> callback(Stage.DOWNLOAD, read, total) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { Pair(it, file) }
|
||||
.onErrorResumeNext {
|
||||
file.delete()
|
||||
when (it) {
|
||||
is InterruptedException, is RuntimeException, is Error -> Single.error(it)
|
||||
is Exception -> Single.error(UpdateException(ErrorType.NETWORK, "Network error", it))
|
||||
is InterruptedException, is RuntimeException, is Error -> Single.error(
|
||||
it
|
||||
)
|
||||
is Exception -> Single.error(
|
||||
UpdateException(
|
||||
ErrorType.NETWORK,
|
||||
"Network error",
|
||||
it
|
||||
)
|
||||
)
|
||||
else -> Single.error(it)
|
||||
}
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processFile(repository: Repository, indexType: IndexType, unstable: Boolean,
|
||||
file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit): Boolean {
|
||||
private fun processFile(
|
||||
repository: Repository, indexType: IndexType, unstable: Boolean,
|
||||
file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit
|
||||
): Boolean {
|
||||
var rollback = true
|
||||
return synchronized(updaterLock) {
|
||||
try {
|
||||
@@ -152,12 +202,17 @@ object RepositoryUpdater {
|
||||
var certificateFromIndex: String? = null
|
||||
val products = mutableListOf<Product>()
|
||||
|
||||
reader.contentHandler = IndexHandler(repository.id, object: IndexHandler.Callback {
|
||||
override fun onRepository(mirrors: List<String>, name: String, description: String,
|
||||
certificate: String, version: Int, timestamp: Long) {
|
||||
changedRepository = repository.update(mirrors, name, description, version,
|
||||
lastModified, entityTag, timestamp)
|
||||
certificateFromIndex = certificate.toLowerCase(Locale.US)
|
||||
reader.contentHandler =
|
||||
IndexHandler(repository.id, object : IndexHandler.Callback {
|
||||
override fun onRepository(
|
||||
mirrors: List<String>, name: String, description: String,
|
||||
certificate: String, version: Int, timestamp: Long
|
||||
) {
|
||||
changedRepository = repository.update(
|
||||
mirrors, name, description, version,
|
||||
lastModified, entityTag, timestamp
|
||||
)
|
||||
certificateFromIndex = certificate.lowercase(Locale.US)
|
||||
}
|
||||
|
||||
override fun onProduct(product: Product) {
|
||||
@@ -172,7 +227,13 @@ object RepositoryUpdater {
|
||||
}
|
||||
})
|
||||
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) }
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) {
|
||||
callback(
|
||||
Stage.PROCESS,
|
||||
it,
|
||||
total
|
||||
)
|
||||
}
|
||||
.use { reader.parse(InputSource(it)) }
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
@@ -191,12 +252,28 @@ object RepositoryUpdater {
|
||||
val unmergedProducts = mutableListOf<Product>()
|
||||
val unmergedReleases = mutableListOf<Pair<String, List<Release>>>()
|
||||
IndexMerger(mergerFile).use { indexMerger ->
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) }.use {
|
||||
IndexV1Parser.parse(repository.id, it, object: IndexV1Parser.Callback {
|
||||
override fun onRepository(mirrors: List<String>, name: String, description: String,
|
||||
version: Int, timestamp: Long) {
|
||||
changedRepository = repository.update(mirrors, name, description, version,
|
||||
lastModified, entityTag, timestamp)
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) {
|
||||
callback(
|
||||
Stage.PROCESS,
|
||||
it,
|
||||
total
|
||||
)
|
||||
}.use { it ->
|
||||
IndexV1Parser.parse(
|
||||
repository.id,
|
||||
it,
|
||||
object : IndexV1Parser.Callback {
|
||||
override fun onRepository(
|
||||
mirrors: List<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
version: Int,
|
||||
timestamp: Long
|
||||
) {
|
||||
changedRepository = repository.update(
|
||||
mirrors, name, description, version,
|
||||
lastModified, entityTag, timestamp
|
||||
)
|
||||
}
|
||||
|
||||
override fun onProduct(product: Product) {
|
||||
@@ -210,7 +287,10 @@ object RepositoryUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReleases(packageName: String, releases: List<Release>) {
|
||||
override fun onReleases(
|
||||
packageName: String,
|
||||
releases: List<Release>
|
||||
) {
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
}
|
||||
@@ -239,7 +319,11 @@ object RepositoryUpdater {
|
||||
throw InterruptedException()
|
||||
}
|
||||
progress += products.size
|
||||
callback(Stage.MERGE, progress.toLong(), totalCount.toLong())
|
||||
callback(
|
||||
Stage.MERGE,
|
||||
progress.toLong(),
|
||||
totalCount.toLong()
|
||||
)
|
||||
Database.UpdaterAdapter.putTemporary(products
|
||||
.map { transformProduct(it, features, unstable) })
|
||||
}
|
||||
@@ -254,18 +338,27 @@ object RepositoryUpdater {
|
||||
|
||||
val workRepository = changedRepository ?: repository
|
||||
if (workRepository.timestamp < repository.timestamp) {
|
||||
throw UpdateException(ErrorType.VALIDATION, "New index is older than current index: " +
|
||||
"${workRepository.timestamp} < ${repository.timestamp}")
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION, "New index is older than current index: " +
|
||||
"${workRepository.timestamp} < ${repository.timestamp}"
|
||||
)
|
||||
} else {
|
||||
val fingerprint = run {
|
||||
val certificateFromJar = run {
|
||||
val codeSigners = indexEntry.codeSigners
|
||||
if (codeSigners == null || codeSigners.size != 1) {
|
||||
throw UpdateException(ErrorType.VALIDATION, "index.jar must be signed by a single code signer")
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"index.jar must be signed by a single code signer"
|
||||
)
|
||||
} else {
|
||||
val certificates = codeSigners[0].signerCertPath?.certificates.orEmpty()
|
||||
val certificates =
|
||||
codeSigners[0].signerCertPath?.certificates.orEmpty()
|
||||
if (certificates.size != 1) {
|
||||
throw UpdateException(ErrorType.VALIDATION, "index.jar code signer should have only one certificate")
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"index.jar code signer should have only one certificate"
|
||||
)
|
||||
} else {
|
||||
certificates[0] as X509Certificate
|
||||
}
|
||||
@@ -273,9 +366,13 @@ object RepositoryUpdater {
|
||||
}
|
||||
val fingerprintFromJar = Utils.calculateFingerprint(certificateFromJar)
|
||||
if (indexType.certificateFromIndex) {
|
||||
val fingerprintFromIndex = certificateFromIndex?.unhex()?.let(Utils::calculateFingerprint)
|
||||
val fingerprintFromIndex =
|
||||
certificateFromIndex?.unhex()?.let(Utils::calculateFingerprint)
|
||||
if (fingerprintFromIndex == null || fingerprintFromJar != fingerprintFromIndex) {
|
||||
throw UpdateException(ErrorType.VALIDATION, "index.xml contains invalid public key")
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"index.xml contains invalid public key"
|
||||
)
|
||||
}
|
||||
fingerprintFromIndex
|
||||
} else {
|
||||
@@ -287,7 +384,10 @@ object RepositoryUpdater {
|
||||
if (workRepository.fingerprint.isEmpty()) {
|
||||
workRepository.copy(fingerprint = fingerprint)
|
||||
} else {
|
||||
throw UpdateException(ErrorType.VALIDATION, "Certificate fingerprints do not match")
|
||||
throw UpdateException(
|
||||
ErrorType.VALIDATION,
|
||||
"Certificate fingerprints do not match"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
workRepository
|
||||
@@ -296,7 +396,12 @@ object RepositoryUpdater {
|
||||
throw InterruptedException()
|
||||
}
|
||||
callback(Stage.COMMIT, 0, null)
|
||||
synchronized(cleanupLock) { Database.UpdaterAdapter.finishTemporary(commitRepository, true) }
|
||||
synchronized(cleanupLock) {
|
||||
Database.UpdaterAdapter.finishTemporary(
|
||||
commitRepository,
|
||||
true
|
||||
)
|
||||
}
|
||||
rollback = false
|
||||
true
|
||||
}
|
||||
@@ -314,8 +419,14 @@ object RepositoryUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformProduct(product: Product, features: Set<String>, unstable: Boolean): Product {
|
||||
val releasePairs = product.releases.distinctBy { it.identifier }.sortedByDescending { it.versionCode }.map {
|
||||
private fun transformProduct(
|
||||
product: Product,
|
||||
features: Set<String>,
|
||||
unstable: Boolean
|
||||
): Product {
|
||||
val releasePairs =
|
||||
product.releases.distinctBy { it.identifier }.sortedByDescending { it.versionCode }
|
||||
.map { it ->
|
||||
val incompatibilities = mutableListOf<Release.Incompatibility>()
|
||||
if (it.minSdkVersion > 0 && Android.sdk < it.minSdkVersion) {
|
||||
incompatibilities += Release.Incompatibility.MinSdk
|
||||
@@ -323,23 +434,32 @@ object RepositoryUpdater {
|
||||
if (it.maxSdkVersion > 0 && Android.sdk > it.maxSdkVersion) {
|
||||
incompatibilities += Release.Incompatibility.MaxSdk
|
||||
}
|
||||
if (it.platforms.isNotEmpty() && it.platforms.intersect(Android.platforms).isEmpty()) {
|
||||
if (it.platforms.isNotEmpty() && it.platforms.intersect(Android.platforms)
|
||||
.isEmpty()
|
||||
) {
|
||||
incompatibilities += Release.Incompatibility.Platform
|
||||
}
|
||||
incompatibilities += (it.features - features).sorted().map { Release.Incompatibility.Feature(it) }
|
||||
incompatibilities += (it.features - features).sorted()
|
||||
.map { Release.Incompatibility.Feature(it) }
|
||||
Pair(it, incompatibilities as List<Release.Incompatibility>)
|
||||
}.toMutableList()
|
||||
|
||||
val predicate: (Release) -> Boolean = { unstable || product.suggestedVersionCode <= 0 ||
|
||||
it.versionCode <= product.suggestedVersionCode }
|
||||
val firstCompatibleReleaseIndex = releasePairs.indexOfFirst { it.second.isEmpty() && predicate(it.first) }
|
||||
val firstReleaseIndex = if (firstCompatibleReleaseIndex >= 0) firstCompatibleReleaseIndex else
|
||||
val predicate: (Release) -> Boolean = {
|
||||
unstable || product.suggestedVersionCode <= 0 ||
|
||||
it.versionCode <= product.suggestedVersionCode
|
||||
}
|
||||
val firstCompatibleReleaseIndex =
|
||||
releasePairs.indexOfFirst { it.second.isEmpty() && predicate(it.first) }
|
||||
val firstReleaseIndex =
|
||||
if (firstCompatibleReleaseIndex >= 0) firstCompatibleReleaseIndex else
|
||||
releasePairs.indexOfFirst { predicate(it.first) }
|
||||
val firstSelected = if (firstReleaseIndex >= 0) releasePairs[firstReleaseIndex] else null
|
||||
|
||||
val releases = releasePairs.map { (release, incompatibilities) -> release
|
||||
val releases = releasePairs.map { (release, incompatibilities) ->
|
||||
release
|
||||
.copy(incompatibilities = incompatibilities, selected = firstSelected
|
||||
?.let { it.first.versionCode == release.versionCode && it.second == incompatibilities } == true) }
|
||||
?.let { it.first.versionCode == release.versionCode && it.second == incompatibilities } == true)
|
||||
}
|
||||
return product.copy(releases = releases)
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
package com.looker.droidify.network
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import com.looker.droidify.utility.ProgressInputStream
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -66,8 +66,10 @@ object Downloader {
|
||||
return client.newCall(newRequest)
|
||||
}
|
||||
|
||||
fun download(url: String, target: File, lastModified: String, entityTag: String, authentication: String,
|
||||
callback: ((read: Long, total: Long?) -> Unit)?): Single<Result> {
|
||||
fun download(
|
||||
url: String, target: File, lastModified: String, entityTag: String, authentication: String,
|
||||
callback: ((read: Long, total: Long?) -> Unit)?
|
||||
): Single<Result> {
|
||||
val start = if (target.exists()) target.length().let { if (it > 0L) it else null } else null
|
||||
val request = Request.Builder().url(url)
|
||||
.apply {
|
||||
@@ -84,15 +86,18 @@ object Downloader {
|
||||
return RxUtils
|
||||
.callSingle { createCall(request, authentication, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { result -> RxUtils
|
||||
.managedSingle { result.use {
|
||||
.flatMap { result ->
|
||||
RxUtils
|
||||
.managedSingle {
|
||||
result.use { it ->
|
||||
if (result.code == 304) {
|
||||
Result(it.code, lastModified, entityTag)
|
||||
} else {
|
||||
val body = it.body!!
|
||||
val append = start != null && it.header("Content-Range") != null
|
||||
val progressStart = if (append && start != null) start else 0L
|
||||
val progressTotal = body.contentLength().let { if (it >= 0L) it else null }
|
||||
val progressTotal =
|
||||
body.contentLength().let { if (it >= 0L) it else null }
|
||||
?.let { progressStart + it }
|
||||
val inputStream = ProgressInputStream(body.byteStream()) {
|
||||
if (Thread.interrupted()) {
|
||||
@@ -101,14 +106,23 @@ object Downloader {
|
||||
callback?.invoke(progressStart + it, progressTotal)
|
||||
}
|
||||
inputStream.use { input ->
|
||||
val outputStream = if (append) FileOutputStream(target, true) else FileOutputStream(target)
|
||||
val outputStream = if (append) FileOutputStream(
|
||||
target,
|
||||
true
|
||||
) else FileOutputStream(target)
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
output.fd.sync()
|
||||
}
|
||||
}
|
||||
Result(it.code, it.header("Last-Modified").orEmpty(), it.header("ETag").orEmpty())
|
||||
}
|
||||
} } }
|
||||
Result(
|
||||
it.code,
|
||||
it.header("Last-Modified").orEmpty(),
|
||||
it.header("ETag").orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,12 +5,13 @@ import android.net.Uri
|
||||
import android.view.View
|
||||
import com.looker.droidify.entity.Product
|
||||
import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Call
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.io.File
|
||||
import kotlin.math.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object PicassoDownloader {
|
||||
private const val HOST_ICON = "icon"
|
||||
@@ -36,9 +37,11 @@ object PicassoDownloader {
|
||||
val address = request.url.queryParameter(QUERY_ADDRESS)?.nullIfEmpty()
|
||||
val authentication = request.url.queryParameter(QUERY_AUTHENTICATION)
|
||||
val path = run {
|
||||
val packageName = request.url.queryParameter(QUERY_PACKAGE_NAME)?.nullIfEmpty()
|
||||
val packageName =
|
||||
request.url.queryParameter(QUERY_PACKAGE_NAME)?.nullIfEmpty()
|
||||
val icon = request.url.queryParameter(QUERY_ICON)?.nullIfEmpty()
|
||||
val metadataIcon = request.url.queryParameter(QUERY_METADATA_ICON)?.nullIfEmpty()
|
||||
val metadataIcon =
|
||||
request.url.queryParameter(QUERY_METADATA_ICON)?.nullIfEmpty()
|
||||
val dpi = request.url.queryParameter(QUERY_DPI)?.nullIfEmpty()
|
||||
when {
|
||||
icon != null -> "${if (dpi != null) "icons-$dpi" else "icons"}/$icon"
|
||||
@@ -49,8 +52,12 @@ object PicassoDownloader {
|
||||
if (address == null || path == null) {
|
||||
Downloader.createCall(request.newBuilder(), "", null)
|
||||
} else {
|
||||
Downloader.createCall(request.newBuilder().url(address.toHttpUrl()
|
||||
.newBuilder().addPathSegments(path).build()), authentication.orEmpty(), cache)
|
||||
Downloader.createCall(
|
||||
request.newBuilder().url(
|
||||
address.toHttpUrl()
|
||||
.newBuilder().addPathSegments(path).build()
|
||||
), authentication.orEmpty(), cache
|
||||
)
|
||||
}
|
||||
}
|
||||
HOST_SCREENSHOT -> {
|
||||
@@ -63,10 +70,16 @@ object PicassoDownloader {
|
||||
if (screenshot.isNullOrEmpty() || address.isNullOrEmpty()) {
|
||||
Downloader.createCall(request.newBuilder(), "", null)
|
||||
} else {
|
||||
Downloader.createCall(request.newBuilder().url(address.toHttpUrl()
|
||||
.newBuilder().addPathSegment(packageName.orEmpty()).addPathSegment(locale.orEmpty())
|
||||
.addPathSegment(device.orEmpty()).addPathSegment(screenshot.orEmpty()).build()),
|
||||
authentication.orEmpty(), cache)
|
||||
Downloader.createCall(
|
||||
request.newBuilder().url(
|
||||
address.toHttpUrl()
|
||||
.newBuilder().addPathSegment(packageName.orEmpty())
|
||||
.addPathSegment(locale.orEmpty())
|
||||
.addPathSegment(device.orEmpty())
|
||||
.addPathSegment(screenshot.orEmpty()).build()
|
||||
),
|
||||
authentication.orEmpty(), cache
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -76,29 +89,43 @@ object PicassoDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
fun createScreenshotUri(repository: Repository, packageName: String, screenshot: Product.Screenshot): Uri {
|
||||
fun createScreenshotUri(
|
||||
repository: Repository,
|
||||
packageName: String,
|
||||
screenshot: Product.Screenshot
|
||||
): Uri {
|
||||
return Uri.Builder().scheme("https").authority(HOST_SCREENSHOT)
|
||||
.appendQueryParameter(QUERY_ADDRESS, repository.address)
|
||||
.appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication)
|
||||
.appendQueryParameter(QUERY_PACKAGE_NAME, packageName)
|
||||
.appendQueryParameter(QUERY_LOCALE, screenshot.locale)
|
||||
.appendQueryParameter(QUERY_DEVICE, when (screenshot.type) {
|
||||
.appendQueryParameter(
|
||||
QUERY_DEVICE, when (screenshot.type) {
|
||||
Product.Screenshot.Type.PHONE -> "phoneScreenshots"
|
||||
Product.Screenshot.Type.SMALL_TABLET -> "sevenInchScreenshots"
|
||||
Product.Screenshot.Type.LARGE_TABLET -> "tenInchScreenshots"
|
||||
})
|
||||
}
|
||||
)
|
||||
.appendQueryParameter(QUERY_SCREENSHOT, screenshot.path)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createIconUri(view: View, packageName: String, icon: String, metadataIcon: String, repository: Repository): Uri {
|
||||
fun createIconUri(
|
||||
view: View,
|
||||
packageName: String,
|
||||
icon: String,
|
||||
metadataIcon: String,
|
||||
repository: Repository
|
||||
): Uri {
|
||||
val size = (view.layoutParams.let { min(it.width, it.height) } /
|
||||
view.resources.displayMetrics.density).roundToInt()
|
||||
return createIconUri(view.context, packageName, icon, metadataIcon, size, repository)
|
||||
}
|
||||
|
||||
private fun createIconUri(context: Context, packageName: String, icon: String, metadataIcon: String,
|
||||
targetSizeDp: Int, repository: Repository): Uri {
|
||||
private fun createIconUri(
|
||||
context: Context, packageName: String, icon: String, metadataIcon: String,
|
||||
targetSizeDp: Int, repository: Repository
|
||||
): Uri {
|
||||
return Uri.Builder().scheme("https").authority(HOST_ICON)
|
||||
.appendQueryParameter(QUERY_ADDRESS, repository.address)
|
||||
.appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication)
|
||||
|
@@ -12,8 +12,8 @@ import com.looker.droidify.R
|
||||
import com.looker.droidify.entity.Release
|
||||
import com.looker.droidify.utility.KParcelable
|
||||
import com.looker.droidify.utility.PackageItemResolver
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
||||
|
||||
class MessageDialog() : DialogFragment() {
|
||||
companion object {
|
||||
@@ -22,11 +22,15 @@ class MessageDialog(): DialogFragment() {
|
||||
|
||||
sealed class Message : KParcelable {
|
||||
object DeleteRepositoryConfirm : Message() {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { DeleteRepositoryConfirm }
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator { DeleteRepositoryConfirm }
|
||||
}
|
||||
|
||||
object CantEditSyncing : Message() {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { CantEditSyncing }
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator { CantEditSyncing }
|
||||
}
|
||||
|
||||
class Link(val uri: Uri) : Message() {
|
||||
@@ -35,7 +39,9 @@ class MessageDialog(): DialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator {
|
||||
val uri = Uri.parse(it.readString()!!)
|
||||
Link(uri)
|
||||
}
|
||||
@@ -49,7 +55,9 @@ class MessageDialog(): DialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator {
|
||||
val group = it.readString()
|
||||
val permissions = it.createStringArrayList()!!
|
||||
Permissions(group, permissions)
|
||||
@@ -57,8 +65,10 @@ class MessageDialog(): DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
class ReleaseIncompatible(val incompatibilities: List<Release.Incompatibility>,
|
||||
val platforms: List<String>, val minSdkVersion: Int, val maxSdkVersion: Int): Message() {
|
||||
class ReleaseIncompatible(
|
||||
val incompatibilities: List<Release.Incompatibility>,
|
||||
val platforms: List<String>, val minSdkVersion: Int, val maxSdkVersion: Int
|
||||
) : Message() {
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeInt(incompatibilities.size)
|
||||
for (incompatibility in incompatibilities) {
|
||||
@@ -84,7 +94,9 @@ class MessageDialog(): DialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator {
|
||||
val count = it.readInt()
|
||||
val incompatibilities = generateSequence {
|
||||
when (it.readInt()) {
|
||||
@@ -104,11 +116,15 @@ class MessageDialog(): DialogFragment() {
|
||||
}
|
||||
|
||||
object ReleaseOlder : Message() {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { ReleaseOlder }
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator { ReleaseOlder }
|
||||
}
|
||||
|
||||
object ReleaseSignatureMismatch : Message() {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { ReleaseSignatureMismatch }
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = KParcelable.creator { ReleaseSignatureMismatch }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +170,13 @@ class MessageDialog(): DialogFragment() {
|
||||
val localCache = PackageItemResolver.LocalCache()
|
||||
val title = if (message.group != null) {
|
||||
val name = try {
|
||||
val permissionGroupInfo = packageManager.getPermissionGroupInfo(message.group, 0)
|
||||
PackageItemResolver.loadLabel(requireContext(), localCache, permissionGroupInfo)
|
||||
val permissionGroupInfo =
|
||||
packageManager.getPermissionGroupInfo(message.group, 0)
|
||||
PackageItemResolver.loadLabel(
|
||||
requireContext(),
|
||||
localCache,
|
||||
permissionGroupInfo
|
||||
)
|
||||
?.nullIfEmpty()?.let { if (it == message.group) null else it }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -167,7 +188,11 @@ class MessageDialog(): DialogFragment() {
|
||||
for (permission in message.permissions) {
|
||||
val description = try {
|
||||
val permissionInfo = packageManager.getPermissionInfo(permission, 0)
|
||||
PackageItemResolver.loadDescription(requireContext(), localCache, permissionInfo)
|
||||
PackageItemResolver.loadDescription(
|
||||
requireContext(),
|
||||
localCache,
|
||||
permissionInfo
|
||||
)
|
||||
?.nullIfEmpty()?.let { if (it == permission) null else it }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -190,17 +215,36 @@ class MessageDialog(): DialogFragment() {
|
||||
val maxSdkVersion = if (Release.Incompatibility.MaxSdk in message.incompatibilities)
|
||||
message.maxSdkVersion else null
|
||||
if (minSdkVersion != null || maxSdkVersion != null) {
|
||||
val versionMessage = minSdkVersion?.let { getString(R.string.incompatible_api_min_DESC_FORMAT, it) }
|
||||
?: maxSdkVersion?.let { getString(R.string.incompatible_api_max_DESC_FORMAT, it) }
|
||||
builder.append(getString(R.string.incompatible_api_DESC_FORMAT,
|
||||
Android.name, Android.sdk, versionMessage.orEmpty())).append("\n\n")
|
||||
val versionMessage = minSdkVersion?.let {
|
||||
getString(
|
||||
R.string.incompatible_api_min_DESC_FORMAT,
|
||||
it
|
||||
)
|
||||
}
|
||||
?: maxSdkVersion?.let {
|
||||
getString(
|
||||
R.string.incompatible_api_max_DESC_FORMAT,
|
||||
it
|
||||
)
|
||||
}
|
||||
builder.append(
|
||||
getString(
|
||||
R.string.incompatible_api_DESC_FORMAT,
|
||||
Android.name, Android.sdk, versionMessage.orEmpty()
|
||||
)
|
||||
).append("\n\n")
|
||||
}
|
||||
if (Release.Incompatibility.Platform in message.incompatibilities) {
|
||||
builder.append(getString(R.string.incompatible_platforms_DESC_FORMAT,
|
||||
builder.append(
|
||||
getString(
|
||||
R.string.incompatible_platforms_DESC_FORMAT,
|
||||
Android.primaryPlatform ?: getString(R.string.unknown),
|
||||
message.platforms.joinToString(separator = ", "))).append("\n\n")
|
||||
message.platforms.joinToString(separator = ", ")
|
||||
)
|
||||
).append("\n\n")
|
||||
}
|
||||
val features = message.incompatibilities.mapNotNull { it as? Release.Incompatibility.Feature }
|
||||
val features =
|
||||
message.incompatibilities.mapNotNull { it as? Release.Incompatibility.Feature }
|
||||
if (features.isNotEmpty()) {
|
||||
builder.append(getString(R.string.incompatible_features_DESC))
|
||||
for (feature in features) {
|
||||
|
@@ -15,10 +15,6 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
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 com.looker.droidify.R
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.entity.Product
|
||||
@@ -26,9 +22,13 @@ import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.graphics.PaddingDrawable
|
||||
import com.looker.droidify.network.PicassoDownloader
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||
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
|
||||
|
||||
class ScreenshotsFragment() : DialogFragment() {
|
||||
companion object {
|
||||
@@ -62,8 +62,15 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
|
||||
val window = dialog.window!!
|
||||
val decorView = window.decorView
|
||||
val background = dialog.context.getColorFromAttr(android.R.attr.colorBackground).defaultColor
|
||||
decorView.setBackgroundColor(background.let { ColorUtils.blendARGB(0x00ffffff and it, it, 0.9f) })
|
||||
val background =
|
||||
dialog.context.getColorFromAttr(android.R.attr.colorBackground).defaultColor
|
||||
decorView.setBackgroundColor(background.let {
|
||||
ColorUtils.blendARGB(
|
||||
0x00ffffff and it,
|
||||
it,
|
||||
0.9f
|
||||
)
|
||||
})
|
||||
decorView.setPadding(0, 0, 0, 0)
|
||||
background.let { ColorUtils.blendARGB(0x00ffffff and it, it, 0.8f) }.let {
|
||||
window.statusBarColor = it
|
||||
@@ -73,8 +80,10 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
title = ScreenshotsFragment::class.java.name
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
windowAnimations = run {
|
||||
val typedArray = dialog.context.obtainStyledAttributes(null,
|
||||
intArrayOf(android.R.attr.windowAnimationStyle), android.R.attr.dialogTheme, 0)
|
||||
val typedArray = dialog.context.obtainStyledAttributes(
|
||||
null,
|
||||
intArrayOf(android.R.attr.windowAnimationStyle), android.R.attr.dialogTheme, 0
|
||||
)
|
||||
try {
|
||||
typedArray.getResourceId(0, 0)
|
||||
} finally {
|
||||
@@ -82,15 +91,18 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
}
|
||||
}
|
||||
if (Android.sdk(28)) {
|
||||
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
}
|
||||
|
||||
val hideFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
decorView.systemUiVisibility =
|
||||
decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
val applyHide = Runnable { decorView.systemUiVisibility = decorView.systemUiVisibility or hideFlags }
|
||||
val applyHide =
|
||||
Runnable { decorView.systemUiVisibility = decorView.systemUiVisibility or hideFlags }
|
||||
val handleClick = {
|
||||
decorView.removeCallbacks(applyHide)
|
||||
if ((decorView.systemUiVisibility and hideFlags) == hideFlags) {
|
||||
@@ -108,8 +120,12 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
viewPager.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
(viewPager.adapter as Adapter).size = Pair(viewPager.width, viewPager.height)
|
||||
}
|
||||
dialog.addContentView(viewPager, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
dialog.addContentView(
|
||||
viewPager, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
this.viewPager = viewPager
|
||||
|
||||
var restored = false
|
||||
@@ -117,9 +133,14 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
.concatWith(Database.observable(Database.Subject.Products))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
|
||||
.map { Pair(it.find { it.repositoryId == repositoryId }, Database.RepositoryAdapter.get(repositoryId)) }
|
||||
.map { it ->
|
||||
Pair(
|
||||
it.find { it.repositoryId == repositoryId },
|
||||
Database.RepositoryAdapter.get(repositoryId)
|
||||
)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
.subscribe { it ->
|
||||
val (product, repository) = it
|
||||
val screenshots = product?.screenshots.orEmpty()
|
||||
(viewPager.adapter as Adapter).update(repository, screenshots)
|
||||
@@ -169,10 +190,13 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
val placeholder: Drawable
|
||||
|
||||
init {
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT)
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
val placeholder = itemView.context.getDrawableCompat(R.drawable.ic_photo_camera).mutate()
|
||||
val placeholder =
|
||||
itemView.context.getDrawableCompat(R.drawable.ic_photo_camera).mutate()
|
||||
placeholder.setTint(itemView.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor
|
||||
.let { ColorUtils.blendARGB(0x00ffffff and it, it, 0.25f) })
|
||||
this.placeholder = PaddingDrawable(placeholder, 4f)
|
||||
@@ -208,7 +232,10 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
override fun getItemDescriptor(position: Int): String = screenshots[position].identifier
|
||||
override fun getItemEnumViewType(position: Int): ViewType = ViewType.SCREENSHOT
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: ViewType
|
||||
): RecyclerView.ViewHolder {
|
||||
return ViewHolder(parent.context).apply {
|
||||
itemView.setOnClickListener { onClick() }
|
||||
}
|
||||
@@ -219,7 +246,13 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
val screenshot = screenshots[position]
|
||||
val (width, height) = size
|
||||
if (width > 0 && height > 0) {
|
||||
holder.image.load(PicassoDownloader.createScreenshotUri(repository!!, packageName, screenshot)) {
|
||||
holder.image.load(
|
||||
PicassoDownloader.createScreenshotUri(
|
||||
repository!!,
|
||||
packageName,
|
||||
screenshot
|
||||
)
|
||||
) {
|
||||
placeholder(holder.placeholder)
|
||||
error(holder.placeholder)
|
||||
resize(width, height)
|
||||
|
@@ -6,9 +6,11 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
class Connection<B: IBinder, S: ConnectionService<B>>(private val serviceClass: Class<S>,
|
||||
class Connection<B : IBinder, S : ConnectionService<B>>(
|
||||
private val serviceClass: Class<S>,
|
||||
private val onBind: ((Connection<B, S>, B) -> Unit)? = null,
|
||||
private val onUnbind: ((Connection<B, S>, B) -> Unit)? = null): ServiceConnection {
|
||||
private val onUnbind: ((Connection<B, S>, B) -> Unit)? = null
|
||||
) : ServiceConnection {
|
||||
var binder: B? = null
|
||||
private set
|
||||
|
||||
|
@@ -3,7 +3,7 @@ package com.looker.droidify.service
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
|
||||
abstract class ConnectionService<T : IBinder> : Service() {
|
||||
abstract override fun onBind(intent: Intent): T
|
||||
|
@@ -9,10 +9,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.Common
|
||||
import com.looker.droidify.MainActivity
|
||||
@@ -25,6 +21,10 @@ import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -35,7 +35,8 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
private const val ACTION_OPEN = "${BuildConfig.APPLICATION_ID}.intent.action.OPEN"
|
||||
private const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
private const val EXTRA_CACHE_FILE_NAME = "${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
private const val EXTRA_CACHE_FILE_NAME =
|
||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
|
||||
private val downloadingSubject = PublishSubject.create<State.Downloading>()
|
||||
}
|
||||
@@ -46,17 +47,23 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
when {
|
||||
action.startsWith("$ACTION_OPEN.") -> {
|
||||
val packageName = action.substring(ACTION_OPEN.length + 1)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW).setData(Uri.parse("package:$packageName"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
action.startsWith("$ACTION_INSTALL.") -> {
|
||||
val packageName = action.substring(ACTION_INSTALL.length + 1)
|
||||
val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL).setData(Uri.parse("package:$packageName"))
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,17 +72,24 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
sealed class State(val packageName: String, val name: String) {
|
||||
class Pending(packageName: String, name: String) : State(packageName, name)
|
||||
class Connecting(packageName: String, name: String) : State(packageName, name)
|
||||
class Downloading(packageName: String, name: String, val read: Long, val total: Long?): State(packageName, name)
|
||||
class Success(packageName: String, name: String, val release: Release,
|
||||
val consume: () -> Unit): State(packageName, name)
|
||||
class Downloading(packageName: String, name: String, val read: Long, val total: Long?) :
|
||||
State(packageName, name)
|
||||
|
||||
class Success(
|
||||
packageName: String, name: String, val release: Release,
|
||||
val consume: () -> Unit
|
||||
) : State(packageName, name)
|
||||
|
||||
class Error(packageName: String, name: String) : State(packageName, name)
|
||||
class Cancel(packageName: String, name: String) : State(packageName, name)
|
||||
}
|
||||
|
||||
private val stateSubject = PublishSubject.create<State>()
|
||||
|
||||
private class Task(val packageName: String, val name: String, val release: Release,
|
||||
val url: String, val authentication: String) {
|
||||
private class Task(
|
||||
val packageName: String, val name: String, val release: Release,
|
||||
val url: String, val authentication: String
|
||||
) {
|
||||
val notificationTag: String
|
||||
get() = "download-$packageName"
|
||||
}
|
||||
@@ -92,7 +106,13 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
|
||||
fun enqueue(packageName: String, name: String, repository: Repository, release: Release) {
|
||||
val task = Task(packageName, name, release, release.getDownloadUrl(repository), repository.authentication)
|
||||
val task = Task(
|
||||
packageName,
|
||||
name,
|
||||
release,
|
||||
release.getDownloadUrl(repository),
|
||||
repository.authentication
|
||||
)
|
||||
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
|
||||
publishSuccess(task)
|
||||
} else {
|
||||
@@ -116,7 +136,8 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
|
||||
fun getState(packageName: String): State? = currentTask
|
||||
?.let { if (it.task.packageName == packageName) it.lastState else null }
|
||||
?: tasks.find { it.packageName == packageName }?.let { State.Pending(it.packageName, it.name) }
|
||||
?: tasks.find { it.packageName == packageName }
|
||||
?.let { State.Pending(it.packageName, it.name) }
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
@@ -128,8 +149,10 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_DOWNLOADING,
|
||||
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW)
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_DOWNLOADING,
|
||||
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
@@ -183,33 +206,63 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
|
||||
private fun showNotificationError(task: Task, errorType: ErrorType) {
|
||||
notificationManager.notify(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
notificationManager.notify(task.notificationTag,
|
||||
Common.NOTIFICATION_ID_DOWNLOADING,
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
when (errorType) {
|
||||
is ErrorType.Network -> {
|
||||
setContentTitle(getString(R.string.could_not_download_FORMAT, task.name))
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_download_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(getString(R.string.network_error_DESC))
|
||||
}
|
||||
is ErrorType.Http -> {
|
||||
setContentTitle(getString(R.string.could_not_download_FORMAT, task.name))
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_download_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(getString(R.string.http_error_DESC))
|
||||
}
|
||||
is ErrorType.Validation -> {
|
||||
setContentTitle(getString(R.string.could_not_validate_FORMAT, task.name))
|
||||
setContentText(getString(when (errorType.validateError) {
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_validate_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(
|
||||
getString(
|
||||
when (errorType.validateError) {
|
||||
ValidationError.INTEGRITY -> R.string.integrity_check_error_DESC
|
||||
ValidationError.FORMAT -> R.string.file_format_error_DESC
|
||||
ValidationError.METADATA -> R.string.invalid_metadata_error_DESC
|
||||
ValidationError.SIGNATURE -> R.string.invalid_signature_error_DESC
|
||||
ValidationError.PERMISSIONS -> R.string.invalid_permissions_error_DESC
|
||||
}))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}::class
|
||||
}
|
||||
@@ -217,23 +270,36 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
|
||||
private fun showNotificationInstall(task: Task) {
|
||||
notificationManager.notify(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
notificationManager.notify(
|
||||
task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_INSTALL.${task.packageName}")
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setContentTitle(getString(R.string.downloaded_FORMAT, task.name))
|
||||
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishSuccess(task: Task) {
|
||||
var consumed = false
|
||||
stateSubject.onNext(State.Success(task.packageName, task.name, task.release) { consumed = true })
|
||||
stateSubject.onNext(State.Success(task.packageName, task.name, task.release) {
|
||||
consumed = true
|
||||
})
|
||||
if (!consumed) {
|
||||
showNotificationInstall(task)
|
||||
}
|
||||
@@ -245,7 +311,8 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
val digest = MessageDigest.getInstance(hashType)
|
||||
file.inputStream().use {
|
||||
val bytes = ByteArray(8 * 1024)
|
||||
generateSequence { it.read(bytes) }.takeWhile { it >= 0 }.forEach { digest.update(bytes, 0, it) }
|
||||
generateSequence { it.read(bytes) }.takeWhile { it >= 0 }
|
||||
.forEach { digest.update(bytes, 0, it) }
|
||||
digest.digest().hex()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -255,7 +322,10 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
ValidationError.INTEGRITY
|
||||
} else {
|
||||
val packageInfo = try {
|
||||
packageManager.getPackageArchiveInfo(file.path, Android.PackageManager.signaturesFlag)
|
||||
packageManager.getPackageArchiveInfo(
|
||||
file.path,
|
||||
Android.PackageManager.signaturesFlag
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
@@ -263,14 +333,16 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
if (packageInfo == null) {
|
||||
ValidationError.FORMAT
|
||||
} else if (packageInfo.packageName != task.packageName ||
|
||||
packageInfo.versionCodeCompat != task.release.versionCode) {
|
||||
packageInfo.versionCodeCompat != task.release.versionCode
|
||||
) {
|
||||
ValidationError.METADATA
|
||||
} else {
|
||||
val signature = packageInfo.singleSignature?.let(Utils::calculateHash).orEmpty()
|
||||
if (signature.isEmpty() || signature != task.release.signature) {
|
||||
ValidationError.SIGNATURE
|
||||
} else {
|
||||
val permissions = packageInfo.permissions?.asSequence().orEmpty().map { it.name }.toSet()
|
||||
val permissions =
|
||||
packageInfo.permissions?.asSequence().orEmpty().map { it.name }.toSet()
|
||||
if (!task.release.permissions.containsAll(permissions)) {
|
||||
ValidationError.PERMISSIONS
|
||||
} else {
|
||||
@@ -281,13 +353,23 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy { NotificationCompat
|
||||
private val stateNotificationBuilder by lazy {
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.addAction(
|
||||
0, getString(R.string.cancel), PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask != null) {
|
||||
@@ -329,12 +411,26 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
val initialState = State.Connecting(task.packageName, task.name)
|
||||
stateNotificationBuilder.setWhen(System.currentTimeMillis())
|
||||
publishForegroundState(true, initialState)
|
||||
val partialReleaseFile = Cache.getPartialReleaseFile(this, task.release.cacheFileName)
|
||||
val partialReleaseFile =
|
||||
Cache.getPartialReleaseFile(this, task.release.cacheFileName)
|
||||
lateinit var disposable: Disposable
|
||||
disposable = Downloader
|
||||
.download(task.url, partialReleaseFile, "", "", task.authentication) { read, total ->
|
||||
.download(
|
||||
task.url,
|
||||
partialReleaseFile,
|
||||
"",
|
||||
"",
|
||||
task.authentication
|
||||
) { read, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
downloadingSubject.onNext(State.Downloading(task.packageName, task.name, read, total))
|
||||
downloadingSubject.onNext(
|
||||
State.Downloading(
|
||||
task.packageName,
|
||||
task.name,
|
||||
read,
|
||||
total
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -342,12 +438,16 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
currentTask = null
|
||||
throwable?.printStackTrace()
|
||||
if (result == null || !result.success) {
|
||||
showNotificationError(task, if (result != null) ErrorType.Http else ErrorType.Network)
|
||||
showNotificationError(
|
||||
task,
|
||||
if (result != null) ErrorType.Http else ErrorType.Network
|
||||
)
|
||||
stateSubject.onNext(State.Error(task.packageName, task.name))
|
||||
} else {
|
||||
val validationError = validatePackage(task, partialReleaseFile)
|
||||
if (validationError == null) {
|
||||
val releaseFile = Cache.getReleaseFile(this, task.release.cacheFileName)
|
||||
val releaseFile =
|
||||
Cache.getReleaseFile(this, task.release.cacheFileName)
|
||||
partialReleaseFile.renameTo(releaseFile)
|
||||
publishSuccess(task)
|
||||
} else {
|
||||
|
@@ -12,11 +12,6 @@ import android.text.style.ForegroundColorSpan
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
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.subjects.PublishSubject
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.Common
|
||||
import com.looker.droidify.MainActivity
|
||||
@@ -27,12 +22,19 @@ import com.looker.droidify.entity.ProductItem
|
||||
import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.android.asSequence
|
||||
import com.looker.droidify.utility.extension.android.notificationManager
|
||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||
import com.looker.droidify.utility.extension.text.formatSize
|
||||
import 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.subjects.PublishSubject
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
companion object {
|
||||
@@ -44,14 +46,20 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
|
||||
private sealed class State {
|
||||
data class Connecting(val name: String) : State()
|
||||
data class Syncing(val name: String, val stage: RepositoryUpdater.Stage,
|
||||
val read: Long, val total: Long?): State()
|
||||
data class Syncing(
|
||||
val name: String, val stage: RepositoryUpdater.Stage,
|
||||
val read: Long, val total: Long?
|
||||
) : State()
|
||||
|
||||
object Finishing : State()
|
||||
}
|
||||
|
||||
private class Task(val repositoryId: Long, val manual: Boolean)
|
||||
private data class CurrentTask(val task: Task?, val disposable: Disposable,
|
||||
val hasUpdates: Boolean, val lastState: State)
|
||||
private data class CurrentTask(
|
||||
val task: Task?, val disposable: Disposable,
|
||||
val hasUpdates: Boolean, val lastState: State
|
||||
)
|
||||
|
||||
private enum class Started { NO, AUTO, MANUAL }
|
||||
|
||||
private var started = Started.NO
|
||||
@@ -67,12 +75,15 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
get() = finishSubject
|
||||
|
||||
private fun sync(ids: List<Long>, request: SyncRequest) {
|
||||
val cancelledTask = cancelCurrentTask { request == SyncRequest.FORCE && it.task?.repositoryId in ids }
|
||||
val cancelledTask =
|
||||
cancelCurrentTask { request == SyncRequest.FORCE && it.task?.repositoryId in ids }
|
||||
cancelTasks { !it.manual && it.repositoryId in ids }
|
||||
val currentIds = tasks.asSequence().map { it.repositoryId }.toSet()
|
||||
val manual = request != SyncRequest.AUTO
|
||||
tasks += ids.asSequence().filter { it !in currentIds &&
|
||||
it != currentTask?.task?.repositoryId }.map { Task(it, manual) }
|
||||
tasks += ids.asSequence().filter {
|
||||
it !in currentIds &&
|
||||
it != currentTask?.task?.repositoryId
|
||||
}.map { Task(it, manual) }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
||||
started = Started.MANUAL
|
||||
@@ -146,12 +157,16 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_SYNCING,
|
||||
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW)
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_SYNCING,
|
||||
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_UPDATES,
|
||||
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW)
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_UPDATES,
|
||||
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
@@ -196,13 +211,18 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
|
||||
private fun showNotificationError(repository: Repository, exception: Exception) {
|
||||
notificationManager.notify("repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat
|
||||
notificationManager.notify(
|
||||
"repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentTitle(getString(R.string.could_not_sync_FORMAT, repository.name))
|
||||
.setContentText(getString(when (exception) {
|
||||
.setContentText(
|
||||
getString(
|
||||
when (exception) {
|
||||
is RepositoryUpdater.UpdateException -> when (exception.errorType) {
|
||||
RepositoryUpdater.ErrorType.NETWORK -> R.string.network_error_DESC
|
||||
RepositoryUpdater.ErrorType.HTTP -> R.string.http_error_DESC
|
||||
@@ -210,17 +230,30 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
RepositoryUpdater.ErrorType.PARSING -> R.string.parsing_index_error_DESC
|
||||
}
|
||||
else -> R.string.unknown_error_DESC
|
||||
}))
|
||||
.build())
|
||||
}
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy { NotificationCompat
|
||||
private val stateNotificationBuilder by lazy {
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.addAction(
|
||||
0, getString(R.string.cancel), PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask?.lastState != state) {
|
||||
@@ -239,20 +272,36 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
RepositoryUpdater.Stage.DOWNLOAD -> {
|
||||
if (state.total != null) {
|
||||
setContentText("${state.read.formatSize()} / ${state.total.formatSize()}")
|
||||
setProgress(100, (100f * state.read / state.total).roundToInt(), false)
|
||||
setProgress(
|
||||
100,
|
||||
(100f * state.read / state.total).roundToInt(),
|
||||
false
|
||||
)
|
||||
} else {
|
||||
setContentText(state.read.formatSize())
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
RepositoryUpdater.Stage.PROCESS -> {
|
||||
val progress = state.total?.let { 100f * state.read / it }?.roundToInt()
|
||||
setContentText(getString(R.string.processing_FORMAT, "${progress ?: 0}%"))
|
||||
val progress =
|
||||
state.total?.let { 100f * state.read / it }?.roundToInt()
|
||||
setContentText(
|
||||
getString(
|
||||
R.string.processing_FORMAT,
|
||||
"${progress ?: 0}%"
|
||||
)
|
||||
)
|
||||
setProgress(100, progress ?: 0, progress == null)
|
||||
}
|
||||
RepositoryUpdater.Stage.MERGE -> {
|
||||
val progress = (100f * state.read / (state.total ?: state.read)).roundToInt()
|
||||
setContentText(getString(R.string.merging_FORMAT, "${state.read} / ${state.total ?: state.read}"))
|
||||
val progress = (100f * state.read / (state.total
|
||||
?: state.read)).roundToInt()
|
||||
setContentText(
|
||||
getString(
|
||||
R.string.merging_FORMAT,
|
||||
"${state.read} / ${state.total ?: state.read}"
|
||||
)
|
||||
)
|
||||
setProgress(100, progress, false)
|
||||
}
|
||||
RepositoryUpdater.Stage.COMMIT -> {
|
||||
@@ -283,7 +332,8 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
val repository = Database.RepositoryAdapter.get(task.repositoryId)
|
||||
if (repository != null && repository.enabled) {
|
||||
val lastStarted = started
|
||||
val newStarted = if (task.manual || lastStarted == Started.MANUAL) Started.MANUAL else Started.AUTO
|
||||
val newStarted =
|
||||
if (task.manual || lastStarted == Started.MANUAL) Started.MANUAL else Started.AUTO
|
||||
started = newStarted
|
||||
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
||||
startSelf()
|
||||
@@ -296,7 +346,14 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
disposable = RepositoryUpdater
|
||||
.update(repository, unstable) { stage, progress, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
stateSubject.onNext(State.Syncing(repository.name, stage, progress, total))
|
||||
stateSubject.onNext(
|
||||
State.Syncing(
|
||||
repository.name,
|
||||
stage,
|
||||
progress,
|
||||
total
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -315,9 +372,21 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { Database.ProductAdapter
|
||||
.query(true, true, "", ProductItem.Section.All, ProductItem.Order.NAME, it)
|
||||
.use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } }
|
||||
.querySingle { it ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = "",
|
||||
section = ProductItem.Section.All,
|
||||
order = ProductItem.Order.NAME,
|
||||
signal = it
|
||||
)
|
||||
.use {
|
||||
it.asSequence().map(Database.ProductAdapter::transformItem)
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
@@ -346,26 +415,43 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager.notify(Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
notificationManager.notify(
|
||||
Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(R.string.new_updates_available))
|
||||
.setContentText(resources.getQuantityString(R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size))
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentText(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size
|
||||
)
|
||||
)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
builder.setSpan(
|
||||
ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
builder.append(' ').append(productItem.version)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > maxUpdates) {
|
||||
val summary = getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
val summary =
|
||||
getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
if (Android.sdk(24)) {
|
||||
addLine(summary)
|
||||
} else {
|
||||
@@ -373,13 +459,15 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
}
|
||||
})
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
class Job : JobService() {
|
||||
private var syncParams: JobParameters? = null
|
||||
private var syncDisposable: Disposable? = null
|
||||
private val syncConnection = Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
private val syncConnection =
|
||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
syncDisposable = binder.finish.subscribe {
|
||||
val params = syncParams
|
||||
if (params != null) {
|
||||
|
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import android.content.pm.PackageItemInfo
|
||||
import android.content.pm.PermissionInfo
|
||||
import android.content.res.Resources
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import java.util.Locale
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import java.util.*
|
||||
|
||||
object PackageItemResolver {
|
||||
class LocalCache {
|
||||
@@ -16,8 +16,10 @@ object PackageItemResolver {
|
||||
|
||||
private val cache = mutableMapOf<CacheKey, String?>()
|
||||
|
||||
private fun load(context: Context, localCache: LocalCache, packageName: String,
|
||||
nonLocalized: CharSequence?, resId: Int): CharSequence? {
|
||||
private fun load(
|
||||
context: Context, localCache: LocalCache, packageName: String,
|
||||
nonLocalized: CharSequence?, resId: Int
|
||||
): CharSequence? {
|
||||
return when {
|
||||
nonLocalized != null -> {
|
||||
nonLocalized
|
||||
@@ -36,7 +38,8 @@ object PackageItemResolver {
|
||||
} else {
|
||||
val resources = localCache.resources[packageName] ?: run {
|
||||
val resources = try {
|
||||
val resources = context.packageManager.getResourcesForApplication(packageName)
|
||||
val resources =
|
||||
context.packageManager.getResourcesForApplication(packageName)
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(context.resources.configuration, null)
|
||||
resources
|
||||
@@ -57,14 +60,26 @@ object PackageItemResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLabel(context: Context, localCache: LocalCache, packageItemInfo: PackageItemInfo): CharSequence? {
|
||||
return load(context, localCache, packageItemInfo.packageName,
|
||||
packageItemInfo.nonLocalizedLabel, packageItemInfo.labelRes)
|
||||
fun loadLabel(
|
||||
context: Context,
|
||||
localCache: LocalCache,
|
||||
packageItemInfo: PackageItemInfo
|
||||
): CharSequence? {
|
||||
return load(
|
||||
context, localCache, packageItemInfo.packageName,
|
||||
packageItemInfo.nonLocalizedLabel, packageItemInfo.labelRes
|
||||
)
|
||||
}
|
||||
|
||||
fun loadDescription(context: Context, localCache: LocalCache, permissionInfo: PermissionInfo): CharSequence? {
|
||||
return load(context, localCache, permissionInfo.packageName,
|
||||
permissionInfo.nonLocalizedDescription, permissionInfo.descriptionRes)
|
||||
fun loadDescription(
|
||||
context: Context,
|
||||
localCache: LocalCache,
|
||||
permissionInfo: PermissionInfo
|
||||
): CharSequence? {
|
||||
return load(
|
||||
context, localCache, permissionInfo.packageName,
|
||||
permissionInfo.nonLocalizedDescription, permissionInfo.descriptionRes
|
||||
)
|
||||
}
|
||||
|
||||
fun getPermissionGroup(permissionInfo: PermissionInfo): String? {
|
||||
|
@@ -2,8 +2,10 @@ package com.looker.droidify.utility
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
class ProgressInputStream(private val inputStream: InputStream,
|
||||
private val callback: (Long) -> Unit): InputStream() {
|
||||
class ProgressInputStream(
|
||||
private val inputStream: InputStream,
|
||||
private val callback: (Long) -> Unit
|
||||
) : InputStream() {
|
||||
private var count = 0L
|
||||
|
||||
private inline fun <reified T : Number> notify(one: Boolean, read: () -> T): T {
|
||||
@@ -15,7 +17,9 @@ class ProgressInputStream(private val inputStream: InputStream,
|
||||
|
||||
override fun read(): Int = notify(true) { inputStream.read() }
|
||||
override fun read(b: ByteArray): Int = notify(false) { inputStream.read(b) }
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int = notify(false) { inputStream.read(b, off, len) }
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int =
|
||||
notify(false) { inputStream.read(b, off, len) }
|
||||
|
||||
override fun skip(n: Long): Long = notify(false) { inputStream.skip(n) }
|
||||
|
||||
override fun available(): Int {
|
||||
|
@@ -12,7 +12,8 @@ import okhttp3.Response
|
||||
|
||||
object RxUtils {
|
||||
private class ManagedDisposable(private val cancel: () -> Unit) : Disposable {
|
||||
@Volatile var disposed = false
|
||||
@Volatile
|
||||
var disposed = false
|
||||
override fun isDisposed(): Boolean = disposed
|
||||
|
||||
override fun dispose() {
|
||||
@@ -21,7 +22,11 @@ object RxUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T, R> managedSingle(create: () -> T, cancel: (T) -> Unit, execute: (T) -> R): Single<R> {
|
||||
private fun <T, R> managedSingle(
|
||||
create: () -> T,
|
||||
cancel: (T) -> Unit,
|
||||
execute: (T) -> R
|
||||
): Single<R> {
|
||||
return Single.create {
|
||||
val task = create()
|
||||
val thread = Thread.currentThread()
|
||||
@@ -53,7 +58,7 @@ object RxUtils {
|
||||
}
|
||||
|
||||
fun <R> managedSingle(execute: () -> R): Single<R> {
|
||||
return managedSingle({ Unit }, { }, { execute() })
|
||||
return managedSingle({ }, { }, { execute() })
|
||||
}
|
||||
|
||||
fun callSingle(create: () -> Call): Single<Response> {
|
||||
|
@@ -9,13 +9,14 @@ import android.os.LocaleList
|
||||
import android.provider.Settings
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||
import com.looker.droidify.utility.extension.resources.getDrawableCompat
|
||||
import com.looker.droidify.utility.extension.text.hex
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
object Utils {
|
||||
private fun createDefaultApplicationIcon(context: Context, tintAttrResId: Int): Drawable {
|
||||
@@ -24,19 +25,22 @@ object Utils {
|
||||
}
|
||||
|
||||
fun getDefaultApplicationIcons(context: Context): Pair<Drawable, Drawable> {
|
||||
val progressIcon: Drawable = createDefaultApplicationIcon(context, android.R.attr.textColorSecondary)
|
||||
val defaultIcon: Drawable = createDefaultApplicationIcon(context, android.R.attr.colorAccent)
|
||||
val progressIcon: Drawable =
|
||||
createDefaultApplicationIcon(context, android.R.attr.textColorSecondary)
|
||||
val defaultIcon: Drawable =
|
||||
createDefaultApplicationIcon(context, android.R.attr.colorAccent)
|
||||
return Pair(progressIcon, defaultIcon)
|
||||
}
|
||||
|
||||
fun getToolbarIcon(context: Context, resId: Int): Drawable {
|
||||
val drawable = context.getDrawableCompat(resId).mutate()
|
||||
drawable.setTintList(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
drawable.setTintList(context.getColorFromAttr(android.R.attr.titleTextColor))
|
||||
return drawable
|
||||
}
|
||||
|
||||
fun calculateHash(signature: Signature): String? {
|
||||
return MessageDigest.getInstance("MD5").digest(signature.toCharsString().toByteArray()).hex()
|
||||
fun calculateHash(signature: Signature): String {
|
||||
return MessageDigest.getInstance("MD5").digest(signature.toCharsString().toByteArray())
|
||||
.hex()
|
||||
}
|
||||
|
||||
fun calculateFingerprint(certificate: Certificate): String {
|
||||
@@ -94,7 +98,11 @@ object Utils {
|
||||
return if (Android.sdk(26)) {
|
||||
ValueAnimator.areAnimatorsEnabled()
|
||||
} else {
|
||||
Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f
|
||||
Settings.Global.getFloat(
|
||||
context.contentResolver,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f
|
||||
) != 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
|
||||
package com.looker.droidify.utility.extension.android
|
||||
|
||||
import android.app.NotificationManager
|
||||
@@ -49,7 +50,8 @@ object Android {
|
||||
val platforms = Build.SUPPORTED_ABIS.toSet()
|
||||
|
||||
val primaryPlatform: String?
|
||||
get() = Build.SUPPORTED_64_BIT_ABIS?.firstOrNull() ?: Build.SUPPORTED_32_BIT_ABIS?.firstOrNull()
|
||||
get() = Build.SUPPORTED_64_BIT_ABIS?.firstOrNull()
|
||||
?: Build.SUPPORTED_32_BIT_ABIS?.firstOrNull()
|
||||
|
||||
fun sdk(sdk: Int): Boolean {
|
||||
return Build.VERSION.SDK_INT >= sdk
|
||||
|
@@ -1,11 +1,8 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
|
||||
package com.looker.droidify.utility.extension.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.core.*
|
||||
|
||||
object Json {
|
||||
val factory = JsonFactory()
|
||||
@@ -62,7 +59,10 @@ fun JsonParser.forEach(requiredToken: JsonToken, callback: JsonParser.() -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> JsonParser.collectNotNull(requiredToken: JsonToken, callback: JsonParser.() -> T?): List<T> {
|
||||
fun <T> JsonParser.collectNotNull(
|
||||
requiredToken: JsonToken,
|
||||
callback: JsonParser.() -> T?
|
||||
): List<T> {
|
||||
val list = mutableListOf<T>()
|
||||
forEach(requiredToken) {
|
||||
val result = callback()
|
||||
|
@@ -1,4 +1,5 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
|
||||
package com.looker.droidify.utility.extension.resources
|
||||
|
||||
import android.content.Context
|
||||
@@ -16,11 +17,11 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.RequestCreator
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import kotlin.math.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object TypefaceExtra {
|
||||
val medium = Typeface.create("sans-serif-medium", Typeface.NORMAL)!!
|
||||
@@ -31,12 +32,17 @@ fun Context.getDrawableCompat(resId: Int): Drawable {
|
||||
val drawable = if (!Android.sdk(24)) {
|
||||
val fileName = TypedValue().apply { resources.getValue(resId, this, true) }.string
|
||||
if (fileName.endsWith(".xml")) {
|
||||
resources.getXml(resId).use {
|
||||
resources.getXml(resId).use { it ->
|
||||
val eventType = generateSequence { it.next() }
|
||||
.find { it == XmlPullParser.START_TAG || it == XmlPullParser.END_DOCUMENT }
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
when (it.name) {
|
||||
"vector" -> VectorDrawableCompat.createFromXmlInner(resources, it, Xml.asAttributeSet(it), theme)
|
||||
"vector" -> VectorDrawableCompat.createFromXmlInner(
|
||||
resources,
|
||||
it,
|
||||
Xml.asAttributeSet(it),
|
||||
theme
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
|
||||
package com.looker.droidify.utility.extension.text
|
||||
|
||||
import android.util.Log
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
fun <T : CharSequence> T.nullIfEmpty(): T? {
|
||||
return if (isNullOrEmpty()) null else this
|
||||
@@ -11,8 +12,10 @@ fun <T: CharSequence> T.nullIfEmpty(): T? {
|
||||
private val sizeFormats = listOf("%.0f B", "%.0f kB", "%.1f MB", "%.2f GB")
|
||||
|
||||
fun Long.formatSize(): String {
|
||||
val (size, index) = generateSequence(Pair(this.toFloat(), 0)) { (size, index) -> if (size >= 1000f)
|
||||
Pair(size / 1000f, index + 1) else null }.take(sizeFormats.size).last()
|
||||
val (size, index) = generateSequence(Pair(this.toFloat(), 0)) { (size, index) ->
|
||||
if (size >= 1000f)
|
||||
Pair(size / 1000f, index + 1) else null
|
||||
}.take(sizeFormats.size).last()
|
||||
return sizeFormats[index].format(Locale.US, size)
|
||||
}
|
||||
|
||||
|
@@ -38,11 +38,30 @@ object ClickableMovementMethod: MovementMethod {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(widget: TextView, text: Spannable, keyCode: Int, event: KeyEvent): Boolean = false
|
||||
override fun onKeyUp(widget: TextView, text: Spannable, keyCode: Int, event: KeyEvent): Boolean = false
|
||||
override fun onKeyDown(
|
||||
widget: TextView,
|
||||
text: Spannable,
|
||||
keyCode: Int,
|
||||
event: KeyEvent
|
||||
): Boolean = false
|
||||
|
||||
override fun onKeyUp(
|
||||
widget: TextView,
|
||||
text: Spannable,
|
||||
keyCode: Int,
|
||||
event: KeyEvent
|
||||
): Boolean = false
|
||||
|
||||
override fun onKeyOther(view: TextView, text: Spannable, event: KeyEvent): Boolean = false
|
||||
override fun onTakeFocus(widget: TextView, text: Spannable, direction: Int) = Unit
|
||||
override fun onTrackballEvent(widget: TextView, text: Spannable, event: MotionEvent): Boolean = false
|
||||
override fun onGenericMotionEvent(widget: TextView, text: Spannable, event: MotionEvent): Boolean = false
|
||||
override fun onTrackballEvent(widget: TextView, text: Spannable, event: MotionEvent): Boolean =
|
||||
false
|
||||
|
||||
override fun onGenericMotionEvent(
|
||||
widget: TextView,
|
||||
text: Spannable,
|
||||
event: MotionEvent
|
||||
): Boolean = false
|
||||
|
||||
override fun canSelectArbitrarily(): Boolean = false
|
||||
}
|
||||
|
@@ -3,7 +3,8 @@ package com.looker.droidify.widget
|
||||
import android.database.Cursor
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class CursorRecyclerAdapter<VT: Enum<VT>, VH: RecyclerView.ViewHolder>: EnumRecyclerAdapter<VT, VH>() {
|
||||
abstract class CursorRecyclerAdapter<VT : Enum<VT>, VH : RecyclerView.ViewHolder> :
|
||||
EnumRecyclerAdapter<VT, VH>() {
|
||||
init {
|
||||
super.setHasStableIds(true)
|
||||
}
|
||||
|
@@ -6,11 +6,15 @@ import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import kotlin.math.*
|
||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DividerItemDecoration(context: Context, private val configure: (context: Context,
|
||||
position: Int, configuration: Configuration) -> Unit): RecyclerView.ItemDecoration() {
|
||||
class DividerItemDecoration(
|
||||
context: Context, private val configure: (
|
||||
context: Context,
|
||||
position: Int, configuration: Configuration
|
||||
) -> Unit
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
interface Configuration {
|
||||
fun set(needDivider: Boolean, toTop: Boolean, paddingStart: Int, paddingEnd: Int)
|
||||
}
|
||||
@@ -39,7 +43,14 @@ class DividerItemDecoration(context: Context, private val configure: (context: C
|
||||
private val divider = context.getDrawableFromAttr(android.R.attr.listDivider)
|
||||
private val bounds = Rect()
|
||||
|
||||
private fun draw(c: Canvas, configuration: ConfigurationHolder, view: View, top: Int, width: Int, rtl: Boolean) {
|
||||
private fun draw(
|
||||
c: Canvas,
|
||||
configuration: ConfigurationHolder,
|
||||
view: View,
|
||||
top: Int,
|
||||
width: Int,
|
||||
rtl: Boolean
|
||||
) {
|
||||
val divider = divider
|
||||
val left = if (rtl) configuration.paddingEnd else configuration.paddingStart
|
||||
val right = width - (if (rtl) configuration.paddingStart else configuration.paddingEnd)
|
||||
@@ -66,17 +77,36 @@ class DividerItemDecoration(context: Context, private val configure: (context: C
|
||||
parent.findViewHolderForAdapterPosition(position + 1)?.itemView else null
|
||||
if (toTopView != null) {
|
||||
parent.getDecoratedBoundsWithMargins(toTopView, bounds)
|
||||
draw(c, configuration, toTopView, bounds.top - divider.intrinsicHeight, parent.width, rtl)
|
||||
draw(
|
||||
c,
|
||||
configuration,
|
||||
toTopView,
|
||||
bounds.top - divider.intrinsicHeight,
|
||||
parent.width,
|
||||
rtl
|
||||
)
|
||||
} else {
|
||||
parent.getDecoratedBoundsWithMargins(view, bounds)
|
||||
draw(c, configuration, view, bounds.bottom - divider.intrinsicHeight, parent.width, rtl)
|
||||
draw(
|
||||
c,
|
||||
configuration,
|
||||
view,
|
||||
bounds.bottom - divider.intrinsicHeight,
|
||||
parent.width,
|
||||
rtl
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val configuration = view.configuration
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
if (position >= 0) {
|
||||
|
@@ -4,7 +4,8 @@ import android.util.SparseArray
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class EnumRecyclerAdapter<VT: Enum<VT>, VH: RecyclerView.ViewHolder>: RecyclerView.Adapter<VH>() {
|
||||
abstract class EnumRecyclerAdapter<VT : Enum<VT>, VH : RecyclerView.ViewHolder> :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
abstract val viewTypeClass: Class<VT>
|
||||
|
||||
private val names = SparseArray<String>()
|
||||
|
@@ -8,7 +8,11 @@ import android.widget.SearchView
|
||||
class FocusSearchView : SearchView {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
var allowFocus = true
|
||||
|
||||
|
@@ -7,7 +7,11 @@ import android.widget.LinearLayout
|
||||
class FragmentLinearLayout : LinearLayout {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
init {
|
||||
fitsSystemWindows = true
|
||||
|
@@ -8,8 +8,10 @@ import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import kotlin.math.*
|
||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
@@ -22,11 +24,17 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
private val statePressed = intArrayOf(android.R.attr.state_pressed)
|
||||
}
|
||||
|
||||
private val thumbDrawable = recyclerView.context.getDrawableFromAttr(android.R.attr.fastScrollThumbDrawable)
|
||||
private val trackDrawable = recyclerView.context.getDrawableFromAttr(android.R.attr.fastScrollTrackDrawable)
|
||||
private val thumbDrawable =
|
||||
recyclerView.context.getDrawableFromAttr(android.R.attr.fastScrollThumbDrawable)
|
||||
private val trackDrawable =
|
||||
recyclerView.context.getDrawableFromAttr(android.R.attr.fastScrollTrackDrawable)
|
||||
private val minTrackSize = recyclerView.resources.sizeScaled(16)
|
||||
|
||||
private data class FastScrolling(val startAtThumbOffset: Float?, val startY: Float, val currentY: Float)
|
||||
private data class FastScrolling(
|
||||
val startAtThumbOffset: Float?,
|
||||
val startY: Float,
|
||||
val currentY: Float
|
||||
)
|
||||
|
||||
private var scrolling = false
|
||||
private var fastScrolling: FastScrolling? = null
|
||||
@@ -79,12 +87,17 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
private inline fun withScroll(callback: (itemHeight: Int, thumbHeight: Int, range: Int) -> Unit): Boolean {
|
||||
val count = recyclerView.adapter?.itemCount ?: 0
|
||||
return count > 0 && run {
|
||||
val itemHeight = Rect().apply { recyclerView
|
||||
.getDecoratedBoundsWithMargins(recyclerView.getChildAt(0), this) }.height()
|
||||
val itemHeight = Rect().apply {
|
||||
recyclerView
|
||||
.getDecoratedBoundsWithMargins(recyclerView.getChildAt(0), this)
|
||||
}.height()
|
||||
val scrollCount = count - recyclerView.height / itemHeight
|
||||
scrollCount > 0 && run {
|
||||
val range = count * itemHeight
|
||||
val thumbHeight = max(recyclerView.height * recyclerView.height / range, thumbDrawable.intrinsicHeight)
|
||||
val thumbHeight = max(
|
||||
recyclerView.height * recyclerView.height / range,
|
||||
thumbDrawable.intrinsicHeight
|
||||
)
|
||||
range >= recyclerView.height * 2 && run {
|
||||
callback(itemHeight, thumbHeight, range)
|
||||
true
|
||||
@@ -98,7 +111,10 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
(fastScrolling.startAtThumbOffset + (fastScrolling.currentY - fastScrolling.startY) /
|
||||
(recyclerView.height - thumbHeight)).coerceIn(0f, 1f)
|
||||
} else {
|
||||
((fastScrolling.currentY - thumbHeight / 2f) / (recyclerView.height - thumbHeight)).coerceIn(0f, 1f)
|
||||
((fastScrolling.currentY - thumbHeight / 2f) / (recyclerView.height - thumbHeight)).coerceIn(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +126,12 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
return scrollPosition.toFloat() / (range - recyclerView.height)
|
||||
}
|
||||
|
||||
private fun scroll(itemHeight: Int, thumbHeight: Int, range: Int, fastScrolling: FastScrolling) {
|
||||
private fun scroll(
|
||||
itemHeight: Int,
|
||||
thumbHeight: Int,
|
||||
range: Int,
|
||||
fastScrolling: FastScrolling
|
||||
) {
|
||||
val offset = calculateOffset(thumbHeight, fastScrolling)
|
||||
val scrollPosition = ((range - recyclerView.height) * offset).roundToInt()
|
||||
val position = scrollPosition / itemHeight
|
||||
@@ -131,15 +152,22 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
}
|
||||
event.action == MotionEvent.ACTION_DOWN -> {
|
||||
val rtl = recyclerView.layoutDirection == RecyclerView.LAYOUT_DIRECTION_RTL
|
||||
val trackWidth = max(minTrackSize, max(thumbDrawable.intrinsicWidth, trackDrawable.intrinsicWidth))
|
||||
val atThumbVertical = if (rtl) event.x <= trackWidth else event.x >= recyclerView.width - trackWidth
|
||||
val trackWidth = max(
|
||||
minTrackSize,
|
||||
max(thumbDrawable.intrinsicWidth, trackDrawable.intrinsicWidth)
|
||||
)
|
||||
val atThumbVertical =
|
||||
if (rtl) event.x <= trackWidth else event.x >= recyclerView.width - trackWidth
|
||||
atThumbVertical && run {
|
||||
withScroll { itemHeight, thumbHeight, range ->
|
||||
(recyclerView.parent as? ViewGroup)?.requestDisallowInterceptTouchEvent(true)
|
||||
(recyclerView.parent as? ViewGroup)?.requestDisallowInterceptTouchEvent(
|
||||
true
|
||||
)
|
||||
val offset = currentOffset(itemHeight, range)
|
||||
val thumbY = ((recyclerView.height - thumbHeight) * offset).roundToInt()
|
||||
val atThumb = event.y >= thumbY && event.y <= thumbY + thumbHeight
|
||||
val fastScrolling = FastScrolling(if (atThumb) offset else null, event.y, event.y)
|
||||
val fastScrolling =
|
||||
FastScrolling(if (atThumb) offset else null, event.y, event.y)
|
||||
scroll(itemHeight, thumbHeight, range, fastScrolling)
|
||||
updateState(scrolling, fastScrolling)
|
||||
recyclerView.invalidate()
|
||||
@@ -153,9 +181,12 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
updateState(scrolling, fastScrolling)
|
||||
recyclerView.invalidate()
|
||||
}
|
||||
val cancel = event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
|
||||
val cancel =
|
||||
event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
|
||||
if (!success || cancel) {
|
||||
(recyclerView.parent as? ViewGroup)?.requestDisallowInterceptTouchEvent(false)
|
||||
(recyclerView.parent as? ViewGroup)?.requestDisallowInterceptTouchEvent(
|
||||
false
|
||||
)
|
||||
updateState(scrolling, null)
|
||||
recyclerView.invalidate()
|
||||
}
|
||||
@@ -212,21 +243,29 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
trackDrawable.state = if (fastScrolling != null) statePressed else stateNormal
|
||||
val trackExtra = (maxWidth - trackDrawable.intrinsicWidth) / 2
|
||||
if (rtl) {
|
||||
trackDrawable.setBounds(trackExtra - translateX, 0,
|
||||
trackExtra + trackDrawable.intrinsicWidth - translateX, recyclerView.height)
|
||||
trackDrawable.setBounds(
|
||||
trackExtra - translateX, 0,
|
||||
trackExtra + trackDrawable.intrinsicWidth - translateX, recyclerView.height
|
||||
)
|
||||
} else {
|
||||
trackDrawable.setBounds(recyclerView.width - trackExtra - trackDrawable.intrinsicWidth + translateX,
|
||||
0, recyclerView.width - trackExtra + translateX, recyclerView.height)
|
||||
trackDrawable.setBounds(
|
||||
recyclerView.width - trackExtra - trackDrawable.intrinsicWidth + translateX,
|
||||
0, recyclerView.width - trackExtra + translateX, recyclerView.height
|
||||
)
|
||||
}
|
||||
trackDrawable.draw(canvas)
|
||||
val thumbExtra = (maxWidth - thumbDrawable.intrinsicWidth) / 2
|
||||
thumbDrawable.state = if (fastScrolling != null) statePressed else stateNormal
|
||||
if (rtl) {
|
||||
thumbDrawable.setBounds(thumbExtra - translateX, thumbY,
|
||||
thumbExtra + thumbDrawable.intrinsicWidth - translateX, thumbY + thumbHeight)
|
||||
thumbDrawable.setBounds(
|
||||
thumbExtra - translateX, thumbY,
|
||||
thumbExtra + thumbDrawable.intrinsicWidth - translateX, thumbY + thumbHeight
|
||||
)
|
||||
} else {
|
||||
thumbDrawable.setBounds(recyclerView.width - thumbExtra - thumbDrawable.intrinsicWidth + translateX,
|
||||
thumbY, recyclerView.width - thumbExtra + translateX, thumbY + thumbHeight)
|
||||
thumbDrawable.setBounds(
|
||||
recyclerView.width - thumbExtra - thumbDrawable.intrinsicWidth + translateX,
|
||||
thumbY, recyclerView.width - thumbExtra + translateX, thumbY + thumbHeight
|
||||
)
|
||||
}
|
||||
thumbDrawable.draw(canvas)
|
||||
}
|
||||
@@ -241,7 +280,8 @@ class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
recyclerView.addOnScrollListener(scrollListener)
|
||||
recyclerView.addOnItemTouchListener(touchListener)
|
||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) = handleDraw(c)
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) =
|
||||
handleDraw(c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,8 @@ package com.looker.droidify.widget
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class StableRecyclerAdapter<VT: Enum<VT>, VH: RecyclerView.ViewHolder>: EnumRecyclerAdapter<VT, VH>() {
|
||||
abstract class StableRecyclerAdapter<VT : Enum<VT>, VH : RecyclerView.ViewHolder> :
|
||||
EnumRecyclerAdapter<VT, VH>() {
|
||||
private var nextId = 1L
|
||||
private val descriptorToId = mutableMapOf<String, Long>()
|
||||
|
||||
|
@@ -7,9 +7,16 @@ import android.widget.Toolbar
|
||||
class Toolbar : Toolbar {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int,
|
||||
defStyleRes: Int): super(context, attrs, defStyleAttr, defStyleRes)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
constructor(
|
||||
context: Context, attrs: AttributeSet?, defStyleAttr: Int,
|
||||
defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
private var initalized = false
|
||||
private var layoutDirectionChanged: Int? = null
|
||||
@@ -18,9 +25,7 @@ class Toolbar: Toolbar {
|
||||
initalized = true
|
||||
val layoutDirection = layoutDirectionChanged
|
||||
layoutDirectionChanged = null
|
||||
if (layoutDirection != null) {
|
||||
onRtlPropertiesChanged(layoutDirection)
|
||||
}
|
||||
if (layoutDirection != null) onRtlPropertiesChanged(layoutDirection)
|
||||
}
|
||||
|
||||
override fun onRtlPropertiesChanged(layoutDirection: Int) {
|
||||
|
Reference in New Issue
Block a user