diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt index f6ea660da..d5ca08be2 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt @@ -5,12 +5,14 @@ import android.content.Context import android.content.Intent import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.Icon import android.net.Uri import android.widget.Toast import com.droiddeck.launcher.MainActivity import com.droiddeck.launcher.R +import java.io.File object GameShortcuts { fun launchIntent(context: Context, gameId: String) = Intent(Intent.ACTION_VIEW, @@ -19,22 +21,35 @@ object GameShortcuts { private fun info(context: Context, game: Library.SteamGame): ShortcutInfo { val name = game.name.ifBlank { context.getString(R.string.app_name) } - val icon = listOfNotNull(game.icon, game.art).firstNotNullOfOrNull { file -> runCatching { - val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(file.path, bounds) - if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return@runCatching null - val options = BitmapFactory.Options().apply { - inSampleSize = 1 - while (maxOf(bounds.outWidth, bounds.outHeight) / inSampleSize > 256) inSampleSize *= 2 - } - BitmapFactory.decodeFile(file.path, options)?.let(Icon::createWithBitmap) - }.getOrNull() } ?: Icon.createWithResource(context, R.mipmap.ic_launcher) + val icon = iconBitmap(game)?.let(Icon::createWithBitmap) ?: Icon.createWithResource(context, R.mipmap.ic_launcher) return ShortcutInfo.Builder(context, "game:${game.gameIdString}") .setActivity(android.content.ComponentName(context, MainActivity::class.java)) .setShortLabel(name.take(40)).setLongLabel(name).setIcon(icon) .setIntent(launchIntent(context, game.gameIdString)).build() } + internal fun iconBitmap(game: Library.SteamGame): Bitmap? { + val icon = game.icon?.let(::decode) + if (icon != null && minOf(icon.width, icon.height) >= SHARP_ICON_PX) return icon + return game.art?.let(::decode)?.let(::square) ?: icon + } + + private fun decode(file: File): Bitmap? = runCatching { + val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.path, bounds) + if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return@runCatching null + val options = BitmapFactory.Options().apply { + inSampleSize = 1 + while (minOf(bounds.outWidth, bounds.outHeight) / (inSampleSize * 2) >= ICON_PX) inSampleSize *= 2 + } + BitmapFactory.decodeFile(file.path, options) + }.getOrNull() + + private fun square(bitmap: Bitmap): Bitmap { + val side = minOf(bitmap.width, bitmap.height) + return Bitmap.createBitmap(bitmap, (bitmap.width - side) / 2, (bitmap.height - side) / 2, side, side) + } + fun pin(activity: Activity, game: Library.SteamGame) { val manager = activity.getSystemService(ShortcutManager::class.java) val requested = runCatching { manager.isRequestPinShortcutSupported && @@ -44,4 +59,7 @@ object GameShortcuts { fun result(activity: Activity, game: Library.SteamGame): Intent = activity.getSystemService(ShortcutManager::class.java).createShortcutResultIntent(info(activity, game)) + + private const val ICON_PX = 256 + private const val SHARP_ICON_PX = 128 } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameShortcutsIconTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameShortcutsIconTest.kt new file mode 100644 index 000000000..7f7f2b2b5 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameShortcutsIconTest.kt @@ -0,0 +1,66 @@ +package com.droiddeck.launcher.frontend + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import java.io.File +import java.nio.file.Files +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33], manifest = Config.NONE) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +class GameShortcutsIconTest { + private lateinit var dir: File + + @Before fun setUp() { + dir = Files.createTempDirectory("shortcut-icons").toFile() + } + + @Test fun aSmallSteamIconGivesWayToTheMiddleOfTheCoverArt() { + val icon = image("icon.png", 32, 32, Color.GREEN) + val cover = image("cover.png", 300, 450, Color.BLUE, band = 75, bandColor = Color.RED) + val bitmap = GameShortcuts.iconBitmap(game(icon = icon, art = cover))!! + assertEquals(300, bitmap.width) + assertEquals(300, bitmap.height) + assertEquals(Color.BLUE, bitmap.getPixel(0, 0)) + assertEquals(Color.BLUE, bitmap.getPixel(299, 299)) + } + + @Test fun anIconLargeEnoughForTheHomeScreenIsKept() { + val icon = image("icon.png", 256, 256, Color.GREEN) + val cover = image("cover.png", 300, 450, Color.BLUE) + val bitmap = GameShortcuts.iconBitmap(game(icon = icon, art = cover))!! + assertEquals(256, bitmap.width) + assertEquals(Color.GREEN, bitmap.getPixel(128, 128)) + } + + @Test fun theSmallIconIsStillUsedWhenThereIsNoArt() { + val icon = image("icon.png", 32, 32, Color.GREEN) + val bitmap = GameShortcuts.iconBitmap(game(icon = icon, art = null))!! + assertEquals(32, bitmap.width) + assertEquals(Color.GREEN, bitmap.getPixel(16, 16)) + } + + private fun game(icon: File?, art: File?) = + Library.SteamGame(appId = 17480, name = "Game", art = art, library = "test", icon = icon) + + private fun image(name: String, width: Int, height: Int, color: Int, band: Int = 0, bandColor: Int = color): File { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(color) + if (band > 0) { + val paint = Paint().apply { this.color = bandColor } + canvas.drawRect(0f, 0f, width.toFloat(), band.toFloat(), paint) + canvas.drawRect(0f, (height - band).toFloat(), width.toFloat(), height.toFloat(), paint) + } + return File(dir, name).also { file -> file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) } } + } +}