mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 13:20:03 +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 |         <activity | ||||||
|             android:name="org.citra.citra_emu.activities.EmulationActivity" |             android:name="org.citra.citra_emu.activities.EmulationActivity" | ||||||
|             android:exported="true" |             android:exported="true" | ||||||
|             android:resizeableActivity="false" |  | ||||||
|             android:theme="@style/Theme.Citra.Main" |             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"> |         <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"/> |             <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/> | ||||||
|  |  | ||||||
|  | @ -252,7 +252,7 @@ object NativeLibrary { | ||||||
| 
 | 
 | ||||||
|     @Keep |     @Keep | ||||||
|     @JvmStatic |     @JvmStatic | ||||||
|     fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout() |     fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout | ||||||
| 
 | 
 | ||||||
|     @Keep |     @Keep | ||||||
|     @JvmStatic |     @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.appcompat.app.AppCompatActivity | ||||||
| import androidx.documentfile.provider.DocumentFile | import androidx.documentfile.provider.DocumentFile | ||||||
| import androidx.lifecycle.ViewModelProvider | import androidx.lifecycle.ViewModelProvider | ||||||
|  | import androidx.navigation.findNavController | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
| import androidx.recyclerview.widget.AsyncDifferConfig | import androidx.recyclerview.widget.AsyncDifferConfig | ||||||
| import androidx.recyclerview.widget.DiffUtil | import androidx.recyclerview.widget.DiffUtil | ||||||
|  | @ -22,6 +23,7 @@ import androidx.recyclerview.widget.ListAdapter | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.google.android.material.color.MaterialColors | import com.google.android.material.color.MaterialColors | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||||
|  | import org.citra.citra_emu.HomeNavigationDirections | ||||||
| import org.citra.citra_emu.CitraApplication | import org.citra.citra_emu.CitraApplication | ||||||
| import org.citra.citra_emu.R | import org.citra.citra_emu.R | ||||||
| import org.citra.citra_emu.activities.EmulationActivity | import org.citra.citra_emu.activities.EmulationActivity | ||||||
|  | @ -77,7 +79,8 @@ class GameAdapter(private val activity: AppCompatActivity) : | ||||||
|             ) |             ) | ||||||
|             .apply() |             .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 com.google.android.material.transition.MaterialSharedAxis | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import org.citra.citra_emu.CitraApplication | import org.citra.citra_emu.CitraApplication | ||||||
|  | import org.citra.citra_emu.HomeNavigationDirections | ||||||
| import org.citra.citra_emu.NativeLibrary | import org.citra.citra_emu.NativeLibrary | ||||||
| import org.citra.citra_emu.R | import org.citra.citra_emu.R | ||||||
| import org.citra.citra_emu.activities.EmulationActivity | import org.citra.citra_emu.activities.EmulationActivity | ||||||
| import org.citra.citra_emu.databinding.FragmentSystemFilesBinding | import org.citra.citra_emu.databinding.FragmentSystemFilesBinding | ||||||
| import org.citra.citra_emu.features.settings.model.Settings | 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.utils.SystemSaveGame | ||||||
| import org.citra.citra_emu.viewmodel.GamesViewModel | import org.citra.citra_emu.viewmodel.GamesViewModel | ||||||
| import org.citra.citra_emu.viewmodel.HomeViewModel | import org.citra.citra_emu.viewmodel.HomeViewModel | ||||||
|  | @ -199,7 +201,13 @@ class SystemFilesFragment : Fragment() { | ||||||
|         populateHomeMenuOptions() |         populateHomeMenuOptions() | ||||||
|         binding.buttonStartHomeMenu.setOnClickListener { |         binding.buttonStartHomeMenu.setOnClickListener { | ||||||
|             val menuPath = homeMenuMap[binding.dropdownSystemRegionStart.text.toString()]!! |             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) { |         for (InputOverlayDrawableDpad dpad : overlayDpads) { | ||||||
|             if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) { |             if (!dpad.updateStatus(event, EmulationMenuSettings.INSTANCE.getDpadSlide())) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus()); |             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus()); | ||||||
|  | @ -608,7 +608,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener { | ||||||
|                         "-Portrait" : ""; |                         "-Portrait" : ""; | ||||||
| 
 | 
 | ||||||
|         // Add all the enabled overlay items back to the HashSet. |         // Add all the enabled overlay items back to the HashSet. | ||||||
|         if (EmulationMenuSettings.getShowOverlay()) { |         if (EmulationMenuSettings.INSTANCE.getShowOverlay()) { | ||||||
|             addOverlayControls(orientation); |             addOverlayControls(orientation); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ public final class InputOverlayDrawableJoystick { | ||||||
|             mPressedState = true; |             mPressedState = true; | ||||||
|             mOuterBitmap.setAlpha(0); |             mOuterBitmap.setAlpha(0); | ||||||
|             mBoundsBoxBitmap.setAlpha(255); |             mBoundsBoxBitmap.setAlpha(255); | ||||||
|             if (EmulationMenuSettings.getJoystickRelCenter()) { |             if (EmulationMenuSettings.INSTANCE.getJoystickRelCenter()) { | ||||||
|                 getVirtBounds().offset(xPosition - getVirtBounds().centerX(), |                 getVirtBounds().offset(xPosition - getVirtBounds().centerX(), | ||||||
|                         yPosition - getVirtBounds().centerY()); |                         yPosition - getVirtBounds().centerY()); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -157,7 +157,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Dismiss previous notifications (should not happen unless a crash occurred) |         // Dismiss previous notifications (should not happen unless a crash occurred) | ||||||
|         EmulationActivity.tryDismissRunningNotification(this) |         EmulationActivity.stopForegroundService(this) | ||||||
| 
 | 
 | ||||||
|         setInsets() |         setInsets() | ||||||
|     } |     } | ||||||
|  | @ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onDestroy() { |     override fun onDestroy() { | ||||||
|         EmulationActivity.tryDismissRunningNotification(this) |         EmulationActivity.stopForegroundService(this) | ||||||
|         super.onDestroy() |         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; | package org.citra.citra_emu.utils | ||||||
| import android.view.KeyEvent; | 
 | ||||||
| import android.view.MotionEvent; | 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. |  * 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. |      * Some controllers report extra button presses that can be ignored. | ||||||
|      */ |      */ | ||||||
|     public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) { |     fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean { | ||||||
|         if (isDualShock4(inputDevice)) { |         return if (isDualShock4(inputDevice)) { | ||||||
|             // The two analog triggers generate analog motion events as well as a keycode. |             // 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 |             // 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; |             keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 | ||||||
|         } |         } else false | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Scale an axis to be zero-centered with a proper range. |      * 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)) { |         if (isDualShock4(inputDevice)) { | ||||||
|             // Android doesn't have correct mappings for this controller's triggers. It reports them |             // 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] |             // 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]. |             // Scale them to properly zero-centered with a range of [0.0, 1.0]. | ||||||
|             if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { |             if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { | ||||||
|                 return (value + 1) / 2.0f; |                 return (value + 1) / 2.0f | ||||||
|             } |             } | ||||||
|         } else if (isXboxOneWireless(inputDevice)) { |         } else if (isXboxOneWireless(inputDevice)) { | ||||||
|             // Same as the DualShock 4, the mappings are missing. |             // Same as the DualShock 4, the mappings are missing. | ||||||
|             if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { |             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) { |             if (axis == MotionEvent.AXIS_GENERIC_1) { | ||||||
|                 // This axis is stuck at ~.5. Ignore it. |                 // This axis is stuck at ~.5. Ignore it. | ||||||
|                 return 0.0f; |                 return 0.0f | ||||||
|             } |             } | ||||||
|         } else if (isMogaPro2Hid(inputDevice)) { |         } else if (isMogaPro2Hid(inputDevice)) { | ||||||
|             // This controller has a broken axis that reports a constant value. Ignore it. |             // This controller has a broken axis that reports a constant value. Ignore it. | ||||||
|             if (axis == MotionEvent.AXIS_GENERIC_1) { |             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 |         // 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 |         // 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 |         // 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" | <androidx.fragment.app.FragmentContainerView | ||||||
|     android:id="@+id/frame_content" |     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_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent" | ||||||
| 
 |     android:keepScreenOn="true" | ||||||
|     <FrameLayout |     app:defaultNavHost="true" /> | ||||||
|         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> |  | ||||||
|  |  | ||||||
|  | @ -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" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:id="@+id/drawer_layout" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent" |     android:layout_height="match_parent" | ||||||
|     android:keepScreenOn="true" |     android:keepScreenOn="true" | ||||||
|     tools:context="org.citra.citra_emu.fragments.EmulationFragment"> |     tools:openDrawer="start"> | ||||||
| 
 | 
 | ||||||
|     <!-- This is what everything is rendered to during emulation --> |     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||||
|     <SurfaceView |  | ||||||
|         android:id="@+id/surface_emulation" |  | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent" |         android:layout_height="match_parent"> | ||||||
|         android:focusable="false" |  | ||||||
|         android:focusableInTouchMode="false" /> |  | ||||||
| 
 | 
 | ||||||
|     <!-- This is the onscreen input overlay --> |         <!-- This is what everything is rendered to during emulation --> | ||||||
|     <org.citra.citra_emu.overlay.InputOverlay |         <SurfaceView | ||||||
|         android:id="@+id/surface_input_overlay" |             android:id="@+id/surface_emulation" | ||||||
|         android:layout_height="match_parent" |             android:layout_width="match_parent" | ||||||
|         android:layout_width="match_parent" |             android:layout_height="match_parent" | ||||||
|         android:focusable="true" |             android:layout_gravity="center" | ||||||
|         android:focusableInTouchMode="true" /> |             android:defaultFocusHighlightEnabled="false" | ||||||
|  |             android:focusable="false" | ||||||
|  |             android:focusableInTouchMode="false" /> | ||||||
| 
 | 
 | ||||||
|     <TextView |         <!-- This is the onscreen input overlay --> | ||||||
|         android:id="@+id/show_fps_text" |         <org.citra.citra_emu.overlay.InputOverlay | ||||||
|         android:layout_marginStart="8dp" |             android:id="@+id/surface_input_overlay" | ||||||
|         android:layout_marginTop="2dp" |             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_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="match_parent" | ||||||
|         android:clickable="false" |         android:layout_gravity="start" | ||||||
|         android:linksClickable="false" |         app:headerLayout="@layout/header_in_game" | ||||||
|         android:longClickable="false" |         app:menu="@menu/menu_in_game" | ||||||
|         android:shadowColor="@android:color/black" |         tools:visibility="gone" /> | ||||||
|         android:textColor="@android:color/white" |  | ||||||
|         android:textSize="12sp" /> |  | ||||||
| 
 | 
 | ||||||
|     <Button | </androidx.drawerlayout.widget.DrawerLayout> | ||||||
|         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> |  | ||||||
|  |  | ||||||
							
								
								
									
										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" /> |             android:defaultValue="@null" /> | ||||||
|     </activity> |     </activity> | ||||||
| 
 | 
 | ||||||
|  |     <action | ||||||
|  |         android:id="@+id/action_global_emulationActivity" | ||||||
|  |         app:destination="@id/emulationActivity" | ||||||
|  |         app:launchSingleTop="true" /> | ||||||
|  | 
 | ||||||
|     <fragment |     <fragment | ||||||
|         android:id="@+id/systemFilesFragment" |         android:id="@+id/systemFilesFragment" | ||||||
|         android:name="org.citra.citra_emu.fragments.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_encrypted">Your ROM is Encrypted</string> | ||||||
|     <string name="loader_error_invalid_format">Invalid ROM format</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="loader_error_file_not_found">ROM file does not exist</string> | ||||||
|  |     <string name="no_game_present">No bootable game present!</string> | ||||||
| 
 | 
 | ||||||
|     <!-- Emulation Menu --> |     <!-- Emulation Menu --> | ||||||
|     <string name="emulation_menu_help">Press Back to access the menu.</string> |     <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_empty_state_slot">Slot %1$d</string> | ||||||
|     <string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</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_show_fps">Show FPS</string> | ||||||
|  |     <string name="emulation_overlay_options">Overlay Options</string> | ||||||
|     <string name="emulation_configure_controls">Configure Controls</string> |     <string name="emulation_configure_controls">Configure Controls</string> | ||||||
|     <string name="emulation_edit_layout">Edit Layout</string> |     <string name="emulation_edit_layout">Edit Layout</string> | ||||||
|     <string name="emulation_done">Done</string> |     <string name="emulation_done">Done</string> | ||||||
|  | @ -345,6 +347,10 @@ | ||||||
|     <string name="select_amiibo">Select Amiibo File</string> |     <string name="select_amiibo">Select Amiibo File</string> | ||||||
|     <string name="amiibo_load_error">Error Loading Amiibo</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="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="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> |     <string name="load_settings">Loading Settings…</string> | ||||||
|  | @ -360,7 +366,7 @@ | ||||||
|     <string name="moving_data">Moving Data…</string> |     <string name="moving_data">Moving Data…</string> | ||||||
|     <string name="copy_file_name">Copy file: %s</string> |     <string name="copy_file_name">Copy file: %s</string> | ||||||
|     <string name="copy_complete">Copy Complete</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> |     <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 --> |     <!-- Software Keyboard --> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue