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.* import nya.kitsunyan.foxydroid.utility.extension.text.*
data class Product(val repositoryId: Long, val packageName: String, val name: String, val summary: String, 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 source: String, val changelog: String, val web: String, val tracker: String,
val added: Long, val updated: Long, val suggestedVersionCode: Long, val added: Long, val updated: Long, val suggestedVersionCode: Long,
val categories: List<String>, val antiFeatures: List<String>, val licenses: List<String>, 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() get() = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct().toList()
fun item(): ProductItem { 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 { 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("summary", summary)
generator.writeStringField("whatsNew", whatsNew) generator.writeStringField("whatsNew", whatsNew)
generator.writeStringField("icon", icon) generator.writeStringField("icon", icon)
generator.writeStringField("metadataIcon", metadataIcon)
generator.writeStringField("authorName", author.name) generator.writeStringField("authorName", author.name)
generator.writeStringField("authorEmail", author.email) generator.writeStringField("authorEmail", author.email)
generator.writeStringField("authorWeb", author.web) generator.writeStringField("authorWeb", author.web)
@@ -138,6 +139,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
var summary = "" var summary = ""
var whatsNew = "" var whatsNew = ""
var icon = "" var icon = ""
var metadataIcon = ""
var authorName = "" var authorName = ""
var authorEmail = "" var authorEmail = ""
var authorWeb = "" 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("summary") -> summary = valueAsString
it.string("whatsNew") -> whatsNew = valueAsString it.string("whatsNew") -> whatsNew = valueAsString
it.string("icon") -> icon = valueAsString it.string("icon") -> icon = valueAsString
it.string("metadataIcon") -> metadataIcon = valueAsString
it.string("authorName") -> authorName = valueAsString it.string("authorName") -> authorName = valueAsString
it.string("authorEmail") -> authorEmail = valueAsString it.string("authorEmail") -> authorEmail = valueAsString
it.string("authorWeb") -> authorWeb = valueAsString it.string("authorWeb") -> authorWeb = valueAsString
@@ -216,7 +219,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
else -> skipChildren() 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, Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated,
suggestedVersionCode, categories, antiFeatures, licenses, donates, screenshots, releases) 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.KParcelable
import nya.kitsunyan.foxydroid.utility.extension.json.* import nya.kitsunyan.foxydroid.utility.extension.json.*
data class ProductItem(val repositoryId: Long, val packageName: String, data class ProductItem(val repositoryId: Long, val packageName: String, val name: String, val summary: String,
val name: String, val summary: String, val icon: String, val version: String, val installedVersion: 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 { sealed class Section: KParcelable {
object All: Section() { object All: Section() {
@@ -53,6 +53,7 @@ data class ProductItem(val repositoryId: Long, val packageName: String,
fun serialize(generator: JsonGenerator) { fun serialize(generator: JsonGenerator) {
generator.writeNumberField("serialVersion", 1) generator.writeNumberField("serialVersion", 1)
generator.writeStringField("icon", icon) generator.writeStringField("icon", icon)
generator.writeStringField("metadataIcon", metadataIcon)
generator.writeStringField("version", version) 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, installedVersion: String, compatible: Boolean, canUpdate: Boolean, matchRank: Int,
parser: JsonParser): ProductItem { parser: JsonParser): ProductItem {
var icon = "" var icon = ""
var metadataIcon = ""
var version = "" var version = ""
parser.forEachKey { parser.forEachKey {
when { when {
it.string("icon") -> icon = valueAsString it.string("icon") -> icon = valueAsString
it.string("metadataIcon") -> metadataIcon = valueAsString
it.string("version") -> version = valueAsString it.string("version") -> version = valueAsString
else -> skipChildren() else -> skipChildren()
} }
} }
return ProductItem(repositoryId, packageName, name, summary, icon, return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon,
version, installedVersion, compatible, canUpdate, matchRank) version, installedVersion, compatible, canUpdate, matchRank)
} }
} }
@@ -21,6 +21,10 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
0L 0L
} }
} }
internal fun validateIcon(icon: String): String {
return if (icon.endsWith(".xml")) "" else icon
}
} }
interface Callback { interface Callback {
@@ -76,7 +80,7 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
val releases = mutableListOf<Release>() val releases = mutableListOf<Release>()
fun build(): Product { 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, Product.Author(authorName, authorEmail, ""), source, changelog, web, tracker, added, updated,
suggestedVersionCode, categories.toList(), antiFeatures.toList(), suggestedVersionCode, categories.toList(), antiFeatures.toList(),
licenses, donates.sortedWith(DonateComparator), emptyList(), releases) licenses, donates.sortedWith(DonateComparator), emptyList(), releases)
@@ -230,7 +234,7 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
"summary" -> productBuilder.summary = content "summary" -> productBuilder.summary = content
"description" -> productBuilder.description = "<p>$content</p>" "description" -> productBuilder.description = "<p>$content</p>"
"desc" -> productBuilder.description = content.replace("\n", "<br/>") "desc" -> productBuilder.description = content.replace("\n", "<br/>")
"icon" -> productBuilder.icon = content "icon" -> productBuilder.icon = validateIcon(content)
"author" -> productBuilder.authorName = content "author" -> productBuilder.authorName = content
"email" -> productBuilder.authorEmail = content "email" -> productBuilder.authorEmail = content
"source" -> productBuilder.source = 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 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, 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? { private fun <T> Map<String, Localized>.getAndCall(key: String, callback: (String, Localized) -> T?): T? {
return this[key]?.let { callback(key, it) } return this[key]?.let { callback(key, it) }
@@ -106,7 +106,7 @@ object IndexV1Parser {
it.string("name") -> nameFallback = valueAsString it.string("name") -> nameFallback = valueAsString
it.string("summary") -> summaryFallback = valueAsString it.string("summary") -> summaryFallback = valueAsString
it.string("description") -> descriptionFallback = 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("authorName") -> authorName = valueAsString
it.string("authorEmail") -> authorEmail = valueAsString it.string("authorEmail") -> authorEmail = valueAsString
it.string("authorWebSite") -> authorWeb = valueAsString it.string("authorWebSite") -> authorWeb = valueAsString
@@ -132,6 +132,7 @@ object IndexV1Parser {
var summary = "" var summary = ""
var description = "" var description = ""
var whatsNew = "" var whatsNew = ""
var metadataIcon = ""
var phone = emptyList<String>() var phone = emptyList<String>()
var smallTablet = emptyList<String>() var smallTablet = emptyList<String>()
var largeTablet = emptyList<String>() var largeTablet = emptyList<String>()
@@ -141,6 +142,7 @@ object IndexV1Parser {
it.string("summary") -> summary = valueAsString it.string("summary") -> summary = valueAsString
it.string("description") -> description = valueAsString it.string("description") -> description = valueAsString
it.string("whatsNew") -> whatsNew = valueAsString it.string("whatsNew") -> whatsNew = valueAsString
it.string("icon") -> metadataIcon = valueAsString
it.array("phoneScreenshots") -> phone = collectDistinctNotEmptyStrings() it.array("phoneScreenshots") -> phone = collectDistinctNotEmptyStrings()
it.array("sevenInchScreenshots") -> smallTablet = collectDistinctNotEmptyStrings() it.array("sevenInchScreenshots") -> smallTablet = collectDistinctNotEmptyStrings()
it.array("tenInchScreenshots") -> largeTablet = collectDistinctNotEmptyStrings() it.array("tenInchScreenshots") -> largeTablet = collectDistinctNotEmptyStrings()
@@ -149,7 +151,8 @@ object IndexV1Parser {
} }
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 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 { } else {
skipChildren() skipChildren()
} }
@@ -161,6 +164,7 @@ object IndexV1Parser {
val summary = localizedMap.findString(summaryFallback) { it.summary } 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 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 val screenshots = screenshotPairs
?.let { (key, screenshots) -> screenshots.phone.asSequence() ?.let { (key, screenshots) -> screenshots.phone.asSequence()
@@ -170,7 +174,7 @@ object IndexV1Parser {
screenshots.largeTablet.asSequence() 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() .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, Product.Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated,
suggestedVersionCode, categories, antiFeatures, licenses, suggestedVersionCode, categories, antiFeatures, licenses,
donates.sortedWith(IndexHandler.DonateComparator), screenshots, emptyList()) donates.sortedWith(IndexHandler.DonateComparator), screenshots, emptyList())
@@ -17,8 +17,9 @@ object PicassoDownloader {
private const val HOST_SCREENSHOT = "screenshot" private const val HOST_SCREENSHOT = "screenshot"
private const val QUERY_ADDRESS = "address" private const val QUERY_ADDRESS = "address"
private const val QUERY_AUTHENTICATION = "authentication" private const val QUERY_AUTHENTICATION = "authentication"
private const val QUERY_ICON = "icon"
private const val QUERY_PACKAGE_NAME = "packageName" 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_LOCALE = "locale"
private const val QUERY_DEVICE = "device" private const val QUERY_DEVICE = "device"
private const val QUERY_SCREENSHOT = "screenshot" private const val QUERY_SCREENSHOT = "screenshot"
@@ -32,16 +33,24 @@ object PicassoDownloader {
override fun newCall(request: okhttp3.Request): Call { override fun newCall(request: okhttp3.Request): Call {
return when (request.url.host) { return when (request.url.host) {
HOST_ICON -> { 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 authentication = request.url.queryParameter(QUERY_AUTHENTICATION)
val icon = request.url.queryParameter(QUERY_ICON) val path = run {
val dpi = request.url.queryParameter(QUERY_DPI)?.nullIfEmpty() val packageName = request.url.queryParameter(QUERY_PACKAGE_NAME)?.nullIfEmpty()
if (address.isNullOrEmpty() || icon.isNullOrEmpty()) { 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) Downloader.createCall(request.newBuilder(), "", null)
} else { } else {
Downloader.createCall(request.newBuilder().url(address.toHttpUrl() Downloader.createCall(request.newBuilder().url(address.toHttpUrl()
.newBuilder().addPathSegment(if (dpi != null) "icons-$dpi" else "icons") .newBuilder().addPathSegments(path).build()), authentication.orEmpty(), cache)
.addPathSegment(icon).build()), authentication.orEmpty(), cache)
} }
} }
HOST_SCREENSHOT -> { HOST_SCREENSHOT -> {
@@ -82,17 +91,20 @@ object PicassoDownloader {
.build() .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) } / val size = (view.layoutParams.let { min(it.width, it.height) } /
view.resources.displayMetrics.density).roundToInt() 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) return Uri.Builder().scheme("https").authority(HOST_ICON)
.appendQueryParameter(QUERY_ADDRESS, repository.address) .appendQueryParameter(QUERY_ADDRESS, repository.address)
.appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication) .appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication)
.appendQueryParameter(QUERY_PACKAGE_NAME, packageName)
.appendQueryParameter(QUERY_ICON, icon) .appendQueryParameter(QUERY_ICON, icon)
.appendQueryParameter(QUERY_METADATA_ICON, metadataIcon)
.apply { .apply {
if (repository.version >= 11) { if (repository.version >= 11) {
val displayDpi = context.resources.displayMetrics.densityDpi 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 updateStatus = Payload.STATUS in payloads
val updateAll = !updateStatus val updateAll = !updateStatus
if (updateAll) { if (updateAll) {
if (item.product.icon.isNotEmpty()) { if (item.product.icon.isNotEmpty() || item.product.metadataIcon.isNotEmpty()) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, item.product.icon, item.repository)) { holder.icon.load(PicassoDownloader.createIconUri(holder.icon, item.product.packageName,
item.product.icon, item.product.metadataIcon, item.repository)) {
placeholder(holder.progressIcon) placeholder(holder.progressIcon)
error(holder.defaultIcon) 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.text = if (productItem.name == productItem.summary) "" else productItem.summary
holder.summary.visibility = if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE holder.summary.visibility = if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE
val repository: Repository? = repositories[productItem.repositoryId] val repository: Repository? = repositories[productItem.repositoryId]
if (productItem.icon.isNotEmpty() && repository != null) { if ((productItem.icon.isNotEmpty() || productItem.metadataIcon.isNotEmpty()) && repository != null) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.icon, repository)) { holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.packageName,
productItem.icon, productItem.metadataIcon, repository)) {
placeholder(holder.progressIcon) placeholder(holder.progressIcon)
error(holder.defaultIcon) error(holder.defaultIcon)
} }