Properly handle multiple suggested versions

This commit is contained in:
kitsunyan
2020-07-29 20:51:05 +03:00
parent b8fb749589
commit 161085ada1
8 changed files with 105 additions and 84 deletions
@@ -78,7 +78,7 @@ object Database {
const val ROW_ADDED = "added"
const val ROW_UPDATED = "updated"
const val ROW_VERSION_CODE = "version_code"
const val ROW_SIGNATURE = "signature"
const val ROW_SIGNATURES = "signatures"
const val ROW_COMPATIBLE = "compatible"
const val ROW_DATA = "data"
const val ROW_DATA_ITEM = "data_item"
@@ -93,7 +93,7 @@ object Database {
$ROW_ADDED INTEGER NOT NULL,
$ROW_UPDATED INTEGER NOT NULL,
$ROW_VERSION_CODE INTEGER NOT NULL,
$ROW_SIGNATURE TEXT NOT NULL,
$ROW_SIGNATURES TEXT NOT NULL,
$ROW_COMPATIBLE INTEGER NOT NULL,
$ROW_DATA BLOB NOT NULL,
$ROW_DATA_ITEM BLOB NOT NULL,
@@ -393,17 +393,20 @@ object Database {
category: String, order: ProductItem.Order, signal: CancellationSignal?): Cursor {
val builder = QueryBuilder()
val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND
product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND
product.${Schema.Product.ROW_SIGNATURES} != ''"""
builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID},
product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME},
product.${Schema.Product.ROW_SUMMARY}, installed.${Schema.Installed.ROW_VERSION},
(COALESCE(lock.${Schema.Lock.ROW_VERSION_CODE}, -1) NOT IN (0, product.${Schema.Product.ROW_VERSION_CODE}) AND
product.${Schema.Product.ROW_COMPATIBLE} != 0 AND product.${Schema.Product.ROW_VERSION_CODE} >
COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND
product.${Schema.Product.ROW_SIGNATURE} = installed.${Schema.Installed.ROW_SIGNATURE} AND
product.${Schema.Product.ROW_SIGNATURE} != '') AS ${Schema.Synthetic.ROW_CAN_UPDATE},
product.${Schema.Product.ROW_COMPATIBLE}, product.${Schema.Product.ROW_DATA_ITEM},
MAX((product.${Schema.Product.ROW_COMPATIBLE} << 32) | product.${Schema.Product.ROW_VERSION_CODE})
FROM ${Schema.Product.name} AS product"""
COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
AS ${Schema.Synthetic.ROW_CAN_UPDATE}, product.${Schema.Product.ROW_COMPATIBLE},
product.${Schema.Product.ROW_DATA_ITEM}, MAX((product.${Schema.Product.ROW_COMPATIBLE} AND
(installed.${Schema.Installed.ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
PRINTF('%016X', product.${Schema.Product.ROW_VERSION_CODE})) FROM ${Schema.Product.name} AS product"""
builder += """JOIN ${Schema.Repository.name} AS repository
ON product.${Schema.Product.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID}"""
@@ -574,6 +577,9 @@ object Database {
db.beginTransaction()
try {
for (product in products) {
// Format signatures like ".signature1.signature2." for easier select
val signatures = product.signatures.joinToString { ".$it" }
.let { if (it.isNotEmpty()) "$it." else "" }
db.insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply {
put(Schema.Product.ROW_REPOSITORY_ID, product.repositoryId)
put(Schema.Product.ROW_PACKAGE_NAME, product.packageName)
@@ -582,7 +588,7 @@ object Database {
put(Schema.Product.ROW_ADDED, product.added)
put(Schema.Product.ROW_UPDATED, product.updated)
put(Schema.Product.ROW_VERSION_CODE, product.versionCode)
put(Schema.Product.ROW_SIGNATURE, product.signature)
put(Schema.Product.ROW_SIGNATURES, signatures)
put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0)
put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize))
put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize))
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import nya.kitsunyan.foxydroid.utility.extension.json.*
import nya.kitsunyan.foxydroid.utility.extension.text.*
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 author: Author,
@@ -33,23 +34,24 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
get() = "$locale.${type.name}.$path"
}
val selectedRelease: Release?
get() = releases.find { it.selected }
// Same releases with different signatures
val selectedReleases: List<Release>
get() = releases.filter { it.selected }
val displayRelease: Release?
get() = selectedRelease ?: releases.firstOrNull()
get() = selectedReleases.firstOrNull() ?: releases.firstOrNull()
val version: String
get() = displayRelease?.version.orEmpty()
val versionCode: Long
get() = selectedRelease?.versionCode ?: 0L
get() = selectedReleases.firstOrNull()?.versionCode ?: 0L
val compatible: Boolean
get() = selectedRelease?.incompatibilities?.isEmpty() == true
get() = selectedReleases.firstOrNull()?.incompatibilities?.isEmpty() == true
val signature: String
get() = selectedRelease?.signature.orEmpty()
val signatures: List<String>
get() = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct().toList()
fun item(): ProductItem {
return ProductItem(repositoryId, packageName, name, summary, icon, version, "", compatible, false)
@@ -57,7 +59,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
fun canUpdate(installedItem: InstalledItem?): Boolean {
return installedItem != null && compatible && versionCode > installedItem.versionCode &&
signature.isNotEmpty() && signature == installedItem.signature
installedItem.signature in signatures
}
fun serialize(generator: JsonGenerator) {
@@ -126,8 +128,9 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
}
companion object {
fun <T> findSuggested(products: List<T>, extract: (T) -> Product): T? {
return products.maxWith(compareBy({ extract(it).compatible }, { extract(it).versionCode }))
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 deserialize(repositoryId: Long, parser: JsonParser): Product {
@@ -17,7 +17,7 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
object MinSdk: Incompatibility()
object MaxSdk: Incompatibility()
object Platform: Incompatibility()
class Feature(val feature: String): Incompatibility()
data class Feature(val feature: String): Incompatibility()
}
val identifier: String
@@ -326,40 +326,20 @@ object RepositoryUpdater {
if (it.platforms.isNotEmpty() && it.platforms.intersect(Android.platforms).isEmpty()) {
incompatibilities += Release.Incompatibility.Platform
}
incompatibilities += (it.features - features).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 releaseIndex = if (firstCompatibleReleaseIndex >= 0) {
val versionCode = releasePairs[firstCompatibleReleaseIndex].first.versionCode
val candidates = releasePairs.mapIndexedNotNull { index, (product, incompatibilities) ->
if (product.versionCode == versionCode && incompatibilities.isEmpty()) index else null
}
if (product.packageName == "org.telegram.messenger") {
val x = candidates
.filter { releasePairs[it].first.platforms.contains(Android.primaryPlatform) }
.minBy { releasePairs[it].first.platforms.size }
val y = candidates.minBy { releasePairs[it].first.platforms.size }
debug("telegram $firstCompatibleReleaseIndex $candidates $x $y")
}
if (candidates.size <= 1) {
firstCompatibleReleaseIndex
} else {
candidates
.filter { releasePairs[it].first.platforms.contains(Android.primaryPlatform) }
.minBy { releasePairs[it].first.platforms.size }
?: candidates.minBy { releasePairs[it].first.platforms.size }
?: firstCompatibleReleaseIndex
}
} else {
val firstReleaseIndex = if (firstCompatibleReleaseIndex >= 0) firstCompatibleReleaseIndex else
releasePairs.indexOfFirst { predicate(it.first) }
}
val firstSelected = if (firstReleaseIndex >= 0) releasePairs[firstReleaseIndex] else null
val releases = releasePairs.mapIndexed { index, (release, incompatibilities) -> release
.copy(incompatibilities = incompatibilities, selected = index == releaseIndex) }
val releases = releasePairs.map { (release, incompatibilities) -> release
.copy(incompatibilities = incompatibilities, selected = firstSelected
?.let { it.first.versionCode == release.versionCode && it.second == incompatibilities } == true) }
return product.copy(releases = releases)
}
}
@@ -249,7 +249,8 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
get() = ViewType.SCREENSHOT
}
class ReleaseItem(val repository: Repository, val release: Release, val selectedRepository: Boolean): Item() {
class ReleaseItem(val repository: Repository, val release: Release, val selectedRepository: Boolean,
val showSignature: Boolean): Item() {
override val descriptor: String
get() = "release.${repository.id}.${release.identifier}"
@@ -459,10 +460,11 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
val source = itemView.findViewById<TextView>(R.id.source)!!
val added = itemView.findViewById<TextView>(R.id.added)!!
val size = itemView.findViewById<TextView>(R.id.size)!!
val signature = itemView.findViewById<TextView>(R.id.signature)!!
val compatibility = itemView.findViewById<TextView>(R.id.compatibility)!!
val statefulViews: Sequence<View>
get() = sequenceOf(itemView, version, status, source, added, size, compatibility)
get() = sequenceOf(itemView, version, status, source, added, size, signature, compatibility)
val setStatusActive: (Boolean) -> Unit
@@ -575,14 +577,23 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
private val items = mutableListOf<Item>()
private val expanded = mutableSetOf<ExpandType>()
private var product: Product? = null
private var installedItem: InstalledItem? = null
fun setProducts(context: Context, products: List<Pair<Product, Repository>>, packageName: String) {
val productRepository = Product.findSuggested(products) { it.first }
fun setProducts(context: Context, packageName: String,
products: List<Pair<Product, Repository>>, installedItem: InstalledItem?) {
val productRepository = Product.findSuggested(products, installedItem) { it.first }
items.clear()
if (productRepository != null) {
items += Item.HeaderItem(productRepository.second, productRepository.first)
if (installedItem != null) {
items.add(Item.SwitchItem(SwitchType.IGNORE_ALL_UPDATES, packageName, productRepository.first.versionCode))
if (productRepository.first.canUpdate(installedItem)) {
items.add(Item.SwitchItem(SwitchType.IGNORE_THIS_UPDATE, packageName, productRepository.first.versionCode))
}
}
val textViewHolder = TextViewHolder(context)
val textViewWidthSpec = context.resources.displayMetrics.widthPixels
.let { View.MeasureSpec.makeMeasureSpec(it, View.MeasureSpec.EXACTLY) }
@@ -739,7 +750,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
}
val screenshotItems = productRepository.first.screenshots
.map { Item.ScreenshotItem(productRepository.second, productRepository.first.packageName, it) }
.map { Item.ScreenshotItem(productRepository.second, packageName, it) }
if (screenshotItems.isNotEmpty()) {
if (ExpandType.SCREENSHOTS in expanded) {
items += Item.SectionItem(SectionType.SCREENSHOTS, ExpandType.SCREENSHOTS, emptyList(), screenshotItems.size)
@@ -751,10 +762,19 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
}
val incompatible = Preferences[Preferences.Key.IncompatibleVersions]
val releaseItems = products.asSequence()
val compatibleReleasePairs = products.asSequence()
.flatMap { (product, repository) -> product.releases.asSequence()
.filter { incompatible || it.incompatibilities.isEmpty() }
.map { Item.ReleaseItem(repository, it, repository.id == productRepository?.second?.id) } }
.map { Pair(it, repository) } }
.toList()
val signaturesForVersionCode = compatibleReleasePairs.asSequence()
.mapNotNull { (release, _) -> if (release.signature.isEmpty()) null else
Pair(release.versionCode, release.signature) }
.distinct().groupBy { it.first }.toMap()
val releaseItems = compatibleReleasePairs.asSequence()
.map { (release, repository) -> Item.ReleaseItem(repository, release,
repository.id == productRepository?.second?.id,
signaturesForVersionCode.getValue(release.versionCode).size >= 2) }
.sortedByDescending { it.release.versionCode }
.toList()
if (releaseItems.isNotEmpty()) {
@@ -772,17 +792,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
items += Item.EmptyItem(packageName)
}
this.product = productRepository?.first
updateSwitches()
this.installedItem = installedItem
notifyDataSetChanged()
}
var installedItem: InstalledItem? = null
set(value) {
field = value
updateSwitches()
notifyDataSetChanged()
}
private var action: Action? = null
fun setAction(action: Action?) {
@@ -823,19 +836,6 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
}
}
private fun updateSwitches() {
val product = product
val installedItem = installedItem
items.removeAll { it is Item.SwitchItem }
val index = items.indexOfFirst { it is Item.HeaderItem }
if (index >= 0 && product != null && installedItem != null) {
items.add(index + 1, Item.SwitchItem(SwitchType.IGNORE_ALL_UPDATES, product.packageName, product.versionCode))
if (product.canUpdate(installedItem)) {
items.add(index + 2, Item.SwitchItem(SwitchType.IGNORE_THIS_UPDATE, product.packageName, product.versionCode))
}
}
}
override val viewTypeClass: Class<ViewType>
get() = ViewType::class.java
@@ -1167,6 +1167,19 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
holder.added.text = holder.dateFormat.format(item.release.added)
holder.added.setTextColor(primarySecondaryColor)
holder.size.text = item.release.size.formatSize()
holder.signature.visibility = if (item.showSignature && item.release.signature.isNotEmpty())
View.VISIBLE else View.GONE
if (item.showSignature && item.release.signature.isNotEmpty()) {
val bytes = item.release.signature.toUpperCase(Locale.US).windowed(2, 2, false).take(8)
val signature = bytes.joinToString(separator = " ")
val builder = SpannableStringBuilder(context.getString(R.string.signature_FORMAT, signature))
val index = builder.indexOf(signature)
if (index >= 0) {
bytes.forEachIndexed { i, _ -> builder.setSpan(TypefaceSpan("monospace"),
index + 3 * i, index + 3 * i + 2, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE) }
}
holder.signature.text = builder
}
holder.compatibility.visibility = if (incompatibility != null || singlePlatform != null)
View.VISIBLE else View.GONE
if (incompatibility != null) {
@@ -34,6 +34,7 @@ import nya.kitsunyan.foxydroid.service.Connection
import nya.kitsunyan.foxydroid.service.DownloadService
import nya.kitsunyan.foxydroid.utility.RxUtils
import nya.kitsunyan.foxydroid.utility.Utils
import nya.kitsunyan.foxydroid.utility.extension.android.*
import nya.kitsunyan.foxydroid.widget.DividerItemDecoration
class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
@@ -191,11 +192,8 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
}
val recyclerView = recyclerView!!
val adapter = recyclerView.adapter as ProductAdapter
if (firstChanged || productChanged) {
adapter.setProducts(recyclerView.context, products, packageName)
}
if (installedItemChanged) {
adapter.installedItem = installedItem.value
if (firstChanged || productChanged || installedItemChanged) {
adapter.setProducts(recyclerView.context, packageName, products, installedItem.value)
}
updateButtons()
}
@@ -231,9 +229,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
}
private fun updateButtons(preference: ProductPreference) {
val product = Product.findSuggested(products) { it.first }?.first
val installed = installed
val compatible = product != null && product.selectedRelease.let { it != null && it.incompatibilities.isEmpty() }
val product = Product.findSuggested(products, installed?.installedItem) { it.first }?.first
val compatible = product != null && product.selectedReleases.firstOrNull()
.let { it != null && it.incompatibilities.isEmpty() }
val canInstall = product != null && installed == null && compatible
val canUpdate = product != null && compatible && product.canUpdate(installed?.installedItem) &&
!preference.shouldIgnoreUpdate(product.versionCode)
@@ -332,10 +331,21 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
when (action) {
ProductAdapter.Action.INSTALL,
ProductAdapter.Action.UPDATE -> {
val productRepository = Product.findSuggested(products) { it.first }
val release = productRepository?.first?.selectedRelease
val installedItem = installed?.installedItem
val productRepository = Product.findSuggested(products, installedItem) { it.first }
val compatibleReleases = productRepository?.first?.selectedReleases.orEmpty()
.filter { installedItem == null || installedItem.signature == it.signature }
val release = if (compatibleReleases.size >= 2) {
compatibleReleases
.filter { it.platforms.contains(Android.primaryPlatform) }
.minBy { it.platforms.size }
?: compatibleReleases.minBy { it.platforms.size }
?: compatibleReleases.firstOrNull()
} else {
compatibleReleases.firstOrNull()
}
val binder = downloadConnection.binder
if (release != null && binder != null) {
if (productRepository != null && release != null && binder != null) {
binder.enqueue(packageName, productRepository.first.name, productRepository.second, release)
}
Unit
+8
View File
@@ -95,6 +95,14 @@
</LinearLayout>
<TextView
android:id="@+id/signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:maxLines="1" />
<TextView
android:id="@+id/compatibility"
android:layout_width="match_parent"
+1
View File
@@ -129,6 +129,7 @@
<string name="select_mirror">Select a mirror</string>
<string name="show_more">Show more</string>
<string name="show_older_versions">Show older versions</string>
<string name="signature_FORMAT">Signature %s</string>
<string name="signed_using_unsafe_algorithm">Signed using an unsafe algorithm</string>
<string name="skip">Skip</string>
<string name="socks_proxy">SOCKS proxy</string>