mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	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
					
				
					 42 changed files with 2307 additions and 1563 deletions
				
			
		|  | @ -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 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/android/app/src/main/res/drawable/ic_code.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/android/app/src/main/res/drawable/ic_code.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										10
									
								
								src/android/app/src/main/res/drawable/ic_exit.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/android/app/src/main/res/drawable/ic_exit.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_fit_screen.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_fit_screen.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_lock.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_lock.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_nfc.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_nfc.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pause.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pause.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_play.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_play.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_save.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_save.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_splitscreen.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_splitscreen.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_unlocked.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_unlocked.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										14
									
								
								src/android/app/src/main/res/layout/header_in_game.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/android/app/src/main/res/layout/header_in_game.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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" /> | ||||
							
								
								
									
										13
									
								
								src/android/app/src/main/res/menu/menu_amiibo_options.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/android/app/src/main/res/menu/menu_amiibo_options.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										55
									
								
								src/android/app/src/main/res/menu/menu_in_game.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/android/app/src/main/res/menu/menu_in_game.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										41
									
								
								src/android/app/src/main/res/menu/menu_overlay_options.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/android/app/src/main/res/menu/menu_overlay_options.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										12
									
								
								src/android/app/src/main/res/menu/menu_savestates.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/android/app/src/main/res/menu/menu_savestates.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 --> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue