diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..df78d64e9a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +name: Build + +on: + push: + branches: + - master + - "claude/**" + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + build: + name: Build Release APK + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + # Use the official release keystore if the secret is available, + # otherwise generate a keystore using the hardcoded credentials from + # build.gradle.kts so the release build type can sign the APK. + - name: Create release keystore from secret + if: ${{ secrets.RELEASE_KEYSTORE != '' }} + run: | + echo "${{ secrets.RELEASE_KEYSTORE }}" > release.keystore.asc + gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSPHRASE }}" --batch release.keystore.asc > release.jks + + - name: Generate release keystore (CI fallback) + if: ${{ secrets.RELEASE_KEYSTORE == '' }} + run: | + keytool -genkeypair -v \ + -keystore release.jks \ + -alias lemuroid \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass lemuroid \ + -keypass lemuroid \ + -dname "CN=Lemuroid CI, O=Lemuroid, C=US" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # freeDynamicRelease: 'free' (open-source) + 'dynamic' (cores downloaded at runtime). + # Matches what publish.yml uses; avoids bundling compiled .so core files which require + # the lemuroid-cores submodule to be fully built. + - name: Build release APK + run: ./gradlew assembleFreeDynamicRelease + + - name: Upload release APK + uses: actions/upload-artifact@v4 + with: + name: lemuroid-release-${{ github.sha }} + path: lemuroid-app/build/outputs/apk/freeDynamic/release/lemuroid-app-free-dynamic-release.apk + retention-days: 30 diff --git a/lemuroid-app/src/main/AndroidManifest.xml b/lemuroid-app/src/main/AndroidManifest.xml index ed9b150c75..94dfcf6803 100644 --- a/lemuroid-app/src/main/AndroidManifest.xml +++ b/lemuroid-app/src/main/AndroidManifest.xml @@ -52,6 +52,7 @@ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode" android:exported="true" android:theme="@style/LemuroidMaterialTheme.Game"> + @@ -60,6 +61,23 @@ android:pathPattern="/play-game/id/.*" android:scheme="lemuroid" /> + + + + + + + + + + + + + + + + + { + val gameId = uri.pathSegments?.let { it[it.size - 1].toInt() }!! + loadGameById(gameId) + } + "file", "content" -> loadGameByFileUri(uri) + else -> throw IllegalArgumentException("Unsupported URI scheme: ${uri?.scheme}") + } } catch (e: Throwable) { displayErrorMessage() } @@ -82,13 +95,38 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { } } - private suspend fun loadGame(gameId: Int) { + private suspend fun loadGameById(gameId: Int) { waitPendingOperations() val game = retrogradeDatabase.gameDao().selectById(gameId) ?: throw IllegalArgumentException("Game not found: $gameId") + launchGame(game) + } + + private suspend fun loadGameByFileUri(uri: Uri) { + waitPendingOperations() + + val uriString = uri.toString() + + // Try exact URI match first + var game: Game? = retrogradeDatabase.gameDao().selectByFileUri(uriString) + + // Fall back to filename match (handles scheme differences between launchers and indexer) + if (game == null) { + val fileName = uri.lastPathSegment + ?: throw IllegalArgumentException("Cannot determine filename from URI: $uriString") + game = retrogradeDatabase.gameDao().selectByFileName(fileName) + } + + val resolvedGame = game + ?: throw IllegalArgumentException("ROM not found in library. Please scan your library first.") + + launchGame(resolvedGame) + } + + private suspend fun launchGame(game: Game) { delay(animationDuration().toLong()) val gameLaunchSuccessful = gameLauncher.launchGameAsync( diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/db/dao/GameDao.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/db/dao/GameDao.kt index 92bffd8b5a..aea7be760d 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/db/dao/GameDao.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/db/dao/GameDao.kt @@ -36,6 +36,9 @@ interface GameDao { @Query("SELECT * FROM games WHERE fileUri = :fileUri") fun selectByFileUri(fileUri: String): Game? + @Query("SELECT * FROM games WHERE fileName = :fileName LIMIT 1") + suspend fun selectByFileName(fileName: String): Game? + @Query("SELECT * FROM games WHERE lastIndexedAt < :lastIndexedAt") fun selectByLastIndexedAtLessThan(lastIndexedAt: Long): List