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
@@ -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