diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 416321ce80..6263a42dbd 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -64,9 +64,18 @@
+ android:launchMode="singleTop">
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
index 3a357ed319..75150fd167 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
@@ -252,7 +252,7 @@ object NativeLibrary {
@Keep
@JvmStatic
- fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
+ fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout
@Keep
@JvmStatic
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
deleted file mode 100644
index 2f631b61ee..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
+++ /dev/null
@@ -1,788 +0,0 @@
-package org.citra.citra_emu.activities;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.util.Pair;
-import android.util.SparseIntArray;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.CheckBox;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.PopupMenu;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.documentfile.provider.DocumentFile;
-import androidx.fragment.app.FragmentActivity;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.contracts.OpenFileResultContract;
-import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
-import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
-import org.citra.citra_emu.features.settings.ui.SettingsActivity;
-import org.citra.citra_emu.features.settings.utils.SettingsFile;
-import org.citra.citra_emu.camera.StillImageCameraHelper;
-import org.citra.citra_emu.fragments.EmulationFragment;
-import org.citra.citra_emu.ui.main.MainActivity;
-import org.citra.citra_emu.utils.ControllerMappingHelper;
-import org.citra.citra_emu.utils.EmulationMenuSettings;
-import org.citra.citra_emu.utils.FileBrowserHelper;
-import org.citra.citra_emu.utils.FileUtil;
-import org.citra.citra_emu.utils.ForegroundService;
-import org.citra.citra_emu.utils.Log;
-import org.citra.citra_emu.utils.ThemeUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.util.Collections;
-import java.util.List;
-
-import static android.Manifest.permission.CAMERA;
-import static android.Manifest.permission.RECORD_AUDIO;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.slider.Slider;
-
-public final class EmulationActivity extends AppCompatActivity {
- public static final String EXTRA_SELECTED_GAME = "SelectedGame";
- public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
- public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
- public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
- public static final int MENU_ACTION_ADJUST_SCALE = 2;
- public static final int MENU_ACTION_EXIT = 3;
- public static final int MENU_ACTION_SHOW_FPS = 4;
- public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
- public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
- public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
- public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
- public static final int MENU_ACTION_SWAP_SCREENS = 9;
- public static final int MENU_ACTION_RESET_OVERLAY = 10;
- public static final int MENU_ACTION_SHOW_OVERLAY = 11;
- public static final int MENU_ACTION_OPEN_SETTINGS = 12;
- public static final int MENU_ACTION_LOAD_AMIIBO = 13;
- public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
- public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
- public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
- public static final int MENU_ACTION_OPEN_CHEATS = 17;
- public static final int MENU_ACTION_CLOSE_GAME = 18;
-
- public static final int REQUEST_SELECT_AMIIBO = 2;
- private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
- private static SparseIntArray buttonsActionsMap = new SparseIntArray();
-
- private final ActivityResultLauncher mOpenFileLauncher =
- registerForActivityResult(new OpenFileResultContract(), result -> {
- if (result == null)
- return;
- String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
- result, getApplicationContext(), Collections.singletonList("bin"));
- if (selectedFiles == null)
- return;
-
- onAmiiboSelected(selectedFiles[0]);
- });
-
- static {
- buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
- EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
- buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
- EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
- buttonsActionsMap
- .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
- buttonsActionsMap.append(R.id.menu_emulation_show_fps,
- EmulationActivity.MENU_ACTION_SHOW_FPS);
- buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
- EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
- buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
- EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
- buttonsActionsMap.append(R.id.menu_screen_layout_single,
- EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
- buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
- EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
- buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
- EmulationActivity.MENU_ACTION_SWAP_SCREENS);
- buttonsActionsMap
- .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
- buttonsActionsMap
- .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
- buttonsActionsMap
- .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
- buttonsActionsMap
- .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
- buttonsActionsMap
- .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
- buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center,
- EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
- buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
- EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
- buttonsActionsMap
- .append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
- buttonsActionsMap
- .append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
- }
-
- private EmulationFragment mEmulationFragment;
- private SharedPreferences mPreferences;
- private ControllerMappingHelper mControllerMappingHelper;
- private Intent foregroundService;
- private boolean activityRecreated;
- private String mSelectedTitle;
- private String mPath;
-
- public static void launch(FragmentActivity activity, String path, String title) {
- Intent launcher = new Intent(activity, EmulationActivity.class);
-
- launcher.putExtra(EXTRA_SELECTED_GAME, path);
- launcher.putExtra(EXTRA_SELECTED_TITLE, title);
- activity.startActivity(launcher);
- }
-
- public static void tryDismissRunningNotification(Activity activity) {
- NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
- }
-
- @Override
- protected void onDestroy() {
- stopService(foregroundService);
- super.onDestroy();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- Log.gameLaunched = true;
- ThemeUtil.INSTANCE.setTheme(this);
- super.onCreate(savedInstanceState);
-
- if (savedInstanceState == null) {
- // Get params we were passed
- Intent gameToEmulate = getIntent();
- mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
- mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
- activityRecreated = false;
- } else {
- activityRecreated = true;
- restoreState(savedInstanceState);
- }
-
- mControllerMappingHelper = new ControllerMappingHelper();
-
- // Set these options now so that the SurfaceView the game renders into is the right size.
- enableFullscreenImmersive();
-
- setContentView(R.layout.activity_emulation);
-
- // Find or create the EmulationFragment
- mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
- .findFragmentById(R.id.frame_emulation_fragment);
- if (mEmulationFragment == null) {
- mEmulationFragment = EmulationFragment.newInstance(mPath);
- getSupportFragmentManager().beginTransaction()
- .add(R.id.frame_emulation_fragment, mEmulationFragment)
- .commit();
- }
-
- setTitle(mSelectedTitle);
-
- mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-
- // Start a foreground service to prevent the app from getting killed in the background
- foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
- startForegroundService(foregroundService);
-
- // Override Citra core INI with the one set by our in game menu
- NativeLibrary.INSTANCE.swapScreens(EmulationMenuSettings.getSwapScreens(),
- getWindowManager().getDefaultDisplay().getRotation());
- }
-
- @Override
- protected void onSaveInstanceState(@NonNull Bundle outState) {
- outState.putString(EXTRA_SELECTED_GAME, mPath);
- outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
- super.onSaveInstanceState(outState);
- }
-
- protected void restoreState(Bundle savedInstanceState) {
- mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
- mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
- }
-
- @Override
- public void onRestart() {
- super.onRestart();
- NativeLibrary.INSTANCE.reloadCameraDevices();
- }
-
- @Override
- public void onBackPressed() {
- View anchor = findViewById(R.id.menu_anchor);
- PopupMenu popupMenu = new PopupMenu(this, anchor);
- onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
- updateSavestateMenuOptions(popupMenu.getMenu());
- popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
- popupMenu.show();
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- switch (requestCode) {
- case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
- if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
- shouldShowRequestPermissionRationale(CAMERA)) {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.camera)
- .setMessage(R.string.camera_permission_needed)
- .setPositiveButton(android.R.string.ok, null)
- .show();
- }
- NativeLibrary.INSTANCE.cameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
- break;
- case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
- if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
- shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.microphone)
- .setMessage(R.string.microphone_permission_needed)
- .setPositiveButton(android.R.string.ok, null)
- .show();
- }
- NativeLibrary.INSTANCE.micPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
- break;
- default:
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- break;
- }
- }
-
- public void onEmulationStarted() {
- Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
- }
-
- private void enableFullscreenImmersive() {
- // TODO: Remove this once we properly account for display insets in the input overlay
- getWindow().getAttributes().layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
-
- getWindow().getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_IMMERSIVE |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- onCreateOptionsMenu(menu, getMenuInflater());
- return true;
- }
-
- private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.menu_emulation, menu);
-
- int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
- switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
- case EmulationMenuSettings.LayoutOption_SingleScreen:
- layoutOptionMenuItem = R.id.menu_screen_layout_single;
- break;
- case EmulationMenuSettings.LayoutOption_SideScreen:
- layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
- break;
- case EmulationMenuSettings.LayoutOption_MobilePortrait:
- layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
- break;
- }
-
- menu.findItem(layoutOptionMenuItem).setChecked(true);
- menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter());
- menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable());
- menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
- menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
- menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
- }
-
- private void DisplaySavestateWarning() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
- if (preferences.getBoolean("savestateWarningShown", false)) {
- return;
- }
-
- LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater();
- View view = inflater.inflate(R.layout.dialog_checkbox, null);
- CheckBox checkBox = view.findViewById(R.id.checkBox);
-
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.savestate_warning_title)
- .setMessage(R.string.savestate_warning_message)
- .setView(view)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> {
- preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply();
- })
- .show();
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
- updateSavestateMenuOptions(menu);
- return true;
- }
-
- private void updateSavestateMenuOptions(Menu menu) {
- final NativeLibrary.SaveStateInfo[] savestates = NativeLibrary.INSTANCE.getSavestateInfo();
- if (savestates == null) {
- menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
- menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
- return;
- }
- menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
- menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
-
- final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu();
- final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu();
- saveStateMenu.clear();
- loadStateMenu.clear();
-
- // Update savestates information
- for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) {
- final int slot = i + 1;
- final String text = getString(R.string.emulation_empty_state_slot, slot);
- saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
- DisplaySavestateWarning();
- NativeLibrary.INSTANCE.saveState(slot);
- return true;
- });
- loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
- NativeLibrary.INSTANCE.loadState(slot);
- return true;
- });
- }
- for (final NativeLibrary.SaveStateInfo info : savestates) {
- final String text = getString(R.string.emulation_occupied_state_slot, info.getSlot(), info.getTime());
- saveStateMenu.getItem(info.getSlot() - 1).setTitle(text);
- loadStateMenu.getItem(info.getSlot() - 1).setTitle(text).setEnabled(true);
- }
- }
-
- @SuppressWarnings("WrongConstant")
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int action = buttonsActionsMap.get(item.getItemId(), -1);
-
- switch (action) {
- // Edit the placement of the controls
- case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
- editControlsPlacement();
- break;
-
- // Enable/Disable specific buttons or the entire input overlay.
- case MENU_ACTION_TOGGLE_CONTROLS:
- toggleControls();
- break;
-
- // Adjust the scale of the overlay controls.
- case MENU_ACTION_ADJUST_SCALE:
- adjustScale();
- break;
-
- // Toggle the visibility of the Performance stats TextView
- case MENU_ACTION_SHOW_FPS: {
- final boolean isEnabled = !EmulationMenuSettings.getShowFps();
- EmulationMenuSettings.setShowFps(isEnabled);
- item.setChecked(isEnabled);
-
- mEmulationFragment.updateShowFpsOverlay();
- break;
- }
- // Sets the screen layout to Landscape
- case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
- changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
- break;
-
- // Sets the screen layout to Portrait
- case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
- changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
- break;
-
- // Sets the screen layout to Single
- case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
- changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
- break;
-
- // Sets the screen layout to Side by Side
- case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
- changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
- break;
-
- // Swap the top and bottom screen locations
- case MENU_ACTION_SWAP_SCREENS: {
- final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
- EmulationMenuSettings.setSwapScreens(isEnabled);
- item.setChecked(isEnabled);
-
- NativeLibrary.INSTANCE.swapScreens(isEnabled, getWindowManager().getDefaultDisplay()
- .getRotation());
- break;
- }
-
- // Reset overlay placement
- case MENU_ACTION_RESET_OVERLAY:
- resetOverlay();
- break;
-
- // Show or hide overlay
- case MENU_ACTION_SHOW_OVERLAY: {
- final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
- EmulationMenuSettings.setShowOverlay(isEnabled);
- item.setChecked(isEnabled);
-
- mEmulationFragment.refreshInputOverlay();
- break;
- }
-
- case MENU_ACTION_EXIT:
- mEmulationFragment.stopEmulation();
- finish();
- break;
-
- case MENU_ACTION_OPEN_SETTINGS:
- SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
- break;
-
- case MENU_ACTION_LOAD_AMIIBO:
- mOpenFileLauncher.launch(false);
- break;
-
- case MENU_ACTION_REMOVE_AMIIBO:
- RemoveAmiibo();
- break;
-
- case MENU_ACTION_JOYSTICK_REL_CENTER:
- final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter();
- EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
- item.setChecked(isJoystickRelCenterEnabled);
- break;
-
- case MENU_ACTION_DPAD_SLIDE_ENABLE:
- final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
- EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
- item.setChecked(isDpadSlideEnabled);
- break;
-
- case MENU_ACTION_OPEN_CHEATS:
- CheatsActivity.launch(this, NativeLibrary.INSTANCE.getRunningTitleId());
- break;
-
- case MENU_ACTION_CLOSE_GAME:
- NativeLibrary.INSTANCE.pauseEmulation();
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.emulation_close_game)
- .setMessage(R.string.emulation_close_game_message)
- .setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
- {
- mEmulationFragment.stopEmulation();
- finish();
- })
- .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.INSTANCE.unPauseEmulation())
- .setOnCancelListener(dialogInterface -> NativeLibrary.INSTANCE.unPauseEmulation())
- .show();
- break;
- }
-
- return true;
- }
-
- private void changeScreenOrientation(int layoutOption, MenuItem item) {
- item.setChecked(true);
- NativeLibrary.INSTANCE.notifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
- .getRotation());
- EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
- }
-
- private void editControlsPlacement() {
- if (mEmulationFragment.isConfiguringControls()) {
- mEmulationFragment.stopConfiguringControls();
- } else {
- mEmulationFragment.startConfiguringControls();
- }
- }
-
- // Gets button presses
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- int action;
- int button = mPreferences.getInt(InputBindingSetting.Companion.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
-
- switch (event.getAction()) {
- case KeyEvent.ACTION_DOWN:
- // Handling the case where the back button is pressed.
- if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
- onBackPressed();
- return true;
- }
-
- // Normal key events.
- action = NativeLibrary.ButtonState.PRESSED;
- break;
- case KeyEvent.ACTION_UP:
- action = NativeLibrary.ButtonState.RELEASED;
- break;
- default:
- return false;
- }
- InputDevice input = event.getDevice();
-
- if (input == null) {
- // Controller was disconnected
- return false;
- }
-
- return NativeLibrary.INSTANCE.onGamePadEvent(input.getDescriptor(), button, action);
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent result) {
- super.onActivityResult(requestCode, resultCode, result);
- if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
- StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
- }
- }
-
- private void onAmiiboSelected(String selectedFile) {
- boolean success = NativeLibrary.INSTANCE.loadAmiibo(selectedFile);
-
- if (!success) {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.amiibo_load_error)
- .setMessage(R.string.amiibo_load_error_message)
- .setPositiveButton(android.R.string.ok, null)
- .show();
- }
- }
-
- private void RemoveAmiibo() {
- NativeLibrary.INSTANCE.removeAmiibo();
- }
-
- private void toggleControls() {
- final SharedPreferences.Editor editor = mPreferences.edit();
- boolean[] enabledButtons = new boolean[14];
-
- for (int i = 0; i < enabledButtons.length; i++) {
- // Buttons that are disabled by default
- boolean defaultValue = true;
- switch (i) {
- case 6: // ZL
- case 7: // ZR
- case 12: // C-stick
- defaultValue = false;
- break;
- }
-
- enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
- }
-
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.emulation_toggle_controls)
- .setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
- (dialog, indexSelected, isChecked) -> editor
- .putBoolean("buttonToggle" + indexSelected, isChecked))
- .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
- {
- editor.apply();
- mEmulationFragment.refreshInputOverlay();
- })
- .show();
- }
-
- private void adjustScale() {
- LayoutInflater inflater = LayoutInflater.from(this);
- View view = inflater.inflate(R.layout.dialog_slider, null);
-
- final Slider slider = view.findViewById(R.id.slider);
- final TextView textValue = view.findViewById(R.id.text_value);
- final TextView units = view.findViewById(R.id.text_units);
-
- slider.setValueTo(150);
- slider.setValue(mPreferences.getInt("controlScale", 50));
- slider.addOnChangeListener((slider1, progress, fromUser) -> {
- textValue.setText(String.valueOf((int) progress + 50));
- setControlScale((int) slider1.getValue());
- });
-
- textValue.setText(String.valueOf((int) slider.getValue() + 50));
- units.setText("%");
-
- final int previousProgress = (int) slider.getValue();
-
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.emulation_control_scale)
- .setView(view)
- .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> setControlScale(previousProgress))
- .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> setControlScale((int) slider.getValue()))
- .setNeutralButton(R.string.slider_default, (dialogInterface, i) -> setControlScale(50))
- .show();
- }
-
- private void setControlScale(int scale) {
- SharedPreferences.Editor editor = mPreferences.edit();
- editor.putInt("controlScale", scale);
- editor.apply();
- mEmulationFragment.refreshInputOverlay();
- }
-
- private void resetOverlay() {
- new MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.emulation_touch_overlay_reset))
- .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
- @Override
- public boolean dispatchGenericMotionEvent(MotionEvent event) {
- if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
- return super.dispatchGenericMotionEvent(event);
- }
-
- // Don't attempt to do anything if we are disconnecting a device.
- if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
- return true;
- }
-
- InputDevice input = event.getDevice();
- List motions = input.getMotionRanges();
-
- float[] axisValuesCirclePad = {0.0f, 0.0f};
- float[] axisValuesCStick = {0.0f, 0.0f};
- float[] axisValuesDPad = {0.0f, 0.0f};
- boolean isTriggerPressedLMapped = false;
- boolean isTriggerPressedRMapped = false;
- boolean isTriggerPressedZLMapped = false;
- boolean isTriggerPressedZRMapped = false;
- boolean isTriggerPressedL = false;
- boolean isTriggerPressedR = false;
- boolean isTriggerPressedZL = false;
- boolean isTriggerPressedZR = false;
-
- for (InputDevice.MotionRange range : motions) {
- int axis = range.getAxis();
- float origValue = event.getAxisValue(axis);
- float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
- int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1);
- int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1);
-
- if (nextMapping == -1 || guestOrientation == -1) {
- // Axis is unmapped
- continue;
- }
-
- if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
- // Skip joystick wobble
- value = 0.f;
- }
-
- if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
- axisValuesCirclePad[guestOrientation] = value;
- } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
- axisValuesCStick[guestOrientation] = value;
- } else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
- axisValuesDPad[guestOrientation] = value;
- } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
- isTriggerPressedLMapped = true;
- isTriggerPressedL = value != 0.f;
- } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
- isTriggerPressedRMapped = true;
- isTriggerPressedR = value != 0.f;
- } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
- isTriggerPressedZLMapped = true;
- isTriggerPressedZL = value != 0.f;
- } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
- isTriggerPressedZRMapped = true;
- isTriggerPressedZR = value != 0.f;
- }
- }
-
- // Circle-Pad and C-Stick status
- NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
- NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
-
- // Triggers L/R and ZL/ZR
- if (isTriggerPressedLMapped) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
- }
- if (isTriggerPressedRMapped) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
- }
- if (isTriggerPressedZLMapped) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
- }
- if (isTriggerPressedZRMapped) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
- }
-
- // Work-around to allow D-pad axis to be bound to emulated buttons
- if (axisValuesDPad[0] == 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
- }
- if (axisValuesDPad[0] < 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
- }
- if (axisValuesDPad[0] > 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
- }
- if (axisValuesDPad[1] == 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
- }
- if (axisValuesDPad[1] < 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
- }
- if (axisValuesDPad[1] > 0.f) {
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
- NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
- }
-
- return true;
- }
-
- public boolean isActivityRecreated() {
- return activityRecreated;
- }
-
- @Retention(SOURCE)
- @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
- MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
- MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
- MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
- public @interface MenuAction {
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt
new file mode 100644
index 0000000000..587233732b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt
@@ -0,0 +1,453 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.activities
+
+import android.Manifest.permission
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.navigation.fragment.NavHostFragment
+import androidx.preference.PreferenceManager
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
+import org.citra.citra_emu.contracts.OpenFileResultContract
+import org.citra.citra_emu.databinding.ActivityEmulationBinding
+import org.citra.citra_emu.features.settings.model.SettingsViewModel
+import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
+import org.citra.citra_emu.fragments.MessageDialogFragment
+import org.citra.citra_emu.utils.ControllerMappingHelper
+import org.citra.citra_emu.utils.EmulationMenuSettings
+import org.citra.citra_emu.utils.FileBrowserHelper
+import org.citra.citra_emu.utils.ForegroundService
+import org.citra.citra_emu.utils.ThemeUtil
+import org.citra.citra_emu.viewmodel.EmulationViewModel
+
+class EmulationActivity : AppCompatActivity() {
+ private val preferences: SharedPreferences
+ get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+ private var foregroundService: Intent? = null
+ var isActivityRecreated = false
+
+ private val settingsViewModel: SettingsViewModel by viewModels()
+ private val emulationViewModel: EmulationViewModel by viewModels()
+
+ private lateinit var binding: ActivityEmulationBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeUtil.setTheme(this)
+
+ settingsViewModel.settings.loadSettings()
+
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityEmulationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ val navController = navHostFragment.navController
+ navController.setGraph(R.navigation.emulation_navigation, intent.extras)
+
+ isActivityRecreated = savedInstanceState != null
+
+ // Set these options now so that the SurfaceView the game renders into is the right size.
+ enableFullscreenImmersive()
+
+ // Override Citra core INI with the one set by our in game menu
+ NativeLibrary.swapScreens(
+ EmulationMenuSettings.swapScreens,
+ windowManager.defaultDisplay.rotation
+ )
+
+ // Start a foreground service to prevent the app from getting killed in the background
+ foregroundService = Intent(this, ForegroundService::class.java)
+ startForegroundService(foregroundService)
+ }
+
+ // On some devices, the system bars will not disappear on first boot or after some
+ // rotations. Here we set full screen immersive repeatedly in onResume and in
+ // onWindowFocusChanged to prevent the unwanted status bar state.
+ override fun onResume() {
+ super.onResume()
+ enableFullscreenImmersive()
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ enableFullscreenImmersive()
+ }
+
+ public override fun onRestart() {
+ super.onRestart()
+ NativeLibrary.reloadCameraDevices()
+ }
+
+ override fun onDestroy() {
+ stopForegroundService(this)
+ super.onDestroy()
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ when (requestCode) {
+ NativeLibrary.REQUEST_CODE_NATIVE_CAMERA -> {
+ if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
+ shouldShowRequestPermissionRationale(permission.CAMERA)
+ ) {
+ MessageDialogFragment.newInstance(
+ R.string.camera,
+ R.string.camera_permission_needed
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ NativeLibrary.cameraPermissionResult(
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
+ )
+ }
+
+ NativeLibrary.REQUEST_CODE_NATIVE_MIC -> {
+ if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
+ shouldShowRequestPermissionRationale(permission.RECORD_AUDIO)
+ ) {
+ MessageDialogFragment.newInstance(
+ R.string.microphone,
+ R.string.microphone_permission_needed
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ NativeLibrary.micPermissionResult(
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
+ )
+ }
+
+ else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+ }
+
+ fun onEmulationStarted() {
+ emulationViewModel.setEmulationStarted(true)
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.emulation_menu_help),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ private fun enableFullscreenImmersive() {
+ // TODO: Remove this once we properly account for display insets in the input overlay
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ WindowInsetsControllerCompat(window, window.decorView).let { controller ->
+ controller.hide(WindowInsetsCompat.Type.systemBars())
+ controller.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+ }
+
+ // Gets button presses
+ @Suppress("DEPRECATION")
+ @SuppressLint("GestureBackNavigation")
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ // TODO: Move this check into native code - prevents crash if input pressed before starting emulation
+ if (!NativeLibrary.isRunning()) {
+ return false
+ }
+
+ val button =
+ preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
+ val action: Int = when (event.action) {
+ KeyEvent.ACTION_DOWN -> {
+ // On some devices, the back gesture / button press is not intercepted by androidx
+ // and fails to open the emulation menu. So we're stuck running deprecated code to
+ // cover for either a fault on androidx's side or in OEM skins (MIUI at least)
+ if (event.keyCode == KeyEvent.KEYCODE_BACK) {
+ onBackPressed()
+ }
+
+ // Normal key events.
+ NativeLibrary.ButtonState.PRESSED
+ }
+
+ KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
+ else -> return false
+ }
+ val input = event.device
+ ?: // Controller was disconnected
+ return false
+ return NativeLibrary.onGamePadEvent(input.descriptor, button, action)
+ }
+
+ private fun onAmiiboSelected(selectedFile: String) {
+ val success = NativeLibrary.loadAmiibo(selectedFile)
+ if (!success) {
+ MessageDialogFragment.newInstance(
+ R.string.amiibo_load_error,
+ R.string.amiibo_load_error_message
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ }
+
+ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
+ // TODO: Move this check into native code - prevents crash if input pressed before starting emulation
+ if (!NativeLibrary.isRunning()) {
+ return super.dispatchGenericMotionEvent(event)
+ }
+
+ if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) {
+ return super.dispatchGenericMotionEvent(event)
+ }
+
+ // Don't attempt to do anything if we are disconnecting a device.
+ if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
+ return true
+ }
+ val input = event.device
+ val motions = input.motionRanges
+ val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f)
+ val axisValuesCStick = floatArrayOf(0.0f, 0.0f)
+ val axisValuesDPad = floatArrayOf(0.0f, 0.0f)
+ var isTriggerPressedLMapped = false
+ var isTriggerPressedRMapped = false
+ var isTriggerPressedZLMapped = false
+ var isTriggerPressedZRMapped = false
+ var isTriggerPressedL = false
+ var isTriggerPressedR = false
+ var isTriggerPressedZL = false
+ var isTriggerPressedZR = false
+ for (range in motions) {
+ val axis = range.axis
+ val origValue = event.getAxisValue(axis)
+ var value = ControllerMappingHelper.scaleAxis(input, axis, origValue)
+ val nextMapping =
+ preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1)
+ val guestOrientation =
+ preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1)
+ if (nextMapping == -1 || guestOrientation == -1) {
+ // Axis is unmapped
+ continue
+ }
+ if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) {
+ // Skip joystick wobble
+ value = 0f
+ }
+ when (nextMapping) {
+ NativeLibrary.ButtonType.STICK_LEFT -> {
+ axisValuesCirclePad[guestOrientation] = value
+ }
+
+ NativeLibrary.ButtonType.STICK_C -> {
+ axisValuesCStick[guestOrientation] = value
+ }
+
+ NativeLibrary.ButtonType.DPAD -> {
+ axisValuesDPad[guestOrientation] = value
+ }
+
+ NativeLibrary.ButtonType.TRIGGER_L -> {
+ isTriggerPressedLMapped = true
+ isTriggerPressedL = value != 0f
+ }
+
+ NativeLibrary.ButtonType.TRIGGER_R -> {
+ isTriggerPressedRMapped = true
+ isTriggerPressedR = value != 0f
+ }
+
+ NativeLibrary.ButtonType.BUTTON_ZL -> {
+ isTriggerPressedZLMapped = true
+ isTriggerPressedZL = value != 0f
+ }
+
+ NativeLibrary.ButtonType.BUTTON_ZR -> {
+ isTriggerPressedZRMapped = true
+ isTriggerPressedZR = value != 0f
+ }
+ }
+ }
+
+ // Circle-Pad and C-Stick status
+ NativeLibrary.onGamePadMoveEvent(
+ input.descriptor,
+ NativeLibrary.ButtonType.STICK_LEFT,
+ axisValuesCirclePad[0],
+ axisValuesCirclePad[1]
+ )
+ NativeLibrary.onGamePadMoveEvent(
+ input.descriptor,
+ NativeLibrary.ButtonType.STICK_C,
+ axisValuesCStick[0],
+ axisValuesCStick[1]
+ )
+
+ // Triggers L/R and ZL/ZR
+ if (isTriggerPressedLMapped) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.TRIGGER_L,
+ if (isTriggerPressedL) {
+ NativeLibrary.ButtonState.PRESSED
+ } else {
+ NativeLibrary.ButtonState.RELEASED
+ }
+ )
+ }
+ if (isTriggerPressedRMapped) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.TRIGGER_R,
+ if (isTriggerPressedR) {
+ NativeLibrary.ButtonState.PRESSED
+ } else {
+ NativeLibrary.ButtonState.RELEASED
+ }
+ )
+ }
+ if (isTriggerPressedZLMapped) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.BUTTON_ZL,
+ if (isTriggerPressedZL) {
+ NativeLibrary.ButtonState.PRESSED
+ } else {
+ NativeLibrary.ButtonState.RELEASED
+ }
+ )
+ }
+ if (isTriggerPressedZRMapped) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.BUTTON_ZR,
+ if (isTriggerPressedZR) {
+ NativeLibrary.ButtonState.PRESSED
+ } else {
+ NativeLibrary.ButtonState.RELEASED
+ }
+ )
+ }
+
+ // Work-around to allow D-pad axis to be bound to emulated buttons
+ if (axisValuesDPad[0] == 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_LEFT,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_RIGHT,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ }
+ if (axisValuesDPad[0] < 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_LEFT,
+ NativeLibrary.ButtonState.PRESSED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_RIGHT,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ }
+ if (axisValuesDPad[0] > 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_LEFT,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_RIGHT,
+ NativeLibrary.ButtonState.PRESSED
+ )
+ }
+ if (axisValuesDPad[1] == 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_UP,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_DOWN,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ }
+ if (axisValuesDPad[1] < 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_UP,
+ NativeLibrary.ButtonState.PRESSED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_DOWN,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ }
+ if (axisValuesDPad[1] > 0f) {
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_UP,
+ NativeLibrary.ButtonState.RELEASED
+ )
+ NativeLibrary.onGamePadEvent(
+ NativeLibrary.TouchScreenDevice,
+ NativeLibrary.ButtonType.DPAD_DOWN,
+ NativeLibrary.ButtonState.PRESSED
+ )
+ }
+ return true
+ }
+
+ val openFileLauncher =
+ registerForActivityResult(OpenFileResultContract()) { result: Intent? ->
+ if (result == null) return@registerForActivityResult
+ val selectedFiles = FileBrowserHelper.getSelectedFiles(
+ result, applicationContext, listOf("bin")
+ ) ?: return@registerForActivityResult
+ onAmiiboSelected(selectedFiles[0])
+ }
+
+ val openImageLauncher =
+ registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { result: Uri? ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ OnFilePickerResult(result.toString())
+ }
+
+ companion object {
+ fun stopForegroundService(activity: Activity) {
+ val startIntent = Intent(activity, ForegroundService::class.java)
+ startIntent.action = ForegroundService.ACTION_STOP
+ activity.startForegroundService(startIntent)
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
index b507ea5bdd..e84aacb1e2 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
@@ -15,6 +15,7 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@@ -22,6 +23,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.activities.EmulationActivity
@@ -77,7 +79,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
)
.apply()
- EmulationActivity.launch(activity, holder.game.path, holder.game.title)
+ val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
+ view.findNavController().navigate(action)
}
/**
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java
deleted file mode 100644
index 55be2660a9..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2020 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-package org.citra.citra_emu.camera;
-
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.provider.MediaStore;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-import org.citra.citra_emu.utils.PicassoUtils;
-
-import androidx.annotation.Keep;
-import androidx.annotation.Nullable;
-
-// Used in native code.
-public final class StillImageCameraHelper {
- public static final int REQUEST_CAMERA_FILE_PICKER = 1;
- private static final Object filePickerLock = new Object();
- private static @Nullable
- String filePickerPath;
-
- // Opens file picker for camera.
- @Keep
- public static @Nullable
- String OpenFilePicker() {
- final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
-
- // At this point, we are assuming that we already have permissions as they are
- // needed to launch a game
- emulationActivity.runOnUiThread(() -> {
- Intent intent = new Intent(Intent.ACTION_PICK);
- intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
- emulationActivity.startActivityForResult(
- Intent.createChooser(intent,
- emulationActivity.getString(R.string.camera_select_image)),
- REQUEST_CAMERA_FILE_PICKER);
- });
-
- synchronized (filePickerLock) {
- try {
- filePickerLock.wait();
- } catch (InterruptedException ignored) {
- }
- }
-
- return filePickerPath;
- }
-
- // Called from EmulationActivity.
- public static void OnFilePickerResult(Intent result) {
- filePickerPath = result == null ? null : result.getDataString();
-
- synchronized (filePickerLock) {
- filePickerLock.notifyAll();
- }
- }
-
- // Blocking call. Load image from file and crop/resize it to fit in width x height.
- @Keep
- @Nullable
- public static Bitmap LoadImageFromFile(String uri, int width, int height) {
- return PicassoUtils.LoadBitmapFromFile(uri, width, height);
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.kt
new file mode 100644
index 0000000000..c1d45e3af2
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.kt
@@ -0,0 +1,67 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.camera
+
+import android.graphics.Bitmap
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.Keep
+import androidx.core.graphics.drawable.toBitmap
+import coil.executeBlocking
+import coil.imageLoader
+import coil.request.ImageRequest
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+
+// Used in native code.
+object StillImageCameraHelper {
+ private val filePickerLock = Object()
+ private var filePickerPath: String? = null
+
+ // Opens file picker for camera.
+ @Keep
+ @JvmStatic
+ fun OpenFilePicker(): String? {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()
+
+ // At this point, we are assuming that we already have permissions as they are
+ // needed to launch a game
+ emulationActivity!!.runOnUiThread {
+ val request = PickVisualMediaRequest.Builder()
+ .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly).build()
+ emulationActivity.openImageLauncher.launch(request)
+ }
+ synchronized(filePickerLock) {
+ try {
+ filePickerLock.wait()
+ } catch (ignored: InterruptedException) {
+ }
+ }
+ return filePickerPath
+ }
+
+ // Called from EmulationActivity.
+ @JvmStatic
+ fun OnFilePickerResult(result: String) {
+ filePickerPath = result
+ synchronized(filePickerLock) { filePickerLock.notifyAll() }
+ }
+
+ // Blocking call. Load image from file and crop/resize it to fit in width x height.
+ @Keep
+ @JvmStatic
+ fun LoadImageFromFile(uri: String?, width: Int, height: Int): Bitmap? {
+ val context = CitraApplication.appContext
+ val request = ImageRequest.Builder(context)
+ .data(uri)
+ .size(width, height)
+ .build()
+ return context.imageLoader.executeBlocking(request).drawable?.toBitmap(
+ width,
+ height,
+ Bitmap.Config.ARGB_8888
+ )
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java
deleted file mode 100644
index 834bd3317a..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java
+++ /dev/null
@@ -1,337 +0,0 @@
-package org.citra.citra_emu.fragments;
-
-import android.content.Context;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.os.Handler;
-import android.preference.PreferenceManager;
-import android.view.Choreographer;
-import android.view.LayoutInflater;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-import org.citra.citra_emu.overlay.InputOverlay;
-import org.citra.citra_emu.utils.DirectoryInitialization;
-import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
-import org.citra.citra_emu.utils.EmulationMenuSettings;
-import org.citra.citra_emu.utils.Log;
-
-public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback {
- private static final String KEY_GAMEPATH = "gamepath";
-
- private static final Handler perfStatsUpdateHandler = new Handler();
-
- private SharedPreferences mPreferences;
-
- private InputOverlay mInputOverlay;
-
- private EmulationState mEmulationState;
-
- private EmulationActivity activity;
-
- private TextView mPerfStats;
-
- private Runnable perfStatsUpdater;
-
- public static EmulationFragment newInstance(String gamePath) {
- Bundle args = new Bundle();
- args.putString(KEY_GAMEPATH, gamePath);
-
- EmulationFragment fragment = new EmulationFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
-
- if (context instanceof EmulationActivity) {
- activity = (EmulationActivity) context;
- NativeLibrary.INSTANCE.setEmulationActivity((EmulationActivity) context);
- } else {
- throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
- }
- }
-
- /**
- * Initialize anything that doesn't depend on the layout / views in here.
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // So this fragment doesn't restart on configuration changes; i.e. rotation.
- setRetainInstance(true);
-
- mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
-
- String gamePath = getArguments().getString(KEY_GAMEPATH);
- mEmulationState = new EmulationState(gamePath);
- }
-
- /**
- * Initialize the UI and start emulation in here.
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
-
- SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation);
- surfaceView.getHolder().addCallback(this);
-
- mInputOverlay = contents.findViewById(R.id.surface_input_overlay);
- mPerfStats = contents.findViewById(R.id.show_fps_text);
-
- Button doneButton = contents.findViewById(R.id.done_control_config);
- if (doneButton != null) {
- doneButton.setOnClickListener(v -> stopConfiguringControls());
- }
-
- // Show/hide the "Show FPS" overlay
- updateShowFpsOverlay();
-
- // The new Surface created here will get passed to the native code via onSurfaceChanged.
- return contents;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- Choreographer.getInstance().postFrameCallback(this);
- mEmulationState.run(activity.isActivityRecreated());
- }
-
- @Override
- public void onPause() {
- if (mEmulationState.isRunning()) {
- mEmulationState.pause();
- }
-
- Choreographer.getInstance().removeFrameCallback(this);
- super.onPause();
- }
-
- @Override
- public void onDetach() {
- NativeLibrary.INSTANCE.clearEmulationActivity();
- super.onDetach();
- }
-
- public void refreshInputOverlay() {
- mInputOverlay.refreshControls();
- }
-
- public void resetInputOverlay() {
- // Reset button scale
- SharedPreferences.Editor editor = mPreferences.edit();
- editor.putInt("controlScale", 50);
- editor.apply();
-
- mInputOverlay.resetButtonPlacement();
- }
-
- public void updateShowFpsOverlay() {
- if (EmulationMenuSettings.getShowFps()) {
- final int SYSTEM_FPS = 0;
- final int FPS = 1;
- final int FRAMETIME = 2;
- final int SPEED = 3;
-
- perfStatsUpdater = () ->
- {
- final double[] perfStats = NativeLibrary.INSTANCE.getPerfStats();
- if (perfStats[FPS] > 0) {
- mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5),
- (int) (perfStats[SPEED] * 100.0 + 0.5)));
- }
-
- perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000);
- };
- perfStatsUpdateHandler.post(perfStatsUpdater);
-
- mPerfStats.setVisibility(View.VISIBLE);
- } else {
- if (perfStatsUpdater != null) {
- perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
- }
-
- mPerfStats.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- // We purposely don't do anything here.
- // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height);
- mEmulationState.newSurface(holder.getSurface());
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- mEmulationState.clearSurface();
- }
-
- @Override
- public void doFrame(long frameTimeNanos) {
- Choreographer.getInstance().postFrameCallback(this);
- NativeLibrary.INSTANCE.doFrame();
- }
-
- public void stopEmulation() {
- mEmulationState.stop();
- }
-
- public void startConfiguringControls() {
- getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE);
- mInputOverlay.setIsInEditMode(true);
- }
-
- public void stopConfiguringControls() {
- getView().findViewById(R.id.done_control_config).setVisibility(View.GONE);
- mInputOverlay.setIsInEditMode(false);
- }
-
- public boolean isConfiguringControls() {
- return mInputOverlay.isInEditMode();
- }
-
- private static class EmulationState {
- private final String mGamePath;
- private State state;
- private Surface mSurface;
- private boolean mRunWhenSurfaceIsValid;
-
- EmulationState(String gamePath) {
- mGamePath = gamePath;
- // Starting state is stopped.
- state = State.STOPPED;
- }
-
- public synchronized boolean isStopped() {
- return state == State.STOPPED;
- }
-
- // Getters for the current state
-
- public synchronized boolean isPaused() {
- return state == State.PAUSED;
- }
-
- public synchronized boolean isRunning() {
- return state == State.RUNNING;
- }
-
- public synchronized void stop() {
- if (state != State.STOPPED) {
- Log.debug("[EmulationFragment] Stopping emulation.");
- state = State.STOPPED;
- NativeLibrary.INSTANCE.stopEmulation();
- } else {
- Log.warning("[EmulationFragment] Stop called while already stopped.");
- }
- }
-
- // State changing methods
-
- public synchronized void pause() {
- if (state != State.PAUSED) {
- state = State.PAUSED;
- Log.debug("[EmulationFragment] Pausing emulation.");
-
- // Release the surface before pausing, since emulation has to be running for that.
- NativeLibrary.INSTANCE.surfaceDestroyed();
- NativeLibrary.INSTANCE.pauseEmulation();
- } else {
- Log.warning("[EmulationFragment] Pause called while already paused.");
- }
- }
-
- public synchronized void run(boolean isActivityRecreated) {
- if (isActivityRecreated) {
- if (NativeLibrary.INSTANCE.isRunning()) {
- state = State.PAUSED;
- }
- } else {
- Log.debug("[EmulationFragment] activity resumed or fresh start");
- }
-
- // If the surface is set, run now. Otherwise, wait for it to get set.
- if (mSurface != null) {
- runWithValidSurface();
- } else {
- mRunWhenSurfaceIsValid = true;
- }
- }
-
- // Surface callbacks
- public synchronized void newSurface(Surface surface) {
- mSurface = surface;
- if (mRunWhenSurfaceIsValid) {
- runWithValidSurface();
- }
- }
-
- public synchronized void clearSurface() {
- if (mSurface == null) {
- Log.warning("[EmulationFragment] clearSurface called, but surface already null.");
- } else {
- mSurface = null;
- Log.debug("[EmulationFragment] Surface destroyed.");
-
- if (state == State.RUNNING) {
- NativeLibrary.INSTANCE.surfaceDestroyed();
- state = State.PAUSED;
- } else if (state == State.PAUSED) {
- Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
- } else {
- Log.warning("[EmulationFragment] Surface cleared while emulation stopped.");
- }
- }
- }
-
- private void runWithValidSurface() {
- mRunWhenSurfaceIsValid = false;
- if (state == State.STOPPED) {
- NativeLibrary.INSTANCE.surfaceChanged(mSurface);
- Thread mEmulationThread = new Thread(() ->
- {
- Log.debug("[EmulationFragment] Starting emulation thread.");
- NativeLibrary.INSTANCE.run(mGamePath);
- }, "NativeEmulation");
- mEmulationThread.start();
-
- } else if (state == State.PAUSED) {
- Log.debug("[EmulationFragment] Resuming emulation.");
- NativeLibrary.INSTANCE.surfaceChanged(mSurface);
- NativeLibrary.INSTANCE.unPauseEmulation();
- } else {
- Log.debug("[EmulationFragment] Bug, run called while already running.");
- }
- state = State.RUNNING;
- }
-
- private enum State {
- STOPPED, RUNNING, PAUSED
- }
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
new file mode 100644
index 0000000000..62c787c1e7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
@@ -0,0 +1,1048 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.fragments
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.view.Choreographer
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.View
+import android.view.ViewGroup
+import android.widget.PopupMenu
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.Insets
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.EmulationNavigationDirections
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.activities.EmulationActivity
+import org.citra.citra_emu.databinding.DialogCheckboxBinding
+import org.citra.citra_emu.databinding.DialogSliderBinding
+import org.citra.citra_emu.databinding.FragmentEmulationBinding
+import org.citra.citra_emu.features.settings.ui.SettingsActivity
+import org.citra.citra_emu.features.settings.utils.SettingsFile
+import org.citra.citra_emu.model.Game
+import org.citra.citra_emu.utils.DirectoryInitialization
+import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
+import org.citra.citra_emu.utils.EmulationMenuSettings
+import org.citra.citra_emu.utils.FileUtil
+import org.citra.citra_emu.utils.GameHelper
+import org.citra.citra_emu.utils.GameIconUtils
+import org.citra.citra_emu.utils.Log
+import org.citra.citra_emu.utils.ViewUtils
+import org.citra.citra_emu.viewmodel.EmulationViewModel
+import java.lang.NullPointerException
+
+class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
+ private val preferences: SharedPreferences
+ get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+
+ private lateinit var emulationState: EmulationState
+ private var perfStatsUpdater: Runnable? = null
+
+ private lateinit var emulationActivity: EmulationActivity
+
+ private var _binding: FragmentEmulationBinding? = null
+ private val binding get() = _binding!!
+
+ private val args by navArgs()
+
+ private lateinit var game: Game
+
+ private val emulationViewModel: EmulationViewModel by activityViewModels()
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is EmulationActivity) {
+ emulationActivity = context
+ NativeLibrary.setEmulationActivity(context)
+ } else {
+ throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
+ }
+ }
+
+ /**
+ * Initialize anything that doesn't depend on the layout / views in here.
+ */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val intent = requireActivity().intent
+ val intentUri: Uri? = intent.data
+ val oldIntentInfo = Pair(
+ intent.getStringExtra("SelectedGame"),
+ intent.getStringExtra("SelectedTitle")
+ )
+ var intentGame: Game? = null
+ if (intentUri != null) {
+ intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
+ GameHelper.getGame(intentUri, isInstalled = false, addedToLibrary = false)
+ } else {
+ null
+ }
+ } else if (oldIntentInfo.first != null) {
+ val gameUri = Uri.parse(oldIntentInfo.first)
+ intentGame = if (Game.extensions.contains(FileUtil.getExtension(gameUri))) {
+ GameHelper.getGame(gameUri, isInstalled = false, addedToLibrary = false)
+ } else {
+ null
+ }
+ }
+
+ try {
+ game = args.game ?: intentGame!!
+ } catch (e: NullPointerException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.no_game_present,
+ Toast.LENGTH_SHORT
+ ).show()
+ requireActivity().finish()
+ return
+ }
+
+ // So this fragment doesn't restart on configuration changes; i.e. rotation.
+ retainInstance = true
+ emulationState = EmulationState(game.path)
+ emulationActivity = requireActivity() as EmulationActivity
+ }
+
+ /**
+ * Initialize the UI and start emulation in here.
+ */
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentEmulationBinding.inflate(inflater)
+ return binding.root
+ }
+
+ // This is using the correct scope, lint is just acting up
+ @SuppressLint("UnsafeRepeatOnLifecycleDetector")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (requireActivity().isFinishing) {
+ return
+ }
+
+ binding.surfaceEmulation.holder.addCallback(this)
+ binding.doneControlConfig.setOnClickListener {
+ binding.doneControlConfig.visibility = View.GONE
+ binding.surfaceInputOverlay.setIsInEditMode(false)
+ }
+
+ // Show/hide the "Show FPS" overlay
+ updateShowFpsOverlay()
+
+ binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+ binding.drawerLayout.addDrawerListener(object : DrawerListener {
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
+ binding.surfaceInputOverlay.dispatchTouchEvent(
+ MotionEvent.obtain(
+ SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis() + 100,
+ MotionEvent.ACTION_UP,
+ 0f,
+ 0f,
+ 0
+ )
+ )
+ }
+
+ override fun onDrawerOpened(drawerView: View) {
+ binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
+ }
+
+ override fun onDrawerClosed(drawerView: View) {
+ binding.drawerLayout.setDrawerLockMode(EmulationMenuSettings.drawerLockMode)
+ }
+
+ override fun onDrawerStateChanged(newState: Int) {
+ // No op
+ }
+ })
+ binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
+ val titleId = if (EmulationMenuSettings.drawerLockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
+ R.string.unlock_drawer
+ } else {
+ R.string.lock_drawer
+ }
+ val iconId = if (EmulationMenuSettings.drawerLockMode == DrawerLayout.LOCK_MODE_UNLOCKED) {
+ R.drawable.ic_unlocked
+ } else {
+ R.drawable.ic_lock
+ }
+
+ title = getString(titleId)
+ icon = ResourcesCompat.getDrawable(
+ resources,
+ iconId,
+ requireContext().theme
+ )
+ }
+
+ binding.inGameMenu.getHeaderView(0).findViewById(R.id.text_game_title).text =
+ game.title
+ binding.inGameMenu.setNavigationItemSelectedListener {
+ when (it.itemId) {
+ R.id.menu_emulation_pause -> {
+ if (emulationState.isPaused) {
+ emulationState.unpause()
+ it.title = resources.getString(R.string.pause_emulation)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_pause,
+ requireContext().theme
+ )
+ } else {
+ emulationState.pause()
+ it.title = resources.getString(R.string.resume_emulation)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_play,
+ requireContext().theme
+ )
+ }
+ true
+ }
+
+ R.id.menu_emulation_savestates -> {
+ showSavestateMenu()
+ true
+ }
+
+ R.id.menu_overlay_options -> {
+ showOverlayMenu()
+ true
+ }
+
+ R.id.menu_amiibo -> {
+ showAmiiboMenu()
+ true
+ }
+
+ R.id.menu_landscape_screen_layout -> {
+ showScreenLayoutMenu()
+ true
+ }
+
+ R.id.menu_swap_screens -> {
+ val isEnabled = !EmulationMenuSettings.swapScreens
+ EmulationMenuSettings.swapScreens = isEnabled
+ NativeLibrary.swapScreens(
+ isEnabled,
+ requireActivity().windowManager.defaultDisplay.rotation
+ )
+ true
+ }
+
+ R.id.menu_lock_drawer -> {
+ when (EmulationMenuSettings.drawerLockMode) {
+ DrawerLayout.LOCK_MODE_UNLOCKED -> {
+ EmulationMenuSettings.drawerLockMode =
+ DrawerLayout.LOCK_MODE_LOCKED_CLOSED
+ it.title = resources.getString(R.string.unlock_drawer)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_lock,
+ requireContext().theme
+ )
+ }
+
+ DrawerLayout.LOCK_MODE_LOCKED_CLOSED -> {
+ EmulationMenuSettings.drawerLockMode = DrawerLayout.LOCK_MODE_UNLOCKED
+ it.title = resources.getString(R.string.lock_drawer)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_unlocked,
+ requireContext().theme
+ )
+ }
+ }
+ true
+ }
+
+ R.id.menu_cheats -> {
+ val action = EmulationNavigationDirections
+ .actionGlobalCheatsActivity(NativeLibrary.getRunningTitleId())
+ binding.root.findNavController().navigate(action)
+ true
+ }
+
+ R.id.menu_settings -> {
+ SettingsActivity.launch(
+ requireContext(),
+ SettingsFile.FILE_NAME_CONFIG,
+ ""
+ )
+ true
+ }
+
+ R.id.menu_exit -> {
+ NativeLibrary.pauseEmulation()
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.emulation_close_game)
+ .setMessage(R.string.emulation_close_game_message)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ emulationState.stop()
+ requireActivity().finish()
+ }
+ .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
+ NativeLibrary.unPauseEmulation()
+ }
+ .setOnCancelListener { NativeLibrary.unPauseEmulation() }
+ .show()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (!emulationViewModel.emulationStarted.value) {
+ return
+ }
+
+ if (binding.drawerLayout.isOpen) {
+ binding.drawerLayout.close()
+ } else {
+ binding.drawerLayout.open()
+ }
+ }
+ }
+ )
+
+ GameIconUtils.loadGameIcon(requireActivity(), game, binding.loadingImage)
+ binding.loadingTitle.text = game.title
+
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ emulationViewModel.shaderProgress.collectLatest {
+ if (it > 0 && it != emulationViewModel.totalShaders.value) {
+ binding.loadingProgressIndicator.isIndeterminate = false
+ binding.loadingProgressText.visibility = View.VISIBLE
+ binding.loadingProgressText.text = String.format(
+ "%d/%d",
+ emulationViewModel.shaderProgress.value,
+ emulationViewModel.totalShaders.value
+ )
+
+ if (it < binding.loadingProgressIndicator.max) {
+ binding.loadingProgressIndicator.progress = it
+ }
+ }
+
+ if (it == emulationViewModel.totalShaders.value) {
+ binding.loadingText.setText(R.string.loading)
+ binding.loadingProgressIndicator.isIndeterminate = true
+ binding.loadingProgressText.visibility = View.GONE
+ }
+ }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ emulationViewModel.totalShaders.collectLatest {
+ binding.loadingProgressIndicator.max = it
+ }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ emulationViewModel.shaderMessage.collectLatest {
+ if (it != "") {
+ binding.loadingText.text = it
+ }
+ }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ emulationViewModel.emulationStarted.collectLatest { started ->
+ if (started) {
+ ViewUtils.hideView(binding.loadingIndicator)
+ ViewUtils.showView(binding.surfaceInputOverlay)
+ binding.inGameMenu.menu.findItem(R.id.menu_emulation_savestates)
+ .setVisible(NativeLibrary.getSavestateInfo() != null)
+ binding.drawerLayout.setDrawerLockMode(EmulationMenuSettings.drawerLockMode)
+ }
+ }
+ }
+ }
+ }
+
+ setInsets()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Choreographer.getInstance().postFrameCallback(this)
+ if (NativeLibrary.isRunning()) {
+ NativeLibrary.unPauseEmulation()
+ return
+ }
+
+ if (DirectoryInitialization.areCitraDirectoriesReady()) {
+ emulationState.run(emulationActivity.isActivityRecreated)
+ } else {
+ setupCitraDirectoriesThenStartEmulation()
+ }
+ }
+
+ override fun onPause() {
+ if (NativeLibrary.isRunning()) {
+ emulationState.pause()
+ }
+ Choreographer.getInstance().removeFrameCallback(this)
+ super.onPause()
+ }
+
+ override fun onDetach() {
+ NativeLibrary.clearEmulationActivity()
+ super.onDetach()
+ }
+
+ private fun setupCitraDirectoriesThenStartEmulation() {
+ val directoryInitializationState = DirectoryInitialization.start()
+ if (directoryInitializationState ===
+ DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED
+ ) {
+ emulationState.run(emulationActivity.isActivityRecreated)
+ } else if (directoryInitializationState ===
+ DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED
+ ) {
+ Toast.makeText(context, R.string.write_permission_needed, Toast.LENGTH_SHORT)
+ .show()
+ } else if (directoryInitializationState ===
+ DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE
+ ) {
+ Toast.makeText(
+ context,
+ R.string.external_storage_not_mounted,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun showSavestateMenu() {
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_emulation_savestates)
+ )
+
+ popupMenu.menuInflater.inflate(R.menu.menu_savestates, popupMenu.menu)
+
+ popupMenu.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_emulation_save_state -> {
+ showSaveStateSubmenu()
+ true
+ }
+
+ R.id.menu_emulation_load_state -> {
+ showLoadStateSubmenu()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ popupMenu.show()
+ }
+
+ private fun showSaveStateSubmenu() {
+ val savestates = NativeLibrary.getSavestateInfo()
+
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_emulation_savestates)
+ )
+
+ popupMenu.menu.apply {
+ for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) {
+ val slot = i + 1
+ val text = getString(R.string.emulation_empty_state_slot, slot)
+ add(text).setEnabled(true).setOnMenuItemClickListener {
+ displaySavestateWarning()
+ NativeLibrary.saveState(slot)
+ true
+ }
+ }
+ }
+
+ savestates?.forEach {
+ val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
+ popupMenu.menu.getItem(it.slot - 1).setTitle(text)
+ }
+
+ popupMenu.show()
+ }
+
+ private fun showLoadStateSubmenu() {
+ val savestates = NativeLibrary.getSavestateInfo()
+
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_emulation_savestates)
+ )
+
+ popupMenu.menu.apply {
+ for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) {
+ val slot = i + 1
+ val text = getString(R.string.emulation_empty_state_slot, slot)
+ add(text).setEnabled(false).setOnMenuItemClickListener {
+ NativeLibrary.loadState(slot)
+ true
+ }
+ }
+ }
+
+ savestates?.forEach {
+ val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
+ popupMenu.menu.getItem(it.slot - 1).setTitle(text).setEnabled(true)
+ }
+
+ popupMenu.show()
+ }
+
+ private fun displaySavestateWarning() {
+ if (preferences.getBoolean("savestateWarningShown", false)) {
+ return
+ }
+
+ val dialogCheckboxBinding = DialogCheckboxBinding.inflate(layoutInflater)
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.savestates)
+ .setMessage(R.string.savestate_warning_message)
+ .setView(dialogCheckboxBinding.root)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ preferences.edit()
+ .putBoolean("savestateWarningShown", dialogCheckboxBinding.checkBox.isChecked)
+ .apply()
+ }
+ .show()
+ }
+
+ private fun showOverlayMenu() {
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_overlay_options)
+ )
+
+ popupMenu.menuInflater.inflate(R.menu.menu_overlay_options, popupMenu.menu)
+
+ popupMenu.menu.apply {
+ findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
+ findItem(R.id.menu_show_fps).isChecked = EmulationMenuSettings.showFps
+ findItem(R.id.menu_emulation_joystick_rel_center).isChecked =
+ EmulationMenuSettings.joystickRelCenter
+ findItem(R.id.menu_emulation_dpad_slide_enable).isChecked =
+ EmulationMenuSettings.dpadSlide
+ }
+
+ popupMenu.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_show_overlay -> {
+ EmulationMenuSettings.showOverlay = !EmulationMenuSettings.showOverlay
+ binding.surfaceInputOverlay.refreshControls()
+ true
+ }
+
+ R.id.menu_show_fps -> {
+ EmulationMenuSettings.showFps = !EmulationMenuSettings.showFps
+ updateShowFpsOverlay()
+ true
+ }
+
+ R.id.menu_emulation_edit_layout -> {
+ editControlsPlacement()
+ binding.drawerLayout.close()
+ true
+ }
+
+ R.id.menu_emulation_toggle_controls -> {
+ showToggleControlsDialog()
+ true
+ }
+
+ R.id.menu_emulation_adjust_scale -> {
+ showAdjustScaleDialog()
+ true
+ }
+
+ R.id.menu_emulation_joystick_rel_center -> {
+ EmulationMenuSettings.joystickRelCenter =
+ !EmulationMenuSettings.joystickRelCenter
+ true
+ }
+
+ R.id.menu_emulation_dpad_slide_enable -> {
+ EmulationMenuSettings.dpadSlide = !EmulationMenuSettings.dpadSlide
+ true
+ }
+
+ R.id.menu_emulation_reset_overlay -> {
+ showResetOverlayDialog()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ popupMenu.show()
+ }
+
+ private fun showAmiiboMenu() {
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_amiibo)
+ )
+
+ popupMenu.menuInflater.inflate(R.menu.menu_amiibo_options, popupMenu.menu)
+
+ popupMenu.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_emulation_amiibo_load -> {
+ emulationActivity.openFileLauncher.launch(false)
+ true
+ }
+
+ R.id.menu_emulation_amiibo_remove -> {
+ NativeLibrary.removeAmiibo()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ popupMenu.show()
+ }
+
+ private fun showScreenLayoutMenu() {
+ val popupMenu = PopupMenu(
+ requireContext(),
+ binding.inGameMenu.findViewById(R.id.menu_landscape_screen_layout)
+ )
+
+ popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu)
+
+ val layoutOptionMenuItem = when (EmulationMenuSettings.landscapeScreenLayout) {
+ EmulationMenuSettings.LayoutOption_SingleScreen ->
+ R.id.menu_screen_layout_single
+
+ EmulationMenuSettings.LayoutOption_SideScreen ->
+ R.id.menu_screen_layout_sidebyside
+
+ EmulationMenuSettings.LayoutOption_MobilePortrait ->
+ R.id.menu_screen_layout_portrait
+
+ else -> R.id.menu_screen_layout_landscape
+ }
+ popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true)
+
+ popupMenu.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_screen_layout_landscape -> {
+ changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, it)
+ true
+ }
+
+ R.id.menu_screen_layout_portrait -> {
+ changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, it)
+ true
+ }
+
+ R.id.menu_screen_layout_single -> {
+ changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, it)
+ true
+ }
+
+ R.id.menu_screen_layout_sidebyside -> {
+ changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, it)
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ popupMenu.show()
+ }
+
+ private fun changeScreenOrientation(layoutOption: Int, item: MenuItem) {
+ item.setChecked(true)
+ NativeLibrary.notifyOrientationChange(
+ layoutOption,
+ requireActivity().windowManager.defaultDisplay.rotation
+ )
+ EmulationMenuSettings.landscapeScreenLayout = layoutOption
+ }
+
+ private fun editControlsPlacement() {
+ if (binding.surfaceInputOverlay.isInEditMode) {
+ binding.doneControlConfig.visibility = View.GONE
+ binding.surfaceInputOverlay.setIsInEditMode(false)
+ } else {
+ binding.doneControlConfig.visibility = View.VISIBLE
+ binding.surfaceInputOverlay.setIsInEditMode(true)
+ }
+ }
+
+ private fun showToggleControlsDialog() {
+ val editor = preferences.edit()
+ val enabledButtons = BooleanArray(14)
+ enabledButtons.forEachIndexed { i: Int, _: Boolean ->
+ // Buttons that are disabled by default
+ var defaultValue = true
+ when (i) {
+ 6, 7, 12, 13 -> defaultValue = false
+ }
+ enabledButtons[i] = preferences.getBoolean("buttonToggle$i", defaultValue)
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.emulation_toggle_controls)
+ .setMultiChoiceItems(
+ R.array.n3dsButtons, enabledButtons
+ ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean ->
+ editor.putBoolean("buttonToggle$indexSelected", isChecked)
+ }
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ editor.apply()
+ binding.surfaceInputOverlay.refreshControls()
+ }
+ .show()
+ }
+
+ private fun showAdjustScaleDialog() {
+ val sliderBinding = DialogSliderBinding.inflate(layoutInflater)
+
+ sliderBinding.apply {
+ slider.valueTo = 150f
+ slider.value = preferences.getInt("controlScale", 50).toFloat()
+ slider.addOnChangeListener(
+ Slider.OnChangeListener { slider: Slider, progress: Float, _: Boolean ->
+ textValue.text = (progress.toInt() + 50).toString()
+ setControlScale(slider.value.toInt())
+ })
+ textValue.text = (sliderBinding.slider.value.toInt() + 50).toString()
+ textUnits.text = "%"
+ }
+ val previousProgress = sliderBinding.slider.value.toInt()
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.emulation_control_scale)
+ .setView(sliderBinding.root)
+ .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
+ setControlScale(previousProgress)
+ }
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ setControlScale(sliderBinding.slider.value.toInt())
+ }
+ .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
+ setControlScale(50)
+ }
+ .show()
+ }
+
+ private fun setControlScale(scale: Int) {
+ preferences.edit()
+ .putInt("controlScale", scale)
+ .apply()
+ binding.surfaceInputOverlay.refreshControls()
+ }
+
+ private fun showResetOverlayDialog() {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(getString(R.string.emulation_touch_overlay_reset))
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ resetInputOverlay()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun resetInputOverlay() {
+ preferences.edit()
+ .putInt("controlScale", 50)
+ .apply()
+
+ val editor = preferences.edit()
+ for (i in 0 until 14) {
+ var defaultValue = true
+ when (i) {
+ 6, 7, 12, 13 -> defaultValue = false
+ }
+ editor.putBoolean("buttonToggle$i", defaultValue)
+ }
+ editor.apply()
+
+ binding.surfaceInputOverlay.resetButtonPlacement()
+ }
+
+ fun updateShowFpsOverlay() {
+ if (EmulationMenuSettings.showFps) {
+ val SYSTEM_FPS = 0
+ val FPS = 1
+ val FRAMETIME = 2
+ val SPEED = 3
+ perfStatsUpdater = Runnable {
+ val perfStats = NativeLibrary.getPerfStats()
+ if (perfStats[FPS] > 0) {
+ binding.showFpsText.text = String.format(
+ "FPS: %d Speed: %d%%",
+ (perfStats[FPS] + 0.5).toInt(),
+ (perfStats[SPEED] * 100.0 + 0.5).toInt()
+ )
+ }
+ perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 3000)
+ }
+ perfStatsUpdateHandler.post(perfStatsUpdater!!)
+ binding.showFpsText.visibility = View.VISIBLE
+ } else {
+ if (perfStatsUpdater != null) {
+ perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
+ }
+ binding.showFpsText.visibility = View.GONE
+ }
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ // We purposely don't do anything here.
+ // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+ Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height)
+ emulationState.newSurface(holder.surface)
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ emulationState.clearSurface()
+ }
+
+ override fun doFrame(frameTimeNanos: Long) {
+ Choreographer.getInstance().postFrameCallback(this)
+ NativeLibrary.doFrame()
+ }
+
+ private fun setInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.inGameMenu
+ ) { v: View, windowInsets: WindowInsetsCompat ->
+ val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ var left = 0
+ var right = 0
+ if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ left = cutInsets.left
+ } else {
+ right = cutInsets.right
+ }
+
+ v.setPadding(left, cutInsets.top, right, 0)
+
+ // Ensure FPS text doesn't get cut off by rounded display corners
+ val sidePadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
+ if (cutInsets.left == 0) {
+ binding.showFpsText.setPadding(
+ sidePadding,
+ cutInsets.top,
+ cutInsets.right,
+ cutInsets.bottom
+ )
+ } else {
+ binding.showFpsText.setPadding(
+ cutInsets.left,
+ cutInsets.top,
+ cutInsets.right,
+ cutInsets.bottom
+ )
+ }
+ windowInsets
+ }
+ }
+
+ private class EmulationState(private val gamePath: String) {
+ private var state: State
+ private var surface: Surface? = null
+
+ init {
+ // Starting state is stopped.
+ state = State.STOPPED
+ }
+
+ @get:Synchronized
+ val isStopped: Boolean
+ get() = state == State.STOPPED
+
+ @get:Synchronized
+ val isPaused: Boolean
+ // Getters for the current state
+ get() = state == State.PAUSED
+
+ @get:Synchronized
+ val isRunning: Boolean
+ get() = state == State.RUNNING
+
+ @Synchronized
+ fun stop() {
+ if (state != State.STOPPED) {
+ Log.debug("[EmulationFragment] Stopping emulation.")
+ state = State.STOPPED
+ NativeLibrary.stopEmulation()
+ } else {
+ Log.warning("[EmulationFragment] Stop called while already stopped.")
+ }
+ }
+
+ // State changing methods
+ @Synchronized
+ fun pause() {
+ if (state != State.PAUSED) {
+ state = State.PAUSED
+ Log.debug("[EmulationFragment] Pausing emulation.")
+
+ // Release the surface before pausing, since emulation has to be running for that.
+ NativeLibrary.surfaceDestroyed()
+ NativeLibrary.pauseEmulation()
+ } else {
+ Log.warning("[EmulationFragment] Pause called while already paused.")
+ }
+ }
+
+ @Synchronized
+ fun unpause() {
+ if (state != State.RUNNING) {
+ state = State.RUNNING
+ Log.debug("[EmulationFragment] Unpausing emulation.")
+
+ NativeLibrary.unPauseEmulation()
+ } else {
+ Log.warning("[EmulationFragment] Unpause called while already running.")
+ }
+ }
+
+ @Synchronized
+ fun run(isActivityRecreated: Boolean) {
+ if (isActivityRecreated) {
+ if (NativeLibrary.isRunning()) {
+ state = State.PAUSED
+ }
+ } else {
+ Log.debug("[EmulationFragment] activity resumed or fresh start")
+ }
+
+ // If the surface is set, run now. Otherwise, wait for it to get set.
+ if (surface != null) {
+ runWithValidSurface()
+ }
+ }
+
+ // Surface callbacks
+ @Synchronized
+ fun newSurface(surface: Surface?) {
+ this.surface = surface
+ if (this.surface != null) {
+ runWithValidSurface()
+ }
+ }
+
+ @Synchronized
+ fun clearSurface() {
+ if (surface == null) {
+ Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
+ } else {
+ surface = null
+ Log.debug("[EmulationFragment] Surface destroyed.")
+ when (state) {
+ State.RUNNING -> {
+ NativeLibrary.surfaceDestroyed()
+ state = State.PAUSED
+ }
+
+ State.PAUSED -> {
+ Log.warning("[EmulationFragment] Surface cleared while emulation paused.")
+ }
+
+ else -> {
+ Log.warning("[EmulationFragment] Surface cleared while emulation stopped.")
+ }
+ }
+ }
+ }
+
+ private fun runWithValidSurface() {
+ NativeLibrary.surfaceChanged(surface!!)
+ when (state) {
+ State.STOPPED -> {
+ Thread({
+ Log.debug("[EmulationFragment] Starting emulation thread.")
+ NativeLibrary.run(gamePath)
+ }, "NativeEmulation").start()
+ }
+
+ State.PAUSED -> {
+ Log.debug("[EmulationFragment] Resuming emulation.")
+ NativeLibrary.unPauseEmulation()
+ }
+
+ else -> {
+ Log.debug("[EmulationFragment] Bug, run called while already running.")
+ }
+ }
+ state = State.RUNNING
+ }
+
+ private enum class State {
+ STOPPED,
+ RUNNING,
+ PAUSED
+ }
+ }
+
+ companion object {
+ private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
index 219f769fee..5868234241 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
@@ -27,11 +27,13 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.launch
import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel
@@ -199,7 +201,13 @@ class SystemFilesFragment : Fragment() {
populateHomeMenuOptions()
binding.buttonStartHomeMenu.setOnClickListener {
val menuPath = homeMenuMap[binding.dropdownSystemRegionStart.text.toString()]!!
- EmulationActivity.launch(requireActivity(), menuPath, getString(R.string.home_menu))
+ val menu = Game(
+ title = getString(R.string.home_menu),
+ path = menuPath,
+ filename = ""
+ )
+ val action = HomeNavigationDirections.actionGlobalEmulationActivity(menu)
+ binding.root.findNavController().navigate(action)
}
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
index e4d8da7912..df41beb4e9 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
@@ -352,7 +352,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
}
for (InputOverlayDrawableDpad dpad : overlayDpads) {
- if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) {
+ if (!dpad.updateStatus(event, EmulationMenuSettings.INSTANCE.getDpadSlide())) {
continue;
}
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus());
@@ -608,7 +608,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
"-Portrait" : "";
// Add all the enabled overlay items back to the HashSet.
- if (EmulationMenuSettings.getShowOverlay()) {
+ if (EmulationMenuSettings.INSTANCE.getShowOverlay()) {
addOverlayControls(orientation);
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
index 0f44d5add1..f25771afc3 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
@@ -94,7 +94,7 @@ public final class InputOverlayDrawableJoystick {
mPressedState = true;
mOuterBitmap.setAlpha(0);
mBoundsBoxBitmap.setAlpha(255);
- if (EmulationMenuSettings.getJoystickRelCenter()) {
+ if (EmulationMenuSettings.INSTANCE.getJoystickRelCenter()) {
getVirtBounds().offset(xPosition - getVirtBounds().centerX(),
yPosition - getVirtBounds().centerY());
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
index 6010851ea2..c2aa87de4e 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
@@ -157,7 +157,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
// Dismiss previous notifications (should not happen unless a crash occurred)
- EmulationActivity.tryDismissRunningNotification(this)
+ EmulationActivity.stopForegroundService(this)
setInsets()
}
@@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
override fun onDestroy() {
- EmulationActivity.tryDismissRunningNotification(this)
+ EmulationActivity.stopForegroundService(this)
super.onDestroy()
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt
similarity index 55%
rename from src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java
rename to src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt
index f801a05f0f..7bba904b51 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt
@@ -1,66 +1,69 @@
-package org.citra.citra_emu.utils;
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+package org.citra.citra_emu.utils
+
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
/**
* Some controllers have incorrect mappings. This class has special-case fixes for them.
*/
-public class ControllerMappingHelper {
+object ControllerMappingHelper {
/**
* Some controllers report extra button presses that can be ignored.
*/
- public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) {
- if (isDualShock4(inputDevice)) {
+ fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean {
+ return if (isDualShock4(inputDevice)) {
// The two analog triggers generate analog motion events as well as a keycode.
// We always prefer to use the analog values, so throw away the button press
- return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2;
- }
- return false;
+ keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2
+ } else false
}
/**
* Scale an axis to be zero-centered with a proper range.
*/
- public float scaleAxis(InputDevice inputDevice, int axis, float value) {
+ fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float {
if (isDualShock4(inputDevice)) {
// Android doesn't have correct mappings for this controller's triggers. It reports them
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
// Scale them to properly zero-centered with a range of [0.0, 1.0].
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
- return (value + 1) / 2.0f;
+ return (value + 1) / 2.0f
}
} else if (isXboxOneWireless(inputDevice)) {
// Same as the DualShock 4, the mappings are missing.
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
- return (value + 1) / 2.0f;
+ return (value + 1) / 2.0f
}
if (axis == MotionEvent.AXIS_GENERIC_1) {
// This axis is stuck at ~.5. Ignore it.
- return 0.0f;
+ return 0.0f
}
} else if (isMogaPro2Hid(inputDevice)) {
// This controller has a broken axis that reports a constant value. Ignore it.
if (axis == MotionEvent.AXIS_GENERIC_1) {
- return 0.0f;
+ return 0.0f
}
}
- return value;
+ return value
}
- private boolean isDualShock4(InputDevice inputDevice) {
+ private fun isDualShock4(inputDevice: InputDevice): Boolean {
// Sony DualShock 4 controller
- return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc;
+ return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc
}
- private boolean isXboxOneWireless(InputDevice inputDevice) {
+ private fun isXboxOneWireless(inputDevice: InputDevice): Boolean {
// Microsoft Xbox One controller
- return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0;
+ return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0
}
- private boolean isMogaPro2Hid(InputDevice inputDevice) {
+ private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean {
// Moga Pro 2 HID
- return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271;
+ return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271
}
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java
deleted file mode 100644
index 5897328aea..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright 2021 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-package org.citra.citra_emu.utils;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-import org.citra.citra_emu.utils.Log;
-
-import java.util.Objects;
-
-@Keep
-public class DiskShaderCacheProgress {
-
- // Equivalent to VideoCore::LoadCallbackStage
- public enum LoadCallbackStage {
- Prepare,
- Decompile,
- Build,
- Complete,
- }
-
- private static final Object finishLock = new Object();
- private static ProgressDialogFragment fragment;
-
- public static class ProgressDialogFragment extends DialogFragment {
- ProgressBar progressBar;
- TextView progressText;
- AlertDialog dialog;
-
- static ProgressDialogFragment newInstance(String title, String message) {
- ProgressDialogFragment frag = new ProgressDialogFragment();
- Bundle args = new Bundle();
- args.putString("title", title);
- args.putString("message", message);
- frag.setArguments(args);
- return frag;
- }
-
- @NonNull
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Activity emulationActivity = requireActivity();
-
- final String title = Objects.requireNonNull(requireArguments().getString("title"));
- final String message = Objects.requireNonNull(requireArguments().getString("message"));
-
- LayoutInflater inflater = LayoutInflater.from(emulationActivity);
- View view = inflater.inflate(R.layout.dialog_progress_bar, null);
-
- progressBar = view.findViewById(R.id.progress_bar);
- progressText = view.findViewById(R.id.progress_text);
- progressText.setText("");
-
- setCancelable(false);
- setRetainInstance(true);
-
- synchronized (finishLock) {
- finishLock.notifyAll();
- }
-
- dialog = new MaterialAlertDialogBuilder(emulationActivity)
- .setView(view)
- .setTitle(title)
- .setMessage(message)
- .setNegativeButton(android.R.string.cancel, (dialog, which) -> emulationActivity.onBackPressed())
- .create();
- return dialog;
- }
-
- private void onUpdateProgress(String msg, int progress, int max) {
- requireActivity().runOnUiThread(() -> {
- progressBar.setProgress(progress);
- progressBar.setMax(max);
- progressText.setText(String.format("%d/%d", progress, max));
- dialog.setMessage(msg);
- });
- }
- }
-
- private static void prepareDialog() {
- NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> {
- final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
- fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders));
- fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders");
- });
-
- synchronized (finishLock) {
- try {
- finishLock.wait();
- } catch (Exception ignored) {
- }
- }
- }
-
- public static void loadProgress(LoadCallbackStage stage, int progress, int max) {
- final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
- if (emulationActivity == null) {
- Log.error("[DiskShaderCacheProgress] EmulationActivity not present");
- return;
- }
-
- switch (stage) {
- case Prepare:
- prepareDialog();
- break;
- case Decompile:
- fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max);
- break;
- case Build:
- fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max);
- break;
- case Complete:
- // Workaround for when dialog is dismissed when the app is in the background
- fragment.dismissAllowingStateLoss();
-
- emulationActivity.runOnUiThread(emulationActivity::onEmulationStarted);
- break;
- }
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt
new file mode 100644
index 0000000000..a34924a9a4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt
@@ -0,0 +1,60 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.utils
+
+import androidx.annotation.Keep
+import androidx.lifecycle.ViewModelProvider
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.activities.EmulationActivity
+import org.citra.citra_emu.viewmodel.EmulationViewModel
+
+@Keep
+object DiskShaderCacheProgress {
+ private lateinit var emulationViewModel: EmulationViewModel
+
+ private fun prepareViewModel() {
+ emulationViewModel =
+ ViewModelProvider(
+ NativeLibrary.sEmulationActivity.get() as EmulationActivity
+ )[EmulationViewModel::class.java]
+ }
+
+ @JvmStatic
+ fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int) {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()
+ if (emulationActivity == null) {
+ Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
+ return
+ }
+
+ emulationActivity.runOnUiThread {
+ when (stage) {
+ LoadCallbackStage.Prepare -> prepareViewModel()
+ LoadCallbackStage.Decompile -> emulationViewModel.updateProgress(
+ emulationActivity.getString(R.string.preparing_shaders),
+ progress,
+ max
+ )
+
+ LoadCallbackStage.Build -> emulationViewModel.updateProgress(
+ emulationActivity.getString(R.string.building_shaders),
+ progress,
+ max
+ )
+
+ LoadCallbackStage.Complete -> emulationActivity.onEmulationStarted()
+ }
+ }
+ }
+
+ // Equivalent to VideoCore::LoadCallbackStage
+ enum class LoadCallbackStage {
+ Prepare,
+ Decompile,
+ Build,
+ Complete
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java
deleted file mode 100644
index 2b31876b65..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-
-import org.citra.citra_emu.CitraApplication;
-
-public class EmulationMenuSettings {
- private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-
- // These must match what is defined in src/common/settings.h
- public static final int LayoutOption_Default = 0;
- public static final int LayoutOption_SingleScreen = 1;
- public static final int LayoutOption_LargeScreen = 2;
- public static final int LayoutOption_SideScreen = 3;
- public static final int LayoutOption_MobilePortrait = 5;
- public static final int LayoutOption_MobileLandscape = 6;
-
- public static boolean getJoystickRelCenter() {
- return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true);
- }
-
- public static void setJoystickRelCenter(boolean value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value);
- editor.apply();
- }
-
- public static boolean getDpadSlideEnable() {
- return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true);
- }
-
- public static void setDpadSlideEnable(boolean value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value);
- editor.apply();
- }
-
- public static int getLandscapeScreenLayout() {
- return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape);
- }
-
- public static void setLandscapeScreenLayout(int value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value);
- editor.apply();
- }
-
- public static boolean getShowFps() {
- return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false);
- }
-
- public static void setShowFps(boolean value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putBoolean("EmulationMenuSettings_ShowFps", value);
- editor.apply();
- }
-
- public static boolean getSwapScreens() {
- return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false);
- }
-
- public static void setSwapScreens(boolean value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putBoolean("EmulationMenuSettings_SwapScreens", value);
- editor.apply();
- }
-
- public static boolean getShowOverlay() {
- return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true);
- }
-
- public static void setShowOverlay(boolean value) {
- final SharedPreferences.Editor editor = mPreferences.edit();
- editor.putBoolean("EmulationMenuSettings_ShowOverylay", value);
- editor.apply();
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt
new file mode 100644
index 0000000000..c07636dbbf
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt
@@ -0,0 +1,78 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.utils
+
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.preference.PreferenceManager
+import org.citra.citra_emu.CitraApplication
+
+object EmulationMenuSettings {
+ private val preferences =
+ PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+
+ // These must match what is defined in src/common/settings.h
+ const val LayoutOption_Default = 0
+ const val LayoutOption_SingleScreen = 1
+ const val LayoutOption_LargeScreen = 2
+ const val LayoutOption_SideScreen = 3
+ const val LayoutOption_MobilePortrait = 5
+ const val LayoutOption_MobileLandscape = 6
+
+ var joystickRelCenter: Boolean
+ get() = preferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true)
+ set(value) {
+ preferences.edit()
+ .putBoolean("EmulationMenuSettings_JoystickRelCenter", value)
+ .apply()
+ }
+ var dpadSlide: Boolean
+ get() = preferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true)
+ set(value) {
+ preferences.edit()
+ .putBoolean("EmulationMenuSettings_DpadSlideEnable", value)
+ .apply()
+ }
+ var landscapeScreenLayout: Int
+ get() = preferences.getInt(
+ "EmulationMenuSettings_LandscapeScreenLayout",
+ LayoutOption_MobileLandscape
+ )
+ set(value) {
+ preferences.edit()
+ .putInt("EmulationMenuSettings_LandscapeScreenLayout", value)
+ .apply()
+ }
+ var showFps: Boolean
+ get() = preferences.getBoolean("EmulationMenuSettings_ShowFps", false)
+ set(value) {
+ preferences.edit()
+ .putBoolean("EmulationMenuSettings_ShowFps", value)
+ .apply()
+ }
+ var swapScreens: Boolean
+ get() = preferences.getBoolean("EmulationMenuSettings_SwapScreens", false)
+ set(value) {
+ preferences.edit()
+ .putBoolean("EmulationMenuSettings_SwapScreens", value)
+ .apply()
+ }
+ var showOverlay: Boolean
+ get() = preferences.getBoolean("EmulationMenuSettings_ShowOverlay", true)
+ set(value) {
+ preferences.edit()
+ .putBoolean("EmulationMenuSettings_ShowOverlay", value)
+ .apply()
+ }
+ var drawerLockMode: Int
+ get() = preferences.getInt(
+ "EmulationMenuSettings_DrawerLockMode",
+ DrawerLayout.LOCK_MODE_UNLOCKED
+ )
+ set(value) {
+ preferences.edit()
+ .putInt("EmulationMenuSettings_DrawerLockMode", value)
+ .apply()
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java
deleted file mode 100644
index 021179ab1d..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright 2014 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
-package org.citra.citra_emu.utils;
-
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-
-/**
- * A service that shows a permanent notification in the background to avoid the app getting
- * cleared from memory by the system.
- */
-public class ForegroundService extends Service {
- private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
-
- private void showRunningNotification() {
- // Intent is used to resume emulation if the notification is clicked
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
- new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
- .setSmallIcon(R.drawable.ic_stat_notification_logo)
- .setContentTitle(getString(R.string.app_name))
- .setContentText(getString(R.string.app_notification_running))
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setOngoing(true)
- .setVibrate(null)
- .setSound(null)
- .setContentIntent(contentIntent);
- startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build());
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public void onCreate() {
- showRunningNotification();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- return START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION);
- }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.kt
new file mode 100644
index 0000000000..2d7f2368c7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.kt
@@ -0,0 +1,70 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.utils
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import org.citra.citra_emu.R
+import org.citra.citra_emu.activities.EmulationActivity
+
+/**
+ * A service that shows a permanent notification in the background to avoid the app getting
+ * cleared from memory by the system.
+ */
+class ForegroundService : Service() {
+ companion object {
+ const val EMULATION_RUNNING_NOTIFICATION = 0x1000
+
+ const val ACTION_STOP = "stop"
+ }
+
+ private fun showRunningNotification() {
+ // Intent is used to resume emulation if the notification is clicked
+ val contentIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ Intent(this, EmulationActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ val builder =
+ NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
+ .setSmallIcon(R.drawable.ic_stat_notification_logo)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.app_notification_running))
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .setVibrate(null)
+ .setSound(null)
+ .setContentIntent(contentIntent)
+ startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build())
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ showRunningNotification()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) {
+ return START_NOT_STICKY
+ }
+ if (intent.action == ACTION_STOP) {
+ NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelfResult(startId)
+ }
+ return START_STICKY
+ }
+
+ override fun onDestroy() =
+ NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt
new file mode 100644
index 0000000000..3a5571e9bc
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt
@@ -0,0 +1,45 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.viewmodel
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class EmulationViewModel : ViewModel() {
+ val emulationStarted get() = _emulationStarted.asStateFlow()
+ private val _emulationStarted = MutableStateFlow(false)
+
+ val shaderProgress get() = _shaderProgress.asStateFlow()
+ private val _shaderProgress = MutableStateFlow(0)
+
+ val totalShaders get() = _totalShaders.asStateFlow()
+ private val _totalShaders = MutableStateFlow(0)
+
+ val shaderMessage get() = _shaderMessage.asStateFlow()
+ private val _shaderMessage = MutableStateFlow("")
+
+ fun setShaderProgress(progress: Int) {
+ _shaderProgress.value = progress
+ }
+
+ fun setTotalShaders(max: Int) {
+ _totalShaders.value = max
+ }
+
+ fun setShaderMessage(msg: String) {
+ _shaderMessage.value = msg
+ }
+
+ fun updateProgress(msg: String, progress: Int, max: Int) {
+ setShaderMessage(msg)
+ setShaderProgress(progress)
+ setTotalShaders(max)
+ }
+
+ fun setEmulationStarted(started: Boolean) {
+ _emulationStarted.value = started
+ }
+}
diff --git a/src/android/app/src/main/res/drawable/ic_code.xml b/src/android/app/src/main/res/drawable/ic_code.xml
new file mode 100644
index 0000000000..8ef40bd2d2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_code.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_exit.xml b/src/android/app/src/main/res/drawable/ic_exit.xml
new file mode 100644
index 0000000000..6ac037504b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_exit.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_fit_screen.xml b/src/android/app/src/main/res/drawable/ic_fit_screen.xml
new file mode 100644
index 0000000000..ba2ef18fcf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_fit_screen.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_lock.xml b/src/android/app/src/main/res/drawable/ic_lock.xml
new file mode 100644
index 0000000000..11918298c5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_lock.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_nfc.xml b/src/android/app/src/main/res/drawable/ic_nfc.xml
new file mode 100644
index 0000000000..3dacf798b5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_nfc.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_pause.xml b/src/android/app/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 0000000000..ccaed71872
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_play.xml b/src/android/app/src/main/res/drawable/ic_play.xml
new file mode 100644
index 0000000000..e702965caa
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_play.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml
new file mode 100644
index 0000000000..5acc2bbab8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_save.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_splitscreen.xml b/src/android/app/src/main/res/drawable/ic_splitscreen.xml
new file mode 100644
index 0000000000..b517b6f48f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_splitscreen.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/drawable/ic_unlocked.xml b/src/android/app/src/main/res/drawable/ic_unlocked.xml
new file mode 100644
index 0000000000..40952cbc51
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_unlocked.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
index 5f5ff777d1..139065d3d6 100644
--- a/src/android/app/src/main/res/layout/activity_emulation.xml
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -1,23 +1,9 @@
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:keepScreenOn="true"
+ app:defaultNavHost="true" />
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
index bd64d5d164..f076cc3378 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -1,46 +1,141 @@
-
+ tools:openDrawer="start">
-
-
+ android:layout_height="match_parent">
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ app:headerLayout="@layout/header_in_game"
+ app:menu="@menu/menu_in_game"
+ tools:visibility="gone" />
-
-
-
+
diff --git a/src/android/app/src/main/res/layout/header_in_game.xml b/src/android/app/src/main/res/layout/header_in_game.xml
new file mode 100644
index 0000000000..eb30dd15f2
--- /dev/null
+++ b/src/android/app/src/main/res/layout/header_in_game.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/src/android/app/src/main/res/menu/menu_amiibo_options.xml b/src/android/app/src/main/res/menu/menu_amiibo_options.xml
new file mode 100644
index 0000000000..a5b4d9a260
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_amiibo_options.xml
@@ -0,0 +1,13 @@
+
+
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml
new file mode 100644
index 0000000000..56bb99ed2d
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_in_game.xml
@@ -0,0 +1,55 @@
+
+
diff --git a/src/android/app/src/main/res/menu/menu_landscape_screen_layout.xml b/src/android/app/src/main/res/menu/menu_landscape_screen_layout.xml
new file mode 100644
index 0000000000..2b9e6f8059
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_landscape_screen_layout.xml
@@ -0,0 +1,24 @@
+
+
diff --git a/src/android/app/src/main/res/menu/menu_overlay_options.xml b/src/android/app/src/main/res/menu/menu_overlay_options.xml
new file mode 100644
index 0000000000..18b315c880
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_overlay_options.xml
@@ -0,0 +1,41 @@
+
+
diff --git a/src/android/app/src/main/res/menu/menu_savestates.xml b/src/android/app/src/main/res/menu/menu_savestates.xml
new file mode 100644
index 0000000000..d85edbfb87
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_savestates.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
new file mode 100644
index 0000000000..c8e628cdad
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 5dfa19e1e1..5770ad58d9 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -65,6 +65,11 @@
android:defaultValue="@null" />
+
+
Your ROM is Encrypted
Invalid ROM format
ROM file does not exist
+ No bootable game present!
Press Back to access the menu.
@@ -320,6 +321,7 @@
Slot %1$d
Slot %1$d - %2$tF %2$tR
Show FPS
+ Overlay Options
Configure Controls
Edit Layout
Done
@@ -345,6 +347,10 @@
Select Amiibo File
Error Loading Amiibo
While loading the specified Amiibo file, an error occurred. Please check that the file is correct.
+ Pause Emulation
+ Resume Emulation
+ Lock Drawer
+ Unlock Drawer
You need to allow write access to external storage for the emulator to work
Loading Settings…
@@ -360,7 +366,7 @@
Moving Data…
Copy file: %s
Copy Complete
- Savestates
+ Save States
Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk!