Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@
<action android:name="app.gamenative.LAUNCH_GAME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="gamenative" android:host="run" />
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
</intent-filter>

<!--Allows app window to be resized on Oculus Quest VR headsets-->
<meta-data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum class AppOptionMenuType(@StringRes val title: Int) {
StorePage(R.string.option_open_store_page),
CreateShortcut(R.string.create_shortcut),
ExportFrontend(R.string.option_export_for_frontend),
CopyURI(R.string.option_copy_web_uri),
RunContainer(R.string.option_open_container),
EditContainer(R.string.option_edit_container),
ResetToDefaults(R.string.option_reset_to_defaults),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.gamenative.ui.screen.library.appscreen

import android.content.ClipData
import android.content.Context
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
Expand All @@ -20,6 +21,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider
Expand Down Expand Up @@ -526,6 +529,31 @@ abstract class BaseAppScreen {
)
}

@Composable
protected open fun getCopyWebUriOption(
context: Context,
libraryItem: LibraryItem,
): AppMenuOption? {
val gameId = getGameId(libraryItem)
val gameSource = getGameSource(libraryItem)
val gameName = getGameName(context, libraryItem)
val uri = "gamenative://run?appid=$gameId&gamesource=$gameSource"
val labelText = context.getString(R.string.app_name) + " $gameName URI"
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { }
return AppMenuOption(
optionType = AppOptionMenuType.CopyURI,
onClick = {
scope.launch {
clipboardManager.setClipEntry(
ClipEntry(ClipData.newPlainText(labelText, uri))
)
}
},
)
}

