Reformated all the code

This commit is contained in:
Mohit
2021-06-08 21:18:44 +05:30
parent 74e8287cf1
commit 29ef88853d
50 changed files with 6043 additions and 4857 deletions

View File

@@ -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)
}
}

View File

@@ -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)) {

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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}")

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -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
)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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 {

View File

@@ -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
)
}
}
}

View File

@@ -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", ""
)
})
}
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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()
)
}
}
}
}
}
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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? {

View File

@@ -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 {

View File

@@ -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> {

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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>()

View File

@@ -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

View File

@@ -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

View File

@@ -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)
})
}
}

View File

@@ -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>()

View File

@@ -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) {