mirror of
https://github.com/Michatec/michas-droid.git
synced 2026-05-30 18:02:43 +02:00
Properly handle multiple suggested versions
This commit is contained in:
@@ -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,14 +792,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
items += Item.EmptyItem(packageName)
|
||||
}
|
||||
this.product = productRepository?.first
|
||||
updateSwitches()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var installedItem: InstalledItem? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updateSwitches()
|
||||
this.installedItem = installedItem
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user