Android UI Overhaul Part 3 (#7216)
* android: Rework Emulation Activity's UI - New in-game menu - Ability to open games from file manager - New shader loading UI - Fixes an issue where the system bars would stay visible during emulation * android: Port yuzu's foreground service logic Fixes an issue where the foreground service notification would be stuck with no way to dismiss it
This commit is contained in:
parent
0ed909e782
commit
59beeac4c7
|
@ -64,9 +64,18 @@
|
|||
<activity
|
||||
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@style/Theme.Citra.Main"
|
||||
android:launchMode="singleTop"/>
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="application/octet-stream"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||
|
|
|
@ -252,7 +252,7 @@ object NativeLibrary {
|
|||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
|
||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
|
|
|
@ -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<Boolean> 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<InputDevice.MotionRange> 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 {
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
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<String>("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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M17,4h3c1.1,0 2,0.9 2,2v2h-2L20,6h-3L17,4zM4,8L4,6h3L7,4L4,4c-1.1,0 -2,0.9 -2,2v2h2zM20,16v2h-3v2h3c1.1,0 2,-0.9 2,-2v-2h-2zM7,18L4,18v-2L2,16v2c0,1.1 0.9,2 2,2h3v-2zM18,8L6,8v8h12L18,8z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8L9,6zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M8,5v14l11,-7z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M18,4v5H6V4H18M18,2H6C4.9,2 4,2.9 4,4v5c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM18,15v5H6v-5H18M18,13H6c-1.1,0 -2,0.9 -2,2v5c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-5C20,13.9 19.1,13 18,13z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
|
||||
</vector>
|
|
@ -1,23 +1,9 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/frame_content"
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/fragment_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_emulation_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionName="image_game_icon" />
|
||||
|
||||
<View
|
||||
android:id="@+id/menu_anchor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top|end" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
app:defaultNavHost="true" />
|
||||
|
|
|
@ -1,46 +1,141 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
tools:context="org.citra.citra_emu.fragments.EmulationFragment">
|
||||
tools:openDrawer="start">
|
||||
|
||||
<!-- This is what everything is rendered to during emulation -->
|
||||
<SurfaceView
|
||||
android:id="@+id/surface_emulation"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- This is the onscreen input overlay -->
|
||||
<org.citra.citra_emu.overlay.InputOverlay
|
||||
android:id="@+id/surface_input_overlay"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
<!-- This is what everything is rendered to during emulation -->
|
||||
<SurfaceView
|
||||
android:id="@+id/surface_emulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/show_fps_text"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
<!-- This is the onscreen input overlay -->
|
||||
<org.citra.citra_emu.overlay.InputOverlay
|
||||
android:id="@+id/surface_input_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/done_control_config"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/emulation_done"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/loading_indicator"
|
||||
style="?attr/materialCardViewOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="false">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loading_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loading_image"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/no_icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="20dp"
|
||||
android:orientation="vertical"
|
||||
android:animateLayoutChanges="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/loading_image"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/loading_title"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/games" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/loading_text"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/loading"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_progress_indicator"
|
||||
android:layout_width="192dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:indeterminate="true"
|
||||
app:trackCornerRadius="8dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/loading_progress_text"
|
||||
style="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="10/100" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/show_fps_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/in_game_menu"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:linksClickable="false"
|
||||
android:longClickable="false"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:headerLayout="@layout/header_in_game"
|
||||
app:menu="@menu/menu_in_game"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/done_control_config"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/spacing_small"
|
||||
android:text="@string/emulation_done"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text_game_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="Super Mario 3D Land" />
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:title="@string/menu_emulation_amiibo">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_amiibo_load"
|
||||
android:title="@string/menu_emulation_amiibo_load" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_amiibo_remove"
|
||||
android:title="@string/menu_emulation_amiibo_remove" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_pause"
|
||||
android:icon="@drawable/ic_pause"
|
||||
android:title="@string/pause_emulation" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_savestates"
|
||||
android:icon="@drawable/ic_save"
|
||||
android:title="@string/savestates"
|
||||
android:visible="false" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_overlay_options"
|
||||
android:icon="@drawable/ic_controller"
|
||||
android:title="@string/emulation_overlay_options" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_amiibo"
|
||||
android:icon="@drawable/ic_nfc"
|
||||
android:title="@string/menu_emulation_amiibo" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_landscape_screen_layout"
|
||||
android:icon="@drawable/ic_fit_screen"
|
||||
android:title="@string/emulation_switch_screen_layout" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_swap_screens"
|
||||
android:icon="@drawable/ic_splitscreen"
|
||||
android:title="@string/emulation_swap_screens" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_lock_drawer"
|
||||
android:icon="@drawable/ic_unlocked"
|
||||
android:title="@string/lock_drawer" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_cheats"
|
||||
android:icon="@drawable/ic_code"
|
||||
android:title="@string/cheats" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/preferences_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_exit"
|
||||
android:icon="@drawable/ic_exit"
|
||||
android:title="@string/emulation_close_game" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<group android:checkableBehavior="single">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_screen_layout_landscape"
|
||||
android:title="@string/emulation_screen_layout_landscape" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_screen_layout_portrait"
|
||||
android:title="@string/emulation_screen_layout_portrait" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_screen_layout_single"
|
||||
android:title="@string/emulation_screen_layout_single" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_screen_layout_sidebyside"
|
||||
android:title="@string/emulation_screen_layout_sidebyside" />
|
||||
|
||||
</group>
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_overlay"
|
||||
android:title="@string/emulation_show_overlay"
|
||||
android:checkable="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_fps"
|
||||
android:title="@string/emulation_show_fps"
|
||||
android:checkable="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_edit_layout"
|
||||
android:title="@string/emulation_edit_layout" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_toggle_controls"
|
||||
android:title="@string/emulation_toggle_controls" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_adjust_scale"
|
||||
android:title="@string/emulation_control_scale" />
|
||||
|
||||
<group android:checkableBehavior="all">
|
||||
<item
|
||||
android:id="@+id/menu_emulation_joystick_rel_center"
|
||||
android:checkable="true"
|
||||
android:title="@string/emulation_control_joystick_rel_center"/>
|
||||
<item
|
||||
android:id="@+id/menu_emulation_dpad_slide_enable"
|
||||
android:checkable="true"
|
||||
android:title="@string/emulation_control_dpad_slide_enable" />
|
||||
</group>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_reset_overlay"
|
||||
android:title="@string/emulation_touch_overlay_reset" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_save_state"
|
||||
android:title="@string/emulation_save_state" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_load_state"
|
||||
android:title="@string/emulation_load_state" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/emulation_navigation"
|
||||
app:startDestination="@id/emulationFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/emulationFragment"
|
||||
android:name="org.citra.citra_emu.fragments.EmulationFragment"
|
||||
android:label="fragment_emulation"
|
||||
tools:layout="@layout/fragment_emulation" >
|
||||
<argument
|
||||
android:name="game"
|
||||
app:argType="org.citra.citra_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/cheatsActivity"
|
||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||
android:label="CheatsActivity">
|
||||
<argument
|
||||
android:name="titleId"
|
||||
app:argType="long"
|
||||
android:defaultValue="-1L" />
|
||||
</activity>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_cheatsActivity"
|
||||
app:destination="@id/cheatsActivity" />
|
||||
|
||||
</navigation>
|
|
@ -65,6 +65,11 @@
|
|||
android:defaultValue="@null" />
|
||||
</activity>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_emulationActivity"
|
||||
app:destination="@id/emulationActivity"
|
||||
app:launchSingleTop="true" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/systemFilesFragment"
|
||||
android:name="org.citra.citra_emu.fragments.SystemFilesFragment"
|
||||
|
|
|
@ -312,6 +312,7 @@
|
|||
<string name="loader_error_encrypted">Your ROM is Encrypted</string>
|
||||
<string name="loader_error_invalid_format">Invalid ROM format</string>
|
||||
<string name="loader_error_file_not_found">ROM file does not exist</string>
|
||||
<string name="no_game_present">No bootable game present!</string>
|
||||
|
||||
<!-- Emulation Menu -->
|
||||
<string name="emulation_menu_help">Press Back to access the menu.</string>
|
||||
|
@ -320,6 +321,7 @@
|
|||
<string name="emulation_empty_state_slot">Slot %1$d</string>
|
||||
<string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</string>
|
||||
<string name="emulation_show_fps">Show FPS</string>
|
||||
<string name="emulation_overlay_options">Overlay Options</string>
|
||||
<string name="emulation_configure_controls">Configure Controls</string>
|
||||
<string name="emulation_edit_layout">Edit Layout</string>
|
||||
<string name="emulation_done">Done</string>
|
||||
|
@ -345,6 +347,10 @@
|
|||
<string name="select_amiibo">Select Amiibo File</string>
|
||||
<string name="amiibo_load_error">Error Loading Amiibo</string>
|
||||
<string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string>
|
||||
<string name="pause_emulation">Pause Emulation</string>
|
||||
<string name="resume_emulation">Resume Emulation</string>
|
||||
<string name="lock_drawer">Lock Drawer</string>
|
||||
<string name="unlock_drawer">Unlock Drawer</string>
|
||||
|
||||
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
|
||||
<string name="load_settings">Loading Settings…</string>
|
||||
|
@ -360,7 +366,7 @@
|
|||
<string name="moving_data">Moving Data…</string>
|
||||
<string name="copy_file_name">Copy file: %s</string>
|
||||
<string name="copy_complete">Copy Complete</string>
|
||||
<string name="savestate_warning_title">Savestates</string>
|
||||
<string name="savestates">Save States</string>
|
||||
<string name="savestate_warning_message">Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk!</string>
|
||||
|
||||
<!-- Software Keyboard -->
|
||||
|
|
Reference in New Issue