Add support for metadata icons as fallback

This commit is contained in:
kitsunyan
2020-07-30 07:58:06 +03:00
parent 09067cd2d4
commit 3ec4eed536
7 changed files with 54 additions and 26 deletions
@@ -7,7 +7,7 @@ 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,
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>,
@@ -54,7 +54,7 @@ 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, version, "", compatible, false, 0)
return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon, version, "", compatible, false, 0)
}
fun canUpdate(installedItem: InstalledItem?): Boolean {
@@ -69,6 +69,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
generator.writeStringField("summary", summary)
generator.writeStringField("whatsNew", whatsNew)
generator.writeStringField("icon", icon)
generator.writeStringField("metadataIcon", metadataIcon)
generator.writeStringField("authorName", author.name)
generator.writeStringField("authorEmail", author.email)
generator.writeStringField("authorWeb", author.web)
@@ -138,6 +139,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
var summary = ""
var whatsNew = ""
var icon = ""
var metadataIcon = ""
var authorName = ""
var authorEmail = ""
var authorWeb = ""
@@ -161,6 +163,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
it.string("summary") -> summary = valueAsString
it.string("whatsNew") -> whatsNew = valueAsString
it.string("icon") -> icon = valueAsString
it.string("metadataIcon") -> metadataIcon = valueAsString
it.string("authorName") -> authorName = valueAsString
it.string("authorEmail") -> authorEmail = valueAsString
it.string("authorWeb") -> authorWeb = valueAsString
@@ -216,7 +219,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
else -> skipChildren()
}
}
return Product(repositoryId, packageName, name, summary, description, whatsNew, icon,
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)
}
@@ -7,8 +7,8 @@ import nya.kitsunyan.foxydroid.R
import nya.kitsunyan.foxydroid.utility.KParcelable
import nya.kitsunyan.foxydroid.utility.extension.json.*
data class ProductItem(val repositoryId: Long, val packageName: String,
val name: String, val summary: String, val icon: String, val version: String, val installedVersion: 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) {
sealed class Section: KParcelable {
object All: Section() {
@@ -53,6 +53,7 @@ data class ProductItem(val repositoryId: Long, val packageName: String,
fun serialize(generator: JsonGenerator) {
generator.writeNumberField("serialVersion", 1)
generator.writeStringField("icon", icon)
generator.writeStringField("metadataIcon", metadataIcon)
generator.writeStringField("version", version)
}
@@ -61,15 +62,17 @@ data class ProductItem(val repositoryId: Long, val packageName: String,
installedVersion: String, compatible: Boolean, canUpdate: Boolean, matchRank: Int,
parser: JsonParser): ProductItem {
var icon = ""
var metadataIcon = ""
var version = ""
parser.forEachKey {
when {
it.string("icon") -> icon = valueAsString
it.string("metadataIcon") -> metadataIcon = valueAsString
it.string("version") -> version = valueAsString
else -> skipChildren()
}
}
return ProductItem(repositoryId, packageName, name, summary, icon,
return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon,
version, installedVersion, compatible, canUpdate, matchRank)
}
}
@@ -21,6 +21,10 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
0L
}
}
internal fun validateIcon(icon: String): String {
return if (icon.endsWith(".xml")) "" else icon
}
}
interface Callback {
@@ -76,7 +80,7 @@ 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,
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)
@@ -230,7 +234,7 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
"summary" -> productBuilder.summary = content
"description" -> productBuilder.description = "<p>$content</p>"
"desc" -> productBuilder.description = content.replace("\n", "<br/>")
"icon" -> productBuilder.icon = content
"icon" -> productBuilder.icon = validateIcon(content)
"author" -> productBuilder.authorName = content
"email" -> productBuilder.authorEmail = content
"source" -> productBuilder.source = content
@@ -18,7 +18,7 @@ object IndexV1Parser {
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 screenshots: Screenshots?)
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) }
@@ -106,7 +106,7 @@ object IndexV1Parser {
it.string("name") -> nameFallback = valueAsString
it.string("summary") -> summaryFallback = valueAsString
it.string("description") -> descriptionFallback = valueAsString
it.string("icon") -> icon = valueAsString
it.string("icon") -> icon = IndexHandler.validateIcon(valueAsString)
it.string("authorName") -> authorName = valueAsString
it.string("authorEmail") -> authorEmail = valueAsString
it.string("authorWebSite") -> authorWeb = valueAsString
@@ -132,6 +132,7 @@ object IndexV1Parser {
var summary = ""
var description = ""
var whatsNew = ""
var metadataIcon = ""
var phone = emptyList<String>()
var smallTablet = emptyList<String>()
var largeTablet = emptyList<String>()
@@ -141,6 +142,7 @@ object IndexV1Parser {
it.string("summary") -> summary = valueAsString
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()
@@ -149,7 +151,8 @@ object IndexV1Parser {
}
val screenshots = if (sequenceOf(phone, smallTablet, largeTablet).any { it.isNotEmpty() })
Screenshots(phone, smallTablet, largeTablet) else null
localizedMap[locale] = Localized(name, summary, description, whatsNew, screenshots)
localizedMap[locale] = Localized(name, summary, description, whatsNew,
metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), screenshots)
} else {
skipChildren()
}
@@ -161,6 +164,7 @@ object IndexV1Parser {
val summary = localizedMap.findString(summaryFallback) { it.summary }
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 screenshots = screenshotPairs
?.let { (key, screenshots) -> screenshots.phone.asSequence()
@@ -170,7 +174,7 @@ object IndexV1Parser {
screenshots.largeTablet.asSequence()
.map { Product.Screenshot(key, Product.Screenshot.Type.LARGE_TABLET, it) } }
.orEmpty().toList()
return Product(repositoryId, packageName, name, summary, description, whatsNew, icon,
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())
@@ -17,8 +17,9 @@ object PicassoDownloader {
private const val HOST_SCREENSHOT = "screenshot"
private const val QUERY_ADDRESS = "address"
private const val QUERY_AUTHENTICATION = "authentication"
private const val QUERY_ICON = "icon"
private const val QUERY_PACKAGE_NAME = "packageName"
private const val QUERY_ICON = "icon"
private const val QUERY_METADATA_ICON = "metadataIcon"
private const val QUERY_LOCALE = "locale"
private const val QUERY_DEVICE = "device"
private const val QUERY_SCREENSHOT = "screenshot"
@@ -32,16 +33,24 @@ object PicassoDownloader {
override fun newCall(request: okhttp3.Request): Call {
return when (request.url.host) {
HOST_ICON -> {
val address = request.url.queryParameter(QUERY_ADDRESS)
val address = request.url.queryParameter(QUERY_ADDRESS)?.nullIfEmpty()
val authentication = request.url.queryParameter(QUERY_AUTHENTICATION)
val icon = request.url.queryParameter(QUERY_ICON)
val dpi = request.url.queryParameter(QUERY_DPI)?.nullIfEmpty()
if (address.isNullOrEmpty() || icon.isNullOrEmpty()) {
val path = run {
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 dpi = request.url.queryParameter(QUERY_DPI)?.nullIfEmpty()
when {
icon != null -> "${if (dpi != null) "icons-$dpi" else "icons"}/$icon"
packageName != null && metadataIcon != null -> "$packageName/$metadataIcon"
else -> null
}
}
if (address == null || path == null) {
Downloader.createCall(request.newBuilder(), "", null)
} else {
Downloader.createCall(request.newBuilder().url(address.toHttpUrl()
.newBuilder().addPathSegment(if (dpi != null) "icons-$dpi" else "icons")
.addPathSegment(icon).build()), authentication.orEmpty(), cache)
.newBuilder().addPathSegments(path).build()), authentication.orEmpty(), cache)
}
}
HOST_SCREENSHOT -> {
@@ -82,17 +91,20 @@ object PicassoDownloader {
.build()
}
fun createIconUri(view: View, icon: 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, icon, size, repository)
return createIconUri(view.context, packageName, icon, metadataIcon, size, repository)
}
private fun createIconUri(context: Context, icon: 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)
.appendQueryParameter(QUERY_PACKAGE_NAME, packageName)
.appendQueryParameter(QUERY_ICON, icon)
.appendQueryParameter(QUERY_METADATA_ICON, metadataIcon)
.apply {
if (repository.version >= 11) {
val displayDpi = context.resources.displayMetrics.densityDpi
@@ -961,8 +961,9 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
val updateStatus = Payload.STATUS in payloads
val updateAll = !updateStatus
if (updateAll) {
if (item.product.icon.isNotEmpty()) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, item.product.icon, item.repository)) {
if (item.product.icon.isNotEmpty() || item.product.metadataIcon.isNotEmpty()) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, item.product.packageName,
item.product.icon, item.product.metadataIcon, item.repository)) {
placeholder(holder.progressIcon)
error(holder.defaultIcon)
}
@@ -139,8 +139,9 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
holder.summary.text = if (productItem.name == productItem.summary) "" else productItem.summary
holder.summary.visibility = if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE
val repository: Repository? = repositories[productItem.repositoryId]
if (productItem.icon.isNotEmpty() && repository != null) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.icon, repository)) {
if ((productItem.icon.isNotEmpty() || productItem.metadataIcon.isNotEmpty()) && repository != null) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.packageName,
productItem.icon, productItem.metadataIcon, repository)) {
placeholder(holder.progressIcon)
error(holder.defaultIcon)
}