2025-08-22 04:11:11 -04:00
|
|
|
package com.izzy2lost.psx2;
|
|
|
|
|
|
|
|
|
|
import android.app.Activity;
|
2025-08-28 08:11:50 -04:00
|
|
|
import androidx.appcompat.app.AlertDialog;
|
2025-08-25 07:58:09 -04:00
|
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
2025-08-22 04:11:11 -04:00
|
|
|
import android.content.Context;
|
|
|
|
|
import android.content.Intent;
|
|
|
|
|
import android.content.res.AssetManager;
|
|
|
|
|
import android.content.res.Configuration;
|
|
|
|
|
import android.database.Cursor;
|
|
|
|
|
import android.os.Bundle;
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
import android.net.Uri;
|
|
|
|
|
import android.view.InputDevice;
|
2025-08-29 12:26:17 -04:00
|
|
|
import android.hardware.input.InputManager;
|
2025-08-25 00:17:24 -04:00
|
|
|
import android.view.InputEvent;
|
2025-08-22 04:11:11 -04:00
|
|
|
import android.view.KeyEvent;
|
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
|
import android.view.View;
|
|
|
|
|
import android.widget.FrameLayout;
|
2025-09-11 23:14:41 -04:00
|
|
|
import android.widget.ArrayAdapter;
|
|
|
|
|
import android.widget.Spinner;
|
2025-08-22 04:11:11 -04:00
|
|
|
import android.view.ViewGroup;
|
|
|
|
|
import android.content.res.ColorStateList;
|
|
|
|
|
import android.graphics.Color;
|
|
|
|
|
import android.content.ClipData;
|
|
|
|
|
import android.content.SharedPreferences;
|
|
|
|
|
import android.os.Build;
|
|
|
|
|
import android.view.Window;
|
|
|
|
|
import android.view.WindowInsets;
|
|
|
|
|
import android.view.WindowInsetsController;
|
|
|
|
|
import android.view.WindowManager;
|
|
|
|
|
import android.util.TypedValue;
|
2025-09-06 02:07:48 -04:00
|
|
|
import android.widget.Toast;
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
import androidx.activity.result.ActivityResultLauncher;
|
|
|
|
|
import androidx.activity.result.contract.ActivityResultContracts;
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
|
|
|
import androidx.documentfile.provider.DocumentFile;
|
|
|
|
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
|
|
|
import androidx.core.content.ContextCompat;
|
2025-09-10 09:24:01 -04:00
|
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
|
|
|
|
import androidx.core.view.GravityCompat;
|
2025-08-22 04:11:11 -04:00
|
|
|
import androidx.activity.OnBackPressedCallback;
|
|
|
|
|
|
|
|
|
|
import com.google.android.material.button.MaterialButton;
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import android.provider.OpenableColumns;
|
|
|
|
|
import org.json.JSONArray;
|
|
|
|
|
import org.json.JSONException;
|
|
|
|
|
import androidx.fragment.app.FragmentManager;
|
2025-09-10 09:24:01 -04:00
|
|
|
import com.google.android.material.navigation.NavigationView;
|
|
|
|
|
import com.google.android.material.button.MaterialButtonToggleGroup;
|
2025-08-22 04:11:11 -04:00
|
|
|
import java.util.List;
|
2025-08-30 04:59:16 -04:00
|
|
|
import java.util.Locale;
|
2025-08-22 04:11:11 -04:00
|
|
|
|
2025-08-25 13:56:13 -04:00
|
|
|
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener, ControllerInputHandler.ControllerInputListener {
|
2025-08-22 04:11:11 -04:00
|
|
|
private String m_szGamefile = "";
|
|
|
|
|
|
|
|
|
|
private HIDDeviceManager mHIDDeviceManager;
|
2025-08-25 13:56:13 -04:00
|
|
|
private ControllerInputHandler mControllerInputHandler;
|
2025-08-22 04:11:11 -04:00
|
|
|
private Thread mEmulationThread = null;
|
2025-09-06 02:07:48 -04:00
|
|
|
private boolean mSetupWizardActive = false;
|
2025-08-22 04:11:11 -04:00
|
|
|
private boolean mHudVisible = false;
|
2025-08-29 12:26:17 -04:00
|
|
|
private InputManager mInputManager;
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
// Track joystick directional pressed state to avoid duplicate down events
|
|
|
|
|
private boolean joyUpPressed = false;
|
|
|
|
|
private boolean joyDownPressed = false;
|
|
|
|
|
private boolean joyLeftPressed = false;
|
|
|
|
|
private boolean joyRightPressed = false;
|
2025-08-29 12:26:17 -04:00
|
|
|
private boolean controllerUiApplied = false;
|
2025-08-28 08:11:50 -04:00
|
|
|
private AlertDialog mBiosPromptDialog = null;
|
2025-09-11 23:14:41 -04:00
|
|
|
private boolean mControllerHintShowing = false;
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
private boolean isThread() {
|
|
|
|
|
if (mEmulationThread != null) {
|
|
|
|
|
Thread.State _thread_state = mEmulationThread.getState();
|
|
|
|
|
return _thread_state == Thread.State.BLOCKED
|
|
|
|
|
|| _thread_state == Thread.State.RUNNABLE
|
|
|
|
|
|| _thread_state == Thread.State.TIMED_WAITING
|
|
|
|
|
|| _thread_state == Thread.State.WAITING;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expose whether a game has been chosen (non-empty path)
|
|
|
|
|
public boolean hasSelectedGame() {
|
|
|
|
|
return !TextUtils.isEmpty(m_szGamefile);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
|
|
|
|
super.onConfigurationChanged(newConfig);
|
|
|
|
|
// Keep fullscreen on rotate and reflow constraints without recreating
|
|
|
|
|
hideStatusBar();
|
|
|
|
|
applyConstraintsForOrientation(newConfig.orientation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void applyConstraintsForOrientation(int orientation) {
|
|
|
|
|
// Views present in both layouts
|
|
|
|
|
View quick = findViewById(R.id.ll_quick_actions);
|
|
|
|
|
View btnSettings = findViewById(R.id.btn_settings);
|
2025-09-10 09:24:01 -04:00
|
|
|
// Visibility toggle removed; no dependency on it for constraints
|
2025-08-22 04:11:11 -04:00
|
|
|
View llJoy = findViewById(R.id.ll_pad_joy);
|
|
|
|
|
View llDpad = findViewById(R.id.ll_pad_dpad);
|
|
|
|
|
View llRight = findViewById(R.id.ll_pad_right_buttons);
|
|
|
|
|
View llSelectStart = findViewById(R.id.ll_pad_select_start);
|
|
|
|
|
|
|
|
|
|
// Use helper dp()
|
|
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
if (quick != null && btnSettings != null) {
|
2025-08-22 04:11:11 -04:00
|
|
|
ConstraintLayout.LayoutParams lp = safeCLP(quick);
|
|
|
|
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
2025-09-10 09:24:01 -04:00
|
|
|
// Top-aligned row stretching from settings to end
|
2025-08-22 04:11:11 -04:00
|
|
|
lp.width = 0; // chain between start/end
|
|
|
|
|
lp.topToTop = btnSettings.getId();
|
|
|
|
|
lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.startToEnd = btnSettings.getId();
|
2025-09-10 09:24:01 -04:00
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
|
2025-08-22 04:11:11 -04:00
|
|
|
lp.startToStart = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topMargin = dp(0);
|
|
|
|
|
lp.horizontalChainStyle = ConstraintLayout.LayoutParams.CHAIN_PACKED;
|
|
|
|
|
} else {
|
|
|
|
|
// Centered under settings
|
|
|
|
|
lp.width = ConstraintLayout.LayoutParams.WRAP_CONTENT;
|
|
|
|
|
lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topToBottom = btnSettings.getId();
|
|
|
|
|
lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.startToEnd = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.endToStart = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topMargin = dp(16);
|
|
|
|
|
lp.horizontalChainStyle = ConstraintLayout.LayoutParams.CHAIN_SPREAD;
|
|
|
|
|
}
|
|
|
|
|
quick.setLayoutParams(lp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (llJoy != null) {
|
|
|
|
|
ConstraintLayout.LayoutParams lp = safeCLP(llJoy);
|
|
|
|
|
lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
int m = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? 6 : 6;
|
|
|
|
|
lp.setMargins(dp(m), dp(m), dp(m), dp(m));
|
|
|
|
|
llJoy.setLayoutParams(lp);
|
|
|
|
|
// Nudge joystick further left in both orientations to avoid Select overlap
|
|
|
|
|
llJoy.setTranslationX(-dp(28));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (llDpad != null && llJoy != null) {
|
|
|
|
|
ConstraintLayout.LayoutParams lp = safeCLP(llDpad);
|
|
|
|
|
// Above-left of joystick for both, with slight spacing
|
|
|
|
|
lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.bottomToTop = llJoy.getId();
|
|
|
|
|
lp.bottomToBottom = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
|
2025-08-29 12:26:17 -04:00
|
|
|
// Shift the whole D-pad slightly to the right with a small start margin
|
|
|
|
|
lp.setMargins(dp(48), dp(0), dp(0), dp(orientation == Configuration.ORIENTATION_LANDSCAPE ? 2 : 0));
|
2025-08-22 04:11:11 -04:00
|
|
|
llDpad.setLayoutParams(lp);
|
2025-08-29 12:26:17 -04:00
|
|
|
// Nudge D-pad downward a little
|
|
|
|
|
llDpad.setTranslationY(dp(14));
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (llRight != null) {
|
|
|
|
|
ConstraintLayout.LayoutParams lp = safeCLP(llRight);
|
|
|
|
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
|
|
|
lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.bottomToTop = ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.setMargins(dp(12), dp(12), dp(12), dp(12));
|
|
|
|
|
} else {
|
|
|
|
|
// Above Select/Start, aligned to end
|
|
|
|
|
if (llSelectStart != null) {
|
|
|
|
|
lp.bottomToTop = llSelectStart.getId();
|
|
|
|
|
} else {
|
|
|
|
|
lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
}
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.bottomToBottom = (llSelectStart == null) ? ConstraintLayout.LayoutParams.PARENT_ID : ConstraintLayout.LayoutParams.UNSET;
|
|
|
|
|
lp.setMargins(0,0,0,dp(8));
|
|
|
|
|
}
|
|
|
|
|
llRight.setLayoutParams(lp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (llSelectStart != null) {
|
|
|
|
|
ConstraintLayout.LayoutParams lp = safeCLP(llSelectStart);
|
|
|
|
|
lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
|
|
|
|
|
lp.setMargins(0,0,0,dp(orientation == Configuration.ORIENTATION_LANDSCAPE ? 8 : 0));
|
|
|
|
|
llSelectStart.setLayoutParams(lp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ConstraintLayout.LayoutParams safeCLP(View v) {
|
|
|
|
|
ViewGroup.LayoutParams p = v.getLayoutParams();
|
|
|
|
|
if (p instanceof ConstraintLayout.LayoutParams) return (ConstraintLayout.LayoutParams) p;
|
|
|
|
|
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(p);
|
|
|
|
|
v.setLayoutParams(lp);
|
|
|
|
|
return lp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int dp(int d) {
|
|
|
|
|
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, d, getResources().getDisplayMetrics());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void hideStatusBar() {
|
2025-08-28 05:11:34 -04:00
|
|
|
// Make immersive (hide status + navigation) and allow swipe to reveal temporarily
|
2025-08-22 04:11:11 -04:00
|
|
|
Window w = getWindow();
|
|
|
|
|
if (w == null) return;
|
2025-08-29 15:47:14 -04:00
|
|
|
// Request full screen window flags for additional assurance
|
|
|
|
|
w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
|
|
|
// Allow drawing into display cutouts (notches) on API 28+
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
|
|
|
WindowManager.LayoutParams lp = w.getAttributes();
|
|
|
|
|
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
|
|
|
|
w.setAttributes(lp);
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
|
|
|
|
|
// Common flags for legacy immersive (works as belt-and-suspenders on newer API too)
|
|
|
|
|
final int legacyFlags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
|
|
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
|
|
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
|
|
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
|
|
|
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
|
|
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
2025-08-28 05:11:34 -04:00
|
|
|
// Tell Window to lay out edge-to-edge and hide all system bars
|
|
|
|
|
w.setDecorFitsSystemWindows(false);
|
2025-08-22 04:11:11 -04:00
|
|
|
WindowInsetsController controller = w.getInsetsController();
|
|
|
|
|
if (controller != null) {
|
2025-08-28 05:11:34 -04:00
|
|
|
controller.hide(WindowInsets.Type.systemBars());
|
2025-08-22 04:11:11 -04:00
|
|
|
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
// Also apply legacy flags to decor; helps on 3-button nav devices
|
|
|
|
|
View decor = w.getDecorView();
|
|
|
|
|
decor.setSystemUiVisibility(legacyFlags);
|
|
|
|
|
decor.setOnSystemUiVisibilityChangeListener(vis -> {
|
|
|
|
|
if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
|
|
|
|
decor.setSystemUiVisibility(legacyFlags);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-22 04:11:11 -04:00
|
|
|
} else {
|
2025-08-28 05:11:34 -04:00
|
|
|
View decor = w.getDecorView();
|
2025-09-11 23:14:41 -04:00
|
|
|
decor.setSystemUiVisibility(legacyFlags);
|
|
|
|
|
decor.setOnSystemUiVisibilityChangeListener(vis -> {
|
|
|
|
|
if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
|
|
|
|
decor.setSystemUiVisibility(legacyFlags);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:36:45 -04:00
|
|
|
// Enable immersive mode to hide navigation and status bars
|
|
|
|
|
private void enableImmersiveMode() {
|
|
|
|
|
hideStatusBar();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 05:11:34 -04:00
|
|
|
@Override
|
|
|
|
|
public void onWindowFocusChanged(boolean hasFocus) {
|
|
|
|
|
super.onWindowFocusChanged(hasFocus);
|
|
|
|
|
if (hasFocus) hideStatusBar();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
public void pickGamesFolder() {
|
2025-08-22 04:11:11 -04:00
|
|
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
|
|
|
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
|
|
|
|
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
|
|
|
|
startActivityResultGamesFolderPick.launch(intent);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// Let the user select a data root (SAF) where app folders/files live (covers, resources, saves, etc.)
|
|
|
|
|
public void pickDataRootFolder() {
|
|
|
|
|
startActivityResultDataRootPick.launch(SafManager.buildOpenTreeIntent());
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
private void showGamesListOrReselect(Uri treeUri) {
|
|
|
|
|
// Re-scan quickly each time to keep list fresh
|
|
|
|
|
String[] names;
|
|
|
|
|
String[] uris;
|
|
|
|
|
try {
|
|
|
|
|
GameList list = scanGamesFromTreeUri(treeUri);
|
|
|
|
|
names = list.names;
|
|
|
|
|
uris = list.uris;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
names = new String[0];
|
|
|
|
|
uris = new String[0];
|
|
|
|
|
}
|
|
|
|
|
// Make effectively-final copies for use in lambda
|
|
|
|
|
final String[] namesFinal = names;
|
|
|
|
|
final String[] urisFinal = uris;
|
|
|
|
|
|
|
|
|
|
if (namesFinal.length == 0) {
|
2025-08-25 07:58:09 -04:00
|
|
|
new MaterialAlertDialogBuilder(this,
|
|
|
|
|
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
2025-09-06 02:07:48 -04:00
|
|
|
.setCustomTitle(UiUtils.centeredDialogTitle(this, "GAMES"))
|
2025-08-22 04:11:11 -04:00
|
|
|
.setMessage("No games found. Pick a folder?")
|
|
|
|
|
.setNegativeButton("Cancel", null)
|
|
|
|
|
.setPositiveButton("Pick Folder", (d,w) -> pickGamesFolder())
|
|
|
|
|
.show();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Show covers grid dialog fragment
|
|
|
|
|
GamesCoverDialogFragment frag = GamesCoverDialogFragment.newInstance(namesFinal, urisFinal);
|
|
|
|
|
FragmentManager fm = getSupportFragmentManager();
|
|
|
|
|
frag.show(fm, "covers_dialog");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onGameSelected(String gameUri) {
|
|
|
|
|
if (!TextUtils.isEmpty(gameUri)) {
|
|
|
|
|
// Avoid any pre-VM native calls here; just set the game and launch.
|
|
|
|
|
m_szGamefile = gameUri;
|
2025-09-01 15:36:45 -04:00
|
|
|
// Record last played timestamp for sorting
|
|
|
|
|
try {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
prefs.edit().putLong("last_played:" + gameUri, System.currentTimeMillis()).apply();
|
|
|
|
|
} catch (Throwable ignored) {}
|
2025-08-22 04:11:11 -04:00
|
|
|
restartEmuThread();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final String[] GAME_EXTS = new String[]{
|
2025-09-11 23:14:41 -04:00
|
|
|
".iso", ".bin", ".img", ".mdf", ".nrg", ".chd", ".cso", ".zso", ".gz"
|
2025-08-22 04:11:11 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static boolean hasGameExt(String name) {
|
|
|
|
|
if (TextUtils.isEmpty(name)) return false;
|
2025-08-30 04:59:16 -04:00
|
|
|
String lower = name.toLowerCase(Locale.ROOT);
|
2025-08-22 04:11:11 -04:00
|
|
|
for (String ext : GAME_EXTS) {
|
|
|
|
|
if (lower.endsWith(ext)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class GameList {
|
|
|
|
|
final String[] names;
|
|
|
|
|
final String[] uris;
|
|
|
|
|
GameList(String[] n, String[] u) { names = n; uris = u; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private GameList scanGamesFromTreeUri(Uri treeUri) {
|
|
|
|
|
DocumentFile dir = DocumentFile.fromTreeUri(this, treeUri);
|
|
|
|
|
if (dir == null || !dir.isDirectory()) return new GameList(new String[0], new String[0]);
|
|
|
|
|
java.util.ArrayList<String> nameList = new java.util.ArrayList<>();
|
|
|
|
|
java.util.ArrayList<String> uriList = new java.util.ArrayList<>();
|
|
|
|
|
scanGamesRecursive(dir, nameList, uriList);
|
|
|
|
|
|
|
|
|
|
// Persist folder and latest list
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
for (int i = 0; i < nameList.size(); i++) {
|
|
|
|
|
JSONArray pair = new JSONArray();
|
|
|
|
|
try {
|
|
|
|
|
pair.put(nameList.get(i));
|
|
|
|
|
pair.put(uriList.get(i));
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
arr.put(pair);
|
|
|
|
|
}
|
|
|
|
|
prefs.edit()
|
|
|
|
|
.putString("games_folder_uri", treeUri.toString())
|
|
|
|
|
.putString("games_list_json", arr.toString())
|
|
|
|
|
.apply();
|
|
|
|
|
|
|
|
|
|
return new GameList(nameList.toArray(new String[0]), uriList.toArray(new String[0]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void scanGamesRecursive(DocumentFile dir, java.util.List<String> names, java.util.List<String> uris) {
|
|
|
|
|
DocumentFile[] children = dir.listFiles();
|
|
|
|
|
if (children == null) return;
|
|
|
|
|
for (DocumentFile child : children) {
|
|
|
|
|
if (child == null) continue;
|
|
|
|
|
if (child.isDirectory()) {
|
|
|
|
|
scanGamesRecursive(child, names, uris);
|
|
|
|
|
} else if (child.isFile()) {
|
|
|
|
|
String name = child.getName();
|
|
|
|
|
if (hasGameExt(name)) {
|
|
|
|
|
names.add(name);
|
|
|
|
|
uris.add(child.getUri().toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
|
|
|
super.onCreate(savedInstanceState);
|
2025-08-29 15:47:14 -04:00
|
|
|
// Hide action bar to improve immersive appearance
|
|
|
|
|
if (getSupportActionBar() != null) {
|
|
|
|
|
getSupportActionBar().hide();
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
setContentView(R.layout.activity_main);
|
2025-09-01 15:36:45 -04:00
|
|
|
enableImmersiveMode();
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
// Setup back button handler
|
|
|
|
|
setupBackPressedHandler();
|
|
|
|
|
|
|
|
|
|
// Default resources
|
|
|
|
|
copyAssetAll(getApplicationContext(), "resources");
|
2025-09-06 02:07:48 -04:00
|
|
|
// If a SAF data root is set, mirror resources to it (first time only)
|
|
|
|
|
copyAssetsToSafDataRoot();
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
Initialize();
|
|
|
|
|
|
2025-08-25 13:56:13 -04:00
|
|
|
// Initialize controller input handler
|
|
|
|
|
mControllerInputHandler = new ControllerInputHandler(this);
|
|
|
|
|
|
|
|
|
|
// Log connected controllers for debugging
|
|
|
|
|
ControllerConfig.logControllerInfo(this);
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
makeButtonTouch();
|
2025-08-28 08:11:50 -04:00
|
|
|
updateRendererButtonLabel();
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
setSurfaceView(new SDLSurface(this));
|
|
|
|
|
|
2025-08-24 09:53:35 -04:00
|
|
|
// Ensure consistent ripple across all MaterialButtons
|
2025-08-22 04:11:11 -04:00
|
|
|
tintAllMaterialButtonOutlines();
|
|
|
|
|
|
|
|
|
|
// Apply orientation-specific constraints once at startup
|
|
|
|
|
int currentOrientation = getResources().getConfiguration().orientation;
|
|
|
|
|
applyConstraintsForOrientation(currentOrientation);
|
2025-09-01 15:36:45 -04:00
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// Prompt for BIOS if missing, but only after first-run setup
|
|
|
|
|
if (getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("first_run_done", false)) {
|
|
|
|
|
maybePromptForBios();
|
|
|
|
|
}
|
2025-08-29 12:26:17 -04:00
|
|
|
|
|
|
|
|
// Listen for controller attach/detach and update UI accordingly
|
|
|
|
|
mInputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
|
|
|
|
if (mInputManager != null) {
|
|
|
|
|
mInputManager.registerInputDeviceListener(mInputDeviceListener, null);
|
|
|
|
|
}
|
|
|
|
|
updateUiForControllerPresence();
|
2025-09-06 02:07:48 -04:00
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// After initial BIOS auto-boot, gently open the Games dialog
|
|
|
|
|
// Only when setup is complete and this is app launch (not rotation)
|
|
|
|
|
if (getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("first_run_done", false)) {
|
|
|
|
|
try {
|
|
|
|
|
final View decor = (getWindow() != null) ? getWindow().getDecorView() : null;
|
|
|
|
|
if (decor != null) {
|
|
|
|
|
decor.postDelayed(() -> {
|
|
|
|
|
if (!isFinishing() && !mSetupWizardActive) {
|
|
|
|
|
openGamesDialog();
|
|
|
|
|
}
|
|
|
|
|
}, 1600); // small delay to let BIOS boot briefly
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// Show first-run setup wizard if needed
|
|
|
|
|
if (!getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("first_run_done", false)) {
|
|
|
|
|
SetupWizardDialogFragment f = SetupWizardDialogFragment.newInstance();
|
|
|
|
|
f.setCancelable(false);
|
|
|
|
|
f.show(getSupportFragmentManager(), "setup_wizard");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setSetupWizardActive(boolean active) {
|
|
|
|
|
mSetupWizardActive = active;
|
2025-08-29 12:26:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Public method to open the games covers dialog via controller quick actions
|
|
|
|
|
public void openGamesDialog() {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
String folderUri = prefs.getString("games_folder_uri", null);
|
|
|
|
|
if (TextUtils.isEmpty(folderUri)) {
|
|
|
|
|
pickGamesFolder();
|
|
|
|
|
} else {
|
|
|
|
|
showGamesListOrReselect(Uri.parse(folderUri));
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void setupBackPressedHandler() {
|
|
|
|
|
OnBackPressedCallback callback = new OnBackPressedCallback(true) {
|
|
|
|
|
@Override
|
|
|
|
|
public void handleOnBackPressed() {
|
|
|
|
|
showExitDialog();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
getOnBackPressedDispatcher().addCallback(this, callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void makeButtonTouch() {
|
2025-09-10 09:24:01 -04:00
|
|
|
// SAVES button removed from main UI; access Save States via drawer or Quick Actions
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// Menu button opens left drawer
|
2025-08-22 04:11:11 -04:00
|
|
|
MaterialButton btn_settings = findViewById(R.id.btn_settings);
|
|
|
|
|
if (btn_settings != null) {
|
|
|
|
|
btn_settings.setOnClickListener(v -> {
|
2025-09-10 09:24:01 -04:00
|
|
|
try {
|
2025-09-11 23:14:41 -04:00
|
|
|
// Refresh drawer settings before opening
|
|
|
|
|
refreshDrawerSettings();
|
2025-09-10 09:24:01 -04:00
|
|
|
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
|
|
|
|
if (drawer != null) drawer.openDrawer(androidx.core.view.GravityCompat.START);
|
|
|
|
|
} catch (Throwable ignored) {}
|
2025-08-22 04:11:11 -04:00
|
|
|
});
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
|
|
|
|
|
// Pause/Play button toggles emulation pause state
|
|
|
|
|
MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play);
|
|
|
|
|
if (btn_pause_play != null) {
|
|
|
|
|
btn_pause_play.setOnClickListener(v -> {
|
|
|
|
|
try {
|
|
|
|
|
togglePauseState();
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-10 09:24:01 -04:00
|
|
|
// Wire left drawer header controls (title/power/reboot/renderer)
|
|
|
|
|
try {
|
|
|
|
|
NavigationView nav = findViewById(R.id.nav_view);
|
|
|
|
|
if (nav != null) {
|
|
|
|
|
View header = (nav.getHeaderCount() > 0) ? nav.getHeaderView(0) : nav.inflateHeaderView(R.layout.drawer_header_settings);
|
|
|
|
|
if (header != null) {
|
|
|
|
|
View btnPower = header.findViewById(R.id.drawer_btn_power);
|
|
|
|
|
if (btnPower != null) {
|
|
|
|
|
btnPower.setOnClickListener(v -> {
|
|
|
|
|
new MaterialAlertDialogBuilder(this)
|
|
|
|
|
.setTitle("Power Off")
|
|
|
|
|
.setMessage("Quit the app?")
|
|
|
|
|
.setNegativeButton("Cancel", null)
|
|
|
|
|
.setPositiveButton("Quit", (d,w) -> { try { NativeApp.shutdown(); } catch (Throwable ignored) {} finishAffinity(); finishAndRemoveTask(); System.exit(0); })
|
|
|
|
|
.show();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
View btnReboot = header.findViewById(R.id.drawer_btn_reboot);
|
|
|
|
|
if (btnReboot != null) {
|
|
|
|
|
btnReboot.setOnClickListener(v -> {
|
|
|
|
|
new MaterialAlertDialogBuilder(this)
|
|
|
|
|
.setTitle("Reboot")
|
|
|
|
|
.setMessage("Restart the current game?")
|
|
|
|
|
.setNegativeButton("Cancel", null)
|
|
|
|
|
.setPositiveButton("Reboot", (d,w) -> rebootEmu())
|
|
|
|
|
.show();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButtonToggleGroup tg = header.findViewById(R.id.drawer_tg_renderer);
|
|
|
|
|
View btnGameState = header.findViewById(R.id.drawer_btn_game_state);
|
|
|
|
|
View tbAt = header.findViewById(R.id.drawer_tb_at);
|
|
|
|
|
View tbVk = header.findViewById(R.id.drawer_tb_vk);
|
|
|
|
|
View tbGl = header.findViewById(R.id.drawer_tb_gl);
|
|
|
|
|
View tbSw = header.findViewById(R.id.drawer_tb_sw);
|
|
|
|
|
if (tg != null) {
|
|
|
|
|
int current = -1;
|
|
|
|
|
try { current = NativeApp.getCurrentRenderer(); } catch (Throwable ignored) {}
|
|
|
|
|
if (current == 14 && tbVk != null) tg.check(tbVk.getId());
|
|
|
|
|
else if (current == 12 && tbGl != null) tg.check(tbGl.getId());
|
|
|
|
|
else if (current == 13 && tbSw != null) tg.check(tbSw.getId());
|
|
|
|
|
else if (tbAt != null) tg.check(tbAt.getId());
|
|
|
|
|
tg.addOnButtonCheckedListener((group, checkedId, isChecked) -> {
|
|
|
|
|
if (!isChecked) return;
|
|
|
|
|
int r = -1;
|
|
|
|
|
if (checkedId == (tbVk != null ? tbVk.getId() : -2)) r = 14;
|
|
|
|
|
else if (checkedId == (tbGl != null ? tbGl.getId() : -2)) r = 12;
|
|
|
|
|
else if (checkedId == (tbSw != null ? tbSw.getId() : -2)) r = 13;
|
|
|
|
|
else r = -1;
|
|
|
|
|
try {
|
|
|
|
|
getSharedPreferences("app_prefs", MODE_PRIVATE).edit().putInt("renderer", r).apply();
|
|
|
|
|
NativeApp.renderGpu(r);
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
View btnGames = header.findViewById(R.id.drawer_btn_games);
|
|
|
|
|
if (btnGames != null) {
|
|
|
|
|
btnGames.setOnClickListener(v -> {
|
|
|
|
|
try {
|
|
|
|
|
// Close drawer first
|
|
|
|
|
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
|
|
|
|
if (drawer != null) drawer.closeDrawer(androidx.core.view.GravityCompat.START);
|
|
|
|
|
// Open games dialog
|
|
|
|
|
openGamesDialog();
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-10 09:24:01 -04:00
|
|
|
if (btnGameState != null) {
|
|
|
|
|
btnGameState.setOnClickListener(v -> {
|
|
|
|
|
try {
|
|
|
|
|
SavesDialogFragment dialog = new SavesDialogFragment();
|
|
|
|
|
dialog.show(getSupportFragmentManager(), "saves_dialog");
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
|
|
|
|
|
// Setup drawer settings controls to mirror quick actions
|
|
|
|
|
setupDrawerSettings(header);
|
2025-09-10 09:24:01 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
// Visibility toggle removed
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
// PAD
|
|
|
|
|
MaterialButton btn_pad_select = findViewById(R.id.btn_pad_select);
|
|
|
|
|
if(btn_pad_select != null) {
|
|
|
|
|
btn_pad_select.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_SELECT);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_start = findViewById(R.id.btn_pad_start);
|
|
|
|
|
if(btn_pad_start != null) {
|
|
|
|
|
btn_pad_start.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_START);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_a = findViewById(R.id.btn_pad_a);
|
|
|
|
|
if(btn_pad_a != null) {
|
|
|
|
|
btn_pad_a.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_A);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_b = findViewById(R.id.btn_pad_b);
|
|
|
|
|
if(btn_pad_b != null) {
|
|
|
|
|
btn_pad_b.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_B);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_x = findViewById(R.id.btn_pad_x);
|
|
|
|
|
if(btn_pad_x != null) {
|
|
|
|
|
btn_pad_x.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_X);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_y = findViewById(R.id.btn_pad_y);
|
|
|
|
|
if(btn_pad_y != null) {
|
|
|
|
|
btn_pad_y.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_Y);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:36:45 -04:00
|
|
|
//// Shoulder buttons
|
2025-08-22 04:11:11 -04:00
|
|
|
MaterialButton btn_pad_l1 = findViewById(R.id.btn_pad_l1);
|
|
|
|
|
if(btn_pad_l1 != null) {
|
|
|
|
|
btn_pad_l1.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L1);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_r1 = findViewById(R.id.btn_pad_r1);
|
|
|
|
|
if(btn_pad_r1 != null) {
|
|
|
|
|
btn_pad_r1.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R1);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MaterialButton btn_pad_l2 = findViewById(R.id.btn_pad_l2);
|
|
|
|
|
if(btn_pad_l2 != null) {
|
|
|
|
|
btn_pad_l2.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L2);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_r2 = findViewById(R.id.btn_pad_r2);
|
|
|
|
|
if(btn_pad_r2 != null) {
|
|
|
|
|
btn_pad_r2.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R2);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MaterialButton btn_pad_l3 = findViewById(R.id.btn_pad_l3);
|
|
|
|
|
if(btn_pad_l3 != null) {
|
|
|
|
|
btn_pad_l3.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBL);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_r3 = findViewById(R.id.btn_pad_r3);
|
|
|
|
|
if(btn_pad_r3 != null) {
|
|
|
|
|
btn_pad_r3.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBR);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:36:45 -04:00
|
|
|
//// D-Pad
|
2025-08-22 04:11:11 -04:00
|
|
|
final int PAD_L_UP = 110;
|
|
|
|
|
final int PAD_L_RIGHT = 111;
|
|
|
|
|
final int PAD_L_DOWN = 112;
|
|
|
|
|
final int PAD_L_LEFT = 113;
|
|
|
|
|
|
|
|
|
|
final int PAD_R_UP = 120;
|
|
|
|
|
final int PAD_R_RIGHT = 121;
|
|
|
|
|
final int PAD_R_DOWN = 122;
|
|
|
|
|
final int PAD_R_LEFT = 123;
|
|
|
|
|
|
|
|
|
|
// Draggable JoystickView (portrait/landscape layouts)
|
|
|
|
|
View joystick = findViewById(R.id.joystick_view);
|
|
|
|
|
if (joystick instanceof JoystickView) {
|
|
|
|
|
JoystickView jv = (JoystickView) joystick;
|
|
|
|
|
jv.setOnMoveListener((nx, ny, action) -> {
|
|
|
|
|
// Thresholds
|
|
|
|
|
final float T = 0.3f;
|
|
|
|
|
boolean up = ny < -T;
|
|
|
|
|
boolean down = ny > T;
|
|
|
|
|
boolean left = nx < -T;
|
|
|
|
|
boolean right = nx > T;
|
|
|
|
|
|
|
|
|
|
// Issue key down/up transitions only when state changes
|
|
|
|
|
if (up != joyUpPressed) {
|
|
|
|
|
sendKeyAction(jv, up ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_UP);
|
|
|
|
|
joyUpPressed = up;
|
|
|
|
|
}
|
|
|
|
|
if (down != joyDownPressed) {
|
|
|
|
|
sendKeyAction(jv, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_DOWN);
|
|
|
|
|
joyDownPressed = down;
|
|
|
|
|
}
|
|
|
|
|
if (left != joyLeftPressed) {
|
|
|
|
|
sendKeyAction(jv, left ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_LEFT);
|
|
|
|
|
joyLeftPressed = left;
|
|
|
|
|
}
|
|
|
|
|
if (right != joyRightPressed) {
|
|
|
|
|
sendKeyAction(jv, right ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_RIGHT);
|
|
|
|
|
joyRightPressed = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
|
|
|
// Ensure all released
|
|
|
|
|
if (joyUpPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_UP);
|
|
|
|
|
if (joyDownPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_DOWN);
|
|
|
|
|
if (joyLeftPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_LEFT);
|
|
|
|
|
if (joyRightPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_RIGHT);
|
|
|
|
|
joyUpPressed = joyDownPressed = joyLeftPressed = joyRightPressed = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:36:45 -04:00
|
|
|
//// D-Pad buttons
|
2025-08-22 04:11:11 -04:00
|
|
|
MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top);
|
|
|
|
|
if(btn_pad_dir_top != null) {
|
|
|
|
|
btn_pad_dir_top.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_UP);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_dir_bottom = findViewById(R.id.btn_pad_dir_bottom);
|
|
|
|
|
if(btn_pad_dir_bottom != null) {
|
|
|
|
|
btn_pad_dir_bottom.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_DOWN);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_dir_left = findViewById(R.id.btn_pad_dir_left);
|
|
|
|
|
if(btn_pad_dir_left != null) {
|
|
|
|
|
btn_pad_dir_left.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_LEFT);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
MaterialButton btn_pad_dir_right = findViewById(R.id.btn_pad_dir_right);
|
|
|
|
|
if(btn_pad_dir_right != null) {
|
|
|
|
|
btn_pad_dir_right.setOnTouchListener((v, event) -> {
|
|
|
|
|
sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_RIGHT);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// Visibility toggle removed; no global hidden state flag
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
private void setControlsVisible(boolean visible) {
|
|
|
|
|
int vis = visible ? View.VISIBLE : View.GONE;
|
|
|
|
|
View llDpad = findViewById(R.id.ll_pad_dpad);
|
|
|
|
|
View llRight = findViewById(R.id.ll_pad_right_buttons);
|
|
|
|
|
View llSelectStart = findViewById(R.id.ll_pad_select_start);
|
|
|
|
|
View llJoy = findViewById(R.id.ll_pad_joy);
|
|
|
|
|
|
|
|
|
|
if (llDpad != null) llDpad.setVisibility(vis);
|
|
|
|
|
if (llRight != null) llRight.setVisibility(vis);
|
|
|
|
|
if (llSelectStart != null) llSelectStart.setVisibility(vis);
|
|
|
|
|
if (llJoy != null) llJoy.setVisibility(vis);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// Removed visibility toggle function
|
2025-08-28 08:11:50 -04:00
|
|
|
|
2025-08-29 12:26:17 -04:00
|
|
|
// --- Controller presence handling ---
|
|
|
|
|
private final InputManager.InputDeviceListener mInputDeviceListener = new InputManager.InputDeviceListener() {
|
|
|
|
|
@Override public void onInputDeviceAdded(int deviceId) { updateUiForControllerPresence(); }
|
|
|
|
|
@Override public void onInputDeviceRemoved(int deviceId) { updateUiForControllerPresence(); }
|
|
|
|
|
@Override public void onInputDeviceChanged(int deviceId) { updateUiForControllerPresence(); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private boolean isControllerDevice(InputDevice device) {
|
|
|
|
|
if (device == null) return false;
|
|
|
|
|
|
|
|
|
|
// Filter out virtual/built-in devices which can falsely report DPAD sources
|
|
|
|
|
try {
|
|
|
|
|
if (device.isVirtual()) return false;
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
|
|
|
|
|
// Heuristic: devices with both vendor and product = 0 are often virtual
|
|
|
|
|
try {
|
|
|
|
|
if (device.getVendorId() == 0 && device.getProductId() == 0) return false;
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
|
|
|
|
|
// Filter by name for common virtual/built-in inputs
|
|
|
|
|
String name = device.getName();
|
|
|
|
|
if (name != null) {
|
2025-08-30 04:59:16 -04:00
|
|
|
String lower = name.toLowerCase(Locale.ROOT);
|
2025-08-29 12:26:17 -04:00
|
|
|
if (lower.contains("virtual") || lower.contains("uinput") || lower.contains("touch")
|
|
|
|
|
|| lower.contains("keyboard") || lower.contains("keypad") || lower.contains("gpio")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int sources = device.getSources();
|
|
|
|
|
boolean gamepad = (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
|
|
|
|
|
boolean joystick = (sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
|
|
|
|
boolean dpad = (sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD;
|
|
|
|
|
|
|
|
|
|
// Consider DPAD-only devices as controllers only if they are external (heuristic above)
|
|
|
|
|
return gamepad || joystick || dpad;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isAnyControllerConnected() {
|
|
|
|
|
try {
|
|
|
|
|
int[] ids = InputDevice.getDeviceIds();
|
|
|
|
|
for (int id : ids) {
|
|
|
|
|
InputDevice dev = InputDevice.getDevice(id);
|
|
|
|
|
if (isControllerDevice(dev)) return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateUiForControllerPresence() {
|
|
|
|
|
boolean hasController = isAnyControllerConnected();
|
|
|
|
|
// Show all UI when no controller; hide UI when a controller is connected.
|
|
|
|
|
int vis = hasController ? View.GONE : View.VISIBLE;
|
|
|
|
|
|
|
|
|
|
View btnSettings = findViewById(R.id.btn_settings);
|
|
|
|
|
View llQuickActions = findViewById(R.id.ll_quick_actions);
|
|
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// Keep menu (settings) button visible even when a controller is connected
|
|
|
|
|
if (btnSettings != null) btnSettings.setVisibility(View.VISIBLE);
|
2025-08-29 12:26:17 -04:00
|
|
|
if (llQuickActions != null) llQuickActions.setVisibility(vis);
|
|
|
|
|
|
|
|
|
|
// Hide on-screen touch controls when a physical controller is connected
|
|
|
|
|
setControlsVisible(!hasController);
|
2025-09-11 23:14:41 -04:00
|
|
|
|
|
|
|
|
// Show controller hint when no controller is connected (only once)
|
|
|
|
|
if (!hasController) {
|
|
|
|
|
showControllerHintIfNeeded();
|
|
|
|
|
}
|
2025-08-29 12:26:17 -04:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 08:11:50 -04:00
|
|
|
private int getCurrentRendererPref() {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
2025-08-30 20:39:56 -04:00
|
|
|
// Default to -1 (Automatic) if not set
|
|
|
|
|
return prefs.getInt("renderer", -1);
|
2025-08-28 08:11:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String rendererShortLabel(int r) {
|
2025-08-30 20:39:56 -04:00
|
|
|
if (r == -1) return "AUTO";
|
2025-08-28 08:11:50 -04:00
|
|
|
if (r == 12) return "OGL";
|
|
|
|
|
if (r == 13) return "SW";
|
|
|
|
|
return "VK"; // 14
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
|
2025-08-28 08:11:50 -04:00
|
|
|
private void updateRendererButtonLabel() {
|
2025-09-10 09:24:01 -04:00
|
|
|
// No-op: renderer label now handled in drawer
|
|
|
|
|
}
|
2025-08-28 08:11:50 -04:00
|
|
|
|
2025-09-10 09:24:01 -04:00
|
|
|
// Public minimal UI refresh hook for dialogs
|
2025-09-02 10:36:29 -04:00
|
|
|
public void refreshQuickUi() {
|
|
|
|
|
updateRendererButtonLabel();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 08:11:50 -04:00
|
|
|
private void setRendererAndSave(int renderer) {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
2025-08-30 04:59:16 -04:00
|
|
|
prefs.edit().putInt("renderer", renderer).apply();
|
2025-08-28 08:11:50 -04:00
|
|
|
// Apply live if possible; this path has proven stable via top bar buttons
|
|
|
|
|
try {
|
|
|
|
|
NativeApp.renderGpu(renderer);
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
android.util.Log.e("MainActivity", "Renderer switch failed: " + t.getMessage());
|
|
|
|
|
}
|
|
|
|
|
updateRendererButtonLabel();
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void tintAllMaterialButtonOutlines() {
|
2025-08-24 09:53:35 -04:00
|
|
|
// Ripple + specific color tweaks, no strokes (transparent container style)
|
2025-08-22 04:11:11 -04:00
|
|
|
View root = findViewById(android.R.id.content);
|
|
|
|
|
if (root instanceof ViewGroup) {
|
2025-08-24 09:53:35 -04:00
|
|
|
traverseAndTintButtons((ViewGroup) root);
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 09:53:35 -04:00
|
|
|
private void traverseAndTintButtons(ViewGroup group) {
|
2025-08-22 04:11:11 -04:00
|
|
|
for (int i = 0; i < group.getChildCount(); i++) {
|
|
|
|
|
View child = group.getChildAt(i);
|
|
|
|
|
if (child instanceof ViewGroup) {
|
2025-08-24 09:53:35 -04:00
|
|
|
traverseAndTintButtons((ViewGroup) child);
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
if (child instanceof MaterialButton) {
|
|
|
|
|
MaterialButton mb = (MaterialButton) child;
|
|
|
|
|
// Ensure ripple matches brand globally
|
|
|
|
|
mb.setRippleColor(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_ripple)));
|
|
|
|
|
int id = mb.getId();
|
2025-08-24 09:53:35 -04:00
|
|
|
if (id == R.id.btn_pad_y) { // Triangle = green
|
2025-08-22 04:11:11 -04:00
|
|
|
final int base = ContextCompat.getColor(this, R.color.ps2_triangle_green);
|
|
|
|
|
ColorStateList stateful = pressedColorStateList(base);
|
|
|
|
|
mb.setTextColor(stateful);
|
|
|
|
|
} else if (id == R.id.btn_pad_b) { // Circle = red
|
|
|
|
|
final int base = ContextCompat.getColor(this, R.color.ps2_circle_red);
|
|
|
|
|
ColorStateList stateful = pressedColorStateList(base);
|
|
|
|
|
mb.setTextColor(stateful);
|
|
|
|
|
} else if (id == R.id.btn_pad_a) { // Cross = blue
|
|
|
|
|
final int base = ContextCompat.getColor(this, R.color.ps2_cross_blue);
|
|
|
|
|
ColorStateList stateful = pressedColorStateList(base);
|
|
|
|
|
mb.setTextColor(stateful);
|
|
|
|
|
} else if (id == R.id.btn_pad_x) { // Square = pink
|
|
|
|
|
final int base = ContextCompat.getColor(this, R.color.ps2_square_pink);
|
|
|
|
|
ColorStateList stateful = pressedColorStateList(base);
|
|
|
|
|
mb.setTextColor(stateful);
|
|
|
|
|
} else if (id == R.id.btn_pad_dir_top || id == R.id.btn_pad_dir_left || id == R.id.btn_pad_dir_right || id == R.id.btn_pad_dir_bottom) {
|
|
|
|
|
// D-pad arrow icon tint to brand accent
|
|
|
|
|
ColorStateList iconTint = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_accent));
|
|
|
|
|
mb.setIconTint(iconTint);
|
|
|
|
|
}
|
2025-08-24 09:53:35 -04:00
|
|
|
// No stroke; background is transparent per style
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 08:11:50 -04:00
|
|
|
// --- BIOS presence check and prompt ---
|
|
|
|
|
private boolean hasAnyBiosFiles(File dir) {
|
|
|
|
|
if (dir == null || !dir.exists()) return false;
|
|
|
|
|
File[] files = dir.listFiles();
|
|
|
|
|
if (files == null) return false;
|
|
|
|
|
for (File f : files) {
|
|
|
|
|
if (f == null) continue;
|
|
|
|
|
if (f.isDirectory()) {
|
|
|
|
|
if (hasAnyBiosFiles(f)) return true;
|
|
|
|
|
} else {
|
|
|
|
|
String name = f.getName();
|
|
|
|
|
// Common PCSX2 BIOS components: SCPH*.bin (main), and component ROMs ROM0/ROM1/ROM2/EROM
|
|
|
|
|
if (name != null) {
|
2025-08-30 04:59:16 -04:00
|
|
|
String lower = name.toLowerCase(Locale.ROOT);
|
2025-08-28 08:11:50 -04:00
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
if (isMainBios) {
|
|
|
|
|
if (f.length() >= 256 * 1024) return true; // main BIOS typically >= 2MB, but allow 256KB+
|
|
|
|
|
} else if (isComponentSuffix || isBareComponent) {
|
|
|
|
|
if (f.length() >= 64 * 1024) return true; // component ROMs can be smaller
|
|
|
|
|
} else if (lower.endsWith(".bin") || lower.endsWith(".rom")) {
|
|
|
|
|
// Fallback: any .bin/.rom over 256KB
|
|
|
|
|
if (f.length() >= 256 * 1024) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void maybePromptForBios() {
|
2025-09-06 02:07:48 -04:00
|
|
|
// Temporarily disable automatic BIOS prompt
|
|
|
|
|
if (!getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean("bios_auto_prompt_enabled", false))
|
|
|
|
|
return;
|
2025-08-28 08:11:50 -04:00
|
|
|
File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
|
|
|
|
|
if (hasAnyBiosFiles(biosDir)) return;
|
|
|
|
|
showBiosPrompt();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean ensureBiosOrPrompt() {
|
2025-09-06 02:07:48 -04:00
|
|
|
// Temporarily disable automatic BIOS prompting; wizard handles manual import
|
|
|
|
|
return true;
|
2025-08-28 08:11:50 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
public void showBiosPrompt() {
|
2025-08-28 08:11:50 -04:00
|
|
|
if (mBiosPromptDialog != null && mBiosPromptDialog.isShowing()) return;
|
|
|
|
|
mBiosPromptDialog = new com.google.android.material.dialog.MaterialAlertDialogBuilder(this,
|
|
|
|
|
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
2025-09-06 02:07:48 -04:00
|
|
|
.setCustomTitle(UiUtils.centeredDialogTitle(this, "BIOS Required"))
|
2025-08-28 08:11:50 -04:00
|
|
|
.setMessage("No PS2 BIOS detected. Import your BIOS files to run games.\n\nHint: Press Select+Start for Quick Actions.")
|
|
|
|
|
.setNegativeButton("Later", (d, w) -> { /* leave dialog dismiss */ })
|
|
|
|
|
.setPositiveButton("Pick Files", (d, w) -> {
|
|
|
|
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
|
|
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
|
|
|
|
intent.setType("*/*");
|
|
|
|
|
startActivityResultBiosPick.launch(intent);
|
|
|
|
|
})
|
|
|
|
|
.setNeutralButton("Pick Folder", (d, w) -> {
|
|
|
|
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
|
|
|
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
|
|
|
|
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
|
|
|
|
startActivityResultBiosFolderPick.launch(intent);
|
|
|
|
|
})
|
|
|
|
|
.create();
|
|
|
|
|
mBiosPromptDialog.setOnDismissListener(dialog -> mBiosPromptDialog = null);
|
|
|
|
|
mBiosPromptDialog.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void dismissBiosPromptIfResolved() {
|
|
|
|
|
File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
|
|
|
|
|
if (hasAnyBiosFiles(biosDir)) {
|
|
|
|
|
if (mBiosPromptDialog != null && mBiosPromptDialog.isShowing()) {
|
|
|
|
|
try { mBiosPromptDialog.dismiss(); } catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
private ColorStateList pressedColorStateList(int base) {
|
|
|
|
|
int pressed = darkenColor(base, 0.85f); // 15% darker when pressed
|
|
|
|
|
int[][] states = new int[][]{
|
|
|
|
|
new int[]{android.R.attr.state_pressed},
|
|
|
|
|
new int[]{}
|
|
|
|
|
};
|
|
|
|
|
int[] colors = new int[]{
|
|
|
|
|
pressed,
|
|
|
|
|
base
|
|
|
|
|
};
|
|
|
|
|
return new ColorStateList(states, colors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int darkenColor(int color, float factor) {
|
|
|
|
|
int a = (color >> 24) & 0xFF;
|
|
|
|
|
int r = (color >> 16) & 0xFF;
|
|
|
|
|
int g = (color >> 8) & 0xFF;
|
|
|
|
|
int b = color & 0xFF;
|
|
|
|
|
r = Math.max(0, Math.min(255, (int)(r * factor)));
|
|
|
|
|
g = Math.max(0, Math.min(255, (int)(g * factor)));
|
|
|
|
|
b = Math.max(0, Math.min(255, (int)(b * factor)));
|
|
|
|
|
return (a << 24) | (r << 16) | (g << 8) | b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void applySavedSettings() {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
2025-08-22 08:04:47 -04:00
|
|
|
// Renderer constants (match SettingsDialogFragment)
|
|
|
|
|
final int RENDERER_OPENGL = 12;
|
|
|
|
|
final int RENDERER_SOFTWARE = 13;
|
|
|
|
|
final int RENDERER_VULKAN = 14;
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// Default to Automatic (-1) so the core can select a compatible renderer on older devices
|
|
|
|
|
int renderer = prefs.getInt("renderer", -1);
|
2025-08-22 04:11:11 -04:00
|
|
|
NativeApp.renderGpu(renderer);
|
|
|
|
|
|
|
|
|
|
// Resolution scale multiplier (float), default 1.0
|
|
|
|
|
float scale = prefs.getFloat("upscale_multiplier", 1.0f);
|
|
|
|
|
NativeApp.renderUpscalemultiplier(scale);
|
|
|
|
|
|
|
|
|
|
// Aspect ratio: 0=Stretch, 1=Auto 4:3/3:2, 2=4:3, 3=16:9, 4=10:7
|
|
|
|
|
int aspectRatio = prefs.getInt("aspect_ratio", 1); // Default to Auto 4:3/3:2
|
|
|
|
|
NativeApp.setAspectRatio(aspectRatio);
|
|
|
|
|
|
|
|
|
|
// Widescreen patches
|
2025-09-02 09:21:48 -04:00
|
|
|
boolean widescreenPatches = prefs.getBoolean("widescreen_patches", true);
|
2025-08-22 04:11:11 -04:00
|
|
|
NativeApp.setWidescreenPatches(widescreenPatches);
|
|
|
|
|
|
|
|
|
|
// No interlacing patches
|
2025-09-02 09:21:48 -04:00
|
|
|
boolean noInterlacingPatches = prefs.getBoolean("no_interlacing_patches", true);
|
2025-08-22 04:11:11 -04:00
|
|
|
NativeApp.setNoInterlacingPatches(noInterlacingPatches);
|
|
|
|
|
|
2025-08-22 08:04:47 -04:00
|
|
|
// Texture loading options
|
|
|
|
|
boolean loadTextures = prefs.getBoolean("load_textures", false);
|
|
|
|
|
NativeApp.setLoadTextures(loadTextures);
|
|
|
|
|
|
|
|
|
|
boolean asyncTextureLoading = prefs.getBoolean("async_texture_loading", true);
|
|
|
|
|
NativeApp.setAsyncTextureLoading(asyncTextureLoading);
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
// HUD visibility
|
|
|
|
|
boolean hudVisible = prefs.getBoolean("hud_visible", false);
|
|
|
|
|
NativeApp.setHudVisible(hudVisible);
|
2025-08-22 08:04:47 -04:00
|
|
|
|
|
|
|
|
// Set brighter default brightness (60 instead of 50)
|
|
|
|
|
NativeApp.setShadeBoost(true);
|
|
|
|
|
NativeApp.setShadeBoostBrightness(60);
|
|
|
|
|
NativeApp.setShadeBoostContrast(50);
|
|
|
|
|
NativeApp.setShadeBoostSaturation(50);
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public final ActivityResultLauncher<Intent> startActivityResultLocalFilePlay = registerForActivityResult(
|
|
|
|
|
new ActivityResultContracts.StartActivityForResult(),
|
|
|
|
|
result -> {
|
|
|
|
|
if(result.getResultCode() == Activity.RESULT_OK) {
|
|
|
|
|
try {
|
|
|
|
|
Intent _intent = result.getData();
|
|
|
|
|
if(_intent != null) {
|
|
|
|
|
m_szGamefile = _intent.getDataString();
|
|
|
|
|
if(!TextUtils.isEmpty(m_szGamefile)) {
|
2025-09-01 15:36:45 -04:00
|
|
|
try {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
prefs.edit().putLong("last_played:" + m_szGamefile, System.currentTimeMillis()).apply();
|
|
|
|
|
} catch (Throwable ignored) {}
|
2025-08-22 04:11:11 -04:00
|
|
|
restartEmuThread();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
public final ActivityResultLauncher<Intent> startActivityResultBiosPick = registerForActivityResult(
|
|
|
|
|
new ActivityResultContracts.StartActivityForResult(),
|
|
|
|
|
result -> {
|
|
|
|
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
|
|
|
try {
|
|
|
|
|
Intent data = result.getData();
|
|
|
|
|
if (data != null) {
|
|
|
|
|
File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
|
|
|
|
|
if (!biosDir.exists()) biosDir.mkdirs();
|
|
|
|
|
|
|
|
|
|
ClipData clipData = data.getClipData();
|
|
|
|
|
if (clipData != null && clipData.getItemCount() > 0) {
|
|
|
|
|
for (int i = 0; i < clipData.getItemCount(); i++) {
|
|
|
|
|
Uri uri = clipData.getItemAt(i).getUri();
|
|
|
|
|
importSingleBiosUri(uri, biosDir);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Uri uri = data.getData();
|
|
|
|
|
if (uri != null) {
|
|
|
|
|
importSingleBiosUri(uri, biosDir);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-28 08:11:50 -04:00
|
|
|
// If BIOS now present, dismiss the prompt
|
|
|
|
|
dismissBiosPromptIfResolved();
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
public final ActivityResultLauncher<Intent> startActivityResultBiosFolderPick = registerForActivityResult(
|
|
|
|
|
new ActivityResultContracts.StartActivityForResult(),
|
|
|
|
|
result -> {
|
|
|
|
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
|
|
|
try {
|
|
|
|
|
Intent data = result.getData();
|
|
|
|
|
if (data != null) {
|
|
|
|
|
Uri treeUri = data.getData();
|
|
|
|
|
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));
|
2025-08-30 04:59:16 -04:00
|
|
|
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);
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
|
|
|
|
|
if (pickedDir != null && pickedDir.isDirectory()) {
|
|
|
|
|
File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
|
|
|
|
|
if (!biosDir.exists()) biosDir.mkdirs();
|
|
|
|
|
copyDocumentTreeToDirectory(pickedDir, biosDir);
|
2025-08-28 08:11:50 -04:00
|
|
|
// If BIOS now present, dismiss the prompt
|
|
|
|
|
dismissBiosPromptIfResolved();
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
public final ActivityResultLauncher<Intent> startActivityResultGamesFolderPick = registerForActivityResult(
|
|
|
|
|
new ActivityResultContracts.StartActivityForResult(),
|
|
|
|
|
result -> {
|
|
|
|
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
|
|
|
try {
|
|
|
|
|
Intent data = result.getData();
|
|
|
|
|
if (data != null) {
|
|
|
|
|
Uri treeUri = data.getData();
|
|
|
|
|
if (treeUri != null) {
|
|
|
|
|
final int takeFlags = (data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
|
2025-08-30 04:59:16 -04:00
|
|
|
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) {}
|
|
|
|
|
}
|
2025-09-06 02:07:48 -04:00
|
|
|
// Save folder and optionally show games
|
2025-08-22 04:11:11 -04:00
|
|
|
getSharedPreferences("app_prefs", MODE_PRIVATE)
|
|
|
|
|
.edit()
|
|
|
|
|
.putString("games_folder_uri", treeUri.toString())
|
|
|
|
|
.apply();
|
2025-09-06 02:07:48 -04:00
|
|
|
if (!mSetupWizardActive) {
|
|
|
|
|
showGamesListOrReselect(treeUri);
|
|
|
|
|
} else {
|
|
|
|
|
Toast.makeText(this, "Games folder set", Toast.LENGTH_SHORT).show();
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// SAF data-root picker result
|
|
|
|
|
public final ActivityResultLauncher<Intent> startActivityResultDataRootPick = registerForActivityResult(
|
|
|
|
|
new ActivityResultContracts.StartActivityForResult(),
|
|
|
|
|
result -> {
|
|
|
|
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
|
|
|
try {
|
|
|
|
|
Intent data = result.getData();
|
|
|
|
|
if (data != null) {
|
|
|
|
|
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) {}
|
|
|
|
|
SafManager.setDataRootUri(this, treeUri);
|
|
|
|
|
// Seed default resources to SAF data root
|
|
|
|
|
copyAssetsToSafDataRoot();
|
|
|
|
|
Toast.makeText(this, "Data folder set", Toast.LENGTH_SHORT).show();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Copies assets/resources under the selected SAF data root (resources/..)
|
|
|
|
|
private void copyAssetsToSafDataRoot() {
|
|
|
|
|
Uri root = SafManager.getDataRootUri(this);
|
|
|
|
|
if (root == null) return;
|
|
|
|
|
// Only seed once
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
if (prefs.getBoolean("saf_resources_seeded", false)) return;
|
|
|
|
|
// Flatten copy of assets/resources directory to SAF
|
|
|
|
|
try {
|
|
|
|
|
copyAssetAllToSaf(getApplicationContext(), "resources");
|
|
|
|
|
prefs.edit().putBoolean("saf_resources_seeded", true).apply();
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
private void importSingleBiosUri(Uri uri, File biosDir) {
|
|
|
|
|
if (uri == null) return;
|
|
|
|
|
String displayName = getDisplayNameFromUri(this, uri);
|
|
|
|
|
if (TextUtils.isEmpty(displayName)) displayName = "bios.bin";
|
|
|
|
|
File outFile = new File(biosDir, displayName);
|
|
|
|
|
copyUriToFile(this, uri, outFile);
|
2025-09-06 02:07:48 -04:00
|
|
|
// Also mirror to SAF data root if set
|
|
|
|
|
android.net.Uri dataRoot = SafManager.getDataRootUri(this);
|
|
|
|
|
if (dataRoot != null) {
|
|
|
|
|
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(this, new String[]{"bios"}, displayName, "application/octet-stream");
|
|
|
|
|
if (target != null) {
|
|
|
|
|
try (java.io.InputStream in = getContentResolver().openInputStream(uri)) {
|
|
|
|
|
SafManager.copyFromStream(this, in, target.getUri());
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void copyDocumentTreeToDirectory(DocumentFile dir, File outDir) {
|
|
|
|
|
if (dir == null || !dir.isDirectory()) return;
|
|
|
|
|
DocumentFile[] children = dir.listFiles();
|
|
|
|
|
if (children == null) return;
|
|
|
|
|
for (DocumentFile child : children) {
|
|
|
|
|
if (child == null) continue;
|
|
|
|
|
if (child.isDirectory()) {
|
|
|
|
|
File sub = new File(outDir, child.getName() != null ? child.getName() : "folder");
|
|
|
|
|
if (!sub.exists()) sub.mkdirs();
|
|
|
|
|
copyDocumentTreeToDirectory(child, sub);
|
|
|
|
|
} else if (child.isFile()) {
|
|
|
|
|
String name = child.getName();
|
|
|
|
|
if (TextUtils.isEmpty(name)) name = "bios.bin";
|
|
|
|
|
File dest = new File(outDir, name);
|
|
|
|
|
copyUriToFile(this, child.getUri(), dest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String getDisplayNameFromUri(Context context, Uri uri) {
|
|
|
|
|
String name = null;
|
|
|
|
|
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
|
|
|
|
if (cursor != null) {
|
|
|
|
|
try {
|
|
|
|
|
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
|
|
|
|
if (nameIndex >= 0 && cursor.moveToFirst()) {
|
|
|
|
|
name = cursor.getString(nameIndex);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
cursor.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (TextUtils.isEmpty(name)) {
|
|
|
|
|
name = uri.getLastPathSegment();
|
|
|
|
|
}
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static boolean copyUriToFile(Context context, Uri uri, File destFile) {
|
|
|
|
|
InputStream is = null;
|
|
|
|
|
FileOutputStream os = null;
|
|
|
|
|
try {
|
|
|
|
|
is = context.getContentResolver().openInputStream(uri);
|
|
|
|
|
if (is == null) return false;
|
|
|
|
|
os = new FileOutputStream(destFile);
|
|
|
|
|
byte[] buffer = new byte[8192];
|
|
|
|
|
int read;
|
|
|
|
|
while ((read = is.read(buffer)) != -1) {
|
|
|
|
|
os.write(buffer, 0, read);
|
|
|
|
|
}
|
|
|
|
|
os.flush();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
try { if (is != null) is.close(); } catch (Exception ignored) {}
|
|
|
|
|
try { if (os != null) os.close(); } catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onPause() {
|
|
|
|
|
NativeApp.pause();
|
|
|
|
|
super.onPause();
|
|
|
|
|
////
|
|
|
|
|
if (mHIDDeviceManager != null) {
|
|
|
|
|
mHIDDeviceManager.setFrozen(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onResume() {
|
|
|
|
|
NativeApp.resume();
|
|
|
|
|
super.onResume();
|
|
|
|
|
////
|
|
|
|
|
if (mHIDDeviceManager != null) {
|
|
|
|
|
mHIDDeviceManager.setFrozen(false);
|
|
|
|
|
}
|
|
|
|
|
// Re-assert full screen when returning to the activity
|
2025-09-01 15:36:45 -04:00
|
|
|
enableImmersiveMode();
|
2025-08-29 12:26:17 -04:00
|
|
|
updateUiForControllerPresence();
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onDestroy() {
|
|
|
|
|
NativeApp.shutdown();
|
|
|
|
|
super.onDestroy();
|
|
|
|
|
////
|
|
|
|
|
if (mHIDDeviceManager != null) {
|
|
|
|
|
HIDDeviceManager.release(mHIDDeviceManager);
|
|
|
|
|
mHIDDeviceManager = null;
|
|
|
|
|
}
|
2025-08-29 12:26:17 -04:00
|
|
|
if (mInputManager != null) {
|
|
|
|
|
try { mInputManager.unregisterInputDeviceListener(mInputDeviceListener); } catch (Exception ignored) {}
|
|
|
|
|
mInputManager = null;
|
|
|
|
|
}
|
2025-08-22 04:11:11 -04:00
|
|
|
////
|
|
|
|
|
if (mEmulationThread != null) {
|
|
|
|
|
try {
|
|
|
|
|
mEmulationThread.join();
|
|
|
|
|
mEmulationThread = null;
|
|
|
|
|
}
|
|
|
|
|
catch (InterruptedException ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int appPid = android.os.Process.myPid();
|
|
|
|
|
android.os.Process.killProcess(appPid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Initialize() {
|
|
|
|
|
NativeApp.initializeOnce(getApplicationContext());
|
|
|
|
|
|
|
|
|
|
// Set up JNI
|
|
|
|
|
SDLControllerManager.nativeSetupJNI();
|
|
|
|
|
|
|
|
|
|
// Initialize state
|
|
|
|
|
SDLControllerManager.initialize();
|
|
|
|
|
|
|
|
|
|
// Load and apply saved settings
|
2025-09-11 23:14:41 -04:00
|
|
|
loadAndApplyStoredSettings();
|
2025-08-22 04:11:11 -04:00
|
|
|
|
|
|
|
|
mHIDDeviceManager = HIDDeviceManager.acquire(this);
|
2025-08-25 13:56:13 -04:00
|
|
|
// Initialize HID device manager for USB and Bluetooth controllers
|
|
|
|
|
mHIDDeviceManager.initialize(true, true);
|
2025-08-22 08:04:47 -04:00
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void setSurfaceView(Object p_value) {
|
|
|
|
|
FrameLayout fl_board = findViewById(R.id.fl_board);
|
|
|
|
|
if(fl_board != null) {
|
|
|
|
|
if(fl_board.getChildCount() > 0) {
|
|
|
|
|
fl_board.removeAllViews();
|
|
|
|
|
}
|
|
|
|
|
////
|
|
|
|
|
if(p_value instanceof SDLSurface) {
|
|
|
|
|
fl_board.addView((SDLSurface)p_value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void startEmuThread() {
|
2025-08-28 08:11:50 -04:00
|
|
|
// Ensure BIOS present before starting emulation
|
|
|
|
|
if (!ensureBiosOrPrompt()) return;
|
2025-08-22 04:11:11 -04:00
|
|
|
if(!isThread()) {
|
|
|
|
|
mEmulationThread = new Thread(() -> NativeApp.runVMThread(m_szGamefile));
|
|
|
|
|
mEmulationThread.start();
|
2025-09-11 23:14:41 -04:00
|
|
|
// Show pause button when game starts (game is always running initially)
|
|
|
|
|
runOnUiThread(() -> {
|
|
|
|
|
MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play);
|
|
|
|
|
if (btn_pause_play != null) {
|
|
|
|
|
btn_pause_play.setVisibility(View.VISIBLE);
|
|
|
|
|
btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.pause_circle_24px));
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void restartEmuThread() {
|
2025-08-28 08:11:50 -04:00
|
|
|
// Ensure BIOS present before starting/restarting emulation
|
|
|
|
|
if (!ensureBiosOrPrompt()) return;
|
2025-08-22 04:11:11 -04:00
|
|
|
NativeApp.shutdown();
|
|
|
|
|
if (mEmulationThread != null) {
|
|
|
|
|
try {
|
|
|
|
|
mEmulationThread.join();
|
|
|
|
|
mEmulationThread = null;
|
|
|
|
|
}
|
|
|
|
|
catch (InterruptedException ignored) {}
|
|
|
|
|
}
|
2025-08-22 08:04:47 -04:00
|
|
|
|
|
|
|
|
// Apply global renderer setting before starting new game
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
2025-09-06 02:07:48 -04:00
|
|
|
// Default to Automatic (-1) so the core can pick the best available backend (Vulkan/OpenGL/Software)
|
|
|
|
|
int renderer = prefs.getInt("renderer", -1);
|
2025-08-22 08:04:47 -04:00
|
|
|
android.util.Log.d("MainActivity", "Applying global renderer before game restart: " + renderer);
|
|
|
|
|
NativeApp.renderGpu(renderer);
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
////
|
|
|
|
|
startEmuThread();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// (Renderer toast temporarily removed per user request — AUTO behavior retained.)
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
// Public API for UI components to reboot the emulator
|
|
|
|
|
public void rebootEmu() {
|
|
|
|
|
if (!TextUtils.isEmpty(m_szGamefile)) {
|
|
|
|
|
restartEmuThread();
|
|
|
|
|
} else {
|
|
|
|
|
// No game loaded; just shutdown for safety
|
|
|
|
|
NativeApp.shutdown();
|
2025-09-11 23:14:41 -04:00
|
|
|
updatePausePlayButton();
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
@Override
|
|
|
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
2025-08-25 13:56:13 -04:00
|
|
|
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
|
|
|
|
if (mControllerInputHandler != null && mControllerInputHandler.handleMotionEvent(event)) {
|
2025-08-22 04:11:11 -04:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
|
|
|
|
|
// Skip SDL controller manager to avoid mapping conflicts
|
2025-08-22 04:11:11 -04:00
|
|
|
return super.onGenericMotionEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 04:09:10 -04:00
|
|
|
@Override
|
|
|
|
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
2025-08-29 12:26:17 -04:00
|
|
|
// Allow dialogs to receive DPAD navigation when visible
|
|
|
|
|
if (!isAnyAppDialogShowing()) {
|
|
|
|
|
// Intercept controller DPAD/gamepad keys before focused views consume them
|
|
|
|
|
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(event)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-08-28 04:09:10 -04:00
|
|
|
}
|
|
|
|
|
return super.dispatchKeyEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-29 12:26:17 -04:00
|
|
|
private boolean isAnyAppDialogShowing() {
|
|
|
|
|
try {
|
|
|
|
|
java.util.List<androidx.fragment.app.Fragment> frags = getSupportFragmentManager().getFragments();
|
|
|
|
|
for (androidx.fragment.app.Fragment f : frags) {
|
|
|
|
|
if (f instanceof androidx.fragment.app.DialogFragment) {
|
|
|
|
|
android.app.Dialog d = ((androidx.fragment.app.DialogFragment) f).getDialog();
|
|
|
|
|
if (d != null && d.isShowing()) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
@Override
|
|
|
|
|
public boolean onKeyDown(int p_keyCode, KeyEvent p_event) {
|
2025-08-25 13:56:13 -04:00
|
|
|
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
|
|
|
|
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
|
|
|
|
|
return true;
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
|
|
|
|
|
// Handle back button for non-gamepad sources
|
|
|
|
|
if (p_keyCode == KeyEvent.KEYCODE_BACK) {
|
|
|
|
|
showExitDialog();
|
|
|
|
|
return true;
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
|
|
|
|
|
// Skip SDL controller manager to avoid mapping conflicts
|
2025-08-22 04:11:11 -04:00
|
|
|
return super.onKeyDown(p_keyCode, p_event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onKeyUp(int p_keyCode, KeyEvent p_event) {
|
2025-08-25 13:56:13 -04:00
|
|
|
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
|
|
|
|
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
|
|
|
|
|
return true;
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
|
|
|
|
|
// Skip SDL controller manager to avoid mapping conflicts
|
2025-08-22 04:11:11 -04:00
|
|
|
return super.onKeyUp(p_keyCode, p_event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void sendKeyAction(View p_view, int p_action, int p_keycode) {
|
|
|
|
|
if(p_action == MotionEvent.ACTION_DOWN) {
|
|
|
|
|
p_view.setPressed(true);
|
|
|
|
|
int pad_force = 0;
|
|
|
|
|
if(p_keycode >= 110) {
|
|
|
|
|
float _abs = 90; // Joystic test value
|
|
|
|
|
_abs = Math.min(_abs, 100);
|
|
|
|
|
pad_force = (int) (_abs * 32766.0f / 100);
|
|
|
|
|
}
|
|
|
|
|
NativeApp.setPadButton(p_keycode, pad_force, true);
|
|
|
|
|
} else if(p_action == MotionEvent.ACTION_UP || p_action == MotionEvent.ACTION_CANCEL) {
|
|
|
|
|
p_view.setPressed(false);
|
|
|
|
|
NativeApp.setPadButton(p_keycode, 0, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Asset copy helpers used on first launch to seed default resources
|
|
|
|
|
private void copyAssetAll(Context context, String srcPath) {
|
|
|
|
|
AssetManager assetMgr = context.getAssets();
|
|
|
|
|
try {
|
|
|
|
|
String[] assets = assetMgr.list(srcPath);
|
|
|
|
|
String destPath = context.getExternalFilesDir(null) + File.separator + srcPath;
|
|
|
|
|
if (assets != null) {
|
|
|
|
|
if (assets.length == 0) {
|
|
|
|
|
copyFile(context, srcPath, destPath);
|
|
|
|
|
} else {
|
|
|
|
|
File dir = new File(destPath);
|
|
|
|
|
if (!dir.exists()) dir.mkdirs();
|
|
|
|
|
for (String element : assets) {
|
|
|
|
|
copyAssetAll(context, srcPath + File.separator + element);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (IOException ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 02:07:48 -04:00
|
|
|
// Mirror asset folder into SAF data root (if set)
|
|
|
|
|
private void copyAssetAllToSaf(Context context, String srcPath) {
|
|
|
|
|
Uri dataRoot = SafManager.getDataRootUri(context);
|
|
|
|
|
if (dataRoot == null) return;
|
|
|
|
|
AssetManager assetMgr = context.getAssets();
|
|
|
|
|
try {
|
|
|
|
|
String[] assets = assetMgr.list(srcPath);
|
|
|
|
|
if (assets != null) {
|
|
|
|
|
if (assets.length == 0) {
|
|
|
|
|
// It's a file under srcPath; create it in SAF
|
|
|
|
|
String[] parts = srcPath.split("/");
|
|
|
|
|
String filename = parts.length > 0 ? parts[parts.length - 1] : srcPath;
|
|
|
|
|
String[] dirSegs = parts.length > 1 ? java.util.Arrays.copyOf(parts, parts.length - 1) : new String[0];
|
|
|
|
|
DocumentFile existing = SafManager.getChild(context, dirSegs, filename);
|
|
|
|
|
if (existing != null && existing.length() > 0) return;
|
|
|
|
|
DocumentFile target = SafManager.createChild(context, dirSegs, filename, guessMime(filename));
|
|
|
|
|
if (target != null) {
|
|
|
|
|
try (InputStream is = assetMgr.open(srcPath)) {
|
|
|
|
|
SafManager.copyFromStream(context, is, target.getUri());
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (String element : assets) {
|
|
|
|
|
copyAssetAllToSaf(context, srcPath + File.separator + element);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (IOException ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String guessMime(String filename) {
|
|
|
|
|
String lower = filename.toLowerCase(java.util.Locale.ROOT);
|
|
|
|
|
if (lower.endsWith(".png")) return "image/png";
|
|
|
|
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
|
|
|
if (lower.endsWith(".yaml") || lower.endsWith(".yml")) return "application/x-yaml";
|
|
|
|
|
if (lower.endsWith(".zip")) return "application/zip";
|
|
|
|
|
if (lower.endsWith(".txt")) return "text/plain";
|
|
|
|
|
return "application/octet-stream";
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
private static void copyFile(Context context, String srcFile, String destFile) {
|
|
|
|
|
AssetManager assetMgr = context.getAssets();
|
|
|
|
|
InputStream is = null;
|
|
|
|
|
FileOutputStream os = null;
|
|
|
|
|
try {
|
|
|
|
|
is = assetMgr.open(srcFile);
|
|
|
|
|
boolean exists = new File(destFile).exists();
|
|
|
|
|
if (srcFile.contains("shaders")) {
|
|
|
|
|
exists = false; // always refresh shaders
|
|
|
|
|
}
|
|
|
|
|
if (!exists) {
|
|
|
|
|
File parent = new File(destFile).getParentFile();
|
|
|
|
|
if (parent != null && !parent.exists()) parent.mkdirs();
|
|
|
|
|
os = new FileOutputStream(destFile);
|
|
|
|
|
byte[] buffer = new byte[4096];
|
|
|
|
|
int read;
|
|
|
|
|
while ((read = is.read(buffer)) != -1) {
|
|
|
|
|
os.write(buffer, 0, read);
|
|
|
|
|
}
|
|
|
|
|
os.flush();
|
|
|
|
|
}
|
|
|
|
|
} catch (IOException ignored) {
|
|
|
|
|
} finally {
|
|
|
|
|
try { if (is != null) is.close(); } catch (IOException ignored) {}
|
|
|
|
|
try { if (os != null) os.close(); } catch (IOException ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onBackPressed() {
|
|
|
|
|
// Fallback for older Android versions
|
2025-08-30 04:59:16 -04:00
|
|
|
super.onBackPressed();
|
2025-08-22 04:11:11 -04:00
|
|
|
showExitDialog();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void showExitDialog() {
|
2025-08-25 07:58:09 -04:00
|
|
|
new MaterialAlertDialogBuilder(this,
|
|
|
|
|
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
2025-09-06 02:07:48 -04:00
|
|
|
.setCustomTitle(UiUtils.centeredDialogTitle(this, "Exit App"))
|
2025-08-22 04:11:11 -04:00
|
|
|
.setMessage("Do you want to exit PSX2?")
|
|
|
|
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
|
|
|
.setPositiveButton("Exit", (dialog, which) -> {
|
|
|
|
|
// Stop emulator first
|
|
|
|
|
NativeApp.shutdown();
|
|
|
|
|
// Quit the app
|
|
|
|
|
finishAffinity();
|
|
|
|
|
finishAndRemoveTask();
|
|
|
|
|
// As a fallback ensure process exit
|
|
|
|
|
System.exit(0);
|
|
|
|
|
})
|
|
|
|
|
.setNegativeButton("Cancel", null)
|
|
|
|
|
.show();
|
|
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
|
|
|
|
|
// ControllerInputHandler.ControllerInputListener implementation
|
|
|
|
|
@Override
|
|
|
|
|
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
|
|
|
|
|
android.util.Log.d("Controller", "Controller " + controllerId + " button " + button + " (" + getButtonName(button) + ") " + (pressed ? "pressed" : "released"));
|
2025-08-29 12:26:17 -04:00
|
|
|
|
2025-08-25 13:56:13 -04:00
|
|
|
// Send controller input to native code using existing setPadButton method
|
|
|
|
|
// Range 255 for pressed, 0 for released (matching PS2 button range)
|
|
|
|
|
int range = pressed ? 255 : 0;
|
|
|
|
|
NativeApp.setPadButton(button, range, pressed);
|
2025-08-29 12:26:17 -04:00
|
|
|
|
|
|
|
|
// Hide touch controls as soon as controller activity is detected
|
|
|
|
|
maybeActivateControllerUi();
|
2025-08-25 13:56:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String getButtonName(int button) {
|
|
|
|
|
switch (button) {
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_A: return "Cross";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_B: return "Circle";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_X: return "Square";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_Y: return "Triangle";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_L1: return "L1";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_R1: return "R1";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_L2: return "L2";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_R2: return "R2";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_SELECT: return "Select";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_START: return "Start";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_THUMBL: return "L3";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_THUMBR: return "R3";
|
|
|
|
|
case KeyEvent.KEYCODE_DPAD_UP: return "D-Up";
|
|
|
|
|
case KeyEvent.KEYCODE_DPAD_DOWN: return "D-Down";
|
|
|
|
|
case KeyEvent.KEYCODE_DPAD_LEFT: return "D-Left";
|
|
|
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT: return "D-Right";
|
|
|
|
|
default: return "Unknown(" + button + ")";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onControllerAnalogInput(int controllerId, int axis, float value) {
|
|
|
|
|
android.util.Log.d("Controller", "Controller " + controllerId + " axis " + axis + " (" + getAxisName(axis) + ") value " + value);
|
2025-08-29 12:26:17 -04:00
|
|
|
|
2025-08-25 13:56:13 -04:00
|
|
|
// Send analog input to native code using setPadButton
|
|
|
|
|
handleAnalogInput(axis, value);
|
2025-08-29 12:26:17 -04:00
|
|
|
|
|
|
|
|
// Hide touch controls on analog movement as well
|
|
|
|
|
if (Math.abs(value) > 0.1f) {
|
|
|
|
|
maybeActivateControllerUi();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void maybeActivateControllerUi() {
|
|
|
|
|
if (!controllerUiApplied) {
|
|
|
|
|
controllerUiApplied = true;
|
|
|
|
|
runOnUiThread(() -> setControlsVisible(false));
|
|
|
|
|
}
|
2025-08-25 13:56:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onControllerCombo(int controllerId, String comboName) {
|
|
|
|
|
android.util.Log.d("Controller", "Controller " + controllerId + " combo: " + comboName);
|
|
|
|
|
|
|
|
|
|
if ("select_start".equals(comboName)) {
|
|
|
|
|
// Show quick actions dialog
|
|
|
|
|
runOnUiThread(() -> {
|
|
|
|
|
QuickActionsDialogFragment dialog = new QuickActionsDialogFragment();
|
|
|
|
|
dialog.show(getSupportFragmentManager(), "quick_actions");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String getAxisName(int axis) {
|
|
|
|
|
switch (axis) {
|
|
|
|
|
case 110: return "L-Up";
|
|
|
|
|
case 111: return "L-Right";
|
|
|
|
|
case 112: return "L-Down";
|
|
|
|
|
case 113: return "L-Left";
|
|
|
|
|
case 120: return "R-Up";
|
|
|
|
|
case 121: return "R-Right";
|
|
|
|
|
case 122: return "R-Down";
|
|
|
|
|
case 123: return "R-Left";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_L2: return "L2-Trigger";
|
|
|
|
|
case KeyEvent.KEYCODE_BUTTON_R2: return "R2-Trigger";
|
|
|
|
|
default: return "Unknown(" + axis + ")";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void handleAnalogInput(int axis, float value) {
|
2025-09-01 15:36:45 -04:00
|
|
|
// Convert analog input to button presses for the native interface
|
2025-08-25 13:56:13 -04:00
|
|
|
// For analog sticks, only send positive values (negative values are handled by opposite direction)
|
|
|
|
|
int intensity = Math.max(0, Math.round(Math.abs(value) * 255));
|
|
|
|
|
boolean pressed = Math.abs(value) > 0.1f;
|
|
|
|
|
|
|
|
|
|
switch (axis) {
|
|
|
|
|
case ControllerInputHandler.PAD_L_UP:
|
|
|
|
|
case ControllerInputHandler.PAD_L_DOWN:
|
|
|
|
|
case ControllerInputHandler.PAD_L_LEFT:
|
|
|
|
|
case ControllerInputHandler.PAD_L_RIGHT:
|
|
|
|
|
case ControllerInputHandler.PAD_R_UP:
|
|
|
|
|
case ControllerInputHandler.PAD_R_DOWN:
|
|
|
|
|
case ControllerInputHandler.PAD_R_LEFT:
|
|
|
|
|
case ControllerInputHandler.PAD_R_RIGHT:
|
|
|
|
|
// Analog stick directions
|
|
|
|
|
NativeApp.setPadButton(axis, intensity, pressed);
|
|
|
|
|
break;
|
|
|
|
|
case ControllerInputHandler.PAD_L2:
|
|
|
|
|
case ControllerInputHandler.PAD_R2:
|
|
|
|
|
// Triggers: 0-255 range
|
|
|
|
|
NativeApp.setPadButton(axis, Math.round(value * 255), value > 0.1f);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-11 23:14:41 -04:00
|
|
|
|
|
|
|
|
private void setupDrawerSettings(View header) {
|
|
|
|
|
if (header == null) return;
|
|
|
|
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
|
|
|
|
|
// Aspect Ratio spinner - setup like QuickActions
|
|
|
|
|
Spinner spAspect = header.findViewById(R.id.drawer_sp_aspect_ratio);
|
|
|
|
|
if (spAspect != null) {
|
|
|
|
|
if (spAspect.getAdapter() == null) {
|
|
|
|
|
ArrayAdapter<CharSequence> aspectAdapter = ArrayAdapter.createFromResource(this,
|
|
|
|
|
R.array.aspect_ratio_entries, android.R.layout.simple_spinner_item);
|
|
|
|
|
aspectAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
|
|
|
spAspect.setAdapter(aspectAdapter);
|
|
|
|
|
spAspect.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
|
|
|
|
@Override public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
|
|
|
|
prefs.edit().putInt("aspect_ratio", position).apply();
|
|
|
|
|
try { NativeApp.setAspectRatio(position); } catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
@Override public void onNothingSelected(android.widget.AdapterView<?> parent) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ArrayAdapter<?> adapter = (ArrayAdapter<?>) spAspect.getAdapter();
|
|
|
|
|
if (adapter != null) {
|
|
|
|
|
int savedAspect = prefs.getInt("aspect_ratio", 1);
|
|
|
|
|
if (savedAspect < 0 || savedAspect >= adapter.getCount()) savedAspect = 1;
|
|
|
|
|
spAspect.setSelection(savedAspect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolution Scale spinner
|
|
|
|
|
Spinner spScale = header.findViewById(R.id.drawer_sp_scale);
|
|
|
|
|
if (spScale != null) {
|
|
|
|
|
if (spScale.getAdapter() == null) {
|
|
|
|
|
ArrayAdapter<CharSequence> scaleAdapter = ArrayAdapter.createFromResource(this,
|
|
|
|
|
R.array.scale_entries, android.R.layout.simple_spinner_item);
|
|
|
|
|
scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
|
|
|
spScale.setAdapter(scaleAdapter);
|
|
|
|
|
spScale.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
|
|
|
|
@Override public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
|
|
|
|
float scale = Math.max(1, Math.min(8, position + 1));
|
|
|
|
|
prefs.edit().putFloat("upscale_multiplier", scale).apply();
|
|
|
|
|
try { NativeApp.renderUpscalemultiplier(scale); } catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
@Override public void onNothingSelected(android.widget.AdapterView<?> parent) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ArrayAdapter<?> adapter = (ArrayAdapter<?>) spScale.getAdapter();
|
|
|
|
|
if (adapter != null) {
|
|
|
|
|
float savedScale = prefs.getFloat("upscale_multiplier", 1.0f);
|
|
|
|
|
int scaleIndex = Math.max(0, Math.min(adapter.getCount() - 1, Math.round(savedScale) - 1));
|
|
|
|
|
spScale.setSelection(scaleIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Blending Accuracy spinner - setup like QuickActions
|
|
|
|
|
Spinner spBlending = header.findViewById(R.id.drawer_sp_blending_accuracy);
|
|
|
|
|
if (spBlending != null) {
|
|
|
|
|
if (spBlending.getAdapter() == null) {
|
|
|
|
|
ArrayAdapter<CharSequence> blendAdapter = ArrayAdapter.createFromResource(this,
|
|
|
|
|
R.array.blending_accuracy_entries, android.R.layout.simple_spinner_item);
|
|
|
|
|
blendAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
|
|
|
spBlending.setAdapter(blendAdapter);
|
|
|
|
|
spBlending.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
|
|
|
|
@Override public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
|
|
|
|
prefs.edit().putInt("blending_accuracy", position).apply();
|
|
|
|
|
try { NativeApp.setBlendingAccuracy(position); } catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
@Override public void onNothingSelected(android.widget.AdapterView<?> parent) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ArrayAdapter<?> adapter = (ArrayAdapter<?>) spBlending.getAdapter();
|
|
|
|
|
if (adapter != null) {
|
|
|
|
|
int savedBlend = prefs.getInt("blending_accuracy", 1);
|
|
|
|
|
if (savedBlend < 0 || savedBlend >= adapter.getCount()) savedBlend = 1;
|
|
|
|
|
spBlending.setSelection(savedBlend);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Setup switch listeners only once
|
|
|
|
|
setupDrawerSwitchListeners(header, prefs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void setupDrawerSwitchListeners(View header, SharedPreferences prefs) {
|
|
|
|
|
// Widescreen Patches switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen);
|
|
|
|
|
if (swWide != null && swWide.getTag() == null) {
|
|
|
|
|
swWide.setTag("setup"); // Mark as setup to avoid duplicate listeners
|
|
|
|
|
swWide.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("widescreen_patches", isChecked).apply();
|
|
|
|
|
try { NativeApp.setWidescreenPatches(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No Interlacing switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing);
|
|
|
|
|
if (swNoInt != null && swNoInt.getTag() == null) {
|
|
|
|
|
swNoInt.setTag("setup");
|
|
|
|
|
swNoInt.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("no_interlacing_patches", isChecked).apply();
|
|
|
|
|
try { NativeApp.setNoInterlacingPatches(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load Textures switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures);
|
|
|
|
|
if (swLoadTex != null && swLoadTex.getTag() == null) {
|
|
|
|
|
swLoadTex.setTag("setup");
|
|
|
|
|
swLoadTex.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("load_textures", isChecked).apply();
|
|
|
|
|
try { NativeApp.setLoadTextures(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Async Textures switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures);
|
|
|
|
|
if (swAsyncTex != null && swAsyncTex.getTag() == null) {
|
|
|
|
|
swAsyncTex.setTag("setup");
|
|
|
|
|
swAsyncTex.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("async_texture_loading", isChecked).apply();
|
|
|
|
|
try { NativeApp.setAsyncTextureLoading(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Precache Textures switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures);
|
|
|
|
|
if (swPrecache != null && swPrecache.getTag() == null) {
|
|
|
|
|
swPrecache.setTag("setup");
|
|
|
|
|
swPrecache.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("precache_textures", isChecked).apply();
|
|
|
|
|
try { NativeApp.setPrecacheTextureReplacements(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HUD Developer switch
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swDevHud = header.findViewById(R.id.drawer_sw_dev_hud);
|
|
|
|
|
if (swDevHud != null && swDevHud.getTag() == null) {
|
|
|
|
|
swDevHud.setTag("setup");
|
|
|
|
|
swDevHud.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
|
prefs.edit().putBoolean("hud_visible", isChecked).apply();
|
|
|
|
|
try { NativeApp.setHudVisible(isChecked); } catch (Throwable ignored) {}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void refreshDrawerSettings() {
|
|
|
|
|
try {
|
|
|
|
|
NavigationView nav = findViewById(R.id.nav_view);
|
|
|
|
|
if (nav != null && nav.getHeaderCount() > 0) {
|
|
|
|
|
View header = nav.getHeaderView(0);
|
|
|
|
|
if (header != null) {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
|
|
|
|
|
// Refresh switch states only (spinners are set up once in setupDrawerSettings)
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen);
|
|
|
|
|
if (swWide != null) {
|
|
|
|
|
swWide.setChecked(prefs.getBoolean("widescreen_patches", true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing);
|
|
|
|
|
if (swNoInt != null) {
|
|
|
|
|
swNoInt.setChecked(prefs.getBoolean("no_interlacing_patches", true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures);
|
|
|
|
|
if (swLoadTex != null) {
|
|
|
|
|
swLoadTex.setChecked(prefs.getBoolean("load_textures", false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures);
|
|
|
|
|
if (swAsyncTex != null) {
|
|
|
|
|
swAsyncTex.setChecked(prefs.getBoolean("async_texture_loading", true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures);
|
|
|
|
|
if (swPrecache != null) {
|
|
|
|
|
swPrecache.setChecked(prefs.getBoolean("precache_textures", false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swDevHud = header.findViewById(R.id.drawer_sw_dev_hud);
|
|
|
|
|
if (swDevHud != null) {
|
|
|
|
|
swDevHud.setChecked(prefs.getBoolean("hud_visible", false));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void loadAndApplyStoredSettings() {
|
|
|
|
|
try {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
|
|
|
|
|
// Apply aspect ratio setting
|
|
|
|
|
int aspectRatio = prefs.getInt("aspect_ratio", 1);
|
|
|
|
|
NativeApp.setAspectRatio(aspectRatio);
|
|
|
|
|
|
|
|
|
|
// Apply blending accuracy setting
|
|
|
|
|
int blendingAccuracy = prefs.getInt("blending_accuracy", 1);
|
|
|
|
|
NativeApp.setBlendingAccuracy(blendingAccuracy);
|
|
|
|
|
|
|
|
|
|
// Apply other settings
|
|
|
|
|
boolean widescreenPatches = prefs.getBoolean("widescreen_patches", true);
|
|
|
|
|
NativeApp.setWidescreenPatches(widescreenPatches);
|
|
|
|
|
|
|
|
|
|
boolean noInterlacing = prefs.getBoolean("no_interlacing_patches", true);
|
|
|
|
|
NativeApp.setNoInterlacingPatches(noInterlacing);
|
|
|
|
|
|
|
|
|
|
boolean loadTextures = prefs.getBoolean("load_textures", false);
|
|
|
|
|
NativeApp.setLoadTextures(loadTextures);
|
|
|
|
|
|
|
|
|
|
boolean asyncTextures = prefs.getBoolean("async_texture_loading", true);
|
|
|
|
|
NativeApp.setAsyncTextureLoading(asyncTextures);
|
|
|
|
|
|
|
|
|
|
boolean precacheTextures = prefs.getBoolean("precache_textures", false);
|
|
|
|
|
NativeApp.setPrecacheTextureReplacements(precacheTextures);
|
|
|
|
|
|
|
|
|
|
// Apply renderer setting
|
|
|
|
|
int renderer = prefs.getInt("renderer", -1);
|
|
|
|
|
NativeApp.renderGpu(renderer);
|
|
|
|
|
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void showControllerHintIfNeeded() {
|
|
|
|
|
// Prevent multiple dialogs from showing
|
|
|
|
|
if (mControllerHintShowing) return;
|
|
|
|
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
boolean hasShownHint = prefs.getBoolean("controller_hint_shown", false);
|
|
|
|
|
|
|
|
|
|
// Only show the hint once, and only if setup is complete
|
|
|
|
|
if (!hasShownHint && prefs.getBoolean("first_run_done", false)) {
|
|
|
|
|
mControllerHintShowing = true;
|
|
|
|
|
// Small delay to avoid showing immediately on startup
|
|
|
|
|
findViewById(android.R.id.content).postDelayed(() -> {
|
|
|
|
|
if (!isFinishing() && !mSetupWizardActive && mControllerHintShowing) {
|
|
|
|
|
showControllerHintDialog();
|
|
|
|
|
}
|
|
|
|
|
}, 2000); // 2 second delay
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void showControllerHintDialog() {
|
|
|
|
|
// Double-check to prevent multiple dialogs
|
|
|
|
|
if (!mControllerHintShowing) return;
|
|
|
|
|
|
|
|
|
|
View dialogView = getLayoutInflater().inflate(R.layout.dialog_controller_hint, null);
|
|
|
|
|
|
|
|
|
|
com.google.android.material.materialswitch.MaterialSwitch swDontShow = dialogView.findViewById(R.id.sw_dont_show_again);
|
|
|
|
|
|
|
|
|
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this,
|
|
|
|
|
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
|
|
|
|
.setCustomTitle(UiUtils.centeredDialogTitle(this, "Controller Tip"))
|
|
|
|
|
.setView(dialogView)
|
|
|
|
|
.setPositiveButton("Got it!", (dialog, which) -> {
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
|
|
|
|
|
if (swDontShow != null && swDontShow.isChecked()) {
|
|
|
|
|
prefs.edit().putBoolean("controller_hint_shown", true).apply();
|
|
|
|
|
}
|
|
|
|
|
mControllerHintShowing = false;
|
|
|
|
|
dialog.dismiss();
|
|
|
|
|
})
|
|
|
|
|
.setCancelable(true)
|
|
|
|
|
.setOnDismissListener(dialog -> {
|
|
|
|
|
mControllerHintShowing = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
AlertDialog dialog = builder.create();
|
|
|
|
|
dialog.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pause/Play toggle functionality
|
|
|
|
|
public void togglePauseState() {
|
|
|
|
|
try {
|
|
|
|
|
boolean isPaused = NativeApp.isPaused();
|
|
|
|
|
if (isPaused) {
|
|
|
|
|
NativeApp.resume();
|
|
|
|
|
} else {
|
|
|
|
|
NativeApp.pause();
|
|
|
|
|
}
|
|
|
|
|
updatePausePlayButton();
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updatePausePlayButton() {
|
|
|
|
|
MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play);
|
|
|
|
|
if (btn_pause_play != null) {
|
|
|
|
|
try {
|
|
|
|
|
boolean isPaused = NativeApp.isPaused();
|
|
|
|
|
boolean hasGame = hasSelectedGame() && isThread();
|
|
|
|
|
|
|
|
|
|
// Show button only when a game is running
|
|
|
|
|
btn_pause_play.setVisibility(hasGame ? View.VISIBLE : View.GONE);
|
|
|
|
|
|
|
|
|
|
if (hasGame) {
|
|
|
|
|
// Update icon to show the action it will perform
|
|
|
|
|
if (isPaused) {
|
|
|
|
|
// Game is paused, show pause icon (will pause more/stay paused)
|
|
|
|
|
btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.pause_circle_24px));
|
|
|
|
|
} else {
|
|
|
|
|
// Game is running, show play icon (will keep playing)
|
|
|
|
|
btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.play_circle_24px));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ignored) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call this method when game starts/stops to update button visibility
|
|
|
|
|
public void updateGameState() {
|
|
|
|
|
updatePausePlayButton();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-22 04:11:11 -04:00
|
|
|
}
|