mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
feat: store games get their logo from the store
GOG (api.gog.com v2 `_links.logo`), Epic (catalog keyImage `DieselGameBoxLogo`) and Amazon (`productDetail.details.logoUrl`) logos are kept in the install record as an optional `logo` field and written next to the game as logo.png when the install finishes. The PNG is kept as it came, transparency included, and anything that does not decode as an image is dropped. Older install records without the field still read as before.
This commit is contained in:
1 parent
5970de928e
commit
8ceff5641a
15 files changed
+193
-12
No files matched your search
@@ -14,13 +14,45 @@ object StoreArt {
|
||||
|
||||
fun fetchInto(folder: File, sidecar: StoreGameSidecar) {
|
||||
sidecar.cover?.let { place(it, File(folder, "cover.jpg")) }
|
||||
val wide = sidecar.hero ?: return
|
||||
if (place(wide, File(folder, "hero.jpg"))) {
|
||||
val header = File(folder, "header.jpg")
|
||||
if (!header.isFile) runCatching { File(folder, "hero.jpg").copyTo(header, overwrite = true) }
|
||||
sidecar.hero?.let { wide ->
|
||||
if (place(wide, File(folder, "hero.jpg"))) {
|
||||
val header = File(folder, "header.jpg")
|
||||
if (!header.isFile) runCatching { File(folder, "hero.jpg").copyTo(header, overwrite = true) }
|
||||
}
|
||||
}
|
||||
sidecar.logo?.let { fetchLogo(folder, it) }
|
||||
}
|
||||
|
||||
fun fetchLogo(folder: File, url: String): Boolean {
|
||||
val dst = File(folder, LOGO)
|
||||
if (dst.isFile && dst.length() > 0) return true
|
||||
return writeLogo(get(url) ?: return false, dst)
|
||||
}
|
||||
|
||||
/** A logo is drawn over the hero, so it stays a PNG with its transparency instead of going through [place]. */
|
||||
internal fun writeLogo(bytes: ByteArray, dst: File): Boolean {
|
||||
if (dst.isFile && dst.length() > 0) return true
|
||||
val bitmap = runCatching { BitmapFactory.decodeByteArray(bytes, 0, bytes.size) }.getOrNull() ?: return false
|
||||
val tmp = File(dst.path + ".tmp")
|
||||
return try {
|
||||
if (bitmap.width <= 0 || bitmap.height <= 0) return false
|
||||
if (isPng(bytes)) tmp.writeBytes(bytes) else tmp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
tmp.renameTo(dst)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${dst.name}: ${e.javaClass.simpleName}")
|
||||
false
|
||||
} finally {
|
||||
tmp.delete()
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private val PNG_MAGIC = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)
|
||||
|
||||
private fun isPng(bytes: ByteArray): Boolean = bytes.size > PNG_MAGIC.size && PNG_MAGIC.indices.all { bytes[it] == PNG_MAGIC[it] }
|
||||
|
||||
const val LOGO = "logo.png"
|
||||
|
||||
private fun place(url: String, dst: File): Boolean {
|
||||
if (dst.isFile && dst.length() > 0) return true
|
||||
val bytes = get(url) ?: return false
|
||||
|
||||
@@ -21,6 +21,7 @@ data class CatalogItem(
|
||||
val sizeBytes: Long = 0L,
|
||||
val extra: Map<String, String> = emptyMap(),
|
||||
val mature: Boolean = false,
|
||||
val logoUrl: String? = null,
|
||||
) {
|
||||
val isDiscounted: Boolean get() = discountPercent > 0 && originalPrice.isNotBlank()
|
||||
val key: String get() = "${store.id}:$id"
|
||||
|
||||
@@ -22,6 +22,7 @@ class StoreGameSidecar(
|
||||
val epic: EpicOptions = EpicOptions(),
|
||||
val cloud: Boolean = true,
|
||||
val achievements: Boolean = true,
|
||||
val logo: String? = null,
|
||||
) {
|
||||
val isInstalled: Boolean get() = state == STATE_INSTALLED
|
||||
|
||||
@@ -33,7 +34,8 @@ class StoreGameSidecar(
|
||||
env: Map<String, String> = this.env, installVersion: String = this.installVersion,
|
||||
installedAt: Long = this.installedAt, cover: String? = this.cover, hero: String? = this.hero,
|
||||
extra: Map<String, String> = this.extra, state: String = this.state, epic: EpicOptions = this.epic, cloud: Boolean = this.cloud, achievements: Boolean = this.achievements,
|
||||
) = StoreGameSidecar(store, id, title, exe, launcher, args, env, installVersion, installedAt, cover, hero, extra, state, epic, cloud, achievements)
|
||||
logo: String? = this.logo,
|
||||
) = StoreGameSidecar(store, id, title, exe, launcher, args, env, installVersion, installedAt, cover, hero, extra, state, epic, cloud, achievements, logo)
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put("version", VERSION)
|
||||
@@ -52,6 +54,7 @@ class StoreGameSidecar(
|
||||
if (installedAt > 0L) put("installedAt", installedAt)
|
||||
if (!cover.isNullOrEmpty()) put("cover", cover)
|
||||
if (!hero.isNullOrEmpty()) put("hero", hero)
|
||||
if (!logo.isNullOrEmpty()) put("logo", logo)
|
||||
if (extra.isNotEmpty()) put("extra", JSONObject(extra as Map<*, *>))
|
||||
}
|
||||
|
||||
@@ -129,6 +132,7 @@ class StoreGameSidecar(
|
||||
epic = o.optJSONObject("epic")?.let { e -> EpicOptions(eos = e.optBoolean("eos", true), offline = e.optBoolean("offline", false)) } ?: EpicOptions(),
|
||||
cloud = o.optBoolean("cloud", true),
|
||||
achievements = o.optBoolean("achievements", true),
|
||||
logo = o.optString("logo", "").ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ object StoreInstalls {
|
||||
val app = context.applicationContext
|
||||
onStep?.invoke(0, STEPS)
|
||||
val before = StoreGameSidecar.read(folder)
|
||||
val finished = StoreLaunch.writeLauncher(folder, sidecar.copy(state = StoreGameSidecar.STATE_INSTALLED, epic = before?.epic ?: sidecar.epic, cloud = before?.cloud ?: sidecar.cloud, achievements = before?.achievements ?: sidecar.achievements))
|
||||
val finished = StoreLaunch.writeLauncher(folder, sidecar.copy(state = StoreGameSidecar.STATE_INSTALLED, epic = before?.epic ?: sidecar.epic, cloud = before?.cloud ?: sidecar.cloud, achievements = before?.achievements ?: sidecar.achievements, logo = sidecar.logo ?: before?.logo))
|
||||
finished.write(folder)
|
||||
if (StoreGameSidecar.read(folder)?.isInstalled != true) throw java.io.IOException("the install record could not be written in ${folder.name}")
|
||||
onStep?.invoke(1, STEPS)
|
||||
|
||||
@@ -124,6 +124,7 @@ internal class AmazonInstaller(
|
||||
installVersion = versionId, installedAt = System.currentTimeMillis(),
|
||||
cover = cover, hero = item.imageUrl,
|
||||
extra = mapOf("entitlementId" to entitlementId, "sku" to sku),
|
||||
logo = item.logoUrl,
|
||||
)
|
||||
StoreInstalls.complete(app, folder, sidecar, onStep = { done, steps -> handle.stageProgress(done.toLong(), steps.toLong()) })
|
||||
return folder.path
|
||||
|
||||
@@ -24,6 +24,7 @@ data class AmazonGame(
|
||||
val publisher: String = "",
|
||||
val productSku: String = "",
|
||||
val versionId: String = "",
|
||||
val logoUrl: String = "",
|
||||
)
|
||||
|
||||
object AmazonLibrary {
|
||||
@@ -42,7 +43,8 @@ object AmazonLibrary {
|
||||
JSONObject()
|
||||
.put("productId", g.productId).put("entitlementId", g.entitlementId).put("title", g.title)
|
||||
.put("artUrl", g.artUrl).put("heroUrl", g.heroUrl).put("developer", g.developer)
|
||||
.put("publisher", g.publisher).put("productSku", g.productSku).put("versionId", g.versionId),
|
||||
.put("publisher", g.publisher).put("productSku", g.productSku).put("versionId", g.versionId)
|
||||
.apply { if (g.logoUrl.isNotEmpty()) put("logoUrl", g.logoUrl) },
|
||||
)
|
||||
}.toString()
|
||||
|
||||
@@ -60,6 +62,7 @@ object AmazonLibrary {
|
||||
publisher = o.optString("publisher", ""),
|
||||
productSku = o.optString("productSku", ""),
|
||||
versionId = o.optString("versionId", ""),
|
||||
logoUrl = o.optString("logoUrl", ""),
|
||||
)
|
||||
}
|
||||
return tidy(games)
|
||||
@@ -94,6 +97,7 @@ object AmazonLibrary {
|
||||
sizeBytes = prefs.getLong("size_${game.productId}", 0L).takeIf { it > 0 } ?: 0L,
|
||||
storeUrl = STORE_URL,
|
||||
extra = mapOf("entitlementId" to game.entitlementId, "sku" to game.productSku, "version" to ""),
|
||||
logoUrl = game.logoUrl.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,7 +110,7 @@ object AmazonLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
private fun gameFrom(o: JSONObject, before: AmazonGame?): AmazonGame = AmazonGame(
|
||||
internal fun gameFrom(o: JSONObject, before: AmazonGame?): AmazonGame = AmazonGame(
|
||||
productId = o.optString("productId"),
|
||||
entitlementId = o.optString("entitlementId"),
|
||||
title = o.optString("title"),
|
||||
@@ -116,6 +120,7 @@ object AmazonLibrary {
|
||||
publisher = o.optString("publisher"),
|
||||
productSku = o.optString("sku"),
|
||||
versionId = before?.versionId.orEmpty(),
|
||||
logoUrl = o.optString("logo").ifEmpty { before?.logoUrl.orEmpty() },
|
||||
)
|
||||
|
||||
private fun backfillPosters(app: Context, games: List<AmazonGame>) {
|
||||
|
||||
@@ -77,6 +77,7 @@ object SavedQueue {
|
||||
private fun itemJson(i: CatalogItem) = JSONObject().apply {
|
||||
put("store", i.store.id); put("id", i.id); put("title", i.title)
|
||||
put("image", i.imageUrl ?: ""); put("tall", i.tallImageUrl ?: "")
|
||||
i.logoUrl?.let { put("logo", it) }
|
||||
put("tags", i.tags); put("developer", i.developer); put("description", i.description)
|
||||
put("size", i.sizeBytes); put("owned", i.owned)
|
||||
put("extra", JSONObject(i.extra))
|
||||
@@ -91,6 +92,7 @@ object SavedQueue {
|
||||
tags = o.optString("tags"), developer = o.optString("developer"), description = o.optString("description"),
|
||||
owned = o.optBoolean("owned", true), sizeBytes = o.optLong("size"),
|
||||
extra = extra?.keys()?.asSequence()?.associateWith { extra.getString(it) } ?: emptyMap(),
|
||||
logoUrl = o.optString("logo").ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ internal class EpicInstaller(
|
||||
val sidecar = StoreGameSidecar(
|
||||
Store.EPIC, item.id, item.title, exe = picked.ifEmpty { "game.exe" }, args = args,
|
||||
installVersion = result.optString("buildVersion"), installedAt = System.currentTimeMillis(),
|
||||
cover = cover, hero = item.imageUrl, extra = extra,
|
||||
cover = cover, hero = item.imageUrl, extra = extra, logo = item.logoUrl,
|
||||
)
|
||||
StoreInstalls.complete(app, folder, sidecar, onStep = { done, steps -> handle.stageProgress(done.toLong(), steps.toLong()) })
|
||||
val disk = result.optLong("diskBytes", 0L)
|
||||
|
||||
@@ -25,6 +25,7 @@ data class EpicGame(
|
||||
val releaseDate: String = "",
|
||||
val publisher: String = "",
|
||||
val isDlc: Boolean = false,
|
||||
val artLogo: String = "",
|
||||
)
|
||||
|
||||
object EpicLibrary {
|
||||
@@ -50,7 +51,8 @@ object EpicLibrary {
|
||||
.put("title", g.title).put("artCover", g.artCover).put("artSquare", g.artSquare)
|
||||
.put("developer", g.developer).put("description", g.description).put("version", g.version)
|
||||
.put("installSize", g.installSize).put("canRunOffline", g.canRunOffline)
|
||||
.put("releaseDate", g.releaseDate).put("publisher", g.publisher),
|
||||
.put("releaseDate", g.releaseDate).put("publisher", g.publisher)
|
||||
.apply { if (g.artLogo.isNotEmpty()) put("artLogo", g.artLogo) },
|
||||
)
|
||||
}.toString()
|
||||
|
||||
@@ -73,6 +75,7 @@ object EpicLibrary {
|
||||
canRunOffline = o.optBoolean("canRunOffline", true),
|
||||
releaseDate = o.optString("releaseDate", ""),
|
||||
publisher = o.optString("publisher", ""),
|
||||
artLogo = o.optString("artLogo", ""),
|
||||
)
|
||||
}
|
||||
return tidy(games)
|
||||
@@ -106,6 +109,7 @@ object EpicLibrary {
|
||||
sizeBytes = prefs.getLong("size_${game.appName}", 0L).takeIf { it > 0 } ?: game.installSize,
|
||||
storeUrl = "https://store.epicgames.com/en-US/browse?q=" + URLEncoder.encode(title, "UTF-8"),
|
||||
extra = mapOf("namespace" to game.namespace, "catalogItemId" to game.catalogItemId, "version" to game.version),
|
||||
logoUrl = game.artLogo.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,6 +132,7 @@ object EpicLibrary {
|
||||
releaseDate = pick("releaseDate", before?.releaseDate.orEmpty()),
|
||||
publisher = pick("publisher", before?.publisher.orEmpty()),
|
||||
isDlc = d.optBoolean("dlc", false),
|
||||
artLogo = pick("logo", before?.artLogo.orEmpty()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import com.droiddeck.launcher.stores.StoreInstallRoot
|
||||
import com.droiddeck.launcher.stores.StoreInstalls
|
||||
import com.droiddeck.launcher.stores.StoreLoginActivity
|
||||
import com.droiddeck.launcher.stores.StoreNet
|
||||
import com.droiddeck.launcher.stores.StoresState
|
||||
import com.droiddeck.launcher.stores.download.DownloadEntry
|
||||
import com.droiddeck.launcher.stores.download.DownloadQueue
|
||||
import com.droiddeck.launcher.stores.download.DownloadStage
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -75,6 +77,13 @@ object GogBackend : StoreBackend {
|
||||
|
||||
override fun uninstall(context: Context, game: InstalledStoreGame) {}
|
||||
|
||||
private fun fetchLogo(id: String): String? =
|
||||
runCatching { StoreNet.get("https://api.gog.com/v2/games/$id?locale=en-US")?.let { logoOf(JSONObject(it)) } }.getOrNull()
|
||||
|
||||
/** `images.logo2x` from the products API is an opaque banner, not a logo; the v2 `logo` link is a transparent PNG. */
|
||||
internal fun logoOf(v2: JSONObject): String? =
|
||||
GogStoreCatalog.absolutize(v2.optJSONObject("_links")?.optJSONObject("logo")?.optString("href")).takeIf { it.startsWith("https://") }
|
||||
|
||||
private class InstallJob(val app: Context, val game: GogGame, val item: CatalogItem, val folder: File) : DownloadQueue.DownloadJob {
|
||||
private val cancelled = AtomicBoolean(false)
|
||||
|
||||
@@ -106,6 +115,7 @@ object GogBackend : StoreBackend {
|
||||
Store.GOG, game.gameId, game.title, exe = result.exeRelative.ifEmpty { "" },
|
||||
installVersion = result.buildId, installedAt = System.currentTimeMillis(),
|
||||
cover = item.tallImageUrl ?: item.imageUrl, hero = item.imageUrl,
|
||||
logo = item.logoUrl ?: fetchLogo(game.gameId),
|
||||
extra = mapOf(com.droiddeck.launcher.frontend.WinCompSources.GOG_DEPENDENCIES to result.dependencies.joinToString(","))
|
||||
+ (if (result.dependenciesRead) mapOf(com.droiddeck.launcher.frontend.WinCompSources.LIST_READ to "1") else emptyMap()),
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct Game {
|
||||
pub parent_product_id: String,
|
||||
pub art: String,
|
||||
pub hero: String,
|
||||
pub logo: String,
|
||||
pub developer: String,
|
||||
pub publisher: String,
|
||||
}
|
||||
@@ -39,6 +40,7 @@ impl Game {
|
||||
"sku": self.sku,
|
||||
"art": self.art,
|
||||
"hero": self.hero,
|
||||
"logo": self.logo,
|
||||
"developer": self.developer,
|
||||
"publisher": self.publisher,
|
||||
"dlc": self.is_dlc(),
|
||||
@@ -83,6 +85,7 @@ pub fn read_entitlement(e: &Value) -> Option<Game> {
|
||||
],
|
||||
)
|
||||
.to_string(),
|
||||
logo: str_at(product, &["productDetail", "details", "logoUrl"]).to_string(),
|
||||
developer: str_at(product, &["productDetail", "details", "developer"]).to_string(),
|
||||
publisher: str_at(product, &["productDetail", "details", "publisher"]).to_string(),
|
||||
})
|
||||
@@ -178,17 +181,20 @@ mod tests {
|
||||
fn entitlement_fields_and_fallbacks() {
|
||||
let e = json!({"id":"ent-1","product":{"id":"amzn1.adg.product.x","title":"Game","sku":"SKU1",
|
||||
"productDetail":{"iconUrl":"","details":{"pgCrownImageUrl":"crown","backgroundUrl1":"bg1",
|
||||
"developer":"Dev","publisher":"Pub","productType":"GAME"}}}});
|
||||
"logoUrl":"logo","developer":"Dev","publisher":"Pub","productType":"GAME"}}}});
|
||||
let g = read_entitlement(&e).unwrap();
|
||||
assert_eq!(g.entitlement_id, "ent-1");
|
||||
assert_eq!(g.art, "crown");
|
||||
assert_eq!(g.hero, "bg1");
|
||||
assert_eq!(g.logo, "logo");
|
||||
assert_eq!(g.to_json()["logo"], "logo");
|
||||
assert_eq!(g.product_type, "GAME");
|
||||
assert!(!g.is_dlc());
|
||||
let untitled = read_entitlement(&json!({"product":{"id":"p","type":"DLC","baseProductId":"q"}})).unwrap();
|
||||
assert_eq!(untitled.title, "Unknown");
|
||||
assert_eq!(untitled.entitlement_id, "");
|
||||
assert_eq!(untitled.parent_product_id, "q");
|
||||
assert_eq!(untitled.logo, "");
|
||||
assert!(untitled.is_dlc());
|
||||
assert!(read_entitlement(&json!({"id":"x"})).is_none());
|
||||
assert!(read_entitlement(&json!({"id":"x","product":{"id":""}})).is_none());
|
||||
|
||||
@@ -71,6 +71,7 @@ pub fn details_of(entry: &Value) -> Value {
|
||||
"releaseDate": release,
|
||||
"tall": image(entry, &["DieselGameBoxTall"]),
|
||||
"wide": image(entry, &["DieselGameBox", "Thumbnail"]),
|
||||
"logo": image(entry, &["DieselGameBoxLogo"]),
|
||||
"canRunOffline": offline,
|
||||
"dlc": main_game.is_some_and(|m| !m.is_null()),
|
||||
"mainGameId": main_game.map(|m| net::str_of(m, "id")).unwrap_or(""),
|
||||
@@ -102,7 +103,8 @@ mod tests {
|
||||
"keyImages": [
|
||||
{"type": "Thumbnail", "url": "thumb"},
|
||||
{"type": "DieselGameBox", "url": ""},
|
||||
{"type": "DieselGameBoxTall", "url": "tall"}
|
||||
{"type": "DieselGameBoxTall", "url": "tall"},
|
||||
{"type": "DieselGameBoxLogo", "url": "logo"}
|
||||
],
|
||||
"customAttributes": {"CanRunOffline": {"value": "FALSE"}, "publisherName": {"value": " Pub "}},
|
||||
"seller": {"name": "Seller"},
|
||||
@@ -113,6 +115,7 @@ mod tests {
|
||||
assert_eq!(d["title"], "Game");
|
||||
assert_eq!(d["tall"], "tall");
|
||||
assert_eq!(d["wide"], "thumb");
|
||||
assert_eq!(d["logo"], "logo");
|
||||
assert_eq!(d["publisher"], "Pub");
|
||||
assert_eq!(d["canRunOffline"], false);
|
||||
assert_eq!(d["releaseDate"], "2020-01-01T00:00:00.000Z");
|
||||
@@ -123,6 +126,7 @@ mod tests {
|
||||
assert_eq!(dlc["publisher"], "S");
|
||||
assert_eq!(dlc["releaseDate"], "v");
|
||||
assert_eq!(dlc["canRunOffline"], true);
|
||||
assert_eq!(dlc["logo"], "");
|
||||
assert_eq!(pick_entry(&json!({"x": {"title": "Only"}}), "y").unwrap()["title"], "Only");
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,17 @@ class StoreGameSidecarTest {
|
||||
assertEquals("ns", back.extra["namespace"])
|
||||
}
|
||||
|
||||
@Test fun theStoreLogoRoundTripsAndOlderFilesHaveNone() {
|
||||
val withLogo = sample.copy(logo = "https://x/logo.png")
|
||||
val back = StoreGameSidecar.parse(withLogo.toJson().toString())!!
|
||||
assertEquals("https://x/logo.png", back.logo)
|
||||
assertEquals("https://x/logo.png", back.copy(state = StoreGameSidecar.STATE_UPDATING).logo)
|
||||
assertFalse(sample.toJson().has("logo"))
|
||||
assertNull(StoreGameSidecar.parse(sample.toJson().toString())!!.logo)
|
||||
assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"game.exe","cover":"c","hero":"h"}""")!!.logo)
|
||||
assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"game.exe","logo":""}""")!!.logo)
|
||||
}
|
||||
|
||||
@Test fun defaultsWhenFieldsAreAbsent() {
|
||||
val back = StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"bin\\game.exe","addToSteam":false}""")!!
|
||||
assertEquals("bin/game.exe", back.exe)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.droiddeck.launcher.stores
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import com.droiddeck.launcher.stores.amazon.AmazonLibrary
|
||||
import com.droiddeck.launcher.stores.gog.GogBackend
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class StoreLogoTest {
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
private fun encoded(format: Bitmap.CompressFormat): ByteArray {
|
||||
val bmp = Bitmap.createBitmap(8, 4, Bitmap.Config.ARGB_8888)
|
||||
bmp.eraseColor(Color.TRANSPARENT)
|
||||
bmp.setPixel(1, 1, Color.RED)
|
||||
return ByteArrayOutputStream().also { bmp.compress(format, 100, it) }.toByteArray()
|
||||
}
|
||||
|
||||
@Test fun aPngLogoIsKeptAsItCameWithItsTransparency() {
|
||||
val png = encoded(Bitmap.CompressFormat.PNG)
|
||||
val dst = File(tmp.root, StoreArt.LOGO)
|
||||
assertTrue(StoreArt.writeLogo(png, dst))
|
||||
assertArrayEquals(png, dst.readBytes())
|
||||
val back = BitmapFactory.decodeFile(dst.path)
|
||||
assertEquals(0, Color.alpha(back.getPixel(0, 0)))
|
||||
assertEquals(Color.RED, back.getPixel(1, 1))
|
||||
assertFalse(File(dst.path + ".tmp").exists())
|
||||
}
|
||||
|
||||
@Test fun anotherImageFormatIsStoredAsPng() {
|
||||
val dst = File(tmp.root, StoreArt.LOGO)
|
||||
assertTrue(StoreArt.writeLogo(encoded(Bitmap.CompressFormat.JPEG), dst))
|
||||
val head = dst.readBytes().take(4)
|
||||
assertEquals(listOf(0x89.toByte(), 'P'.code.toByte(), 'N'.code.toByte(), 'G'.code.toByte()), head)
|
||||
assertEquals(8, BitmapFactory.decodeFile(dst.path).width)
|
||||
}
|
||||
|
||||
@Test fun whatIsNotAnImageIsRefusedAndAnExistingLogoStays() {
|
||||
val dst = File(tmp.root, StoreArt.LOGO)
|
||||
assertFalse(StoreArt.writeLogo("<html>not found</html>".toByteArray(), dst))
|
||||
assertFalse(dst.exists())
|
||||
dst.writeBytes(byteArrayOf(1, 2, 3))
|
||||
assertTrue(StoreArt.writeLogo(encoded(Bitmap.CompressFormat.PNG), dst))
|
||||
assertArrayEquals(byteArrayOf(1, 2, 3), dst.readBytes())
|
||||
assertTrue(StoreArt.fetchLogo(tmp.root, "https://invalid.example/logo.png"))
|
||||
}
|
||||
|
||||
@Test fun gogTakesTheV2LogoLinkOnly() {
|
||||
val v2 = JSONObject("""{"_links":{"logo":{"href":"https://images.gog-statics.com/abc_logo.png"},"galaxyBackgroundImage":{"href":"https://x/bg.jpg"}}}""")
|
||||
assertEquals("https://images.gog-statics.com/abc_logo.png", GogBackend.logoOf(v2))
|
||||
assertEquals("https://images.gog-statics.com/rel.png", GogBackend.logoOf(JSONObject("""{"_links":{"logo":{"href":"//images.gog-statics.com/rel.png"}}}""")))
|
||||
assertNull(GogBackend.logoOf(JSONObject("""{"_links":{}}""")))
|
||||
assertNull(GogBackend.logoOf(JSONObject("""{"images":{"logo2x":"https://x/glx_logo_2x.jpg"}}""")))
|
||||
assertNull(GogBackend.logoOf(JSONObject("""{"_links":{"logo":{"href":"http://x/logo.png"}}}""")))
|
||||
}
|
||||
|
||||
@Test fun amazonKeepsTheLogoFromTheLibraryAndItsCache() {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
val fresh = AmazonLibrary.gameFrom(JSONObject().put("productId", "p").put("title", "Game").put("logo", "https://m/logo.png"), null)
|
||||
assertEquals("https://m/logo.png", fresh.logoUrl)
|
||||
assertEquals("https://m/logo.png", AmazonLibrary.toCatalogItem(app, fresh).logoUrl)
|
||||
assertEquals(listOf(fresh), AmazonLibrary.fromCacheJson(AmazonLibrary.toCacheJson(listOf(fresh))))
|
||||
val kept = AmazonLibrary.gameFrom(JSONObject().put("productId", "p").put("title", "Game"), fresh)
|
||||
assertEquals("https://m/logo.png", kept.logoUrl)
|
||||
val none = AmazonLibrary.gameFrom(JSONObject().put("productId", "q").put("title", "Other"), null)
|
||||
assertNull(AmazonLibrary.toCatalogItem(app, none).logoUrl)
|
||||
assertEquals("", AmazonLibrary.fromCacheJson("""[{"productId":"old","title":"Old"}]""").single().logoUrl)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,18 @@ class EpicLibraryTest {
|
||||
assertEquals("Fig", EpicLibrary.gameFrom(record, null, null).title)
|
||||
}
|
||||
|
||||
@Test fun theCatalogLogoIsKeptInTheCacheAndHandedToTheInstall() {
|
||||
val record = JSONObject().put("appName", "Fig").put("namespace", "ns").put("catalogItemId", "cat")
|
||||
val fresh = EpicLibrary.gameFrom(record, JSONObject().put("logo", "https://e/logo.png"), null)
|
||||
assertEquals("https://e/logo.png", fresh.artLogo)
|
||||
assertEquals("https://e/logo.png", EpicLibrary.gameFrom(record, JSONObject().put("title", "Fig"), fresh).artLogo)
|
||||
assertEquals(listOf(fresh), EpicLibrary.fromCacheJson(EpicLibrary.toCacheJson(listOf(fresh))))
|
||||
assertEquals("https://e/logo.png", EpicLibrary.toCatalogItem(app, fresh).logoUrl)
|
||||
val plain = EpicGame("Old", "ns", "c", "Old")
|
||||
assertFalse(JSONArray(EpicLibrary.toCacheJson(listOf(plain))).getJSONObject(0).has("artLogo"))
|
||||
assertEquals(null, EpicLibrary.toCatalogItem(app, plain).logoUrl)
|
||||
}
|
||||
|
||||
@Test fun signOutForgetsOnlyTheLibrary() {
|
||||
EpicPrefs.get(app).edit().putString(EpicLibrary.KEY_CACHE, "[]").putLong(EpicLibrary.KEY_SYNCED_AT, 1L)
|
||||
.putString("cloud_template_Fig", "{AppData}/x").commit()
|
||||
|
||||
Reference in new issue
Block a user