From c22353ebb27f1d92660b2d5c2279e8768cb9199a Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Tue, 21 Oct 2025 03:55:04 -0400 Subject: [PATCH] Fixed crash when pressing menu/settings button and fixed crash on first boot. --- app/build.gradle | 4 +- .../psx2/GamesCoverDialogFragment.java | 95 ++++++++++++++----- .../java/com/izzy2lost/psx2/MainActivity.java | 72 +++++++------- 3 files changed, 109 insertions(+), 62 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d2129b9..1c2d236 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.izzy2lost.psx2" minSdk 26 targetSdk 36 - versionCode 9 - versionName "1.0.7" + versionCode 10 + versionName "1.0.8" // APK setProperty("archivesBaseName","PSX2_${versionCode}_${new Date().format('yyyyMMddHHmm')}") diff --git a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java index b3f5090..421c59e 100644 --- a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java @@ -189,17 +189,25 @@ public class GamesCoverDialogFragment extends DialogFragment { coverUrls = new String[uris.length]; localPaths = new String[uris.length]; SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + boolean isFirstBoot = !prefs.getBoolean("has_resolved_titles_once", false); + for (int i = 0; i < uris.length; i++) { String saved = prefs.getString("serial:" + uris[i], null); String serial = saved; + // On first boot, skip native serial extraction to avoid crashes + // The background thread will handle it later with proper delays if (serial == null || serial.isEmpty()) { - try { - String nativeSerial = NativeApp.getGameSerialSafe(uris[i]); - if (nativeSerial != null && !nativeSerial.isEmpty()) { - serial = normalizeSerial(nativeSerial); - prefs.edit().putString("serial:" + uris[i], serial).apply(); + if (!isFirstBoot) { + try { + String nativeSerial = NativeApp.getGameSerialSafe(uris[i]); + if (nativeSerial != null && !nativeSerial.isEmpty()) { + serial = normalizeSerial(nativeSerial); + prefs.edit().putString("serial:" + uris[i], serial).apply(); + } + } catch (Throwable e) { + android.util.Log.w("GamesCoverDialog", "Error getting serial for " + uris[i] + ": " + e.getMessage()); } - } catch (Throwable ignored) {} + } } if (serial == null || serial.isEmpty()) { serial = buildSerialFromUri(uris[i]); @@ -295,25 +303,66 @@ public class GamesCoverDialogFragment extends DialogFragment { // Resolve proper game titles using local YAML index if available (GameIndex/Redump). // Falls back to native URI API, then filename if needed. + // Capture context early to avoid requireContext() crashes if fragment detaches + final Context ctx = requireContext().getApplicationContext(); + new Thread(() -> { - boolean changed = false; - for (int i = 0; i < uris.length; i++) { - try { - String t = TitleResolver.resolveTitleForUri(requireContext(), uris[i], titles[i]); - if (t != null && !t.isEmpty() && i < titles.length && !t.equals(titles[i])) { - titles[i] = t; - if (origTitles != null && i < origTitles.length) origTitles[i] = t; - changed = true; - } - } catch (Throwable ignored) {} - } - if (changed && isAdded()) requireActivity().runOnUiThread(() -> { - if (sortMode != SORT_ALPHA || (query != null && !query.isEmpty())) { - applyFilterAndSort(); - } else { - adapter.notifyDataSetChanged(); + try { + // On first boot, add a delay to let native library fully initialize + if (isFirstBoot) { + Thread.sleep(2000); // 2 second delay on first boot } - }); + + // Check if fragment is still attached before proceeding + if (!isAdded()) return; + + boolean changed = false; + for (int i = 0; i < uris.length; i++) { + // Check if fragment is still attached on each iteration + if (!isAdded()) break; + + try { + String t = TitleResolver.resolveTitleForUri(ctx, uris[i], titles[i]); + if (t != null && !t.isEmpty() && i < titles.length && !t.equals(titles[i])) { + titles[i] = t; + if (origTitles != null && i < origTitles.length) origTitles[i] = t; + changed = true; + } + } catch (Throwable e) { + android.util.Log.w("GamesCoverDialog", "Error resolving title for " + uris[i] + ": " + e.getMessage()); + } + } + + // Mark that we've resolved titles at least once + if (isFirstBoot && isAdded()) { + try { + ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + .edit().putBoolean("has_resolved_titles_once", true).apply(); + } catch (Throwable ignored) {} + } + + // Only update UI if fragment is still attached + if (changed && isAdded()) { + try { + requireActivity().runOnUiThread(() -> { + try { + if (!isAdded()) return; + if (sortMode != SORT_ALPHA || (query != null && !query.isEmpty())) { + applyFilterAndSort(); + } else { + adapter.notifyDataSetChanged(); + } + } catch (Throwable e) { + android.util.Log.w("GamesCoverDialog", "Error updating UI after title resolution: " + e.getMessage()); + } + }); + } catch (Throwable e) { + android.util.Log.w("GamesCoverDialog", "Error posting to UI thread: " + e.getMessage()); + } + } + } catch (Throwable e) { + android.util.Log.e("GamesCoverDialog", "Error in title resolution thread: " + e.getMessage()); + } }).start(); // Dynamically size items based on RecyclerView size and orientation diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java index 90441b2..58c91a1 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -491,26 +491,30 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } updateUiForControllerPresence(); - // After initial BIOS auto-boot, gently open the Games dialog - // Only when setup is complete and this is app launch (not rotation) - if (getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("first_run_done", false)) { - try { - final View decor = (getWindow() != null) ? getWindow().getDecorView() : null; - if (decor != null) { - decor.postDelayed(() -> { - if (!isFinishing() && !mSetupWizardActive) { - openGamesDialog(); - } - }, 1600); // small delay to let BIOS boot briefly - } - } catch (Throwable ignored) {} - } - // Show first-run setup wizard if needed - if (!getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("first_run_done", false)) { + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + boolean firstRunDone = prefs.getBoolean("first_run_done", false); + + if (!firstRunDone) { SetupWizardDialogFragment f = SetupWizardDialogFragment.newInstance(); f.setCancelable(false); f.show(getSupportFragmentManager(), "setup_wizard"); + } else { + // Only auto-open games dialog if this is NOT the first boot after setup + // (Setup wizard handles opening the games dialog on first completion) + boolean hasOpenedGamesAfterSetup = prefs.getBoolean("has_opened_games_after_setup", false); + if (hasOpenedGamesAfterSetup) { + try { + final View decor = (getWindow() != null) ? getWindow().getDecorView() : null; + if (decor != null) { + decor.postDelayed(() -> { + if (!isFinishing() && !mSetupWizardActive) { + openGamesDialog(); + } + }, 1600); // small delay to let BIOS boot briefly + } + } catch (Throwable ignored) {} + } } // Setup right drawer quick actions @@ -541,6 +545,12 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF // Public method to open the games covers dialog via controller quick actions public void openGamesDialog() { SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + // Mark that we've opened games dialog after setup (prevents double-open on first boot) + if (!prefs.getBoolean("has_opened_games_after_setup", false)) { + prefs.edit().putBoolean("has_opened_games_after_setup", true).apply(); + } + String folderUri = prefs.getString("games_folder_uri", null); if (TextUtils.isEmpty(folderUri)) { pickGamesFolder(); @@ -568,33 +578,13 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF if (btn_settings != null) { btn_settings.setOnClickListener(v -> { try { - // Pause game when opening settings drawer using the same logic as the pause/play button - if (hasSelectedGame() && isThread() && !NativeApp.isPaused()) { - togglePauseState(); // This will pause the game and update button state - } - // Get drawer layout first + // Just open the drawer - let the drawer listener handle pausing DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) { - // Refresh drawer settings before opening - add error handling - try { - refreshDrawerSettings(); - } catch (Exception e) { - android.util.Log.e("MainActivity", "Error refreshing drawer settings: " + e.getMessage()); - // Continue anyway even if refresh fails - } drawer.openDrawer(androidx.core.view.GravityCompat.START); } } catch (Throwable t) { android.util.Log.e("MainActivity", "Error opening settings drawer: " + t.getMessage()); - // Try to open drawer directly as fallback - try { - DrawerLayout drawer = findViewById(R.id.drawer_layout); - if (drawer != null) { - drawer.openDrawer(androidx.core.view.GravityCompat.START); - } - } catch (Throwable fallback) { - android.util.Log.e("MainActivity", "Fallback drawer open also failed: " + fallback.getMessage()); - } } }); } @@ -2449,6 +2439,14 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF public void onDrawerOpened() { mDrawerOpen = true; android.util.Log.d("DrawerTracking", "Drawer opened"); + + // Refresh drawer settings to reflect current state + try { + refreshDrawerSettings(); + } catch (Exception e) { + android.util.Log.e("DrawerTracking", "Error refreshing drawer settings: " + e.getMessage()); + } + // Drawer opened, pause the game try { if (hasSelectedGame() && isThread() && !NativeApp.isPaused()) {