mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
cleaned up a bunch of warnings more ui stuff
This commit is contained in:
@@ -21,3 +21,4 @@ app/.cxx/
|
||||
*.exe
|
||||
/PCSX2_ARM64
|
||||
build/reports/problems/problems-report.html
|
||||
crash.txt
|
||||
|
||||
+422
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user