From 52efdabf61bd32f0d2f3b11ac5017eaa02ae2383 Mon Sep 17 00:00:00 2001 From: anxiousintrovert <82425472+anxiousintrovert@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:04:43 -0500 Subject: [PATCH] Harden required import picker for Android 13 --- .../java/org/love2d/android/GameActivity.java | 18 ++++++++++++++++- tests/engine/android_host_extension_test.lua | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java index dbd409e8..7a30850c 100644 --- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java +++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java @@ -90,6 +90,9 @@ public class GameActivity extends SDLActivity { private static final String PICKED_ROM_FILENAME = "picked_rom.gb"; private static final String PICKED_MOD_FILENAME = "picked_mod.zip"; private static final String PICKED_SAVE_FILENAME = "picked_save.sav"; + // Kept separate from the game-ROM destination so a dependency pick can + // never be mistaken for a game import when the picker returns on Android. + private static final String PICKED_REQUIRED_IMPORT_FILENAME = "picked_required_import.bin"; private static final String PENDING_EXPORT_FILENAME = "pending_export.sav"; private static final String EXPORT_DONE_FILENAME = "export_done.flag"; // Written when a SAF pick cannot be read at all, with the destination @@ -504,7 +507,8 @@ public class GameActivity extends SDLActivity { * picker-agnostic and unchanged. * * @param destFilename basename under the app save identity (e.g. - * picked_rom.gb, picked_mod.zip, picked_save.sav) + * picked_rom.gb, picked_mod.zip, picked_save.sav, or + * picked_required_import.bin) */ /** Legacy single-argument entry; resolves the save dir itself. */ @Keep @@ -535,6 +539,11 @@ public class GameActivity extends SDLActivity { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); + // The Storage Access Framework grants the returned content URI + // directly to this activity. Request the read grant explicitly as + // well: Android 13's scoped storage deliberately does not expose + // arbitrary paths or require broad media/storage permissions. + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { self.startActivityForResult(intent, FILE_PICKER_REQUEST_CODE); return true; @@ -547,6 +556,7 @@ public class GameActivity extends SDLActivity { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { self.startActivityForResult( Intent.createChooser(intent, "Choose a file"), @@ -576,6 +586,12 @@ public class GameActivity extends SDLActivity { return showFilePicker(PICKED_SAVE_FILENAME); } + /** Required-mod-file wrapper used by love.system.pickFile("required_import"). */ + @Keep + public static boolean showRequiredImportFilePicker() { + return showFilePicker(PICKED_REQUIRED_IMPORT_FILENAME); + } + /** * Relaunches the whole app for love.system.restartApp, used by * src/core/HostShell.lua when a mod toggle needs a cold boot (#575). diff --git a/tests/engine/android_host_extension_test.lua b/tests/engine/android_host_extension_test.lua index 14d7ae5a..13d25a5e 100644 --- a/tests/engine/android_host_extension_test.lua +++ b/tests/engine/android_host_extension_test.lua @@ -55,4 +55,24 @@ check(not source:find("QuestActivity", 1, true) and not source:find("QuestBridge", 1, true), "generic Android activity must not require Quest classes") +-- Required mod files use Android's Storage Access Framework, which works with +-- Android 13 scoped storage without broad media/storage permissions. Keep the +-- native destination distinct so it cannot be consumed as a game ROM. +check(source:find('PICKED_REQUIRED_IMPORT_FILENAME = "picked_required_import.bin"', + 1, true), "required imports use their own Android picker destination") +check(source:find("showRequiredImportFilePicker", 1, true), + "Android exposes a required-import picker entry point") +check(source:find("Intent.ACTION_OPEN_DOCUMENT", 1, true) + and source:find("Intent.FLAG_GRANT_READ_URI_PERMISSION", 1, true), + "Android 13 uses SAF with an explicit read grant") + +local systemPath = "mobile/android/love/src/jni/love/src/modules/system/System.cpp" +local systemFile = assert(io.open(systemPath, "rb")) +local system = systemFile:read("*a") +systemFile:close() +check(system:find('strcmp(kind, "required_import")', 1, true) + and system:find('dest = "picked_required_import.bin"', 1, true) + and system:find('return "rom,mod,sav,required_import"', 1, true), + "native Android bridge advertises and routes required imports") + print("android_host_extension_test: ok")