CLOSES #575, CLOSES #578, CLOSES #584, CLOSES #589

This commit is contained in:
bryanthaboi
2026-08-01 12:56:13 -04:00
parent ca1beaefe7
commit 2009df3dd1
17 changed files with 819 additions and 54 deletions
@@ -231,6 +231,28 @@ bool syncHealthSteps()
return result;
}
bool restartApp()
{
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = env->FindClass("org/love2d/android/GameActivity");
// Old APK / new liblove skew: fail soft so HostShell.restart can fall
// back to a clean quit instead of aborting on a missing method (#575).
jmethodID method = env->GetStaticMethodID(activity, "restartApp", "()Z");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
// Does not return on success: the Java side exits the process.
jboolean result = env->CallStaticBooleanMethod(activity, method);
env->DeleteLocalRef(activity);
return result;
}
/*
* Helper functions for the filesystem module
*/
@@ -81,6 +81,15 @@ bool showCreateDocument(const char *suggestedName = nullptr);
*/
bool syncHealthSteps();
/**
* Full process relaunch (GameActivity.restartApp): schedules the app's
* launch intent and kills the process, because the in-process
* quit("restart") loop double-inits physfs and crashes (#575). On success
* the process dies inside the Java call and this never returns; false
* means the relaunch could not be scheduled.
**/
bool restartApp();
/*
* Helper functions for the filesystem module
*/
@@ -221,6 +221,15 @@ bool System::syncHealthSteps() const
#endif
}
bool System::restartApp() const
{
#ifdef LOVE_ANDROID
return love::android::restartApp();
#else
return false;
#endif
}
bool System::hasBackgroundMusic() const
{
#if defined(LOVE_ANDROID)
@@ -133,6 +133,14 @@ public:
*/
virtual bool syncHealthSteps() const;
/**
* Relaunches the whole app with a fresh process (Android only; false
* elsewhere). The in-process love.event.quit("restart") double-inits
* physfs on Android and crashes, so src/core/HostShell.lua calls this
* instead (#575). Does not return on success -- the process exits.
**/
virtual bool restartApp() const;
/**
* Gets if the user is playing music on background.
* Throws an exception on unsupported platforms.
@@ -115,6 +115,14 @@ int w_syncHealthSteps(lua_State *L)
return 1;
}
int w_restartApp(lua_State *L)
{
// Does not return on success: GameActivity.restartApp exits the process
// after scheduling the relaunch (#575).
luax_pushboolean(L, instance()->restartApp());
return 1;
}
int w_hasBackgroundMusic(lua_State *L)
{
lua_pushboolean(L, instance()->hasBackgroundMusic());
@@ -133,6 +141,7 @@ static const luaL_Reg functions[] =
{ "pickFile", w_pickFile },
{ "createFile", w_createFile },
{ "syncHealthSteps", w_syncHealthSteps },
{ "restartApp", w_restartApp },
{ "hasBackgroundMusic", w_hasBackgroundMusic },
{ 0, 0 }
};
@@ -37,7 +37,9 @@ import java.util.List;
import java.util.Map;
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -75,6 +77,7 @@ public class GameActivity extends SDLActivity {
public static final int FILE_PICKER_REQUEST_CODE = 4;
public static final int FILE_CREATE_REQUEST_CODE = 5;
public static final int STEP_PERMISSION_REQUEST_CODE = 6;
public static final int RESTART_REQUEST_CODE = 7;
/** @deprecated Prefer FILE_PICKER_REQUEST_CODE; kept for older call sites. */
public static final int ROM_PICKER_REQUEST_CODE = FILE_PICKER_REQUEST_CODE;
// Mirrors conf.lua's t.identity ("pokemon-love2d"): where the picked file
@@ -447,15 +450,23 @@ public class GameActivity extends SDLActivity {
* Shows the system document picker (Storage Access Framework) so the
* player can pick a ROM / mod / save from anywhere (Downloads, Drive,
* etc.) without needing to know where the app's external files folder
* is. Requires API 19+ (ACTION_OPEN_DOCUMENT); the picked file (if any)
* arrives later in onActivityResult, not synchronously here.
* is. The picked file (if any) arrives later in onActivityResult, not
* synchronously here.
*
* API 21+ uses ACTION_OPEN_DOCUMENT; API 16-20 uses an ACTION_GET_CONTENT
* chooser instead. Below 19 OPEN_DOCUMENT does not exist, and on 19/20
* the stock DocumentsUI is unreliable -- it launches and then hands back
* RESULT_CANCELED with no data, which onActivityResult cannot tell apart
* from the player cancelling (#584). GET_CONTENT lets any installed file
* manager serve the pick, and both intents return the same content:// or
* file:// URI shapes, so the result path in onActivityResult stays
* picker-agnostic and unchanged.
*
* @param destFilename basename under the app save identity (e.g.
* picked_rom.gb, picked_mod.zip, picked_save.sav)
*/
@Keep
public static boolean showFilePicker(String destFilename) {
if (android.os.Build.VERSION.SDK_INT < 19) return false;
GameActivity self = (GameActivity) mSingleton;
if (self == null) return false;
if (destFilename == null || destFilename.length() == 0) {
@@ -469,11 +480,26 @@ public class GameActivity extends SDLActivity {
}
self.pendingPickFilename = destFilename;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
if (android.os.Build.VERSION.SDK_INT >= 21) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
try {
self.startActivityForResult(intent, FILE_PICKER_REQUEST_CODE);
return true;
} catch (Exception e) {
// Some OEM / TV builds ship without DocumentsUI; fall through
// to the GET_CONTENT chooser below instead of giving up (#584).
Log.d("GameActivity", "could not open document picker: " + e.getMessage());
}
}
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
try {
self.startActivityForResult(intent, FILE_PICKER_REQUEST_CODE);
self.startActivityForResult(
Intent.createChooser(intent, "Choose a file"),
FILE_PICKER_REQUEST_CODE);
return true;
} catch (Exception e) {
Log.d("GameActivity", "could not open file picker: " + e.getMessage());
@@ -499,14 +525,60 @@ public class GameActivity extends SDLActivity {
return showFilePicker(PICKED_SAVE_FILENAME);
}
/**
* Relaunches the whole app for love.system.restartApp, used by
* src/core/HostShell.lua when a mod toggle needs a cold boot (#575).
* love.event.quit("restart") re-runs LOVE's boot inside the same
* process, and the second love.filesystem.init throws once physfs
* failed to deinit ("already initialized"), killing the app. Instead
* we hand our launch intent to AlarmManager and then exit the process:
* the alarm lives in system_server, so it survives our death and
* cannot race the exit the way a plain startActivity right before
* Runtime.exit can on some OEMs, and the dead process guarantees no
* native (physfs / SDL / JNI) state leaks into the fresh run.
*/
@Keep
public static boolean restartApp() {
GameActivity self = (GameActivity) mSingleton;
if (self == null) return false;
try {
Context context = self.getApplicationContext();
Intent intent = context.getPackageManager()
.getLaunchIntentForPackage(context.getPackageName());
if (intent == null) return false;
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int pendingFlags = PendingIntent.FLAG_CANCEL_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= 23) {
// Mandatory mutability flag on API 31+; harmless from 23 up.
pendingFlags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pending = PendingIntent.getActivity(
context, RESTART_REQUEST_CODE, intent, pendingFlags);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarm == null) return false;
alarm.set(AlarmManager.RTC, System.currentTimeMillis() + 250, pending);
} catch (Exception e) {
Log.d("GameActivity", "could not schedule restart: " + e.getMessage());
return false;
}
Runtime.getRuntime().exit(0);
return true; // unreachable, but keeps the JNI signature honest
}
/**
* Shows ACTION_CREATE_DOCUMENT so the player can save a staged export
* (pending_export.sav in the app save identity) to Downloads / Drive /
* etc. Suggested name is the dialog's default filename.
*
* CREATE_DOCUMENT does not exist below API 19 and has no pre-SAF
* equivalent, so unlike showFilePicker this stays 19+ (#584); the false
* return degrades on the Lua side (RomImporter export) to "Exported
* inside the app folder", which is the correct pre-KitKat behavior.
*/
@Keep
public static boolean showCreateDocument(String suggestedName) {
if (android.os.Build.VERSION.SDK_INT < 19) return false;
// (see showFilePicker for why the import side got a pre-19 path)
GameActivity self = (GameActivity) mSingleton;
if (self == null) return false;
if (suggestedName == null || suggestedName.length() == 0) {