cleaned up a bunch of warnings more ui stuff

This commit is contained in:
izzy2lost
2025-08-30 04:59:16 -04:00
parent 2599d1ba7e
commit a27b5b143b
32 changed files with 827 additions and 272 deletions
+1
View File
@@ -21,3 +21,4 @@ app/.cxx/
*.exe
/PCSX2_ARM64
build/reports/problems/problems-report.html
crash.txt
+422
View File
@@ -0,0 +1,422 @@
0: package com.izzy2lost.psx2;
1:
2: import android.app.Dialog;
3: import android.content.Context;
4: import android.net.Uri;
5: import android.content.SharedPreferences;
6: import android.os.Bundle;
7: import android.view.LayoutInflater;
8: import android.view.View;
9: import android.view.ViewGroup;
10: import android.view.Window;
11: import android.view.WindowManager;
12: import android.widget.Toast;
13: import android.os.Build;
14: import android.view.WindowInsets;
15: import android.view.WindowInsetsController;
16:
17: import androidx.annotation.NonNull;
18: import androidx.annotation.Nullable;
19: import androidx.appcompat.app.AlertDialog;
20: // Import MaterialAlertDialogBuilder for Material 3 styling
21: import com.google.android.material.dialog.MaterialAlertDialogBuilder;
22: import androidx.fragment.app.DialogFragment;
23: import androidx.recyclerview.widget.GridLayoutManager;
24: import androidx.recyclerview.widget.RecyclerView;
25: import java.util.Locale;
26:
27: public class GamesCoverDialogFragment extends DialogFragment {
28: private CoversAdapter adapter;
29: private String[] titles;
30: private String[] uris;
31: private String[] coverUrls;
32: private String[] localPaths;
33: private RecyclerView rv;
34: private GridLayoutManager glm;
35:
36: public interface OnGameSelectedListener {
37: void onGameSelected(String gameUri);
38: }
39:
40: private static final String ARG_TITLES = "titles";
41: private static final String ARG_URIS = "uris";
42:
43: public static GamesCoverDialogFragment newInstance(String[] titles, String[] uris) {
44: GamesCoverDialogFragment f = new GamesCoverDialogFragment();
45: Bundle b = new Bundle();
46: b.putStringArray(ARG_TITLES, titles);
47: b.putStringArray(ARG_URIS, uris);
48: f.setArguments(b);
49: return f;
50: }
51:
52: private OnGameSelectedListener listener;
53:
54: @Override
55: public void onAttach(@NonNull Context context) {
56: super.onAttach(context);
57: if (context instanceof OnGameSelectedListener) {
58: listener = (OnGameSelectedListener) context;
59: }
60: }
61:
62: @Override
63: public void onResume() {
64: super.onResume();
65: // Re-assert fixed span (2/4) after resume to avoid any flips
66: if (rv != null && glm != null) {
67: int currentOrientation = getResources().getConfiguration().orientation;
68: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
69: if (fixedSpan != glm.getSpanCount()) {
70: glm.setSpanCount(fixedSpan);
71: }
72: }
73: }
74:
75: @NonNull
76: @Override
77: public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
78: LayoutInflater inflater = getLayoutInflater();
79: View root = inflater.inflate(R.layout.dialog_covers_grid, null, false);
80:
81: rv = root.findViewById(R.id.recycler_covers);
82: rv.setHasFixedSize(true);
83: glm = new GridLayoutManager(requireContext(), 3);
84: rv.setLayoutManager(glm);
85: // spacing decoration (8dp) using half on each side so the gap between items is exactly spacingPx
86: final int spacingPx = (int) (8 * getResources().getDisplayMetrics().density);
87: final int half = Math.max(1, spacingPx / 2);
88: rv.addItemDecoration(new RecyclerView.ItemDecoration() {
89: @Override
90: public void getItemOffsets(android.graphics.Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
91: outRect.set(half, half, half, half);
92: }
93: });
94: // Hard lock spans based on orientation only
95: rv.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
96: int currentOrientation = getResources().getConfiguration().orientation;
97: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
98: if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); }
99: });
100: // Set initial fixed span as soon as possible
101: root.post(() -> {
102: int currentOrientation = getResources().getConfiguration().orientation;
103: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
104: if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); }
105: });
106:
107: titles = getArguments() != null ? getArguments().getStringArray(ARG_TITLES) : new String[0];
108: uris = getArguments() != null ? getArguments().getStringArray(ARG_URIS) : new String[0];
109: coverUrls = new String[uris.length];
110: localPaths = new String[uris.length];
111: SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
112: for (int i = 0; i < uris.length; i++) {
113: String saved = prefs.getString("serial:" + uris[i], null);
114: String serial = saved;
115: if (serial == null || serial.isEmpty()) {
116: // Ask native core for the real serial (supports ISO/CHD and content://)
117: try {
118: String nativeSerial = NativeApp.getGameSerial(uris[i]);
119: if (nativeSerial != null && !nativeSerial.isEmpty()) {
120: serial = normalizeSerial(nativeSerial);
121: prefs.edit().putString("serial:" + uris[i], serial).apply();
122: }
123: } catch (Throwable ignored) {}
124: }
125: if (serial == null || serial.isEmpty()) {
126: // Heuristic fallback from filename
127: serial = buildSerialFromUri(uris[i]);
128: }
129: coverUrls[i] = buildCoverUrlFromSerial(serial);
130: localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
131: }
132:
133: adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths,
134: position -> {
135: // Regular click - start game
136: if (listener != null && position >= 0 && position < uris.length) {
137: listener.onGameSelected(uris[position]);
138: dismissAllowingStateLoss();
139: }
140: },
141: position -> {
142: // Long click - show game settings
143: if (position >= 0 && position < uris.length) {
144: showGameSettings(titles[position], uris[position]);
145: }
146: });
147: rv.setAdapter(adapter);
148:
149: // Toolbar buttons
150: View btnHome = root.findViewById(R.id.btn_home);
151: if (btnHome != null) btnHome.setOnClickListener(v -> dismissAllowingStateLoss());
152: View btnDownload = root.findViewById(R.id.btn_download);
153: if (btnDownload != null) btnDownload.setOnClickListener(v -> startDownloadCovers());
154:
155: AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext(),
156: com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
157: .setView(root)
158: .create();
159: return dialog;
160: }
161:
162: @Override
163: public void onStart() {
164: super.onStart();
165: Dialog d = getDialog();
166: if (d == null) return;
167: Window w = d.getWindow();
168: if (w == null) return;
169: // Make dialog fullscreen
170: w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
171: // Enter immersive fullscreen to avoid showing system navigation buttons
172: if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
173: w.setDecorFitsSystemWindows(false);
174: WindowInsetsController c = w.getInsetsController();
175: if (c != null) {
176: c.hide(WindowInsets.Type.systemBars());
177: c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
178: }
179: } else {
180: View decor = w.getDecorView();
181: int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
182: | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
183: | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
184: | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
185: | View.SYSTEM_UI_FLAG_FULLSCREEN
186: | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
187: decor.setSystemUiVisibility(flags);
188: w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
189: }
190: }
191:
192: @Override
193: public void onStart() {
194: super.onStart();
195: Dialog d = getDialog();
196: if (d != null) {
197: Window w = d.getWindow();
198: if (w != null) {
199: w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
200: w.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
201: // Hide status bar for true full-screen dialog
202: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
203: w.setDecorFitsSystemWindows(false);
204: WindowInsetsController controller = w.getInsetsController();
205: if (controller != null) {
206: controller.hide(WindowInsets.Type.statusBars());
207: controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
208: }
209: } else {
210: w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
211: }
212: }
213: }
214: }
215:
216: private int calculateSpanForWidth(int rvWidthPx, int itemDp, int spacingPx) {
217: float density = getResources().getDisplayMetrics().density;
218: int usable = Math.max(0, rvWidthPx);
219: int itemPx = (int) (itemDp * density);
220: // Include spacing in the packing calculation to avoid oscillation
221: // span = floor((usable + spacing) / (itemPx + spacing))
222: int span = (itemPx > 0) ? (int) Math.floor((usable + (double) spacingPx) / (itemPx + (double) spacingPx)) : 1;
223: return Math.max(2, Math.max(1, span));
224: }
225:
226: private void preloadCovers(String[] urls) {
227: // Use Glide to warm cache
228: for (String url : urls) {
229: if (url == null) continue;
230: com.bumptech.glide.Glide.with(requireContext()).load(url).preload();
231: }
232: }
233:
234: private void startDownloadCovers() {
235: Toast.makeText(requireContext(), "Downloading covers in background", Toast.LENGTH_SHORT).show();
236: new Thread(() -> {
237: // Try to refine serials/URLs by scanning disc contents first
238: SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
239: SharedPreferences.Editor editor = prefs.edit();
240: for (int i = 0; i < uris.length; i++) {
241: try {
242: // Prefer native serial extraction so CHDs work
243: String better = null;
244: try { better = NativeApp.getGameSerial(uris[i]); } catch (Throwable ignored) {}
245: if (better == null) better = extractSerialFromUri(uris[i]);
246: if (better != null && !better.equalsIgnoreCase(serialFromUrl(coverUrls[i]))) {
247: String serial = normalizeSerial(better);
248: coverUrls[i] = buildCoverUrlFromSerial(serial);
249: localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
250: editor.putString("serial:" + uris[i], serial);
251: }
252: } catch (Exception ignored) { }
253: }
254: editor.apply();
255:
256: int total = coverUrls.length;
257: int ok = 0;
258: java.io.File dir = getCoversDir();
259: if (!dir.exists()) dir.mkdirs();
260: for (int i = 0; i < total; i++) {
261: String url = coverUrls[i];
262: String outPath = localPaths[i];
263: if (isFileValid(outPath)) { ok++; continue; }
264: try {
265: if (downloadToFile(url, outPath)) ok++;
266: } catch (Exception ignored) { }
267: }
268: final int downloaded = ok;
269: if (isAdded()) requireActivity().runOnUiThread(() -> {
270: Toast.makeText(requireContext(), "Covers ready: " + downloaded + "/" + total, Toast.LENGTH_SHORT).show();
271: // refresh adapter to prefer local files now
272: if (adapter != null) adapter.notifyDataSetChanged();
273: });
274: }).start();
275: }
276:
277: private static String serialFromUrl(String url) {
278: if (url == null) return null;
279: int slash = url.lastIndexOf('/');
280: int dot = url.lastIndexOf('.');
281: if (slash >= 0 && dot > slash) return url.substring(slash + 1, dot);
282: return null;
283: }
284:
285: private java.io.File getCoversDir() {
286: java.io.File base = requireContext().getExternalFilesDir("covers");
287: if (base == null) base = new java.io.File(requireContext().getFilesDir(), "covers");
288: return base;
289: }
290:
291: private static boolean isFileValid(String path) {
292: if (path == null) return false;
293: java.io.File f = new java.io.File(path);
294: return f.exists() && f.length() > 0;
295: }
296:
297: private static boolean downloadToFile(String urlStr, String outPath) throws Exception {
298: java.net.URL url = new java.net.URL(urlStr);
299: java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
300: conn.setConnectTimeout(10000);
301: conn.setReadTimeout(15000);
302: conn.setInstanceFollowRedirects(true);
303: conn.connect();
304: int code = conn.getResponseCode();
305: if (code != 200) { conn.disconnect(); return false; }
306: java.io.File outFile = new java.io.File(outPath);
307: java.io.File parent = outFile.getParentFile();
308: if (parent != null && !parent.exists()) parent.mkdirs();
309: java.io.InputStream in = conn.getInputStream();
310: java.io.FileOutputStream fos = new java.io.FileOutputStream(outFile);
311: byte[] buf = new byte[8192];
312: int n;
313: while ((n = in.read(buf)) != -1) fos.write(buf, 0, n);
314: fos.flush();
315: fos.close();
316: in.close();
317: conn.disconnect();
318: return true;
319: }
320:
321: private static String buildSerialFromUri(String gameUri) {
322: // Try to infer PS2 serial from file name: e.g., SLUS-20312 or SLPS_123.45 style
323: String last = Uri.parse(gameUri).getLastPathSegment();
324: if (last == null) last = "";
325: last = last.replace('_', '-');
326: // remove extension
327: int dot = last.lastIndexOf('.');
328: if (dot > 0) last = last.substring(0, dot);
329: String serial = null;
330: // Very simple heuristic: find token like XXXX-XXXXX
331: String upper = last.toUpperCase(Locale.ROOT);
332: java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{4,5}-[0-9]{3,5})").matcher(upper);
333: if (m.find()) {
334: serial = m.group(1);
335: }
336: if (serial == null) {
337: serial = upper;
338: }
339: return serial;
340: }
341:
342: private static String buildCoverUrlFromSerial(String serial) {
343: return "https://raw.githubusercontent.com/izzy2lost/ps2-covers/main/covers/3d/" + serial + ".png";
344: }
345:
346: private String extractSerialFromUri(String gameUri) {
347: try {
348: java.io.InputStream in = requireContext().getContentResolver().openInputStream(Uri.parse(gameUri));
349: if (in == null) return null;
350: // Read first 8MB searching for SYSTEM.CNF contents, e.g., "BOOT2 = cdrom0:\\SLUS_203.12;1"
351: final int MAX_BYTES = 8 * 1024 * 1024;
352: final byte[] buf = new byte[64 * 1024];
353: int read;
354: int total = 0;
355: StringBuilder sb = new StringBuilder();
356: while ((read = in.read(buf)) != -1 && total < MAX_BYTES) {
357: total += read;
358: // append as ASCII
359: sb.append(new String(buf, 0, read));
360: // try to match as we go to avoid huge strings
361: String found = findSerialInString(sb);
362: if (found != null) { in.close(); return found; }
363: if (sb.length() > 512 * 1024) sb.delete(0, sb.length() - 128 * 1024); // keep window
364: }
365: in.close();
366: } catch (Exception ignored) { }
367: return null;
368: }
369:
370: private static String findSerialInString(CharSequence cs) {
371: // Match common forms: SLUS_203.12, SLPM_650.51, SCES_123.45 etc.
372: java.util.regex.Matcher m = java.util.regex.Pattern
373: .compile("([A-Z]{4,5})[_-]([0-9]{3})\\.([0-9]{2})")
374: .matcher(cs);
375: if (m.find()) {
376: String prefix = m.group(1);
377: String part1 = m.group(2);
378: String part2 = m.group(3);
379: return prefix + "-" + part1 + part2; // SLUS-20312
380: }
381: return null;
382: }
383:
384: private static String normalizeSerial(String serial) {
385: if (serial == null) return null;
386: String s = serial.toUpperCase(Locale.ROOT).replace('_', '-');
387: // If form like XXXX-123.45 -> XXXX-12345
388: s = s.replaceAll("([A-Z]{4,5})-([0-9]{3})\\.([0-9]{2})", "$1-$2$3");
389: return s;
390: }
391:
392: private void showGameSettings(String gameTitle, String gameUri) {
393: // Prefer native extraction so CHDs work
394: String gameSerial = null;
395: try { gameSerial = NativeApp.getGameSerial(gameUri); } catch (Throwable ignored) {}
396: if (gameSerial == null || gameSerial.isEmpty()) {
397: gameSerial = extractSerialFromUri(gameUri);
398: }
399: if (gameSerial == null || gameSerial.isEmpty()) {
400: gameSerial = buildSerialFromUri(gameUri);
401: }
402: gameSerial = normalizeSerial(gameSerial);
403:
404: // CRC (native if available)
405: String gameCrc = null;
406: try { gameCrc = NativeApp.getGameCrc(gameUri); } catch (Throwable ignored) {}
407: if (gameCrc == null || gameCrc.isEmpty()) {
408: gameCrc = String.format("%08X", Math.abs(gameUri.hashCode()));
409: }
410:
411: // Debug logging
412: android.util.Log.d("GameSettings", "Opening game settings for: " + gameTitle);
413: android.util.Log.d("GameSettings", "URI: " + gameUri);
414: android.util.Log.d("GameSettings", "Extracted Serial: " + gameSerial);
415: android.util.Log.d("GameSettings", "Generated CRC: " + gameCrc);
416:
417: GameSettingsDialogFragment dialog = GameSettingsDialogFragment.newInstance(
418: gameTitle, gameUri, gameSerial, gameCrc);
419: dialog.show(getParentFragmentManager(), "game_settings");
420: }
421: }
@@ -5,6 +5,7 @@ import android.view.InputDevice;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Controller configuration and detection utility
@@ -43,7 +44,7 @@ public class ControllerConfig {
* Get controller type description
*/
private static String getControllerType(InputDevice device) {
String name = device.getName().toLowerCase();
String name = device.getName().toLowerCase(Locale.ROOT);
if (name.contains("xbox")) {
return "Xbox Controller";
@@ -5,6 +5,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
/**
* Controller input handler based on AetherSX2's PAD implementation
@@ -50,10 +51,10 @@ public class ControllerInputHandler {
private static final long COMBO_TIMEOUT_MS = 500; // 500ms window for combo
// Key mapping from Android KeyEvent to PS2 buttons
private static final SparseArray<Integer> sKeyMapping = new SparseArray<>();
private static final SparseIntArray sKeyMapping = new SparseIntArray();
// Motion axis mapping from Android MotionEvent to PS2 analog inputs
private static final SparseArray<Integer> sAxisMapping = new SparseArray<>();
private static final SparseIntArray sAxisMapping = new SparseIntArray();
static {
// Initialize key mappings (Android KeyEvent -> PS2 button)
@@ -128,9 +129,9 @@ public class ControllerInputHandler {
}
int keyCode = event.getKeyCode();
Integer ps2Button = sKeyMapping.get(keyCode);
int ps2Button = sKeyMapping.get(keyCode, Integer.MIN_VALUE);
if (ps2Button != null) {
if (ps2Button != Integer.MIN_VALUE) {
int controllerId = event.getDeviceId();
boolean pressed = (event.getAction() == KeyEvent.ACTION_DOWN);
@@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.List;
import java.util.Locale;
/**
* Dialog for testing controller input
@@ -29,7 +30,7 @@ public class ControllerTestDialogFragment extends DialogFragment implements Cont
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_controller_test, null);
View view = getLayoutInflater().inflate(R.layout.dialog_controller_test, null);
mControllerListText = view.findViewById(R.id.tv_controller_list);
mInputLogText = view.findViewById(R.id.tv_input_log);
@@ -66,7 +67,7 @@ public class ControllerTestDialogFragment extends DialogFragment implements Cont
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
String buttonName = getButtonName(button);
String action = pressed ? "PRESSED" : "RELEASED";
String logEntry = String.format("Controller %d: %s %s\n", controllerId, buttonName, action);
String logEntry = String.format(Locale.ROOT, "Controller %d: %s %s\n", controllerId, buttonName, action);
mInputLog.append(logEntry);
@@ -88,7 +89,7 @@ public class ControllerTestDialogFragment extends DialogFragment implements Cont
public void onControllerAnalogInput(int controllerId, int axis, float value) {
if (Math.abs(value) > 0.1f) { // Only log significant analog input
String axisName = getAxisName(axis);
String logEntry = String.format("Controller %d: %s %.2f\n", controllerId, axisName, value);
String logEntry = String.format(Locale.ROOT, "Controller %d: %s %.2f\n", controllerId, axisName, value);
mInputLog.append(logEntry);
@@ -145,7 +146,7 @@ public class ControllerTestDialogFragment extends DialogFragment implements Cont
@Override
public void onControllerCombo(int controllerId, String comboName) {
String logEntry = String.format("Controller %d: COMBO %s\n", controllerId, comboName);
String logEntry = String.format(Locale.ROOT, "Controller %d: COMBO %s\n", controllerId, comboName);
mInputLog.append(logEntry);
@@ -53,22 +53,25 @@ public class CoversAdapter extends RecyclerView.Adapter<CoversAdapter.VH> {
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
holder.title.setText(titles[position]);
String url = coverUrls[position];
String local = (localPaths != null && position < localPaths.length) ? localPaths[position] : null;
Object source = null;
File localFile = null;
if (local != null) {
File f = new File(local);
if (f.exists() && f.length() > 0) source = f;
if (f.exists() && f.length() > 0) localFile = f;
}
if (source == null) source = url;
Glide.with(context)
.load(source)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.fitCenter()
.placeholder(android.R.color.transparent)
.error(android.R.color.transparent)
.into(holder.cover);
if (localFile != null) {
Glide.with(context)
.load(localFile)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.fitCenter()
.placeholder(android.R.color.transparent)
.error(android.R.color.transparent)
.into(holder.cover);
} else {
// Do not load from network automatically; wait for explicit download
holder.cover.setImageDrawable(null);
}
holder.itemView.setOnClickListener(v -> {
if (onItemClick != null) onItemClick.onClick(position);
});
@@ -9,7 +9,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Switch;
import com.google.android.material.materialswitch.MaterialSwitch;
import android.widget.TextView;
import android.net.Uri;
@@ -41,7 +41,7 @@ public class GameSettingsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Context ctx = requireContext();
View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_game_settings, null, false);
View view = getLayoutInflater().inflate(R.layout.dialog_game_settings, null, false);
Bundle args = getArguments();
String gameTitle = args != null ? args.getString(ARG_GAME_TITLE, "Unknown Game") : "Unknown Game";
@@ -85,10 +85,10 @@ public class GameSettingsDialogFragment extends DialogFragment {
spResolution.setAdapter(resolutionAdapter);
// Switches
Switch swWidescreenPatches = view.findViewById(R.id.sw_widescreen_patches);
Switch swNoInterlacingPatches = view.findViewById(R.id.sw_no_interlacing_patches);
Switch swEnablePatchCodes = view.findViewById(R.id.sw_enable_patch_codes);
Switch swEnableCheats = view.findViewById(R.id.sw_enable_cheats);
MaterialSwitch swWidescreenPatches = view.findViewById(R.id.sw_widescreen_patches);
MaterialSwitch swNoInterlacingPatches = view.findViewById(R.id.sw_no_interlacing_patches);
MaterialSwitch swEnablePatchCodes = view.findViewById(R.id.sw_enable_patch_codes);
MaterialSwitch swEnableCheats = view.findViewById(R.id.sw_enable_cheats);
// Load existing per-game settings from INI and prefill widgets; if missing, use global
try {
@@ -17,14 +17,18 @@ import android.view.WindowInsetsController;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
// Import MaterialAlertDialogBuilder for Material 3 styling
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import android.app.Dialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Locale;
public class GamesCoverDialogFragment extends DialogFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME, R.style.AppTheme);
}
private CoversAdapter adapter;
private String[] titles;
private String[] uris;
@@ -75,14 +79,19 @@ public class GamesCoverDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
LayoutInflater inflater = LayoutInflater.from(requireContext());
View root = inflater.inflate(R.layout.dialog_covers_grid, null, false);
// Return a styled dialog; content is provided by onCreateView
return new Dialog(requireContext(), R.style.PSX2_FullScreenDialog);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.dialog_covers_grid, container, false);
rv = root.findViewById(R.id.recycler_covers);
rv.setHasFixedSize(true);
glm = new GridLayoutManager(requireContext(), 3);
rv.setLayoutManager(glm);
// spacing decoration (8dp) using half on each side so the gap between items is exactly spacingPx
final int spacingPx = (int) (8 * getResources().getDisplayMetrics().density);
final int half = Math.max(1, spacingPx / 2);
rv.addItemDecoration(new RecyclerView.ItemDecoration() {
@@ -91,13 +100,11 @@ public class GamesCoverDialogFragment extends DialogFragment {
outRect.set(half, half, half, half);
}
});
// Hard lock spans based on orientation only
rv.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
int currentOrientation = getResources().getConfiguration().orientation;
int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); }
});
// Set initial fixed span as soon as possible
root.post(() -> {
int currentOrientation = getResources().getConfiguration().orientation;
int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
@@ -113,7 +120,6 @@ public class GamesCoverDialogFragment extends DialogFragment {
String saved = prefs.getString("serial:" + uris[i], null);
String serial = saved;
if (serial == null || serial.isEmpty()) {
// Ask native core for the real serial (supports ISO/CHD and content://)
try {
String nativeSerial = NativeApp.getGameSerial(uris[i]);
if (nativeSerial != null && !nativeSerial.isEmpty()) {
@@ -123,63 +129,58 @@ public class GamesCoverDialogFragment extends DialogFragment {
} catch (Throwable ignored) {}
}
if (serial == null || serial.isEmpty()) {
// Heuristic fallback from filename
serial = buildSerialFromUri(uris[i]);
}
coverUrls[i] = buildCoverUrlFromSerial(serial);
localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
}
adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths,
position -> {
// Regular click - start game
if (listener != null && position >= 0 && position < uris.length) {
listener.onGameSelected(uris[position]);
dismissAllowingStateLoss();
}
},
position -> {
// Long click - show game settings
if (position >= 0 && position < uris.length) {
showGameSettings(titles[position], uris[position]);
}
});
adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths,
position -> {
if (listener != null && position >= 0 && position < uris.length) {
listener.onGameSelected(uris[position]);
dismissAllowingStateLoss();
}
},
position -> {
if (position >= 0 && position < uris.length) {
showGameSettings(titles[position], uris[position]);
}
});
rv.setAdapter(adapter);
// Toolbar buttons
View btnHome = root.findViewById(R.id.btn_home);
if (btnHome != null) btnHome.setOnClickListener(v -> dismissAllowingStateLoss());
View btnDownload = root.findViewById(R.id.btn_download);
if (btnDownload != null) btnDownload.setOnClickListener(v -> startDownloadCovers());
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setView(root)
.create();
return dialog;
return root;
}
@Override
public void onStart() {
super.onStart();
Dialog d = getDialog();
if (d != null) {
Window w = d.getWindow();
if (w != null) {
w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
w.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
// Hide status bar for true full-screen dialog
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
w.setDecorFitsSystemWindows(false);
WindowInsetsController controller = w.getInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.statusBars());
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
if (d == null) return;
Window w = d.getWindow();
if (w == null) return;
w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
w.setDecorFitsSystemWindows(false);
WindowInsetsController controller = w.getInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.systemBars());
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
View decor = w.getDecorView();
int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decor.setSystemUiVisibility(flags);
}
}
@@ -298,7 +299,7 @@ public class GamesCoverDialogFragment extends DialogFragment {
if (dot > 0) last = last.substring(0, dot);
String serial = null;
// Very simple heuristic: find token like XXXX-XXXXX
String upper = last.toUpperCase();
String upper = last.toUpperCase(Locale.ROOT);
java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{4,5}-[0-9]{3,5})").matcher(upper);
if (m.find()) {
serial = m.group(1);
@@ -353,7 +354,7 @@ public class GamesCoverDialogFragment extends DialogFragment {
private static String normalizeSerial(String serial) {
if (serial == null) return null;
String s = serial.toUpperCase().replace('_', '-');
String s = serial.toUpperCase(Locale.ROOT).replace('_', '-');
// If form like XXXX-123.45 -> XXXX-12345
s = s.replaceAll("([A-Z]{4,5})-([0-9]{3})\\.([0-9]{2})", "$1-$2$3");
return s;
@@ -9,11 +9,13 @@ import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.util.Arrays;
import java.util.LinkedList;
@@ -80,23 +82,33 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
try {
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for reading characteristic: " + e.getMessage());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
try {
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for writing characteristic: " + e.getMessage());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
@@ -116,14 +128,19 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
try {
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for notification setup: " + e.getMessage());
mResult = false;
return;
}
mResult = true;
}
}
}
@@ -176,6 +193,14 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
return String.format("SteamController.%s", mDevice.getAddress());
}
private boolean hasBluetoothPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ContextCompat.checkSelfPermission(mManager.getContext(),
android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
}
return true; // Pre-Android 12 doesn't need runtime permission
}
BluetoothGatt getGatt() {
return mGatt;
}
@@ -183,14 +208,19 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
try {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for GATT connection: " + e.getMessage());
return null;
}
}
@@ -213,13 +243,22 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
try {
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for getting connection state: " + e.getMessage());
return BluetoothProfile.STATE_DISCONNECTED;
}
}
void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
try {
mGatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt();
}
@@ -241,7 +280,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
try {
mGatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt(false);
break;
}
@@ -253,7 +296,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
try {
mGatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt(false);
break;
}
@@ -268,7 +315,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
try {
mGatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt(false);
break;
@@ -328,7 +379,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
try {
mGatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt(false);
}
@@ -423,7 +478,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
try {
mGatt.discoverServices();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for service discovery: " + e.getMessage());
}
}
});
}
@@ -443,7 +502,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
try {
gatt.disconnect();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for disconnect: " + e.getMessage());
}
mGatt = connectGatt(false);
}
else {
@@ -505,7 +568,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
if (reportChr != null) {
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
gatt.writeCharacteristic(reportChr);
try {
gatt.writeCharacteristic(reportChr);
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for writing report characteristic: " + e.getMessage());
}
}
}
@@ -638,8 +705,12 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
try {
g.disconnect();
g.close();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for shutdown operations: " + e.getMessage());
}
mGatt = null;
}
mManager = null;
@@ -12,6 +12,7 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbConstants;
import androidx.core.content.ContextCompat;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
@@ -134,7 +135,7 @@ public class HIDDeviceManager {
}
spedit.putInt(identifier, result);
spedit.commit();
spedit.apply();
return result;
}
@@ -192,11 +193,7 @@ public class HIDDeviceManager {
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mUsbBroadcast, filter);
}
ContextCompat.registerReceiver(mContext, mUsbBroadcast, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
@@ -446,7 +443,13 @@ public class HIDDeviceManager {
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
List<BluetoothDevice> currentConnected;
try {
currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for getting connected devices: " + e.getMessage());
return;
}
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
@@ -519,11 +522,21 @@ public class HIDDeviceManager {
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
String deviceName;
int deviceType;
try {
deviceName = bluetoothDevice.getName();
deviceType = bluetoothDevice.getType();
} catch (SecurityException e) {
Log.e(TAG, "Permission denied for getting device info: " + e.getMessage());
return false;
}
if (deviceName == null) {
return false;
}
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
return deviceName.equals("SteamController") && ((deviceType & BluetoothDevice.DEVICE_TYPE_LE) != 0);
}
private void close() {
@@ -9,6 +9,7 @@ import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.Locale;
class HIDDeviceUSB implements HIDDevice {
@@ -36,7 +37,7 @@ class HIDDeviceUSB implements HIDDevice {
}
String getIdentifier() {
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
return String.format(Locale.ROOT, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
@@ -51,6 +51,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import androidx.fragment.app.FragmentManager;
import java.util.List;
import java.util.Locale;
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener, ControllerInputHandler.ControllerInputListener {
private String m_szGamefile = "";
@@ -306,7 +307,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
private static boolean hasGameExt(String name) {
if (TextUtils.isEmpty(name)) return false;
String lower = name.toLowerCase();
String lower = name.toLowerCase(Locale.ROOT);
for (String ext : GAME_EXTS) {
if (lower.endsWith(ext)) return true;
}
@@ -528,7 +529,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
if(btn_ogl != null) {
btn_ogl.setOnClickListener(v -> {
// Save setting first with commit() to ensure immediate write
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 12).commit();
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 12).apply();
// Small delay to ensure setting is persisted
try {
@@ -546,7 +547,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
if(btn_vulkan != null) {
btn_vulkan.setOnClickListener(v -> {
// Save setting first with commit() to ensure immediate write
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 14).commit();
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 14).apply();
// Small delay to ensure setting is persisted
try {
@@ -566,7 +567,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
android.util.Log.d("MainActivity", "SW button clicked - saving renderer 13");
// Save setting first with commit() to ensure immediate write
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 13).commit();
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", 13).apply();
android.util.Log.d("MainActivity", "SW renderer setting saved");
// Small delay to ensure setting is persisted
@@ -908,7 +909,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
// Filter by name for common virtual/built-in inputs
String name = device.getName();
if (name != null) {
String lower = name.toLowerCase();
String lower = name.toLowerCase(Locale.ROOT);
if (lower.contains("virtual") || lower.contains("uinput") || lower.contains("touch")
|| lower.contains("keyboard") || lower.contains("keypad") || lower.contains("gpio")) {
return false;
@@ -986,7 +987,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
private void setRendererAndSave(int renderer) {
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
prefs.edit().putInt("renderer", renderer).commit();
prefs.edit().putInt("renderer", renderer).apply();
// Apply live if possible; this path has proven stable via top bar buttons
try {
NativeApp.renderGpu(renderer);
@@ -1054,7 +1055,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
String name = f.getName();
// Common PCSX2 BIOS components: SCPH*.bin (main), and component ROMs ROM0/ROM1/ROM2/EROM
if (name != null) {
String lower = name.toLowerCase();
String lower = name.toLowerCase(Locale.ROOT);
boolean isMainBios = lower.startsWith("scph") && (lower.endsWith(".bin") || lower.endsWith(".rom"));
boolean isComponentSuffix = lower.endsWith(".rom0") || lower.endsWith(".rom1") || lower.endsWith(".rom2") || lower.endsWith(".erom");
boolean isBareComponent = lower.equals("rom0") || lower.equals("rom1") || lower.equals("rom2") || lower.equals("erom");
@@ -1247,7 +1248,12 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
if (treeUri != null) {
// Persist read permission for future imports, optional
final int takeFlags = (data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
if ((takeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
if ((takeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
if (pickedDir != null && pickedDir.isDirectory()) {
@@ -1273,9 +1279,16 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
Uri treeUri = data.getData();
if (treeUri != null) {
final int takeFlags = (data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
try {
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
} catch (SecurityException ignored) {}
if ((takeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
try {
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException ignored) {}
}
if ((takeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
try {
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} catch (SecurityException ignored) {}
}
// Save folder and show games
getSharedPreferences("app_prefs", MODE_PRIVATE)
.edit()
@@ -1423,13 +1436,8 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
// Initialize HID device manager for USB and Bluetooth controllers
mHIDDeviceManager.initialize(true, true);
// Apply renderer setting again after a delay to override any automatic detection
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
int renderer = prefs.getInt("renderer", 14); // Default to Vulkan if no setting
android.util.Log.d("MainActivity", "Re-applying renderer setting after delay: " + renderer);
NativeApp.renderGpu(renderer);
}, 1000); // 1 second delay
// Avoid forcing renderer init when no game/surface is active.
// Renderer is applied on game start (restartEmuThread) and from settings when appropriate.
}
private void setSurfaceView(Object p_value) {
@@ -1622,6 +1630,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
@Override
public void onBackPressed() {
// Fallback for older Android versions
super.onBackPressed();
showExitDialog();
}
@@ -17,7 +17,7 @@ public class QuickActionsDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_quick_actions, null, false);
View view = getLayoutInflater().inflate(R.layout.dialog_quick_actions, null, false);
MaterialButton btnOpenGames = view.findViewById(R.id.btn_open_games);
MaterialButton btnExitToMenu = view.findViewById(R.id.btn_exit_to_menu);
@@ -504,14 +504,23 @@ class SDLHapticHandler_API31 extends SDLHapticHandler {
return;
}
VibratorManager manager = device.getVibratorManager();
int[] vibrators = manager.getVibratorIds();
if (vibrators.length >= 2) {
vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
} else if (vibrators.length == 1) {
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
vibrate(manager.getVibrator(vibrators[0]), intensity, length);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
VibratorManager manager = device.getVibratorManager();
int[] vibrators = manager.getVibratorIds();
if (vibrators.length >= 2) {
vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
} else if (vibrators.length == 1) {
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
vibrate(manager.getVibrator(vibrators[0]), intensity, length);
}
} else {
// Fallback for older Android versions
Vibrator vibrator = device.getVibrator();
if (vibrator != null) {
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
vibrate(vibrator, intensity, length);
}
}
}
@@ -149,7 +149,7 @@ public class SavesDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Context ctx = requireContext();
View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_saves, null, false);
View view = getLayoutInflater().inflate(R.layout.dialog_saves, null, false);
RecyclerView recyclerView = view.findViewById(R.id.rv_save_slots);
recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
@@ -10,7 +10,7 @@ import android.widget.ArrayAdapter;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.Switch;
import com.google.android.material.materialswitch.MaterialSwitch;
import android.content.res.ColorStateList;
import androidx.annotation.NonNull;
@@ -70,7 +70,7 @@ public class SettingsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Context ctx = requireContext();
View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_settings, null, false);
View view = getLayoutInflater().inflate(R.layout.dialog_settings, null, false);
RadioGroup rgRenderer = view.findViewById(R.id.rg_renderer);
RadioButton rbGl = view.findViewById(R.id.rb_renderer_gl);
@@ -79,11 +79,11 @@ public class SettingsDialogFragment extends DialogFragment {
Spinner spScale = view.findViewById(R.id.sp_scale);
Spinner spBlending = view.findViewById(R.id.sp_blending_accuracy);
Spinner spAspectRatio = view.findViewById(R.id.sp_aspect_ratio);
Switch swWidescreen = view.findViewById(R.id.sw_widescreen);
Switch swNoInterlacing = view.findViewById(R.id.sw_no_interlacing);
Switch swLoadTextures = view.findViewById(R.id.sw_load_textures);
Switch swAsyncTextureLoading = view.findViewById(R.id.sw_async_texture_loading);
Switch swDevHud = view.findViewById(R.id.sw_dev_hud);
MaterialSwitch swWidescreen = view.findViewById(R.id.sw_widescreen);
MaterialSwitch swNoInterlacing = view.findViewById(R.id.sw_no_interlacing);
MaterialSwitch swLoadTextures = view.findViewById(R.id.sw_load_textures);
MaterialSwitch swAsyncTextureLoading = view.findViewById(R.id.sw_async_texture_loading);
MaterialSwitch swDevHud = view.findViewById(R.id.sw_dev_hud);
View btnPower = view.findViewById(R.id.btn_power);
View btnReboot = view.findViewById(R.id.btn_reboot);
View btnTestController = view.findViewById(R.id.btn_test_controller);
@@ -106,37 +106,7 @@ public class SettingsDialogFragment extends DialogFragment {
if (rbVk != null) CompoundButtonCompat.setButtonTintList(rbVk, brandChecked);
if (rbSw != null) CompoundButtonCompat.setButtonTintList(rbSw, brandChecked);
// Switches (thumb = brand when checked; track = subtle brand when checked)
ColorStateList thumbTint = new ColorStateList(
new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
new int[]{brand, outline}
);
int brandTrack = ColorStateList.valueOf(brand).withAlpha(100).getDefaultColor();
int outlineTrack = ColorStateList.valueOf(outline).withAlpha(80).getDefaultColor();
ColorStateList trackTint = new ColorStateList(
new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
new int[]{brandTrack, outlineTrack}
);
if (swWidescreen != null) {
swWidescreen.setThumbTintList(thumbTint);
swWidescreen.setTrackTintList(trackTint);
}
if (swNoInterlacing != null) {
swNoInterlacing.setThumbTintList(thumbTint);
swNoInterlacing.setTrackTintList(trackTint);
}
if (swLoadTextures != null) {
swLoadTextures.setThumbTintList(thumbTint);
swLoadTextures.setTrackTintList(trackTint);
}
if (swAsyncTextureLoading != null) {
swAsyncTextureLoading.setThumbTintList(thumbTint);
swAsyncTextureLoading.setTrackTintList(trackTint);
}
if (swDevHud != null) {
swDevHud.setThumbTintList(thumbTint);
swDevHud.setTrackTintList(trackTint);
}
// MaterialSwitch: rely on default Material3 theme styling from XML style/overlay
if (btnPower != null) {
btnPower.setOnClickListener(v -> {
@@ -264,7 +234,7 @@ public class SettingsDialogFragment extends DialogFragment {
" (12=OpenGL, 13=Software, 14=Vulkan)");
// Save renderer setting first with commit() to ensure immediate write
prefs.edit().putInt("renderer", renderer).commit();
prefs.edit().putInt("renderer", renderer).apply();
android.util.Log.d("SettingsDialog", "Renderer setting saved successfully");
// Small delay to ensure setting is persisted
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Base vertical gradient: near-black to deep blue -->
<item>
<shape android:shape="rectangle">
<gradient
android:type="linear"
android:angle="270"
android:startColor="#0A0E14"
android:centerColor="#12202C"
android:endColor="#0C3253" />
</shape>
</item>
<!-- Subtle vignette around edges to focus center content -->
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:useLevel="false"
android:gradientRadius="900dp"
android:centerX="50%"
android:centerY="45%"
android:startColor="#00000000"
android:endColor="#AA000000" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Base vertical gradient: near-black to deep purple (brand tertiary) -->
<item>
<shape android:shape="rectangle">
<gradient
android:type="linear"
android:angle="270"
android:startColor="#0A0E14"
android:centerColor="#1A1122"
android:endColor="#330045" />
</shape>
</item>
<!-- Subtle vignette around edges to focus center content -->
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:useLevel="false"
android:gradientRadius="900dp"
android:centerX="50%"
android:centerY="45%"
android:startColor="#00000000"
android:endColor="#AA000000" />
</shape>
</item>
</layer-list>
+2 -2
View File
@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="512dp"
android:height="512dp"
android:width="200dp"
android:height="200dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="512dp"
android:height="512dp"
android:width="200dp"
android:height="200dp"
android:viewportWidth="512"
android:viewportHeight="512">
@@ -310,4 +310,4 @@
android:strokeColor="#8f61ff"
android:strokeLineCap="round"/>
</vector>
</vector>

Some files were not shown because too many files have changed in this diff Show More