mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Use plain-ID .steam files for frontend exports (#277)
This commit is contained in:
1 parent
46cdc32cf1
commit
d9bb5e15b0
7 files changed
+217
-31
No files matched your search
@@ -71,6 +71,10 @@
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
|
||||
|
||||
@@ -65,6 +65,9 @@ import com.droiddeck.launcher.ui.FrontEndState
|
||||
import com.droiddeck.launcher.ui.FrontEndActions
|
||||
import com.droiddeck.launcher.frontend.CoverArt
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.frontend.GameFiles
|
||||
import com.droiddeck.launcher.frontend.GameFileSync
|
||||
import com.droiddeck.launcher.frontend.GameLaunchIntent
|
||||
import com.droiddeck.launcher.ui.DroidDeckTheme
|
||||
import com.droiddeck.launcher.ui.RomsDialog
|
||||
import com.droiddeck.launcher.files.InAppFilePicker
|
||||
@@ -399,10 +402,15 @@ class MainActivity : ComponentActivity() {
|
||||
private fun pickGameExport(game: Library.SteamGame?) {
|
||||
onSavePicked = { folder ->
|
||||
saveAction(getString(R.string.game_frontend_files)) {
|
||||
if (game != null) com.droiddeck.launcher.frontend.GameFiles.export(folder, game)
|
||||
else com.droiddeck.launcher.frontend.GameFileSync.enable(this, folder)
|
||||
ui.post { gameSyncFolder = com.droiddeck.launcher.frontend.GameFileSync.folder(this) }
|
||||
getString(R.string.game_file_exported, folder.path)
|
||||
val saved = if (game != null) GameFiles.export(folder, game)
|
||||
else {
|
||||
GameFileSync.enable(this, folder)
|
||||
folder
|
||||
}
|
||||
ui.post {
|
||||
gameSyncFolder = GameFileSync.folder(this)
|
||||
}
|
||||
getString(R.string.game_file_exported, saved.path)
|
||||
}
|
||||
}
|
||||
pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.game_file_folder), gameSyncFolder))
|
||||
@@ -416,10 +424,10 @@ class MainActivity : ComponentActivity() {
|
||||
shortcutLibraryScanning = true
|
||||
} else shortcutLibraryScanning = false
|
||||
pendingGameLink = null
|
||||
if (request.action == Intent.ACTION_VIEW) {
|
||||
if (GameLaunchIntent.accepts(request.action)) {
|
||||
val copy = Intent(request)
|
||||
Thread({
|
||||
val id = com.droiddeck.launcher.frontend.GameFiles.readIntent(this, copy)
|
||||
val id = GameLaunchIntent.read(this, copy)
|
||||
ui.post {
|
||||
if (intent === request && !isDestroyed) {
|
||||
pendingGameLink = id
|
||||
@@ -462,10 +470,14 @@ class MainActivity : ComponentActivity() {
|
||||
if (game == null) {
|
||||
pendingGameLink = null
|
||||
intent.data = null
|
||||
intent.action = Intent.ACTION_MAIN
|
||||
intent.removeExtra(GameLaunchIntent.EXTRA_APP_ID)
|
||||
android.widget.Toast.makeText(this, R.string.game_link_missing, android.widget.Toast.LENGTH_LONG).show()
|
||||
} else if (launchGame(game)) {
|
||||
pendingGameLink = null
|
||||
intent.data = null
|
||||
intent.action = Intent.ACTION_MAIN
|
||||
intent.removeExtra(GameLaunchIntent.EXTRA_APP_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +489,7 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
readGameIntent(intent)
|
||||
gameSyncFolder = com.droiddeck.launcher.frontend.GameFileSync.folder(this)
|
||||
gameSyncFolder = GameFileSync.folder(this)
|
||||
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
|
||||
refreshPhantomStatus()
|
||||
theme = SessionPrefs.theme(this)
|
||||
@@ -556,7 +568,7 @@ class MainActivity : ComponentActivity() {
|
||||
onSyncGameFiles = { pickGameExport(null) },
|
||||
onStopGameFileSync = {
|
||||
Thread({
|
||||
com.droiddeck.launcher.frontend.GameFileSync.disable(this)
|
||||
GameFileSync.disable(this)
|
||||
ui.post { gameSyncFolder = null }
|
||||
}, "game-file-stop-sync").start()
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.io.InputStream
|
||||
|
||||
/** Small, portable launch files for frontends that scan ROM folders. No guest paths or commands. */
|
||||
object GameFiles {
|
||||
const val EXTENSION = "droiddeck"
|
||||
const val EXTENSION = "steam"
|
||||
private const val MAX_BYTES = 128
|
||||
private const val INVENTORY = ".droiddeck-sync.json"
|
||||
private const val FORMAT = "droiddeck-game-files-v1"
|
||||
@@ -46,35 +46,49 @@ object GameFiles {
|
||||
}
|
||||
if (count > MAX_BYTES) return null
|
||||
val text = String(bytes, 0, count, Charsets.UTF_8).trim()
|
||||
return GameLaunchLink.parse(text) ?: text.takeIf(GameLaunchLink::validId)
|
||||
return text.takeIf(GameLaunchLink::validId)
|
||||
}
|
||||
|
||||
fun filename(game: Library.SteamGame): String {
|
||||
fun filename(game: Library.SteamGame, disambiguate: Boolean = false): String {
|
||||
val sanitized = game.name.replace(Regex("[\\p{Cntrl}\\\\/:*?\"<>|]"), "_")
|
||||
.trim().trim('.').ifBlank { "Game" }
|
||||
val points = sanitized.codePoints().limit(40).toArray()
|
||||
val title = String(points, 0, points.size)
|
||||
return "$title (${game.gameIdString}).$EXTENSION"
|
||||
val suffix = if (disambiguate) " (${game.gameIdString})" else ""
|
||||
return "$title$suffix.$EXTENSION"
|
||||
}
|
||||
|
||||
private fun contents(id: String) = GameLaunchLink.uri(id) + "\n"
|
||||
private fun contents(id: String): String {
|
||||
require(GameLaunchLink.validId(id))
|
||||
return id
|
||||
}
|
||||
|
||||
@Synchronized fun export(folder: File, game: Library.SteamGame): File {
|
||||
@Synchronized fun export(folder: File, game: Library.SteamGame): File =
|
||||
exportNamed(folder, game, filename(game))
|
||||
|
||||
private fun exportNamed(folder: File, game: Library.SteamGame, name: String): File {
|
||||
require(folder.isDirectory) { "Folder is unavailable" }
|
||||
val target = child(folder, filename(game))
|
||||
val text = contents(game.gameIdString)
|
||||
val target = exportTarget(folder, game, name)
|
||||
require(!target.exists() || matches(target, text)) { "A different file already uses ${target.name}" }
|
||||
if (!target.exists()) write(target, text)
|
||||
exportArt(folder, game)
|
||||
return target
|
||||
}
|
||||
|
||||
private fun exportTarget(folder: File, game: Library.SteamGame, name: String): File {
|
||||
val target = child(folder, name)
|
||||
return if (target.exists() && !matches(target, contents(game.gameIdString))) {
|
||||
child(folder, filename(game, disambiguate = true))
|
||||
} else target
|
||||
}
|
||||
|
||||
/** Cocoon matches icons in its data folder's images directory. Never replace custom art. */
|
||||
private fun exportArt(folder: File, game: Library.SteamGame) {
|
||||
val art = listOfNotNull(game.icon, game.art).firstOrNull {
|
||||
it.isFile && it.extension.lowercase() in listOf("jpg", "jpeg", "png", "webp")
|
||||
} ?: return
|
||||
val title = filename(game).substringBeforeLast(" (${game.gameIdString})")
|
||||
val title = filename(game).substringBeforeLast(".")
|
||||
.replace(Regex("\\[[^]]*]|\\([^)]*\\)"), "")
|
||||
.replace(Regex("[^a-zA-Z0-9\\s]"), "").trim()
|
||||
.replace(Regex("\\s+"), "-").lowercase(java.util.Locale.ROOT).take(50).trimEnd('-')
|
||||
@@ -95,12 +109,20 @@ object GameFiles {
|
||||
record.getJSONObject("games")
|
||||
} else JSONObject()
|
||||
val next = JSONObject()
|
||||
val exports = games.distinctBy { it.gameId }.map { game ->
|
||||
val target = child(folder, filename(game))
|
||||
val unique = games.distinctBy { it.gameId }
|
||||
val names = unique.groupingBy { filename(it).lowercase(java.util.Locale.ROOT) }.eachCount()
|
||||
val exports = unique.map { game ->
|
||||
val name = filename(game, disambiguate = names.getValue(filename(game).lowercase(java.util.Locale.ROOT)) > 1)
|
||||
val preferred = child(folder, name)
|
||||
val target = if (inventoryOwns(folder, previous, preferred, game.gameIdString)) preferred
|
||||
else exportTarget(folder, game, name)
|
||||
val text = contents(game.gameIdString)
|
||||
val existed = target.exists()
|
||||
// Keep an edited export without letting it block the rest of the library sync.
|
||||
val file = if (previous.optString(target.name) == game.gameIdString && target.exists() &&
|
||||
!matches(target, contents(game.gameIdString))) target else export(folder, game)
|
||||
next.put(file.name, game.gameIdString)
|
||||
val owned = inventoryOwns(folder, previous, target, game.gameIdString)
|
||||
val file = if (owned && target.exists() &&
|
||||
!matches(target, text)) target else exportNamed(folder, game, target.name)
|
||||
if (!existed || owned) next.put(file.name, game.gameIdString)
|
||||
file
|
||||
}
|
||||
// Write new exports before pruning; a failed write must not remove old launch files.
|
||||
@@ -119,6 +141,16 @@ object GameFiles {
|
||||
if (previous.toString() != next.toString()) write(inventory, JSONObject().put("format", FORMAT).put("games", next).toString())
|
||||
}
|
||||
|
||||
private fun inventoryOwns(folder: File, record: JSONObject, file: File, id: String): Boolean {
|
||||
if (record.optString(file.name) == id) return true
|
||||
if (!file.exists()) return false
|
||||
return record.keys().asSequence().any { name ->
|
||||
record.optString(name) == id && child(folder, name).let { old ->
|
||||
old.exists() && java.nio.file.Files.isSameFile(old.toPath(), file.toPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun child(folder: File, name: String): File {
|
||||
require(name.isNotEmpty() && '/' !in name && '\\' !in name && name != "." && name != "..")
|
||||
val file = File(folder, name)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
object GameLaunchIntent {
|
||||
const val ACTION = "com.droiddeck.launcher.LAUNCH_GAME"
|
||||
const val EXTRA_APP_ID = "app_id"
|
||||
const val EXTRA_GAME_SOURCE = "game_source"
|
||||
private const val GAMENATIVE_ACTION = "app.gamenative.LAUNCH_GAME"
|
||||
|
||||
fun accepts(action: String?): Boolean = action == Intent.ACTION_VIEW || action == ACTION || action == GAMENATIVE_ACTION
|
||||
|
||||
fun read(context: Context, intent: Intent): String? = when (intent.action) {
|
||||
Intent.ACTION_VIEW -> GameFiles.readIntent(context, intent)
|
||||
ACTION, GAMENATIVE_ACTION -> readId(intent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
internal fun readId(intent: Intent): String? = runCatching {
|
||||
if (intent.action != ACTION && intent.action != GAMENATIVE_ACTION) return null
|
||||
val source = intent.extras?.get(EXTRA_GAME_SOURCE)
|
||||
if (source != null && (source !is String || !source.equals("STEAM", ignoreCase = true))) return null
|
||||
val id = when (val value = intent.extras?.get(EXTRA_APP_ID)) {
|
||||
is Int -> value.takeIf { it > 0 }?.toString()
|
||||
is String -> value.takeIf(GameLaunchLink::validId)
|
||||
else -> null
|
||||
}
|
||||
id
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class GameFileSyncTest {
|
||||
}
|
||||
val folder = File(context.filesDir, "exports").apply { mkdirs() }
|
||||
GameFileSync.enable(context, folder)
|
||||
assertEquals("8400", File(folder, "Geometry Wars.steam").readText())
|
||||
assertEquals(folder.path, GameFileSync.folder(context))
|
||||
assertEquals(1, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION })
|
||||
// An unavailable source must leave the existing export intact.
|
||||
@@ -35,6 +36,8 @@ class GameFileSyncTest {
|
||||
assertThrows(IllegalStateException::class.java) { GameFileSync.syncConfigured(context) }
|
||||
assertEquals(1, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION })
|
||||
assertTrue(unavailable.renameTo(steamapps))
|
||||
GameFileSync.syncConfigured(context)
|
||||
assertTrue(File(folder, "Geometry Wars.steam").exists())
|
||||
// A genuine uninstall removes the owned export at the next tick.
|
||||
assertTrue(manifest.delete())
|
||||
GameFileSync.syncConfigured(context)
|
||||
@@ -44,6 +47,7 @@ class GameFileSyncTest {
|
||||
manifest.writeText("\"name\" \"Geometry Wars\"\n\"StateFlags\" \"4\"")
|
||||
GameFileSync.syncConfigured(context)
|
||||
assertNull(GameFileSync.folder(context))
|
||||
assertFalse(File(folder, "Geometry Wars.steam").exists())
|
||||
assertEquals(0, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION })
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ class GameFilesTest {
|
||||
@Test fun exportsPortableFilesAndAcceptsFileUriRawPathAndContentUri() {
|
||||
val folder = tmp.newFolder()
|
||||
val file = GameFiles.export(folder, game("18446744073709551615", "A/B: C"))
|
||||
assertEquals("A_B_ C (18446744073709551615).droiddeck", file.name)
|
||||
assertEquals("droiddeck://game/18446744073709551615\n", file.readText())
|
||||
assertEquals("A_B_ C.steam", file.name)
|
||||
assertEquals("18446744073709551615", file.readText())
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
listOf(Uri.fromFile(file), Uri.parse(file.path)).forEach { uri ->
|
||||
assertEquals("18446744073709551615", GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, uri)))
|
||||
@@ -44,15 +44,15 @@ class GameFilesTest {
|
||||
|
||||
@Test fun rejectsCommandsMalformedIdsLargeFilesAndUnsupportedIntents() {
|
||||
listOf("", "0", "01", "-1", "18446744073709551616", "steam://rungameid/8400",
|
||||
"8400\n8401", "droiddeck://game/8400?command=quit", "x".repeat(129)).forEach { value ->
|
||||
"8400\n8401", "droiddeck://game/8400", "droiddeck://game/8400?command=quit", "x".repeat(129)).forEach { value ->
|
||||
assertNull(value, GameFiles.read(ByteArrayInputStream(value.toByteArray())))
|
||||
}
|
||||
assertEquals("8400", GameFiles.read(ByteArrayInputStream("8400\n".toByteArray())))
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_SEND, Uri.parse("droiddeck://game/8400"))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/8400.droiddeck"))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.parse("file://remote/game.droiddeck"))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.fromFile(File(tmp.root, "missing.droiddeck")))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/8400.steam"))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.parse("file://remote/game.steam"))))
|
||||
assertNull(GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.fromFile(File(tmp.root, "missing.steam")))))
|
||||
assertEquals("8400", GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, Uri.parse("droiddeck://game/8400"))))
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ class GameFilesTest {
|
||||
assertEquals("custom artwork", image.readText())
|
||||
GameFiles.sync(folder, emptyList())
|
||||
assertEquals("custom artwork", image.readText())
|
||||
assertEquals(0, folder.listFiles { f -> f.extension == GameFiles.EXTENSION }!!.size)
|
||||
assertEquals("8400", File(folder, GameFiles.filename(game)).readText())
|
||||
assertEquals(1, folder.listFiles { f -> f.extension == GameFiles.EXTENSION }!!.size)
|
||||
icon.delete()
|
||||
val fallback = tmp.newFolder()
|
||||
GameFiles.export(fallback, game)
|
||||
@@ -80,7 +81,7 @@ class GameFilesTest {
|
||||
|
||||
@Test fun syncAddsRenamesAndRemovesOwnedFilesAndPreservesUnrelatedAndEditedFiles() {
|
||||
val folder = tmp.newFolder()
|
||||
val unrelated = File(folder, "manual.droiddeck").apply { writeText("droiddeck://game/1\n") }
|
||||
val unrelated = File(folder, "manual.steam").apply { writeText("1") }
|
||||
val note = File(folder, "notes.txt").apply { writeText("Keep me") }
|
||||
val first = game()
|
||||
val second = game("620", "Portal 2")
|
||||
@@ -110,10 +111,12 @@ class GameFilesTest {
|
||||
GameFiles.sync(folder, listOf(first))
|
||||
val second = game("620", "Portal 2")
|
||||
val collision = File(folder, GameFiles.filename(second)).apply { writeText("personal file") }
|
||||
val fallback = File(folder, GameFiles.filename(second, disambiguate = true)).apply { writeText("another personal file") }
|
||||
assertThrows(IllegalArgumentException::class.java) { GameFiles.sync(folder, listOf(second)) }
|
||||
assertEquals("personal file", collision.readText())
|
||||
assertTrue(File(folder, GameFiles.filename(first)).isFile)
|
||||
collision.delete()
|
||||
fallback.delete()
|
||||
GameFiles.sync(folder, listOf(second))
|
||||
assertFalse(File(folder, GameFiles.filename(first)).exists())
|
||||
assertTrue(collision.isFile)
|
||||
@@ -132,10 +135,10 @@ class GameFilesTest {
|
||||
@Test fun refusesSymlinksLeavingTheExportFolder() {
|
||||
val folder = tmp.newFolder()
|
||||
val outside = tmp.newFile()
|
||||
outside.writeText("droiddeck://game/8400\n")
|
||||
outside.writeText("8400")
|
||||
java.nio.file.Files.createSymbolicLink(File(folder, GameFiles.filename(game())).toPath(), outside.toPath())
|
||||
assertThrows(IllegalArgumentException::class.java) { GameFiles.export(folder, game()) }
|
||||
assertEquals("droiddeck://game/8400\n", outside.readText())
|
||||
assertEquals("8400", outside.readText())
|
||||
}
|
||||
|
||||
@Test fun caseOnlyRenameKeepsTheLiveExportOnCaseInsensitiveStorage() {
|
||||
@@ -148,6 +151,8 @@ class GameFilesTest {
|
||||
assertTrue(file.isFile)
|
||||
assertEquals("8400", file.inputStream().use(GameFiles::read))
|
||||
assertEquals(1, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION })
|
||||
GameFiles.sync(folder, emptyList())
|
||||
assertFalse(file.exists())
|
||||
}
|
||||
|
||||
@Test fun unrelatedInventoryIsPreserved() {
|
||||
@@ -158,6 +163,52 @@ class GameFilesTest {
|
||||
assertEquals(0, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION })
|
||||
}
|
||||
|
||||
@Test fun steamExportContainsOnlyIdForSteamAndAddedGames() {
|
||||
val folder = tmp.newFolder()
|
||||
val file = GameFiles.export(folder, game("620", "Portal 2"))
|
||||
assertEquals("Portal 2.steam", file.name)
|
||||
assertEquals("620", file.readText())
|
||||
assertEquals("620", GameFiles.readIntent(RuntimeEnvironment.getApplication(), Intent(Intent.ACTION_VIEW, Uri.fromFile(file))))
|
||||
val added = Library.SteamGame(1, "Added Game", null, Library.ADDED, -1L)
|
||||
val addedFile = GameFiles.export(folder, added)
|
||||
assertEquals("Added Game.steam", addedFile.name)
|
||||
assertEquals("18446744073709551615", addedFile.readText())
|
||||
}
|
||||
|
||||
@Test fun steamSyncDisambiguatesTitlesAndPreservesEditedAndForeignFiles() {
|
||||
val folder = tmp.newFolder()
|
||||
val first = game("620", "A/B")
|
||||
val second = game("8400", "A:B")
|
||||
val foreign = File(folder, "Existing.steam").apply { writeText("1") }
|
||||
val existing = game("1", "Existing")
|
||||
GameFiles.sync(folder, listOf(first, second, existing))
|
||||
val owned = File(folder, "A_B (620).steam")
|
||||
assertEquals("620", owned.readText())
|
||||
assertEquals("8400", File(folder, "A_B (8400).steam").readText())
|
||||
owned.writeText("user edit")
|
||||
GameFiles.sync(folder, listOf(first, second, existing))
|
||||
assertEquals("user edit", owned.readText())
|
||||
GameFiles.sync(folder, emptyList())
|
||||
assertEquals("user edit", owned.readText())
|
||||
assertEquals("1", foreign.readText())
|
||||
assertFalse(File(folder, "A_B (8400).steam").exists())
|
||||
}
|
||||
|
||||
@Test fun steamCollisionFallbackDoesNotClaimAnExistingFile() {
|
||||
val folder = tmp.newFolder()
|
||||
val collision = File(folder, "Geometry Wars.steam").apply { writeText("other file") }
|
||||
val existing = File(folder, "Geometry Wars (8400).steam").apply { writeText("8400") }
|
||||
GameFiles.sync(folder, listOf(game()))
|
||||
GameFiles.sync(folder, emptyList())
|
||||
assertEquals("other file", collision.readText())
|
||||
assertEquals("8400", existing.readText())
|
||||
existing.writeText("another file")
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
GameFiles.sync(folder, listOf(game()))
|
||||
}
|
||||
assertEquals("another file", existing.readText())
|
||||
}
|
||||
|
||||
private class LaunchFileProvider(private val file: File) : ContentProvider() {
|
||||
override fun onCreate() = true
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Intent
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class GameLaunchIntentTest {
|
||||
@Test fun acceptsNativeAndExplicitlyRetargetedGameNativeProfiles() {
|
||||
listOf(GameLaunchIntent.ACTION, "app.gamenative.LAUNCH_GAME").forEach { action ->
|
||||
val request = Intent(action).putExtra("app_id", 620).putExtra("game_source", "STEAM")
|
||||
assertEquals("620", GameLaunchIntent.read(RuntimeEnvironment.getApplication(), request))
|
||||
assertEquals("8400", GameLaunchIntent.readId(Intent(action).putExtra("app_id", "8400")))
|
||||
assertEquals("620", GameLaunchIntent.readId(request.putExtra("game_source", "steam")))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun stringIdsPreserveUnsignedShortcutIds() {
|
||||
listOf("2147483648", "18446744073709551615").forEach { id ->
|
||||
assertEquals(id, GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", id)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun rejectsUnsupportedSourcesTypesAndIdsWithoutFallingBackToData() {
|
||||
listOf("", "0", "01", "+1", "-1", "18446744073709551616", "620\n", "steam://rungameid/620").forEach { id ->
|
||||
assertNull(id, GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", id)))
|
||||
}
|
||||
listOf("EPIC", "GOG", "CUSTOM_GAME", "", "STEAM ").forEach { source ->
|
||||
assertNull(GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", 620).putExtra("game_source", source)))
|
||||
}
|
||||
assertNull(GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", 620L)))
|
||||
assertNull(GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", -1)))
|
||||
assertNull(GameLaunchIntent.readId(Intent(GameLaunchIntent.ACTION).putExtra("app_id", 620).putExtra("game_source", 1)))
|
||||
assertNull(GameLaunchIntent.readId(Intent(Intent.ACTION_MAIN).putExtra("app_id", 620)))
|
||||
assertNull(GameLaunchIntent.read(RuntimeEnvironment.getApplication(), Intent(GameLaunchIntent.ACTION,
|
||||
android.net.Uri.parse("droiddeck://game/620"))))
|
||||
}
|
||||
|
||||
@Test fun deepLinksStillAcceptUnsignedShortcutIds() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
assertEquals("18446744073709551615", GameLaunchIntent.read(context,
|
||||
Intent(Intent.ACTION_VIEW, android.net.Uri.parse("droiddeck://game/18446744073709551615"))))
|
||||
assertFalse(GameLaunchIntent.accepts(Intent.ACTION_SEND))
|
||||
assertTrue(GameLaunchIntent.accepts(GameLaunchIntent.ACTION))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user