/**
* Get "Use known config" menu option. Subclasses can override to customize behavior
* or disable it entirely by returning null.
Expand Down Expand Up @@ -957,6 +985,7 @@ abstract class BaseAppScreen {
getResetContainerOption(context, libraryItem)?.let { menuOptions.add(it) }
getCreateShortcutOption(context, libraryItem)?.let { menuOptions.add(it) }
getExportContainerOption(context, libraryItem, exportFrontendLauncher)?.let { menuOptions.add(it) }
getCopyWebUriOption(context, libraryItem)?.let { menuOptions.add(it) }
}

// Always available options
Expand Down Expand Up @@ -1103,6 +1132,11 @@ abstract class BaseAppScreen {
},
)

// Export for web URI
val copyWebUri = {
val content = getGameId(libraryItem).toString()
}

var exportConfigRequested by remember(appId) {
mutableStateOf(shouldExportConfig(appId))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ private fun getIconForOption(type: AppOptionMenuType): ImageVector {
AppOptionMenuType.StorePage -> Icons.AutoMirrored.Filled.OpenInNew
AppOptionMenuType.CreateShortcut -> Icons.AutoMirrored.Filled.AddToHomeScreen
AppOptionMenuType.ExportFrontend -> Icons.Default.Share
AppOptionMenuType.CopyURI -> Icons.Default.Share
AppOptionMenuType.RunContainer -> Icons.Default.PlayArrow
AppOptionMenuType.EditContainer -> Icons.Default.Settings
AppOptionMenuType.ResetToDefaults -> Icons.Default.RestartAlt
Expand Down Expand Up @@ -368,6 +369,7 @@ private fun groupOptions(options: List<AppMenuOption>): Map<OptionCategory, List
AppOptionMenuType.RunContainer,
AppOptionMenuType.CreateShortcut,
AppOptionMenuType.ExportFrontend,
AppOptionMenuType.CopyURI,
-> quickActions.add(option)

// Game Management
Expand Down
62 changes: 42 additions & 20 deletions app/src/main/java/app/gamenative/utils/IntentLaunchManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ object IntentLaunchManager {
private const val EXTRA_GAME_SOURCE = "game_source"
private const val EXTRA_CONTAINER_CONFIG = "container_config"
private const val ACTION_LAUNCH_GAME = "app.gamenative.LAUNCH_GAME"
private const val ACTION_VIEW = "android.intent.action.VIEW"
private const val URI_SCHEME = "gamenative"
private const val URI_HOST = "run"
private const val MAX_CONFIG_JSON_SIZE = 50000 // 50KB limit to prevent memory exhaustion

data class LaunchRequest(
Expand All @@ -30,41 +33,60 @@ object IntentLaunchManager {
fun parseLaunchIntent(intent: Intent): LaunchRequest? {
Timber.d("[IntentLaunchManager]: Parsing intent: action=${intent.action}")

if (intent.action != ACTION_LAUNCH_GAME) {
Timber.d("[IntentLaunchManager]: Intent action '${intent.action}' doesn't match expected action '$ACTION_LAUNCH_GAME'")
if (intent.action != ACTION_LAUNCH_GAME && intent.action != ACTION_VIEW) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Timber.d("[IntentLaunchManager]: Intent action '${intent.action}' doesn't match expected action '$ACTION_LAUNCH_GAME' or '$ACTION_VIEW'")
return null
}
var gameId: Int? = 0
var gameSource: String? = ""
val containerConfigJson = if (intent.action == ACTION_LAUNCH_GAME) {
intent.getStringExtra(EXTRA_CONTAINER_CONFIG)
} else {
null
}
val containerConfig = if (containerConfigJson != null) {
try {
parseContainerConfig(containerConfigJson)
} catch (e: Exception) {
Timber.e(e, "[IntentLaunchManager]: Failed to parse container configuration JSON")
null
}
} else {
null
}

val gameId = intent.getIntExtra(EXTRA_APP_ID, -1)
Timber.d("[IntentLaunchManager]: Extracted app_id: $gameId from intent extras")
if (intent.action == ACTION_VIEW) {
if (intent.data?.scheme != URI_SCHEME || intent.data?.host != URI_HOST) {
Timber.d("[IntentLaunchManager]: URI '${intent.dataString}' doesn't match expected URI '$URI_SCHEME://$URI_HOST'")
return null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
val data = intent.data
Timber.d(data?.getQueryParameterNames().toString())
gameId = data?.getQueryParameter("appid")?.toIntOrNull() ?:0
gameSource = data?.getQueryParameter("gamesource")?.uppercase(java.util.Locale.ROOT)
}

if (gameId <= 0) {
if (intent.action == ACTION_LAUNCH_GAME) {
gameId = intent.getIntExtra(EXTRA_APP_ID, -1)
Timber.d("[IntentLaunchManager]: Extracted app_id: $gameId from intent extras")
// Get Game Source for launch intent
gameSource = intent.getStringExtra(EXTRA_GAME_SOURCE)?.uppercase(java.util.Locale.ROOT)
}

if (gameId == null || gameId <= 0) {
Timber.w("[IntentLaunchManager]: Invalid or missing app_id in launch intent: $gameId")
return null
}

// Get Game Source for launch intent
var gameSource = intent.getStringExtra(EXTRA_GAME_SOURCE)?.uppercase(java.util.Locale.ROOT)
val isValidGameSource = GameSource.entries.any { it.name == gameSource }
if (!isValidGameSource) {
gameSource = GameSource.STEAM.name
Timber.w("[IntentLaunchManager]: Invalid or missing game source in launch intent: $gameSource")
return null
}

val appId = "${gameSource}_$gameId"
Timber.d("[IntentLaunchManager]: Converted to appId: $appId")

val containerConfigJson = intent.getStringExtra(EXTRA_CONTAINER_CONFIG)
val containerConfig = if (containerConfigJson != null) {
try {
parseContainerConfig(containerConfigJson)
} catch (e: Exception) {
Timber.e(e, "[IntentLaunchManager]: Failed to parse container configuration JSON")
null
}
} else {
null
}

return LaunchRequest(appId, containerConfig)
}

Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@
<!-- App option menu labels -->
<string name="option_open_store_page">Open store page</string>
<string name="option_export_for_frontend">Export for frontend</string>
<string name="option_copy_web_uri">Copy web URI</string>
<string name="option_open_container">Open container</string>
<string name="option_edit_container">Edit container</string>
<string name="option_reset_to_defaults">Reset container</string>
Expand Down