Fix missing-assets detection race, 16 KB page alignment, and Android audio backend
build / build (push) Successful in 2m11s
build / build (push) Successful in 2m11s
- QuestShockActivity now actually blocks the native engine from starting when game data is missing, closing three gaps found via on-device testing: super.onCreate() must run unconditionally first (Android throws SuperNotCalledException otherwise); SDLActivity.mBrokenLibraries is now set provisionally before the storage-permission check, since onWindowFocusChanged() closing the permission dialog could otherwise start the engine before the async onRequestPermissionsResult() callback ran; and a new GameSurface (SDLSurface subclass) closes the actual gap that let the init_popups NULL-deref crash through even with mBrokenLibraries set - SDLSurface.surfaceChanged() starts the native thread directly without ever checking that flag. - Force Android to use SDL2's openslES audio backend instead of AAudio (android/engine-patches/05-android-audio-driver.patch): AAudio only allows one open playback device at a time, but the engine opens two (cutscene audio via SDL_OpenAudioDevice, SFX/MIDI via Mix_OpenAudio), hitting an assertion failure on real hardware. - Add 16 KB ELF page-size alignment (-Wl,-z,max-page-size=16384) to every Android shared library - the four prebuilts (SDL2, SDL2_mixer, fluidsynth-lite, gl4es, in build-image/Dockerfile) and the engine's own libmain.so (build.gradle) - matching Google's Play Store requirement for Android 15+ and clearing Android Studio's compatibility warning. - Add a stageEngine Gradle task that automatically re-stages the patched engine/ copy and prebuilt libraries before any Android Studio build (hooked into preBuild, with proper up-to-date checking), so source/ patch changes can't silently go stale in the build/android-engine scratch copy - previously a manual, easy-to-forget step. Skips automatically inside the build-image container so make apk/CI are unaffected.
This commit is contained in:
@@ -2,10 +2,12 @@ package de.ladkau.questshock;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceHolder;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -13,16 +15,26 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
|
||||
/**
|
||||
* Everything here runs before super.onCreate() (which is what loads the
|
||||
* native libraries and eventually calls Shockolate's own main()) - see
|
||||
* android/engine-patches/ for why: engine/ itself is never modified, so
|
||||
* this Java-side setup is the only place left to prepare
|
||||
* /sdcard/questshock/ the way Shockolate's plain relative-path file I/O
|
||||
* ("res/data/...", confirmed via grep - none of it goes through SDL_RWops,
|
||||
* so Android's asset-manager fallback for SDL_RWFromFile doesn't apply
|
||||
* here) expects to find it.
|
||||
* Prepares /sdcard/questshock/ the way Shockolate's plain relative-path
|
||||
* file I/O ("res/data/...", confirmed via grep - none of it goes through
|
||||
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
|
||||
* doesn't apply here) expects to find it - see android/engine-patches/ for
|
||||
* why this is done from Java instead of patching engine/ itself, which is
|
||||
* never modified.
|
||||
*
|
||||
* super.onCreate() (SDLActivity's) must always run first and unconditionally
|
||||
* - Android throws SuperNotCalledException otherwise, checked right after
|
||||
* onCreate() returns, regardless of what this subclass does afterwards.
|
||||
* SDLActivity's own onCreate() only loads libraries, sets up JNI and the
|
||||
* surface - the actual native SDL_main thread doesn't start until later, from
|
||||
* one of several lifecycle paths (onResume(), onWindowFocusChanged(), and
|
||||
* SDLSurface.surfaceChanged() - see GameSurface below for why that last one
|
||||
* needs its own fix) - so it's safe to do our own checks (and set
|
||||
* SDLActivity.mBrokenLibraries, which most - but not all - of those paths
|
||||
* gate on) afterwards.
|
||||
*/
|
||||
public class QuestShockActivity extends SDLActivity {
|
||||
private static final String TAG = "QuestShock";
|
||||
@@ -45,8 +57,6 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// has no public chdir() (confirmed against the actual API 34 stub jar).
|
||||
private static native void nativeChdir(String path);
|
||||
|
||||
private Bundle mSavedInstanceState;
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
return new String[] {
|
||||
@@ -57,9 +67,52 @@ public class QuestShockActivity extends SDLActivity {
|
||||
};
|
||||
}
|
||||
|
||||
// SDLSurface.surfaceChanged() (org/libsdl/app/, vendored from SDL2's own
|
||||
// template) starts the native SDL thread directly - unlike onResume()/
|
||||
// onWindowFocusChanged(), it never checks SDLActivity.mBrokenLibraries
|
||||
// first. Since surfaceChanged() fires on essentially every launch
|
||||
// regardless of that flag, setting mBrokenLibraries alone (see onCreate()/
|
||||
// setUpGameDirAndContinue()) does NOT actually stop the engine from
|
||||
// starting - confirmed on-device: the missing-assets crash still happened
|
||||
// with mBrokenLibraries set, from exactly this path. Route through a
|
||||
// subclass that adds the missing check instead of patching the vendored
|
||||
// file directly.
|
||||
private static class GameSurface extends SDLSurface {
|
||||
GameSurface(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
if (SDLActivity.mBrokenLibraries) {
|
||||
return;
|
||||
}
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new GameSurface(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mSavedInstanceState = savedInstanceState;
|
||||
super.onCreate(savedInstanceState);
|
||||
if (SDLActivity.mBrokenLibraries) {
|
||||
// SDLActivity's own onCreate() already showed its "SDL Error"
|
||||
// dialog for this - nothing left for us to do.
|
||||
return;
|
||||
}
|
||||
// Provisionally block the native engine from starting - cleared only
|
||||
// once setUpGameDirAndContinue() confirms game data is present. Must
|
||||
// happen before requestPermissions() below: the storage-permission
|
||||
// dialog closing can fire onWindowFocusChanged(true) - which starts
|
||||
// the native SDLThread - before the async onRequestPermissionsResult()
|
||||
// callback (which is what actually calls setUpGameDirAndContinue())
|
||||
// gets a chance to run, so mBrokenLibraries has to already be true
|
||||
// going into that race, not set afterwards.
|
||||
SDLActivity.mBrokenLibraries = true;
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
@@ -102,19 +155,28 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// and, on a fresh install with no game data copied in yet, that
|
||||
// fails deep inside startup (init_popups(), which doesn't NULL-check
|
||||
// the load) as a hard native crash instead of a message. Catch the
|
||||
// missing-data case here instead, before super.onCreate() ever
|
||||
// starts Shockolate's native main().
|
||||
// missing-data case here instead, before Shockolate's native
|
||||
// SDL_main ever starts.
|
||||
if (!isNonEmptyDir(new File(gameDir, "res/data")) || !isNonEmptyDir(new File(gameDir, "res/sound"))) {
|
||||
// mBrokenLibraries is already true (set in onCreate()) - leave it
|
||||
// that way. Every path that starts the native SDL_main thread
|
||||
// (onWindowFocusChanged(), resumeNativeThread(), etc.) already
|
||||
// checks this flag before doing anything native, so this
|
||||
// reliably prevents the engine from starting without having to
|
||||
// duplicate all of SDLActivity's own lifecycle guards ourselves.
|
||||
showMissingAssetsDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// chdir() is process-wide, not per-thread - already in effect for
|
||||
// every thread (including the one that will run Shockolate's own
|
||||
// SDL_main) by the time super.onCreate() below starts it.
|
||||
// SDL_main) by the time the native thread actually starts. Done
|
||||
// before clearing mBrokenLibraries below so the engine can never
|
||||
// start pre-chdir.
|
||||
nativeChdir(GAME_DIR);
|
||||
|
||||
super.onCreate(mSavedInstanceState);
|
||||
// Assets confirmed present - safe to let the native engine start now.
|
||||
SDLActivity.mBrokenLibraries = false;
|
||||
}
|
||||
|
||||
private static boolean isNonEmptyDir(File dir) {
|
||||
|
||||
Reference in New Issue
Block a user