summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/android/.gitignore62
-rw-r--r--src/android/app/build.gradle163
-rw-r--r--src/android/app/proguard-rules.pro21
-rw-r--r--src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java3
-rw-r--r--src/android/app/src/main/AndroidManifest.xml99
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java56
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java631
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java38
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java755
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java247
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java122
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java264
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java65
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java140
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java138
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java57
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java13
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java177
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java174
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java46
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java56
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java161
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java72
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java23
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java23
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java23
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java42
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java55
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java132
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java23
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java80
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java40
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java14
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java382
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java12
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java59
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java107
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java60
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java101
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java82
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java21
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java215
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java124
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java103
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java487
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java136
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java416
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java78
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java48
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java54
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java47
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java32
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java55
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java57
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java49
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java76
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java45
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java45
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java341
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java120
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java380
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/model/Game.java76
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java276
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java138
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java878
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java122
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java193
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java264
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java130
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java37
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java267
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java82
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java25
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java86
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java42
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java21
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java5
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java38
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java22
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java215
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java66
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java186
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java22
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java78
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java73
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java37
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java63
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java27
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java39
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java35
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java45
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java57
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java45
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java34
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java46
-rw-r--r--src/android/app/src/main/res/animator/settings_enter.xml28
-rw-r--r--src/android/app/src/main/res/animator/settings_exit.xml28
-rw-r--r--src/android/app/src/main/res/animator/settings_pop_enter.xml28
-rw-r--r--src/android/app/src/main/res/animator/setttings_pop_exit.xml27
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_a.pngbin0 -> 10674 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_a_pressed.pngbin0 -> 10738 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_b.pngbin0 -> 9479 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_b_pressed.pngbin0 -> 9555 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_l.pngbin0 -> 2738 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_l_pressed.pngbin0 -> 2795 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_r.pngbin0 -> 5680 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_r_pressed.pngbin0 -> 5784 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_select.pngbin0 -> 13280 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_select_pressed.pngbin0 -> 13344 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_start.pngbin0 -> 9518 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_start_pressed.pngbin0 -> 14872 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_x.pngbin0 -> 12124 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_x_pressed.pngbin0 -> 12390 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_y.pngbin0 -> 9321 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_y_pressed.pngbin0 -> 9498 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_zl.pngbin0 -> 4423 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.pngbin0 -> 4426 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_zr.pngbin0 -> 6239 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.pngbin0 -> 6201 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/dpad.pngbin0 -> 4273 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.pngbin0 -> 3824 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.pngbin0 -> 5658 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_cia_install.pngbin0 -> 514 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_folder.pngbin0 -> 275 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_premium.pngbin0 -> 961 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_settings_core.pngbin0 -> 793 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.pngbin0 -> 2824 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_c.pngbin0 -> 14819 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.pngbin0 -> 14825 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_c_range.pngbin0 -> 8813 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_main.pngbin0 -> 12828 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.pngbin0 -> 8244 bytes
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/stick_main_range.pngbin0 -> 32592 bytes
-rw-r--r--src/android/app/src/main/res/drawable-mdpi/ic_cia_install.pngbin0 -> 364 bytes
-rw-r--r--src/android/app/src/main/res/drawable-mdpi/ic_folder.pngbin0 -> 214 bytes
-rw-r--r--src/android/app/src/main/res/drawable-mdpi/ic_premium.pngbin0 -> 605 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.pngbin0 -> 556 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-hdpi/ic_folder.pngbin0 -> 289 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-hdpi/ic_premium.pngbin0 -> 955 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.pngbin0 -> 1152 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.pngbin0 -> 405 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-mdpi/ic_folder.pngbin0 -> 227 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-mdpi/ic_premium.pngbin0 -> 595 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.pngbin0 -> 729 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.pngbin0 -> 347 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.pngbin0 -> 1281 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.pngbin0 -> 1431 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.pngbin0 -> 1168 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.pngbin0 -> 555 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.pngbin0 -> 2049 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.pngbin0 -> 2125 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.pngbin0 -> 1433 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.pngbin0 -> 657 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.pngbin0 -> 2614 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.pngbin0 -> 2587 bytes
-rw-r--r--src/android/app/src/main/res/drawable-night/no_icon.pngbin0 -> 9238 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_a.pngbin0 -> 14645 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.pngbin0 -> 14643 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_b.pngbin0 -> 13040 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.pngbin0 -> 13046 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_l.pngbin0 -> 3461 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.pngbin0 -> 3471 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_r.pngbin0 -> 7603 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.pngbin0 -> 7595 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_select.pngbin0 -> 17681 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.pngbin0 -> 17648 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_start.pngbin0 -> 19588 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.pngbin0 -> 19743 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_x.pngbin0 -> 16315 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.pngbin0 -> 16543 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_y.pngbin0 -> 12529 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.pngbin0 -> 12698 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_zl.pngbin0 -> 5584 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.pngbin0 -> 5616 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_zr.pngbin0 -> 8283 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.pngbin0 -> 8330 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/dpad.pngbin0 -> 5296 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.pngbin0 -> 4781 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.pngbin0 -> 7857 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.pngbin0 -> 656 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_folder.pngbin0 -> 325 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_premium.pngbin0 -> 1334 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.pngbin0 -> 1029 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.pngbin0 -> 4026 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_c.pngbin0 -> 23215 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.pngbin0 -> 20594 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_c_range.pngbin0 -> 18277 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_main.pngbin0 -> 19086 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.pngbin0 -> 11657 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/stick_main_range.pngbin0 -> 53646 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_a.pngbin0 -> 23552 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.pngbin0 -> 23611 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_b.pngbin0 -> 20371 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.pngbin0 -> 20591 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_l.pngbin0 -> 5288 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.pngbin0 -> 5352 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_r.pngbin0 -> 11960 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.pngbin0 -> 11969 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_select.pngbin0 -> 27251 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.pngbin0 -> 27436 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_start.pngbin0 -> 30505 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.pngbin0 -> 30785 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_x.pngbin0 -> 27021 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.pngbin0 -> 27645 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_y.pngbin0 -> 19978 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.pngbin0 -> 20426 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_zl.pngbin0 -> 8675 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.pngbin0 -> 8675 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_zr.pngbin0 -> 13105 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.pngbin0 -> 13182 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/dpad.pngbin0 -> 7816 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.pngbin0 -> 6977 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.pngbin0 -> 12762 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.pngbin0 -> 967 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_folder.pngbin0 -> 487 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_premium.pngbin0 -> 2096 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.pngbin0 -> 1647 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.pngbin0 -> 5936 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_c.pngbin0 -> 41218 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.pngbin0 -> 32729 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.pngbin0 -> 28519 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_main.pngbin0 -> 35658 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.pngbin0 -> 19150 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.pngbin0 -> 99656 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_a.pngbin0 -> 29133 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.pngbin0 -> 29190 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_b.pngbin0 -> 24653 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.pngbin0 -> 24931 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_l.pngbin0 -> 6396 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.pngbin0 -> 6455 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_r.pngbin0 -> 14580 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.pngbin0 -> 14493 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_select.pngbin0 -> 32098 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.pngbin0 -> 32299 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_start.pngbin0 -> 36683 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.pngbin0 -> 36775 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_x.pngbin0 -> 33016 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.pngbin0 -> 34053 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_y.pngbin0 -> 24127 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.pngbin0 -> 24408 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_zl.pngbin0 -> 10479 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.pngbin0 -> 10484 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_zr.pngbin0 -> 15653 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.pngbin0 -> 15648 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/dpad.pngbin0 -> 9253 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.pngbin0 -> 8434 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.pngbin0 -> 16159 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.pngbin0 -> 1244 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.pngbin0 -> 591 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.pngbin0 -> 2654 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.pngbin0 -> 2093 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_c.pngbin0 -> 57013 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.pngbin0 -> 40273 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.pngbin0 -> 34281 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_main.pngbin0 -> 45881 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.pngbin0 -> 24942 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.pngbin0 -> 136109 bytes
-rw-r--r--src/android/app/src/main/res/drawable/gamelist_divider.xml11
-rw-r--r--src/android/app/src/main/res/drawable/ic_add.xml9
-rw-r--r--src/android/app/src/main/res/drawable/no_icon.pngbin0 -> 8610 bytes
-rw-r--r--src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml38
-rw-r--r--src/android/app/src/main/res/layout/activity_cheats.xml22
-rw-r--r--src/android/app/src/main/res/layout/activity_emulation.xml17
-rw-r--r--src/android/app/src/main/res/layout/activity_main.xml27
-rw-r--r--src/android/app/src/main/res/layout/activity_settings.xml5
-rw-r--r--src/android/app/src/main/res/layout/card_game.xml81
-rw-r--r--src/android/app/src/main/res/layout/dialog_checkbox.xml16
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml26
-rw-r--r--src/android/app/src/main/res/layout/dialog_seekbar.xml37
-rw-r--r--src/android/app/src/main/res/layout/filepicker_toolbar.xml32
-rw-r--r--src/android/app/src/main/res/layout/fragment_cheat_details.xml163
-rw-r--r--src/android/app/src/main/res/layout/fragment_cheat_list.xml27
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml47
-rw-r--r--src/android/app/src/main/res/layout/fragment_grid.xml33
-rw-r--r--src/android/app/src/main/res/layout/fragment_settings.xml12
-rw-r--r--src/android/app/src/main/res/layout/list_item_cheat.xml38
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting.xml43
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting_checkbox.xml52
-rw-r--r--src/android/app/src/main/res/layout/list_item_settings_header.xml19
-rw-r--r--src/android/app/src/main/res/layout/premium_item_setting.xml43
-rw-r--r--src/android/app/src/main/res/layout/sysclock_datetime_picker.xml22
-rw-r--r--src/android/app/src/main/res/menu/menu_emulation.xml118
-rw-r--r--src/android/app/src/main/res/menu/menu_game_grid.xml34
-rw-r--r--src/android/app/src/main/res/menu/menu_settings.xml2
-rw-r--r--src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--src/android/app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 5899 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 7416 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 3377 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 4413 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 8742 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 10530 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 14300 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 17511 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 20804 bytes
-rw-r--r--src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 24886 bytes
-rw-r--r--src/android/app/src/main/res/values-night/colors.xml17
-rw-r--r--src/android/app/src/main/res/values-night/styles_filepicker.xml5
-rw-r--r--src/android/app/src/main/res/values-w1000dp/integers.xml4
-rw-r--r--src/android/app/src/main/res/values-w1050dp/dimens.xml6
-rw-r--r--src/android/app/src/main/res/values-w500dp/integers.xml4
-rw-r--r--src/android/app/src/main/res/values-w750dp/integers.xml4
-rw-r--r--src/android/app/src/main/res/values-w820dp/dimens.xml5
-rw-r--r--src/android/app/src/main/res/values/arrays.xml174
-rw-r--r--src/android/app/src/main/res/values/colors.xml17
-rw-r--r--src/android/app/src/main/res/values/dimens.xml10
-rw-r--r--src/android/app/src/main/res/values/ic_launcher_background.xml4
-rw-r--r--src/android/app/src/main/res/values/integers.xml65
-rw-r--r--src/android/app/src/main/res/values/strings.xml246
-rw-r--r--src/android/app/src/main/res/values/styles.xml65
-rw-r--r--src/android/app/src/main/res/values/styles_filepicker.xml5
-rw-r--r--src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java17
-rw-r--r--src/android/build.gradle26
-rw-r--r--src/android/code-style-java.xml240
-rw-r--r--src/android/gradle.properties15
-rw-r--r--src/android/gradle/wrapper/gradle-wrapper.jarbin0 -> 54708 bytes
-rw-r--r--src/android/gradle/wrapper/gradle-wrapper.properties6
-rw-r--r--src/android/gradlew172
-rw-r--r--src/android/gradlew.bat84
-rw-r--r--src/android/settings.gradle1
319 files changed, 13799 insertions, 0 deletions
diff --git a/src/android/.gitignore b/src/android/.gitignore
new file mode 100644
index 000000000..40b6c5cd0
--- /dev/null
+++ b/src/android/.gitignore
@@ -0,0 +1,62 @@
1# Built application files
2*.apk
3*.ap_
4
5# Files for the ART/Dalvik VM
6*.dex
7
8# Java class files
9*.class
10
11# Generated files
12bin/
13gen/
14out/
15
16# Gradle files
17.gradle/
18build/
19
20# Local configuration file (sdk path, etc)
21local.properties
22
23# Proguard folder generated by Eclipse
24proguard/
25
26# Log Files
27*.log
28
29# Android Studio Navigation editor temp files
30.navigation/
31
32# Android Studio captures folder
33captures/
34
35# IntelliJ
36*.iml
37.idea/
38
39# Keystore files
40# Uncomment the following line if you do not want to check your keystore files in.
41#*.jks
42
43# External native build folder generated in Android Studio 2.2 and later
44.externalNativeBuild
45
46# CXX compile cache
47app/.cxx
48
49# Google Services (e.g. APIs or Firebase)
50google-services.json
51
52# Freeline
53freeline.py
54freeline/
55freeline_project_description.json
56
57# fastlane
58fastlane/report.xml
59fastlane/Preview.html
60fastlane/screenshots
61fastlane/test_output
62fastlane/readme.md
diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
new file mode 100644
index 000000000..5a108743b
--- /dev/null
+++ b/src/android/app/build.gradle
@@ -0,0 +1,163 @@
1apply plugin: 'com.android.application'
2
3/**
4 * Use the number of seconds/10 since Jan 1 2016 as the versionCode.
5 * This lets us upload a new build at most every 10 seconds for the
6 * next 680 years.
7 */
8def autoVersion = (int) (((new Date().getTime() / 1000) - 1451606400) / 10)
9def buildType
10def abiFilter = "arm64-v8a" //, "x86"
11
12android {
13 compileSdkVersion 32
14 ndkVersion "25.1.8937393"
15
16 compileOptions {
17 sourceCompatibility JavaVersion.VERSION_1_8
18 targetCompatibility JavaVersion.VERSION_1_8
19 }
20
21 lintOptions {
22 // This is important as it will run lint but not abort on error
23 // Lint has some overly obnoxious "errors" that should really be warnings
24 abortOnError false
25
26 //Uncomment disable lines for test builds...
27 //disable 'MissingTranslation'bin
28 //disable 'ExtraTranslation'
29 }
30
31 defaultConfig {
32 // TODO If this is ever modified, change application_id in strings.xml
33 applicationId "org.citra.citra_emu"
34 minSdkVersion 28
35 targetSdkVersion 29
36 versionCode autoVersion
37 versionName getVersion()
38 ndk.abiFilters abiFilter
39 }
40
41 signingConfigs {
42 //release {
43 // storeFile file('')
44 // storePassword System.getenv('ANDROID_KEYPASS')
45 // keyAlias = 'key0'
46 // keyPassword System.getenv('ANDROID_KEYPASS')
47 //}
48 }
49
50 applicationVariants.all { variant ->
51 buildType = variant.buildType.name // sets the current build type
52 }
53
54 // Define build types, which are orthogonal to product flavors.
55 buildTypes {
56
57 // Signed by release key, allowing for upload to Play Store.
58 release {
59 signingConfig signingConfigs.debug
60 }
61
62 // builds a release build that doesn't need signing
63 // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
64 relWithDebInfo {
65 initWith release
66 applicationIdSuffix ".debug"
67 versionNameSuffix '-debug'
68 signingConfig signingConfigs.debug
69 minifyEnabled false
70 testCoverageEnabled false
71 debuggable true
72 jniDebuggable true
73 }
74
75 // Signed by debug key disallowing distribution on Play Store.
76 // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
77 debug {
78 // TODO If this is ever modified, change application_id in debug/strings.xml
79 applicationIdSuffix ".debug"
80 versionNameSuffix '-debug'
81 debuggable true
82 jniDebuggable true
83 }
84 }
85
86 flavorDimensions "version"
87 productFlavors {
88 canary {
89 dimension "version"
90 applicationIdSuffix ".canary"
91 }
92 nightly {
93 dimension "version"
94 }
95 }
96
97 externalNativeBuild {
98 cmake {
99 version "3.22.1"
100 path "../../../CMakeLists.txt"
101 }
102 }
103
104 defaultConfig {
105 externalNativeBuild {
106 cmake {
107 arguments "-DENABLE_QT=0", // Don't use QT
108 "-DENABLE_SDL2=0", // Don't use SDL
109 "-DENABLE_WEB_SERVICE=0", // Don't use telemetry
110 "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
111 "-DYUZU_USE_BUNDLED_VCPKG=ON",
112 "-DYUZU_USE_BUNDLED_FFMPEG=ON"
113
114 abiFilters abiFilter
115 }
116 }
117 }
118}
119
120dependencies {
121 implementation 'androidx.appcompat:appcompat:1.5.1'
122 implementation 'androidx.exifinterface:exifinterface:1.3.4'
123 implementation 'androidx.cardview:cardview:1.0.0'
124 implementation 'androidx.recyclerview:recyclerview:1.2.1'
125 implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
126 implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
127 implementation 'androidx.fragment:fragment:1.5.3'
128 implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
129 implementation 'com.google.android.material:material:1.6.1'
130
131 // For loading huge screenshots from the disk.
132 implementation 'com.squareup.picasso:picasso:2.71828'
133
134 // Allows FRP-style asynchronous operations in Android.
135 implementation 'io.reactivex:rxandroid:1.2.1'
136 implementation 'com.nononsenseapps:filepicker:4.2.1'
137 implementation 'org.ini4j:ini4j:0.5.4'
138 implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
139 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
140 implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
141
142 // Please don't upgrade the billing library as the newer version is not GPL-compatible
143 implementation 'com.android.billingclient:billing:2.0.3'
144}
145
146def getVersion() {
147 def versionName = '0.0'
148
149 try {
150 versionName = 'git describe --always --long'.execute([], project.rootDir).text
151 .trim()
152 .replaceAll(/(-0)?-[^-]+$/, "")
153 } catch (Exception) {
154 logger.error('Cannot find git, defaulting to dummy version number')
155 }
156
157 if (System.getenv("GITHUB_ACTIONS") != null) {
158 def gitTag = System.getenv("GIT_TAG_NAME")
159 versionName = gitTag ?: versionName
160 }
161
162 return versionName
163}
diff --git a/src/android/app/proguard-rules.pro b/src/android/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/src/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
1# Add project specific ProGuard rules here.
2# You can control the set of applied configuration files using the
3# proguardFiles setting in build.gradle.
4#
5# For more details, see
6# http://developer.android.com/guide/developing/tools/proguard.html
7
8# If your project uses WebView with JS, uncomment the following
9# and specify the fully qualified class name to the JavaScript interface
10# class:
11#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12# public *;
13#}
14
15# Uncomment this to preserve the line number information for
16# debugging stack traces.
17#-keepattributes SourceFile,LineNumberTable
18
19# If you keep the line number information, uncomment this to
20# hide the original source file name.
21#-renamesourcefileattribute SourceFile
diff --git a/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java b/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..6a25f2ce6
--- /dev/null
+++ b/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java
@@ -0,0 +1,3 @@
1package org.citra.citra_emu;
2
3import android.content.Context;
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c2463e079
--- /dev/null
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,99 @@
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="org.citra.citra_emu">
4 <uses-feature
5 android:name="android.hardware.touchscreen"
6 android:required="false"/>
7 <uses-feature
8 android:name="android.hardware.gamepad"
9 android:required="false"/>
10
11 <uses-feature android:glEsVersion="0x00030002" android:required="true" />
12
13 <uses-feature android:name="android.hardware.opengles.aep" android:required="true" />
14 <uses-feature
15 android:name="android.hardware.camera.any"
16 android:required="false" />
17
18 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
19 <uses-permission android:name="android.permission.INTERNET" />
20 <uses-permission android:name="android.permission.CAMERA" />
21 <uses-permission android:name="android.permission.RECORD_AUDIO" />
22 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
23
24
25 <application
26 android:name="org.citra.citra_emu.CitraApplication"
27 android:label="@string/app_name"
28 android:icon="@mipmap/ic_launcher"
29 android:allowBackup="false"
30 android:supportsRtl="true"
31 android:isGame="true"
32 android:banner="@mipmap/ic_launcher"
33 android:requestLegacyExternalStorage="true">
34
35 <activity
36 android:name="org.citra.citra_emu.ui.main.MainActivity"
37 android:theme="@style/CitraBase"
38 android:resizeableActivity="false">
39
40 <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
41 <intent-filter>
42 <action android:name="android.intent.action.MAIN"/>
43
44 <category android:name="android.intent.category.LAUNCHER"/>
45 </intent-filter>
46 </activity>
47
48 <activity
49 android:name="org.citra.citra_emu.features.settings.ui.SettingsActivity"
50 android:configChanges="orientation|screenSize|uiMode"
51 android:theme="@style/CitraSettingsBase"
52 android:label="@string/preferences_settings"/>
53
54 <activity
55 android:name="org.citra.citra_emu.activities.EmulationActivity"
56 android:resizeableActivity="false"
57 android:theme="@style/CitraEmulationBase"
58 android:launchMode="singleTop"
59 android:screenOrientation="landscape"/>
60
61 <service android:name="org.citra.citra_emu.utils.ForegroundService"/>
62
63 <activity
64 android:name="org.citra.citra_emu.activities.CustomFilePickerActivity"
65 android:label="@string/app_name"
66 android:theme="@style/FilePickerTheme">
67 <intent-filter>
68 <action android:name="android.intent.action.GET_CONTENT" />
69 <category android:name="android.intent.category.DEFAULT" />
70 </intent-filter>
71 </activity>
72
73 <activity
74 android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
75 android:exported="false"
76 android:theme="@style/CitraSettingsBase"
77 android:label="@string/cheats"/>
78
79 <service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
80
81 <provider
82 android:name="org.citra.citra_emu.model.GameProvider"
83 android:authorities="${applicationId}.provider"
84 android:enabled="true"
85 android:exported="false">
86 </provider>
87
88 <provider
89 android:name="androidx.core.content.FileProvider"
90 android:authorities="${applicationId}.filesprovider"
91 android:exported="false"
92 android:grantUriPermissions="true">
93 <meta-data
94 android:name="android.support.FILE_PROVIDER_PATHS"
95 android:resource="@xml/nnf_provider_paths" />
96 </provider>
97 </application>
98
99</manifest>
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java
new file mode 100644
index 000000000..41ac7e27c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java
@@ -0,0 +1,56 @@
1// Copyright 2019 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5package org.citra.citra_emu;
6
7import android.app.Application;
8import android.app.NotificationChannel;
9import android.app.NotificationManager;
10import android.content.Context;
11import android.os.Build;
12
13import org.citra.citra_emu.model.GameDatabase;
14import org.citra.citra_emu.utils.DirectoryInitialization;
15import org.citra.citra_emu.utils.PermissionsHandler;
16
17public class CitraApplication extends Application {
18 public static GameDatabase databaseHelper;
19 private static CitraApplication application;
20
21 private void createNotificationChannel() {
22 // Create the NotificationChannel, but only on API 26+ because
23 // the NotificationChannel class is new and not in the support library
24 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
25 CharSequence name = getString(R.string.app_notification_channel_name);
26 String description = getString(R.string.app_notification_channel_description);
27 NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW);
28 channel.setDescription(description);
29 channel.setSound(null, null);
30 channel.setVibrationPattern(null);
31 // Register the channel with the system; you can't change the importance
32 // or other notification behaviors after this
33 NotificationManager notificationManager = getSystemService(NotificationManager.class);
34 notificationManager.createNotificationChannel(channel);
35 }
36 }
37
38 @Override
39 public void onCreate() {
40 super.onCreate();
41 application = this;
42
43 if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
44 DirectoryInitialization.start(getApplicationContext());
45 }
46
47 NativeLibrary.LogDeviceInfo();
48 createNotificationChannel();
49
50 databaseHelper = new GameDatabase(this);
51 }
52
53 public static Context getAppContext() {
54 return application.getApplicationContext();
55 }
56}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
new file mode 100644
index 000000000..baff99dc8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
@@ -0,0 +1,631 @@
1/*
2 * Copyright 2013 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu;
8
9import android.app.Activity;
10import android.app.Dialog;
11import android.content.pm.PackageManager;
12import android.content.res.Configuration;
13import android.os.Bundle;
14import android.text.Html;
15import android.text.method.LinkMovementMethod;
16import android.view.Surface;
17import android.view.ViewGroup;
18import android.widget.EditText;
19import android.widget.FrameLayout;
20import android.widget.TextView;
21
22import androidx.annotation.NonNull;
23import androidx.appcompat.app.AlertDialog;
24import androidx.core.content.ContextCompat;
25import androidx.fragment.app.DialogFragment;
26
27import org.citra.citra_emu.activities.EmulationActivity;
28import org.citra.citra_emu.utils.EmulationMenuSettings;
29import org.citra.citra_emu.utils.Log;
30
31import java.lang.ref.WeakReference;
32import java.util.Objects;
33
34import static android.Manifest.permission.CAMERA;
35import static android.Manifest.permission.RECORD_AUDIO;
36
37/**
38 * Class which contains methods that interact
39 * with the native side of the Citra code.
40 */
41public final class NativeLibrary {
42 /**
43 * Default touchscreen device
44 */
45 public static final String TouchScreenDevice = "Touchscreen";
46 public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
47
48 private static boolean alertResult = false;
49 private static String alertPromptResult = "";
50 private static int alertPromptButton = 0;
51 private static final Object alertPromptLock = new Object();
52 private static boolean alertPromptInProgress = false;
53 private static String alertPromptCaption = "";
54 private static int alertPromptButtonConfig = 0;
55 private static EditText alertPromptEditText = null;
56
57 static {
58 try {
59 System.loadLibrary("yuzu-android");
60 } catch (UnsatisfiedLinkError ex) {
61 Log.error("[NativeLibrary] " + ex.toString());
62 }
63 }
64
65 private NativeLibrary() {
66 // Disallows instantiation.
67 }
68
69 /**
70 * Handles button press events for a gamepad.
71 *
72 * @param Device The input descriptor of the gamepad.
73 * @param Button Key code identifying which button was pressed.
74 * @param Action Mask identifying which action is happening (button pressed down, or button released).
75 * @return If we handled the button press.
76 */
77 public static native boolean onGamePadEvent(String Device, int Button, int Action);
78
79 /**
80 * Handles gamepad movement events.
81 *
82 * @param Device The device ID of the gamepad.
83 * @param Axis The axis ID
84 * @param x_axis The value of the x-axis represented by the given ID.
85 * @param y_axis The value of the y-axis represented by the given ID
86 */
87 public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
88
89 /**
90 * Handles gamepad movement events.
91 *
92 * @param Device The device ID of the gamepad.
93 * @param Axis_id The axis ID
94 * @param axis_val The value of the axis represented by the given ID.
95 */
96 public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
97
98 /**
99 * Handles touch events.
100 *
101 * @param x_axis The value of the x-axis.
102 * @param y_axis The value of the y-axis
103 * @param pressed To identify if the touch held down or released.
104 * @return true if the pointer is within the touchscreen
105 */
106 public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed);
107
108 /**
109 * Handles touch movement.
110 *
111 * @param x_axis The value of the instantaneous x-axis.
112 * @param y_axis The value of the instantaneous y-axis.
113 */
114 public static native void onTouchMoved(float x_axis, float y_axis);
115
116 public static native void ReloadSettings();
117
118 public static native String GetUserSetting(String gameID, String Section, String Key);
119
120 public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
121
122 public static native void InitGameIni(String gameID);
123
124 /**
125 * Gets the embedded icon within the given ROM.
126 *
127 * @param filename the file path to the ROM.
128 * @return an integer array containing the color data for the icon.
129 */
130 public static native int[] GetIcon(String filename);
131
132 /**
133 * Gets the embedded title of the given ISO/ROM.
134 *
135 * @param filename The file path to the ISO/ROM.
136 * @return the embedded title of the ISO/ROM.
137 */
138 public static native String GetTitle(String filename);
139
140 public static native String GetDescription(String filename);
141
142 public static native String GetGameId(String filename);
143
144 public static native String GetRegions(String filename);
145
146 public static native String GetCompany(String filename);
147
148 public static native String GetGitRevision();
149
150 /**
151 * Sets the current working user directory
152 * If not set, it auto-detects a location
153 */
154 public static native void SetUserDirectory(String directory);
155
156 // Create the config.ini file.
157 public static native void CreateConfigFile();
158
159 public static native int DefaultCPUCore();
160
161 /**
162 * Begins emulation.
163 */
164 public static native void Run(String path);
165
166 /**
167 * Begins emulation from the specified savestate.
168 */
169 public static native void Run(String path, String savestatePath, boolean deleteSavestate);
170
171 // Surface Handling
172 public static native void SurfaceChanged(Surface surf);
173
174 public static native void SurfaceDestroyed();
175
176 public static native void DoFrame();
177
178 /**
179 * Unpauses emulation from a paused state.
180 */
181 public static native void UnPauseEmulation();
182
183 /**
184 * Pauses emulation.
185 */
186 public static native void PauseEmulation();
187
188 /**
189 * Stops emulation.
190 */
191 public static native void StopEmulation();
192
193 /**
194 * Returns true if emulation is running (or is paused).
195 */
196 public static native boolean IsRunning();
197
198 /**
199 * Returns the performance stats for the current game
200 **/
201 public static native double[] GetPerfStats();
202
203 /**
204 * Notifies the core emulation that the orientation has changed.
205 */
206 public static native void NotifyOrientationChange(int layout_option, int rotation);
207
208 public enum CoreError {
209 ErrorSystemFiles,
210 ErrorSavestate,
211 ErrorUnknown,
212 }
213
214 private static boolean coreErrorAlertResult = false;
215 private static final Object coreErrorAlertLock = new Object();
216
217 public static class CoreErrorDialogFragment extends DialogFragment {
218 static CoreErrorDialogFragment newInstance(String title, String message) {
219 CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
220 Bundle args = new Bundle();
221 args.putString("title", title);
222 args.putString("message", message);
223 frag.setArguments(args);
224 return frag;
225 }
226
227 @NonNull
228 @Override
229 public Dialog onCreateDialog(Bundle savedInstanceState) {
230 final Activity emulationActivity = Objects.requireNonNull(getActivity());
231
232 final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
233 final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
234
235 return new AlertDialog.Builder(emulationActivity)
236 .setTitle(title)
237 .setMessage(message)
238 .setPositiveButton(R.string.continue_button, (dialog, which) -> {
239 coreErrorAlertResult = true;
240 synchronized (coreErrorAlertLock) {
241 coreErrorAlertLock.notify();
242 }
243 })
244 .setNegativeButton(R.string.abort_button, (dialog, which) -> {
245 coreErrorAlertResult = false;
246 synchronized (coreErrorAlertLock) {
247 coreErrorAlertLock.notify();
248 }
249 }).setOnDismissListener(dialog -> {
250 coreErrorAlertResult = true;
251 synchronized (coreErrorAlertLock) {
252 coreErrorAlertLock.notify();
253 }
254 }).create();
255 }
256 }
257
258 private static void OnCoreErrorImpl(String title, String message) {
259 final EmulationActivity emulationActivity = sEmulationActivity.get();
260 if (emulationActivity == null) {
261 Log.error("[NativeLibrary] EmulationActivity not present");
262 return;
263 }
264
265 CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
266 fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
267 }
268
269 /**
270 * Handles a core error.
271 * @return true: continue; false: abort
272 */
273 public static boolean OnCoreError(CoreError error, String details) {
274 final EmulationActivity emulationActivity = sEmulationActivity.get();
275 if (emulationActivity == null) {
276 Log.error("[NativeLibrary] EmulationActivity not present");
277 return false;
278 }
279
280 String title, message;
281 switch (error) {
282 case ErrorSystemFiles: {
283 title = emulationActivity.getString(R.string.system_archive_not_found);
284 message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
285 break;
286 }
287 case ErrorSavestate: {
288 title = emulationActivity.getString(R.string.save_load_error);
289 message = details;
290 break;
291 }
292 case ErrorUnknown: {
293 title = emulationActivity.getString(R.string.fatal_error);
294 message = emulationActivity.getString(R.string.fatal_error_message);
295 break;
296 }
297 default: {
298 return true;
299 }
300 }
301
302 // Show the AlertDialog on the main thread.
303 emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));
304
305 // Wait for the lock to notify that it is complete.
306 synchronized (coreErrorAlertLock) {
307 try {
308 coreErrorAlertLock.wait();
309 } catch (Exception ignored) {
310 }
311 }
312
313 return coreErrorAlertResult;
314 }
315
316 public static boolean isPortraitMode() {
317 return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
318 Configuration.ORIENTATION_PORTRAIT;
319 }
320
321 public static int landscapeScreenLayout() {
322 return EmulationMenuSettings.getLandscapeScreenLayout();
323 }
324
325 public static boolean displayAlertMsg(final String caption, final String text,
326 final boolean yesNo) {
327 Log.error("[NativeLibrary] Alert: " + text);
328 final EmulationActivity emulationActivity = sEmulationActivity.get();
329 boolean result = false;
330 if (emulationActivity == null) {
331 Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
332 } else {
333 // Create object used for waiting.
334 final Object lock = new Object();
335 AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
336 .setTitle(caption)
337 .setMessage(text);
338
339 // If not yes/no dialog just have one button that dismisses modal,
340 // otherwise have a yes and no button that sets alertResult accordingly.
341 if (!yesNo) {
342 builder
343 .setCancelable(false)
344 .setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
345 {
346 dialog.dismiss();
347 synchronized (lock) {
348 lock.notify();
349 }
350 });
351 } else {
352 alertResult = false;
353
354 builder
355 .setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
356 {
357 alertResult = true;
358 dialog.dismiss();
359 synchronized (lock) {
360 lock.notify();
361 }
362 })
363 .setNegativeButton(android.R.string.no, (dialog, whichButton) ->
364 {
365 alertResult = false;
366 dialog.dismiss();
367 synchronized (lock) {
368 lock.notify();
369 }
370 });
371 }
372
373 // Show the AlertDialog on the main thread.
374 emulationActivity.runOnUiThread(builder::show);
375
376 // Wait for the lock to notify that it is complete.
377 synchronized (lock) {
378 try {
379 lock.wait();
380 } catch (Exception e) {
381 }
382 }
383
384 if (yesNo)
385 result = alertResult;
386 }
387 return result;
388 }
389
390 public static void retryDisplayAlertPrompt() {
391 if (!alertPromptInProgress) {
392 return;
393 }
394 displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
395 }
396
397 public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
398 alertPromptCaption = caption;
399 alertPromptButtonConfig = buttonConfig;
400 alertPromptInProgress = true;
401
402 // Show the AlertDialog on the main thread
403 sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
404
405 // Wait for the lock to notify that it is complete
406 synchronized (alertPromptLock) {
407 try {
408 alertPromptLock.wait();
409 } catch (Exception e) {
410 }
411 }
412 alertPromptInProgress = false;
413
414 return alertPromptResult;
415 }
416
417 public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
418 final EmulationActivity emulationActivity = sEmulationActivity.get();
419 alertPromptResult = "";
420 alertPromptButton = 0;
421
422 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
423 params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
424
425 // Set up the input
426 alertPromptEditText = new EditText(CitraApplication.getAppContext());
427 alertPromptEditText.setText(text);
428 alertPromptEditText.setSingleLine();
429 alertPromptEditText.setLayoutParams(params);
430
431 FrameLayout container = new FrameLayout(emulationActivity);
432 container.addView(alertPromptEditText);
433
434 AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
435 .setTitle(caption)
436 .setView(container)
437 .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
438 {
439 alertPromptButton = buttonConfig;
440 alertPromptResult = alertPromptEditText.getText().toString();
441 synchronized (alertPromptLock) {
442 alertPromptLock.notifyAll();
443 }
444 })
445 .setOnDismissListener(dialogInterface ->
446 {
447 alertPromptResult = "";
448 synchronized (alertPromptLock) {
449 alertPromptLock.notifyAll();
450 }
451 });
452
453 if (buttonConfig > 0) {
454 builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
455 {
456 alertPromptResult = "";
457 synchronized (alertPromptLock) {
458 alertPromptLock.notifyAll();
459 }
460 });
461 }
462
463 return builder;
464 }
465
466 public static int alertPromptButton() {
467 return alertPromptButton;
468 }
469
470 public static void exitEmulationActivity(int resultCode) {
471 final int Success = 0;
472 final int ErrorNotInitialized = 1;
473 final int ErrorGetLoader = 2;
474 final int ErrorSystemMode = 3;
475 final int ErrorLoader = 4;
476 final int ErrorLoader_ErrorEncrypted = 5;
477 final int ErrorLoader_ErrorInvalidFormat = 6;
478 final int ErrorSystemFiles = 7;
479 final int ErrorVideoCore = 8;
480 final int ErrorVideoCore_ErrorGenericDrivers = 9;
481 final int ErrorVideoCore_ErrorBelowGL33 = 10;
482 final int ShutdownRequested = 11;
483 final int ErrorUnknown = 12;
484
485 final EmulationActivity emulationActivity = sEmulationActivity.get();
486 if (emulationActivity == null) {
487 Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
488 return;
489 }
490
491 int captionId = R.string.loader_error_invalid_format;
492 if (resultCode == ErrorLoader_ErrorEncrypted) {
493 captionId = R.string.loader_error_encrypted;
494 }
495
496 AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
497 .setTitle(captionId)
498 .setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
499 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
500 .setOnDismissListener(dialogInterface -> emulationActivity.finish());
501 emulationActivity.runOnUiThread(() -> {
502 AlertDialog alert = builder.create();
503 alert.show();
504 ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
505 });
506 }
507
508 public static void setEmulationActivity(EmulationActivity emulationActivity) {
509 Log.verbose("[NativeLibrary] Registering EmulationActivity.");
510 sEmulationActivity = new WeakReference<>(emulationActivity);
511 }
512
513 public static void clearEmulationActivity() {
514 Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
515
516 sEmulationActivity.clear();
517 }
518
519 private static final Object cameraPermissionLock = new Object();
520 private static boolean cameraPermissionGranted = false;
521 public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
522
523 public static boolean RequestCameraPermission() {
524 final EmulationActivity emulationActivity = sEmulationActivity.get();
525 if (emulationActivity == null) {
526 Log.error("[NativeLibrary] EmulationActivity not present");
527 return false;
528 }
529 if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
530 // Permission already granted
531 return true;
532 }
533 emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
534
535 // Wait until result is returned
536 synchronized (cameraPermissionLock) {
537 try {
538 cameraPermissionLock.wait();
539 } catch (InterruptedException ignored) {
540 }
541 }
542 return cameraPermissionGranted;
543 }
544
545 public static void CameraPermissionResult(boolean granted) {
546 cameraPermissionGranted = granted;
547 synchronized (cameraPermissionLock) {
548 cameraPermissionLock.notify();
549 }
550 }
551
552 private static final Object micPermissionLock = new Object();
553 private static boolean micPermissionGranted = false;
554 public static final int REQUEST_CODE_NATIVE_MIC = 900;
555
556 public static boolean RequestMicPermission() {
557 final EmulationActivity emulationActivity = sEmulationActivity.get();
558 if (emulationActivity == null) {
559 Log.error("[NativeLibrary] EmulationActivity not present");
560 return false;
561 }
562 if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
563 // Permission already granted
564 return true;
565 }
566 emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
567
568 // Wait until result is returned
569 synchronized (micPermissionLock) {
570 try {
571 micPermissionLock.wait();
572 } catch (InterruptedException ignored) {
573 }
574 }
575 return micPermissionGranted;
576 }
577
578 public static void MicPermissionResult(boolean granted) {
579 micPermissionGranted = granted;
580 synchronized (micPermissionLock) {
581 micPermissionLock.notify();
582 }
583 }
584
585 /**
586 * Logs the Citra version, Android version and, CPU.
587 */
588 public static native void LogDeviceInfo();
589
590 /**
591 * Button type for use in onTouchEvent
592 */
593 public static final class ButtonType {
594 public static final int BUTTON_A = 0;
595 public static final int BUTTON_B = 1;
596 public static final int BUTTON_X = 2;
597 public static final int BUTTON_Y = 3;
598 public static final int BUTTON_START = 11;
599 public static final int BUTTON_SELECT = 12;
600 public static final int BUTTON_HOME = 19;
601 public static final int BUTTON_ZL = 9;
602 public static final int BUTTON_ZR = 10;
603 public static final int DPAD_UP = 14;
604 public static final int DPAD_DOWN = 16;
605 public static final int DPAD_LEFT = 13;
606 public static final int DPAD_RIGHT = 15;
607 public static final int STICK_LEFT = 5;
608 public static final int STICK_LEFT_UP = 714;
609 public static final int STICK_LEFT_DOWN = 715;
610 public static final int STICK_LEFT_LEFT = 716;
611 public static final int STICK_LEFT_RIGHT = 717;
612 public static final int STICK_C = 6;
613 public static final int STICK_C_UP = 719;
614 public static final int STICK_C_DOWN = 720;
615 public static final int STICK_C_LEFT = 771;
616 public static final int STICK_C_RIGHT = 772;
617 public static final int TRIGGER_L = 7;
618 public static final int TRIGGER_R = 8;
619 public static final int DPAD = 780;
620 public static final int BUTTON_DEBUG = 781;
621 public static final int BUTTON_GPIO14 = 782;
622 }
623
624 /**
625 * Button states
626 */
627 public static final class ButtonState {
628 public static final int RELEASED = 0;
629 public static final int PRESSED = 1;
630 }
631}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java
new file mode 100644
index 000000000..3083286e2
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java
@@ -0,0 +1,38 @@
1package org.citra.citra_emu.activities;
2
3import android.content.Intent;
4import android.os.Environment;
5
6import androidx.annotation.Nullable;
7
8import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
9import com.nononsenseapps.filepicker.FilePickerActivity;
10
11import org.citra.citra_emu.fragments.CustomFilePickerFragment;
12
13import java.io.File;
14
15public class CustomFilePickerActivity extends FilePickerActivity {
16 public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
17 public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
18
19 @Override
20 protected AbstractFilePickerFragment<File> getFragment(
21 @Nullable final String startPath, final int mode, final boolean allowMultiple,
22 final boolean allowCreateDir, final boolean allowExistingFile,
23 final boolean singleClick) {
24 CustomFilePickerFragment fragment = new CustomFilePickerFragment();
25 // startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
26 fragment.setArgs(
27 startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
28 mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
29
30 Intent intent = getIntent();
31 int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
32 fragment.setTitle(title);
33 String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
34 fragment.setAllowedExtensions(allowedExtensions);
35
36 return fragment;
37 }
38}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
new file mode 100644
index 000000000..47ef0fd23
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
@@ -0,0 +1,755 @@
1package org.citra.citra_emu.activities;
2
3import android.app.Activity;
4import android.content.Intent;
5import android.content.SharedPreferences;
6import android.content.pm.PackageManager;
7import android.os.Bundle;
8import android.os.Handler;
9import android.preference.PreferenceManager;
10import android.util.SparseIntArray;
11import android.view.InputDevice;
12import android.view.KeyEvent;
13import android.view.LayoutInflater;
14import android.view.Menu;
15import android.view.MenuItem;
16import android.view.MotionEvent;
17import android.view.View;
18import android.widget.CheckBox;
19import android.widget.SeekBar;
20import android.widget.TextView;
21
22import androidx.annotation.IntDef;
23import androidx.annotation.NonNull;
24import androidx.appcompat.app.AlertDialog;
25import androidx.appcompat.app.AppCompatActivity;
26import androidx.core.app.NotificationManagerCompat;
27import androidx.fragment.app.FragmentActivity;
28
29import org.citra.citra_emu.CitraApplication;
30import org.citra.citra_emu.NativeLibrary;
31import org.citra.citra_emu.R;
32import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
33import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
34import org.citra.citra_emu.features.settings.ui.SettingsActivity;
35import org.citra.citra_emu.features.settings.utils.SettingsFile;
36import org.citra.citra_emu.camera.StillImageCameraHelper;
37import org.citra.citra_emu.fragments.EmulationFragment;
38import org.citra.citra_emu.ui.main.MainActivity;
39import org.citra.citra_emu.utils.ControllerMappingHelper;
40import org.citra.citra_emu.utils.EmulationMenuSettings;
41import org.citra.citra_emu.utils.FileBrowserHelper;
42import org.citra.citra_emu.utils.FileUtil;
43import org.citra.citra_emu.utils.ForegroundService;
44
45import java.io.File;
46import java.io.IOException;
47import java.lang.annotation.Retention;
48import java.util.Collections;
49import java.util.List;
50
51import static android.Manifest.permission.CAMERA;
52import static android.Manifest.permission.RECORD_AUDIO;
53import static java.lang.annotation.RetentionPolicy.SOURCE;
54
55public final class EmulationActivity extends AppCompatActivity {
56 public static final String EXTRA_SELECTED_GAME = "SelectedGame";
57 public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
58 public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
59 public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
60 public static final int MENU_ACTION_ADJUST_SCALE = 2;
61 public static final int MENU_ACTION_EXIT = 3;
62 public static final int MENU_ACTION_SHOW_FPS = 4;
63 public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
64 public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
65 public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
66 public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
67 public static final int MENU_ACTION_SWAP_SCREENS = 9;
68 public static final int MENU_ACTION_RESET_OVERLAY = 10;
69 public static final int MENU_ACTION_SHOW_OVERLAY = 11;
70 public static final int MENU_ACTION_OPEN_SETTINGS = 12;
71 public static final int MENU_ACTION_LOAD_AMIIBO = 13;
72 public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
73 public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
74 public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
75 public static final int MENU_ACTION_OPEN_CHEATS = 17;
76
77 public static final int REQUEST_SELECT_AMIIBO = 2;
78 private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
79 private static SparseIntArray buttonsActionsMap = new SparseIntArray();
80
81 static {
82 buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
83 EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
84 buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
85 EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
86 buttonsActionsMap
87 .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
88 buttonsActionsMap.append(R.id.menu_emulation_show_fps,
89 EmulationActivity.MENU_ACTION_SHOW_FPS);
90 buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
91 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
92 buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
93 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
94 buttonsActionsMap.append(R.id.menu_screen_layout_single,
95 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
96 buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
97 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
98 buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
99 EmulationActivity.MENU_ACTION_SWAP_SCREENS);
100 buttonsActionsMap
101 .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
102 buttonsActionsMap
103 .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
104 buttonsActionsMap
105 .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
106 buttonsActionsMap
107 .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
108 buttonsActionsMap
109 .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
110 buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center,
111 EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
112 buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
113 EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
114 buttonsActionsMap
115 .append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
116 }
117
118 private View mDecorView;
119 private EmulationFragment mEmulationFragment;
120 private SharedPreferences mPreferences;
121 private ControllerMappingHelper mControllerMappingHelper;
122 private Intent foregroundService;
123 private boolean activityRecreated;
124 private String mSelectedTitle;
125 private String mPath;
126
127 public static void launch(FragmentActivity activity, String path, String title) {
128 Intent launcher = new Intent(activity, EmulationActivity.class);
129
130 launcher.putExtra(EXTRA_SELECTED_GAME, path);
131 launcher.putExtra(EXTRA_SELECTED_TITLE, title);
132 activity.startActivity(launcher);
133 }
134
135 public static void tryDismissRunningNotification(Activity activity) {
136 NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
137 }
138
139 @Override
140 protected void onDestroy() {
141 stopService(foregroundService);
142 super.onDestroy();
143 }
144
145 @Override
146 protected void onCreate(Bundle savedInstanceState) {
147 super.onCreate(savedInstanceState);
148
149 if (savedInstanceState == null) {
150 // Get params we were passed
151 Intent gameToEmulate = getIntent();
152 mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
153 mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
154 activityRecreated = false;
155 } else {
156 activityRecreated = true;
157 restoreState(savedInstanceState);
158 }
159
160 mControllerMappingHelper = new ControllerMappingHelper();
161
162 // Get a handle to the Window containing the UI.
163 mDecorView = getWindow().getDecorView();
164 mDecorView.setOnSystemUiVisibilityChangeListener(visibility ->
165 {
166 if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
167 // Go back to immersive fullscreen mode in 3s
168 Handler handler = new Handler(getMainLooper());
169 handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */);
170 }
171 });
172 // Set these options now so that the SurfaceView the game renders into is the right size.
173 enableFullscreenImmersive();
174
175 setTheme(R.style.CitraEmulationBase);
176
177 setContentView(R.layout.activity_emulation);
178
179 // Find or create the EmulationFragment
180 mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
181 .findFragmentById(R.id.frame_emulation_fragment);
182 if (mEmulationFragment == null) {
183 mEmulationFragment = EmulationFragment.newInstance(mPath);
184 getSupportFragmentManager().beginTransaction()
185 .add(R.id.frame_emulation_fragment, mEmulationFragment)
186 .commit();
187 }
188
189 setTitle(mSelectedTitle);
190
191 mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
192
193 // Start a foreground service to prevent the app from getting killed in the background
194 foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
195 startForegroundService(foregroundService);
196 }
197
198 @Override
199 protected void onSaveInstanceState(@NonNull Bundle outState) {
200 outState.putString(EXTRA_SELECTED_GAME, mPath);
201 outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
202 super.onSaveInstanceState(outState);
203 }
204
205 protected void restoreState(Bundle savedInstanceState) {
206 mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
207 mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
208
209 // If an alert prompt was in progress when state was restored, retry displaying it
210 NativeLibrary.retryDisplayAlertPrompt();
211 }
212
213 @Override
214 public void onRestart() {
215 super.onRestart();
216 }
217
218 @Override
219 public void onBackPressed() {
220 NativeLibrary.PauseEmulation();
221 new AlertDialog.Builder(this)
222 .setTitle(R.string.emulation_close_game)
223 .setMessage(R.string.emulation_close_game_message)
224 .setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
225 {
226 mEmulationFragment.stopEmulation();
227 finish();
228 })
229 .setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
230 NativeLibrary.UnPauseEmulation())
231 .setOnCancelListener(dialogInterface ->
232 NativeLibrary.UnPauseEmulation())
233 .create()
234 .show();
235 }
236
237 @Override
238 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
239 switch (requestCode) {
240 case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
241 if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
242 shouldShowRequestPermissionRationale(CAMERA)) {
243 new AlertDialog.Builder(this)
244 .setTitle(R.string.camera)
245 .setMessage(R.string.camera_permission_needed)
246 .setPositiveButton(android.R.string.ok, null)
247 .show();
248 }
249 NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
250 break;
251 case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
252 if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
253 shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
254 new AlertDialog.Builder(this)
255 .setTitle(R.string.microphone)
256 .setMessage(R.string.microphone_permission_needed)
257 .setPositiveButton(android.R.string.ok, null)
258 .show();
259 }
260 NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
261 break;
262 default:
263 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
264 break;
265 }
266 }
267
268 private void enableFullscreenImmersive() {
269 // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
270 mDecorView.setSystemUiVisibility(
271 View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
272 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
273 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
274 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
275 View.SYSTEM_UI_FLAG_FULLSCREEN |
276 View.SYSTEM_UI_FLAG_IMMERSIVE);
277 }
278
279 @Override
280 public boolean onCreateOptionsMenu(Menu menu) {
281 // Inflate the menu; this adds items to the action bar if it is present.
282 getMenuInflater().inflate(R.menu.menu_emulation, menu);
283
284 int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
285 switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
286 case EmulationMenuSettings.LayoutOption_SingleScreen:
287 layoutOptionMenuItem = R.id.menu_screen_layout_single;
288 break;
289 case EmulationMenuSettings.LayoutOption_SideScreen:
290 layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
291 break;
292 case EmulationMenuSettings.LayoutOption_MobilePortrait:
293 layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
294 break;
295 }
296
297 menu.findItem(layoutOptionMenuItem).setChecked(true);
298 menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter());
299 menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable());
300 menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
301 menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
302 menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
303
304 return true;
305 }
306
307 private void DisplaySavestateWarning() {
308 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
309 if (preferences.getBoolean("savestateWarningShown", false)) {
310 return;
311 }
312
313 LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater();
314 View view = inflater.inflate(R.layout.dialog_checkbox, null);
315 CheckBox checkBox = view.findViewById(R.id.checkBox);
316
317 new AlertDialog.Builder(this)
318 .setTitle(R.string.savestate_warning_title)
319 .setMessage(R.string.savestate_warning_message)
320 .setView(view)
321 .setPositiveButton(android.R.string.ok, (dialog, which) -> {
322 preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply();
323 })
324 .show();
325 }
326
327 @Override
328 public boolean onPrepareOptionsMenu(Menu menu) {
329 super.onPrepareOptionsMenu(menu);
330 menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
331 menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
332 return true;
333 }
334
335 @SuppressWarnings("WrongConstant")
336 @Override
337 public boolean onOptionsItemSelected(MenuItem item) {
338 int action = buttonsActionsMap.get(item.getItemId(), -1);
339
340 switch (action) {
341 // Edit the placement of the controls
342 case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
343 editControlsPlacement();
344 break;
345
346 // Enable/Disable specific buttons or the entire input overlay.
347 case MENU_ACTION_TOGGLE_CONTROLS:
348 toggleControls();
349 break;
350
351 // Adjust the scale of the overlay controls.
352 case MENU_ACTION_ADJUST_SCALE:
353 adjustScale();
354 break;
355
356 // Toggle the visibility of the Performance stats TextView
357 case MENU_ACTION_SHOW_FPS: {
358 final boolean isEnabled = !EmulationMenuSettings.getShowFps();
359 EmulationMenuSettings.setShowFps(isEnabled);
360 item.setChecked(isEnabled);
361
362 mEmulationFragment.updateShowFpsOverlay();
363 break;
364 }
365 // Sets the screen layout to Landscape
366 case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
367 changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
368 break;
369
370 // Sets the screen layout to Portrait
371 case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
372 changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
373 break;
374
375 // Sets the screen layout to Single
376 case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
377 changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
378 break;
379
380 // Sets the screen layout to Side by Side
381 case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
382 changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
383 break;
384
385 // Swap the top and bottom screen locations
386 case MENU_ACTION_SWAP_SCREENS: {
387 final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
388 EmulationMenuSettings.setSwapScreens(isEnabled);
389 item.setChecked(isEnabled);
390 break;
391 }
392
393 // Reset overlay placement
394 case MENU_ACTION_RESET_OVERLAY:
395 resetOverlay();
396 break;
397
398 // Show or hide overlay
399 case MENU_ACTION_SHOW_OVERLAY: {
400 final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
401 EmulationMenuSettings.setShowOverlay(isEnabled);
402 item.setChecked(isEnabled);
403
404 mEmulationFragment.refreshInputOverlay();
405 break;
406 }
407
408 case MENU_ACTION_EXIT:
409 mEmulationFragment.stopEmulation();
410 finish();
411 break;
412
413 case MENU_ACTION_OPEN_SETTINGS:
414 SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
415 break;
416
417 case MENU_ACTION_LOAD_AMIIBO:
418 FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO,
419 R.string.select_amiibo,
420 Collections.singletonList("bin"), false);
421 break;
422
423 case MENU_ACTION_REMOVE_AMIIBO:
424 RemoveAmiibo();
425 break;
426
427 case MENU_ACTION_JOYSTICK_REL_CENTER:
428 final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter();
429 EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
430 item.setChecked(isJoystickRelCenterEnabled);
431 break;
432
433 case MENU_ACTION_DPAD_SLIDE_ENABLE:
434 final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
435 EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
436 item.setChecked(isDpadSlideEnabled);
437 break;
438
439 case MENU_ACTION_OPEN_CHEATS:
440 CheatsActivity.launch(this);
441 break;
442 }
443
444 return true;
445 }
446
447 private void changeScreenOrientation(int layoutOption, MenuItem item) {
448 item.setChecked(true);
449 NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
450 .getRotation());
451 EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
452 }
453
454 private void editControlsPlacement() {
455 if (mEmulationFragment.isConfiguringControls()) {
456 mEmulationFragment.stopConfiguringControls();
457 } else {
458 mEmulationFragment.startConfiguringControls();
459 }
460 }
461
462 // Gets button presses
463 @Override
464 public boolean dispatchKeyEvent(KeyEvent event) {
465 int action;
466 int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
467
468 switch (event.getAction()) {
469 case KeyEvent.ACTION_DOWN:
470 // Handling the case where the back button is pressed.
471 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
472 onBackPressed();
473 return true;
474 }
475
476 // Normal key events.
477 action = NativeLibrary.ButtonState.PRESSED;
478 break;
479 case KeyEvent.ACTION_UP:
480 action = NativeLibrary.ButtonState.RELEASED;
481 break;
482 default:
483 return false;
484 }
485 InputDevice input = event.getDevice();
486
487 if (input == null) {
488 // Controller was disconnected
489 return false;
490 }
491
492 return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
493 }
494
495 @Override
496 protected void onActivityResult(int requestCode, int resultCode, Intent result) {
497 super.onActivityResult(requestCode, resultCode, result);
498 switch (requestCode) {
499 case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER:
500 StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
501 break;
502 case REQUEST_SELECT_AMIIBO:
503 // If the user picked a file, as opposed to just backing out.
504 if (resultCode == MainActivity.RESULT_OK) {
505 String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result);
506 if (selectedFiles == null)
507 return;
508
509 onAmiiboSelected(selectedFiles[0]);
510 }
511 break;
512 }
513 }
514
515 private void onAmiiboSelected(String selectedFile) {
516 File file = new File(selectedFile);
517 boolean success = false;
518 try {
519 byte[] bytes = FileUtil.getBytesFromFile(file);
520 } catch (IOException e) {
521 e.printStackTrace();
522 }
523
524 if (!success) {
525 new AlertDialog.Builder(this)
526 .setTitle(R.string.amiibo_load_error)
527 .setMessage(R.string.amiibo_load_error_message)
528 .setPositiveButton(android.R.string.ok, null)
529 .create()
530 .show();
531 }
532 }
533
534 private void RemoveAmiibo() {
535
536 }
537
538 private void toggleControls() {
539 final SharedPreferences.Editor editor = mPreferences.edit();
540 boolean[] enabledButtons = new boolean[14];
541 AlertDialog.Builder builder = new AlertDialog.Builder(this);
542 builder.setTitle(R.string.emulation_toggle_controls);
543
544 for (int i = 0; i < enabledButtons.length; i++) {
545 // Buttons that are disabled by default
546 boolean defaultValue = true;
547 switch (i) {
548 case 6: // ZL
549 case 7: // ZR
550 case 12: // C-stick
551 defaultValue = false;
552 break;
553 }
554
555 enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
556 }
557 builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
558 (dialog, indexSelected, isChecked) -> editor
559 .putBoolean("buttonToggle" + indexSelected, isChecked));
560 builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
561 {
562 editor.apply();
563
564 mEmulationFragment.refreshInputOverlay();
565 });
566
567 AlertDialog alertDialog = builder.create();
568 alertDialog.show();
569 }
570
571 private void adjustScale() {
572 LayoutInflater inflater = LayoutInflater.from(this);
573 View view = inflater.inflate(R.layout.dialog_seekbar, null);
574
575 final SeekBar seekbar = view.findViewById(R.id.seekbar);
576 final TextView value = view.findViewById(R.id.text_value);
577 final TextView units = view.findViewById(R.id.text_units);
578
579 seekbar.setMax(150);
580 seekbar.setProgress(mPreferences.getInt("controlScale", 50));
581 seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
582 public void onStartTrackingTouch(SeekBar seekBar) {
583 }
584
585 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
586 value.setText(String.valueOf(progress + 50));
587 }
588
589 public void onStopTrackingTouch(SeekBar seekBar) {
590 setControlScale(seekbar.getProgress());
591 }
592 });
593
594 value.setText(String.valueOf(seekbar.getProgress() + 50));
595 units.setText("%");
596
597 AlertDialog.Builder builder = new AlertDialog.Builder(this);
598 builder.setTitle(R.string.emulation_control_scale);
599 builder.setView(view);
600 final int previousProgress = seekbar.getProgress();
601 builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
602 setControlScale(previousProgress);
603 });
604 builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
605 {
606 setControlScale(seekbar.getProgress());
607 });
608 builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> {
609 setControlScale(50);
610 });
611
612 AlertDialog alertDialog = builder.create();
613 alertDialog.show();
614 }
615
616 private void setControlScale(int scale) {
617 SharedPreferences.Editor editor = mPreferences.edit();
618 editor.putInt("controlScale", scale);
619 editor.apply();
620 mEmulationFragment.refreshInputOverlay();
621 }
622
623 private void resetOverlay() {
624 new AlertDialog.Builder(this)
625 .setTitle(getString(R.string.emulation_touch_overlay_reset))
626 .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
627 .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
628 })
629 .create()
630 .show();
631 }
632
633 @Override
634 public boolean dispatchGenericMotionEvent(MotionEvent event) {
635 if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
636 return super.dispatchGenericMotionEvent(event);
637 }
638
639 // Don't attempt to do anything if we are disconnecting a device.
640 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
641 return true;
642 }
643
644 InputDevice input = event.getDevice();
645 List<InputDevice.MotionRange> motions = input.getMotionRanges();
646
647 float[] axisValuesCirclePad = {0.0f, 0.0f};
648 float[] axisValuesCStick = {0.0f, 0.0f};
649 float[] axisValuesDPad = {0.0f, 0.0f};
650 boolean isTriggerPressedLMapped = false;
651 boolean isTriggerPressedRMapped = false;
652 boolean isTriggerPressedZLMapped = false;
653 boolean isTriggerPressedZRMapped = false;
654 boolean isTriggerPressedL = false;
655 boolean isTriggerPressedR = false;
656 boolean isTriggerPressedZL = false;
657 boolean isTriggerPressedZR = false;
658
659 for (InputDevice.MotionRange range : motions) {
660 int axis = range.getAxis();
661 float origValue = event.getAxisValue(axis);
662 float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
663 int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
664 int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
665
666 if (nextMapping == -1 || guestOrientation == -1) {
667 // Axis is unmapped
668 continue;
669 }
670
671 if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
672 // Skip joystick wobble
673 value = 0.f;
674 }
675
676 if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
677 axisValuesCirclePad[guestOrientation] = value;
678 } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
679 axisValuesCStick[guestOrientation] = value;
680 } else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
681 axisValuesDPad[guestOrientation] = value;
682 } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
683 isTriggerPressedLMapped = true;
684 isTriggerPressedL = value != 0.f;
685 } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
686 isTriggerPressedRMapped = true;
687 isTriggerPressedR = value != 0.f;
688 } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
689 isTriggerPressedZLMapped = true;
690 isTriggerPressedZL = value != 0.f;
691 } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
692 isTriggerPressedZRMapped = true;
693 isTriggerPressedZR = value != 0.f;
694 }
695 }
696
697 // Circle-Pad and C-Stick status
698 NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
699 NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
700
701 // Triggers L/R and ZL/ZR
702 if (isTriggerPressedLMapped) {
703 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
704 }
705 if (isTriggerPressedRMapped) {
706 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
707 }
708 if (isTriggerPressedZLMapped) {
709 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
710 }
711 if (isTriggerPressedZRMapped) {
712 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
713 }
714
715 // Work-around to allow D-pad axis to be bound to emulated buttons
716 if (axisValuesDPad[0] == 0.f) {
717 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
718 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
719 }
720 if (axisValuesDPad[0] < 0.f) {
721 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
722 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
723 }
724 if (axisValuesDPad[0] > 0.f) {
725 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
726 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
727 }
728 if (axisValuesDPad[1] == 0.f) {
729 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
730 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
731 }
732 if (axisValuesDPad[1] < 0.f) {
733 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
734 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
735 }
736 if (axisValuesDPad[1] > 0.f) {
737 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
738 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
739 }
740
741 return true;
742 }
743
744 public boolean isActivityRecreated() {
745 return activityRecreated;
746 }
747
748 @Retention(SOURCE)
749 @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
750 MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
751 MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
752 MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
753 public @interface MenuAction {
754 }
755}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java
new file mode 100644
index 000000000..bc791638a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java
@@ -0,0 +1,247 @@
1package org.citra.citra_emu.adapters;
2
3import android.database.Cursor;
4import android.database.DataSetObserver;
5import android.graphics.Rect;
6import android.graphics.drawable.Drawable;
7import android.os.Build;
8import android.os.SystemClock;
9import android.view.LayoutInflater;
10import android.view.View;
11import android.view.ViewGroup;
12
13import androidx.annotation.NonNull;
14import androidx.annotation.RequiresApi;
15import androidx.core.content.ContextCompat;
16import androidx.fragment.app.FragmentActivity;
17import androidx.recyclerview.widget.RecyclerView;
18
19import org.citra.citra_emu.R;
20import org.citra.citra_emu.activities.EmulationActivity;
21import org.citra.citra_emu.model.GameDatabase;
22import org.citra.citra_emu.ui.DividerItemDecoration;
23import org.citra.citra_emu.utils.Log;
24import org.citra.citra_emu.utils.PicassoUtils;
25import org.citra.citra_emu.viewholders.GameViewHolder;
26
27import java.nio.file.Path;
28import java.nio.file.Paths;
29import java.util.stream.Stream;
30
31/**
32 * This adapter gets its information from a database Cursor. This fact, paired with the usage of
33 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
34 * large dataset.
35 */
36public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
37 View.OnClickListener {
38 private Cursor mCursor;
39 private GameDataSetObserver mObserver;
40
41 private boolean mDatasetValid;
42 private long mLastClickTime = 0;
43
44 /**
45 * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
46 * display no data until a Cursor is supplied by a CursorLoader.
47 */
48 public GameAdapter() {
49 mDatasetValid = false;
50 mObserver = new GameDataSetObserver();
51 }
52
53 /**
54 * Called by the LayoutManager when it is necessary to create a new view.
55 *
56 * @param parent The RecyclerView (I think?) the created view will be thrown into.
57 * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
58 * @return The created ViewHolder with references to all the child view's members.
59 */
60 @Override
61 public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
62 // Create a new view.
63 View gameCard = LayoutInflater.from(parent.getContext())
64 .inflate(R.layout.card_game, parent, false);
65
66 gameCard.setOnClickListener(this);
67
68 // Use that view to create a ViewHolder.
69 return new GameViewHolder(gameCard);
70 }
71
72 /**
73 * Called by the LayoutManager when a new view is not necessary because we can recycle
74 * an existing one (for example, if a view just scrolled onto the screen from the bottom, we
75 * can use the view that just scrolled off the top instead of inflating a new one.)
76 *
77 * @param holder A ViewHolder representing the view we're recycling.
78 * @param position The position of the 'new' view in the dataset.
79 */
80 @RequiresApi(api = Build.VERSION_CODES.O)
81 @Override
82 public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
83 if (mDatasetValid) {
84 if (mCursor.moveToPosition(position)) {
85 PicassoUtils.loadGameIcon(holder.imageIcon,
86 mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
87
88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
89 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
90
91 final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
92 holder.textFileName.setText(gamePath.getFileName().toString());
93
94 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
95 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
96 holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
97 holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
98 holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
99 holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
100 holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
101
102 final int backgroundColorId = isValidGame(holder.path) ? R.color.card_view_background : R.color.card_view_disabled;
103 View itemView = holder.getItemView();
104 itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
105 } else {
106 Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
107 }
108 } else {
109 Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
110 }
111 }
112
113 /**
114 * Called by the LayoutManager to find out how much data we have.
115 *
116 * @return Size of the dataset.
117 */
118 @Override
119 public int getItemCount() {
120 if (mDatasetValid && mCursor != null) {
121 return mCursor.getCount();
122 }
123 Log.error("[GameAdapter] Dataset is not valid.");
124 return 0;
125 }
126
127 /**
128 * Return the contents of the _id column for a given row.
129 *
130 * @param position The row for which Android wants an ID.
131 * @return A valid ID from the database, or 0 if not available.
132 */
133 @Override
134 public long getItemId(int position) {
135 if (mDatasetValid && mCursor != null) {
136 if (mCursor.moveToPosition(position)) {
137 return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
138 }
139 }
140
141 Log.error("[GameAdapter] Dataset is not valid.");
142 return 0;
143 }
144
145 /**
146 * Tell Android whether or not each item in the dataset has a stable identifier.
147 * Which it does, because it's a database, so always tell Android 'true'.
148 *
149 * @param hasStableIds ignored.
150 */
151 @Override
152 public void setHasStableIds(boolean hasStableIds) {
153 super.setHasStableIds(true);
154 }
155
156 /**
157 * When a load is finished, call this to replace the existing data with the newly-loaded
158 * data.
159 *
160 * @param cursor The newly-loaded Cursor.
161 */
162 public void swapCursor(Cursor cursor) {
163 // Sanity check.
164 if (cursor == mCursor) {
165 return;
166 }
167
168 // Before getting rid of the old cursor, disassociate it from the Observer.
169 final Cursor oldCursor = mCursor;
170 if (oldCursor != null && mObserver != null) {
171 oldCursor.unregisterDataSetObserver(mObserver);
172 }
173
174 mCursor = cursor;
175 if (mCursor != null) {
176 // Attempt to associate the new Cursor with the Observer.
177 if (mObserver != null) {
178 mCursor.registerDataSetObserver(mObserver);
179 }
180
181 mDatasetValid = true;
182 } else {
183 mDatasetValid = false;
184 }
185
186 notifyDataSetChanged();
187 }
188
189 /**
190 * Launches the game that was clicked on.
191 *
192 * @param view The card representing the game the user wants to play.
193 */
194 @Override
195 public void onClick(View view) {
196 // Double-click prevention, using threshold of 1000 ms
197 if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
198 return;
199 }
200 mLastClickTime = SystemClock.elapsedRealtime();
201
202 GameViewHolder holder = (GameViewHolder) view.getTag();
203
204 EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
205 }
206
207 public static class SpacesItemDecoration extends DividerItemDecoration {
208 private int space;
209
210 public SpacesItemDecoration(Drawable divider, int space) {
211 super(divider);
212 this.space = space;
213 }
214
215 @Override
216 public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
217 @NonNull RecyclerView.State state) {
218 outRect.left = 0;
219 outRect.right = 0;
220 outRect.bottom = space;
221 outRect.top = 0;
222 }
223 }
224
225 private boolean isValidGame(String path) {
226 return Stream.of(
227 ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
228 }
229
230 private final class GameDataSetObserver extends DataSetObserver {
231 @Override
232 public void onChanged() {
233 super.onChanged();
234
235 mDatasetValid = true;
236 notifyDataSetChanged();
237 }
238
239 @Override
240 public void onInvalidated() {
241 super.onInvalidated();
242
243 mDatasetValid = false;
244 notifyDataSetChanged();
245 }
246 }
247}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java
new file mode 100644
index 000000000..3586a9b34
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java
@@ -0,0 +1,122 @@
1// Copyright 2020 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5package org.citra.citra_emu.applets;
6
7import android.app.Activity;
8import android.app.Dialog;
9import android.os.Bundle;
10
11import org.citra.citra_emu.NativeLibrary;
12import org.citra.citra_emu.R;
13import org.citra.citra_emu.activities.EmulationActivity;
14
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Objects;
18
19import androidx.annotation.NonNull;
20import androidx.appcompat.app.AlertDialog;
21import androidx.fragment.app.DialogFragment;
22
23public final class MiiSelector {
24 public static class MiiSelectorConfig implements java.io.Serializable {
25 public boolean enable_cancel_button;
26 public String title;
27 public long initially_selected_mii_index;
28 // List of Miis to display
29 public String[] mii_names;
30 }
31
32 public static class MiiSelectorData {
33 public long return_code;
34 public int index;
35
36 private MiiSelectorData(long return_code, int index) {
37 this.return_code = return_code;
38 this.index = index;
39 }
40 }
41
42 public static class MiiSelectorDialogFragment extends DialogFragment {
43 static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
44 MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
45 Bundle args = new Bundle();
46 args.putSerializable("config", config);
47 frag.setArguments(args);
48 return frag;
49 }
50
51 @NonNull
52 @Override
53 public Dialog onCreateDialog(Bundle savedInstanceState) {
54 final Activity emulationActivity = Objects.requireNonNull(getActivity());
55
56 MiiSelectorConfig config =
57 Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
58 .getSerializable("config"));
59
60 // Note: we intentionally leave out the Standard Mii in the native code so that
61 // the string can get translated
62 ArrayList<String> list = new ArrayList<>();
63 list.add(emulationActivity.getString(R.string.standard_mii));
64 list.addAll(Arrays.asList(config.mii_names));
65
66 final int initialIndex = config.initially_selected_mii_index < list.size()
67 ? (int) config.initially_selected_mii_index
68 : 0;
69 data.index = initialIndex;
70 AlertDialog.Builder builder =
71 new AlertDialog.Builder(emulationActivity)
72 .setTitle(config.title.isEmpty()
73 ? emulationActivity.getString(R.string.mii_selector)
74 : config.title)
75 .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
76 (dialog, which) -> {
77 data.index = which;
78 })
79 .setPositiveButton(android.R.string.ok, (dialog, which) -> {
80 data.return_code = 0;
81 synchronized (finishLock) {
82 finishLock.notifyAll();
83 }
84 });
85 if (config.enable_cancel_button) {
86 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
87 data.return_code = 1;
88 synchronized (finishLock) {
89 finishLock.notifyAll();
90 }
91 });
92 }
93 setCancelable(false);
94 return builder.create();
95 }
96 }
97
98 private static MiiSelectorData data;
99 private static final Object finishLock = new Object();
100
101 private static void ExecuteImpl(MiiSelectorConfig config) {
102 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
103
104 data = new MiiSelectorData(0, 0);
105
106 MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
107 fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
108 }
109
110 public static MiiSelectorData Execute(MiiSelectorConfig config) {
111 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
112
113 synchronized (finishLock) {
114 try {
115 finishLock.wait();
116 } catch (Exception ignored) {
117 }
118 }
119
120 return data;
121 }
122}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java
new file mode 100644
index 000000000..7be5f6d97
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java
@@ -0,0 +1,264 @@
1// Copyright 2020 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5package org.citra.citra_emu.applets;
6
7import android.app.Activity;
8import android.app.Dialog;
9import android.content.DialogInterface;
10import android.os.Bundle;
11import android.text.InputFilter;
12import android.text.Spanned;
13import android.view.ViewGroup;
14import android.widget.EditText;
15import android.widget.FrameLayout;
16
17import androidx.annotation.NonNull;
18import androidx.annotation.Nullable;
19import androidx.appcompat.app.AlertDialog;
20import androidx.fragment.app.DialogFragment;
21
22import org.citra.citra_emu.CitraApplication;
23import org.citra.citra_emu.NativeLibrary;
24import org.citra.citra_emu.R;
25import org.citra.citra_emu.activities.EmulationActivity;
26import org.citra.citra_emu.utils.Log;
27
28import java.util.Objects;
29
30public final class SoftwareKeyboard {
31 /// Corresponds to Frontend::ButtonConfig
32 private interface ButtonConfig {
33 int Single = 0; /// Ok button
34 int Dual = 1; /// Cancel | Ok buttons
35 int Triple = 2; /// Cancel | I Forgot | Ok buttons
36 int None = 3; /// No button (returned by swkbdInputText in special cases)
37 }
38
39 /// Corresponds to Frontend::ValidationError
40 public enum ValidationError {
41 None,
42 // Button Selection
43 ButtonOutOfRange,
44 // Configured Filters
45 MaxDigitsExceeded,
46 AtSignNotAllowed,
47 PercentNotAllowed,
48 BackslashNotAllowed,
49 ProfanityNotAllowed,
50 CallbackFailed,
51 // Allowed Input Type
52 FixedLengthRequired,
53 MaxLengthExceeded,
54 BlankInputNotAllowed,
55 EmptyInputNotAllowed,
56 }
57
58 public static class KeyboardConfig implements java.io.Serializable {
59 public int button_config;
60 public int max_text_length;
61 public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
62 public String hint_text; /// Displayed in the field as a hint before
63 @Nullable
64 public String[] button_text; /// Contains the button text that the caller provides
65 }
66
67 /// Corresponds to Frontend::KeyboardData
68 public static class KeyboardData {
69 public int button;
70 public String text;
71
72 private KeyboardData(int button, String text) {
73 this.button = button;
74 this.text = text;
75 }
76 }
77
78 private static class Filter implements InputFilter {
79 @Override
80 public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
81 int dstart, int dend) {
82 String text = new StringBuilder(dest)
83 .replace(dstart, dend, source.subSequence(start, end).toString())
84 .toString();
85 if (ValidateFilters(text) == ValidationError.None) {
86 return null; // Accept replacement
87 }
88 return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
89 }
90 }
91
92 public static class KeyboardDialogFragment extends DialogFragment {
93 static KeyboardDialogFragment newInstance(KeyboardConfig config) {
94 KeyboardDialogFragment frag = new KeyboardDialogFragment();
95 Bundle args = new Bundle();
96 args.putSerializable("config", config);
97 frag.setArguments(args);
98 return frag;
99 }
100
101 @NonNull
102 @Override
103 public Dialog onCreateDialog(Bundle savedInstanceState) {
104 final Activity emulationActivity = getActivity();
105 assert emulationActivity != null;
106
107 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
108 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
109 params.leftMargin = params.rightMargin =
110 CitraApplication.getAppContext().getResources().getDimensionPixelSize(
111 R.dimen.dialog_margin);
112
113 KeyboardConfig config = Objects.requireNonNull(
114 (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
115
116 // Set up the input
117 EditText editText = new EditText(CitraApplication.getAppContext());
118 editText.setHint(config.hint_text);
119 editText.setSingleLine(!config.multiline_mode);
120 editText.setLayoutParams(params);
121 editText.setFilters(new InputFilter[]{
122 new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
123
124 FrameLayout container = new FrameLayout(emulationActivity);
125 container.addView(editText);
126
127 AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
128 .setTitle(R.string.software_keyboard)
129 .setView(container);
130 setCancelable(false);
131
132 switch (config.button_config) {
133 case ButtonConfig.Triple: {
134 final String text = config.button_text[1].isEmpty()
135 ? emulationActivity.getString(R.string.i_forgot)
136 : config.button_text[1];
137 builder.setNeutralButton(text, null);
138 }
139 // fallthrough
140 case ButtonConfig.Dual: {
141 final String text = config.button_text[0].isEmpty()
142 ? emulationActivity.getString(android.R.string.cancel)
143 : config.button_text[0];
144 builder.setNegativeButton(text, null);
145 }
146 // fallthrough
147 case ButtonConfig.Single: {
148 final String text = config.button_text[2].isEmpty()
149 ? emulationActivity.getString(android.R.string.ok)
150 : config.button_text[2];
151 builder.setPositiveButton(text, null);
152 break;
153 }
154 }
155
156 final AlertDialog dialog = builder.create();
157 dialog.create();
158 if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
159 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
160 data.button = config.button_config;
161 data.text = editText.getText().toString();
162 final ValidationError error = ValidateInput(data.text);
163 if (error != ValidationError.None) {
164 HandleValidationError(config, error);
165 return;
166 }
167
168 dialog.dismiss();
169
170 synchronized (finishLock) {
171 finishLock.notifyAll();
172 }
173 });
174 }
175 if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
176 dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
177 data.button = 1;
178 dialog.dismiss();
179 synchronized (finishLock) {
180 finishLock.notifyAll();
181 }
182 });
183 }
184 if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
185 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
186 data.button = 0;
187 dialog.dismiss();
188 synchronized (finishLock) {
189 finishLock.notifyAll();
190 }
191 });
192 }
193
194 return dialog;
195 }
196 }
197
198 private static KeyboardData data;
199 private static final Object finishLock = new Object();
200
201 private static void ExecuteImpl(KeyboardConfig config) {
202 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
203
204 data = new KeyboardData(0, "");
205
206 KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
207 fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
208 }
209
210 private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
211 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
212 String message = "";
213 switch (error) {
214 case FixedLengthRequired:
215 message =
216 emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
217 break;
218 case MaxLengthExceeded:
219 message =
220 emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
221 break;
222 case BlankInputNotAllowed:
223 message = emulationActivity.getString(R.string.blank_input_not_allowed);
224 break;
225 case EmptyInputNotAllowed:
226 message = emulationActivity.getString(R.string.empty_input_not_allowed);
227 break;
228 }
229
230 new AlertDialog.Builder(emulationActivity)
231 .setTitle(R.string.software_keyboard)
232 .setMessage(message)
233 .setPositiveButton(android.R.string.ok, null)
234 .show();
235 }
236
237 public static KeyboardData Execute(KeyboardConfig config) {
238 if (config.button_config == ButtonConfig.None) {
239 Log.error("Unexpected button config None");
240 return new KeyboardData(0, "");
241 }
242
243 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
244
245 synchronized (finishLock) {
246 try {
247 finishLock.wait();
248 } catch (Exception ignored) {
249 }
250 }
251
252 return data;
253 }
254
255 public static void ShowError(String error) {
256 NativeLibrary.displayAlertMsg(
257 CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
258 error, false);
259 }
260
261 private static native ValidationError ValidateFilters(String text);
262
263 private static native ValidationError ValidateInput(String text);
264}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java
new file mode 100644
index 000000000..701cb0710
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java
@@ -0,0 +1,65 @@
1// Copyright 2020 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5package org.citra.citra_emu.camera;
6
7import android.content.Intent;
8import android.graphics.Bitmap;
9import android.provider.MediaStore;
10
11import org.citra.citra_emu.NativeLibrary;
12import org.citra.citra_emu.R;
13import org.citra.citra_emu.activities.EmulationActivity;
14import org.citra.citra_emu.utils.PicassoUtils;
15
16import androidx.annotation.Nullable;
17
18// Used in native code.
19public final class StillImageCameraHelper {
20 public static final int REQUEST_CAMERA_FILE_PICKER = 1;
21 private static final Object filePickerLock = new Object();
22 private static @Nullable
23 String filePickerPath;
24
25 // Opens file picker for camera.
26 public static @Nullable
27 String OpenFilePicker() {
28 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
29
30 // At this point, we are assuming that we already have permissions as they are
31 // needed to launch a game
32 emulationActivity.runOnUiThread(() -> {
33 Intent intent = new Intent(Intent.ACTION_PICK);
34 intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
35 emulationActivity.startActivityForResult(
36 Intent.createChooser(intent,
37 emulationActivity.getString(R.string.camera_select_image)),
38 REQUEST_CAMERA_FILE_PICKER);
39 });
40
41 synchronized (filePickerLock) {
42 try {
43 filePickerLock.wait();
44 } catch (InterruptedException ignored) {
45 }
46 }
47
48 return filePickerPath;
49 }
50
51 // Called from EmulationActivity.
52 public static void OnFilePickerResult(Intent result) {
53 filePickerPath = result == null ? null : result.getDataString();
54
55 synchronized (filePickerLock) {
56 filePickerLock.notifyAll();
57 }
58 }
59
60 // Blocking call. Load image from file and crop/resize it to fit in width x height.
61 @Nullable
62 public static Bitmap LoadImageFromFile(String uri, int width, int height) {
63 return PicassoUtils.LoadBitmapFromFile(uri, width, height);
64 }
65}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java
new file mode 100644
index 000000000..0f10f1858
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java
@@ -0,0 +1,140 @@
1package org.citra.citra_emu.dialogs;
2
3import android.content.Context;
4import android.view.InputDevice;
5import android.view.KeyEvent;
6import android.view.MotionEvent;
7
8import androidx.annotation.NonNull;
9import androidx.appcompat.app.AlertDialog;
10
11import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
12import org.citra.citra_emu.utils.Log;
13
14import java.util.ArrayList;
15import java.util.List;
16
17/**
18 * {@link AlertDialog} derivative that listens for
19 * motion events from controllers and joysticks.
20 */
21public final class MotionAlertDialog extends AlertDialog {
22 // The selected input preference
23 private final InputBindingSetting setting;
24 private final ArrayList<Float> mPreviousValues = new ArrayList<>();
25 private int mPrevDeviceId = 0;
26 private boolean mWaitingForEvent = true;
27
28 /**
29 * Constructor
30 *
31 * @param context The current {@link Context}.
32 * @param setting The Preference to show this dialog for.
33 */
34 public MotionAlertDialog(Context context, InputBindingSetting setting) {
35 super(context);
36
37 this.setting = setting;
38 }
39
40 public boolean onKeyEvent(int keyCode, KeyEvent event) {
41 Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
42 switch (event.getAction()) {
43 case KeyEvent.ACTION_UP:
44 setting.onKeyInput(event);
45 dismiss();
46 // Even if we ignore the key, we still consume it. Thus return true regardless.
47 return true;
48
49 default:
50 return false;
51 }
52 }
53
54 @Override
55 public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
56 return super.onKeyLongPress(keyCode, event);
57 }
58
59 @Override
60 public boolean dispatchKeyEvent(KeyEvent event) {
61 // Handle this key if we care about it, otherwise pass it down the framework
62 return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
63 }
64
65 @Override
66 public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
67 // Handle this event if we care about it, otherwise pass it down the framework
68 return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
69 }
70
71 private boolean onMotionEvent(MotionEvent event) {
72 if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
73 return false;
74 if (event.getAction() != MotionEvent.ACTION_MOVE)
75 return false;
76
77 InputDevice input = event.getDevice();
78
79 List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
80
81 if (input.getId() != mPrevDeviceId) {
82 mPreviousValues.clear();
83 }
84 mPrevDeviceId = input.getId();
85 boolean firstEvent = mPreviousValues.isEmpty();
86
87 int numMovedAxis = 0;
88 float axisMoveValue = 0.0f;
89 InputDevice.MotionRange lastMovedRange = null;
90 char lastMovedDir = '?';
91 if (mWaitingForEvent) {
92 for (int i = 0; i < motionRanges.size(); i++) {
93 InputDevice.MotionRange range = motionRanges.get(i);
94 int axis = range.getAxis();
95 float origValue = event.getAxisValue(axis);
96 float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
97 if (firstEvent) {
98 mPreviousValues.add(value);
99 } else {
100 float previousValue = mPreviousValues.get(i);
101
102 // Only handle the axes that are not neutral (more than 0.5)
103 // but ignore any axis that has a constant value (e.g. always 1)
104 if (Math.abs(value) > 0.5f && value != previousValue) {
105 // It is common to have multiple axes with the same physical input. For example,
106 // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
107 // To handle this, we ignore an axis motion that's the exact same as a motion
108 // we already saw. This way, we ignore axes with two names, but catch the case
109 // where a joystick is moved in two directions.
110 // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
111 if (value != axisMoveValue) {
112 axisMoveValue = value;
113 numMovedAxis++;
114 lastMovedRange = range;
115 lastMovedDir = value < 0.0f ? '-' : '+';
116 }
117 }
118 // Special case for d-pads (axis value jumps between 0 and 1 without any values
119 // in between). Without this, the user would need to press the d-pad twice
120 // due to the first press being caught by the "if (firstEvent)" case further up.
121 else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
122 numMovedAxis++;
123 lastMovedRange = range;
124 lastMovedDir = previousValue < 0.0f ? '-' : '+';
125 }
126 }
127
128 mPreviousValues.set(i, value);
129 }
130
131 // If only one axis moved, that's the winner.
132 if (numMovedAxis == 1) {
133 mWaitingForEvent = false;
134 setting.onMotionInput(input, lastMovedRange, lastMovedDir);
135 dismiss();
136 }
137 }
138 return true;
139 }
140} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java
new file mode 100644
index 000000000..d6d14cc5f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java
@@ -0,0 +1,138 @@
1// Copyright 2021 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5package org.citra.citra_emu.disk_shader_cache;
6
7import android.app.Activity;
8import android.app.Dialog;
9import android.content.DialogInterface;
10import android.os.Bundle;
11import android.view.LayoutInflater;
12import android.view.View;
13import android.widget.ProgressBar;
14import android.widget.TextView;
15
16import androidx.annotation.NonNull;
17import androidx.appcompat.app.AlertDialog;
18import androidx.fragment.app.DialogFragment;
19
20import org.citra.citra_emu.NativeLibrary;
21import org.citra.citra_emu.R;
22import org.citra.citra_emu.activities.EmulationActivity;
23import org.citra.citra_emu.utils.Log;
24
25import java.util.Objects;
26
27public class DiskShaderCacheProgress {
28
29 // Equivalent to VideoCore::LoadCallbackStage
30 public enum LoadCallbackStage {
31 Prepare,
32 Decompile,
33 Build,
34 Complete,
35 }
36
37 private static final Object finishLock = new Object();
38 private static ProgressDialogFragment fragment;
39
40 public static class ProgressDialogFragment extends DialogFragment {
41 ProgressBar progressBar;
42 TextView progressText;
43 AlertDialog dialog;
44
45 static ProgressDialogFragment newInstance(String title, String message) {
46 ProgressDialogFragment frag = new ProgressDialogFragment();
47 Bundle args = new Bundle();
48 args.putString("title", title);
49 args.putString("message", message);
50 frag.setArguments(args);
51 return frag;
52 }
53
54 @NonNull
55 @Override
56 public Dialog onCreateDialog(Bundle savedInstanceState) {
57 final Activity emulationActivity = Objects.requireNonNull(getActivity());
58
59 final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
60 final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
61
62 LayoutInflater inflater = LayoutInflater.from(emulationActivity);
63 View view = inflater.inflate(R.layout.dialog_progress_bar, null);
64
65 progressBar = view.findViewById(R.id.progress_bar);
66 progressText = view.findViewById(R.id.progress_text);
67 progressText.setText("");
68
69 setCancelable(false);
70 setRetainInstance(true);
71
72 AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity);
73 builder.setTitle(title);
74 builder.setMessage(message);
75 builder.setView(view);
76 builder.setNegativeButton(android.R.string.cancel, null);
77
78 dialog = builder.create();
79 dialog.create();
80
81 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v) -> emulationActivity.onBackPressed());
82
83 synchronized (finishLock) {
84 finishLock.notifyAll();
85 }
86
87 return dialog;
88 }
89
90 private void onUpdateProgress(String msg, int progress, int max) {
91 Objects.requireNonNull(getActivity()).runOnUiThread(() -> {
92 progressBar.setProgress(progress);
93 progressBar.setMax(max);
94 progressText.setText(String.format("%d/%d", progress, max));
95 dialog.setMessage(msg);
96 });
97 }
98 }
99
100 private static void prepareDialog() {
101 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> {
102 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
103 fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders));
104 fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders");
105 });
106
107 synchronized (finishLock) {
108 try {
109 finishLock.wait();
110 } catch (Exception ignored) {
111 }
112 }
113 }
114
115 public static void loadProgress(LoadCallbackStage stage, int progress, int max) {
116 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
117 if (emulationActivity == null) {
118 Log.error("[DiskShaderCacheProgress] EmulationActivity not present");
119 return;
120 }
121
122 switch (stage) {
123 case Prepare:
124 prepareDialog();
125 break;
126 case Decompile:
127 fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max);
128 break;
129 case Build:
130 fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max);
131 break;
132 case Complete:
133 // Workaround for when dialog is dismissed when the app is in the background
134 fragment.dismissAllowingStateLoss();
135 break;
136 }
137 }
138}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java
new file mode 100644
index 000000000..93b026364
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java
@@ -0,0 +1,57 @@
1package org.citra.citra_emu.features.cheats.model;
2
3import androidx.annotation.Keep;
4import androidx.annotation.NonNull;
5import androidx.annotation.Nullable;
6
7public class Cheat {
8 @Keep
9 private final long mPointer;
10
11 private Runnable mEnabledChangedCallback = null;
12
13 @Keep
14 private Cheat(long pointer) {
15 mPointer = pointer;
16 }
17
18 @Override
19 protected native void finalize();
20
21 @NonNull
22 public native String getName();
23
24 @NonNull
25 public native String getNotes();
26
27 @NonNull
28 public native String getCode();
29
30 public native boolean getEnabled();
31
32 public void setEnabled(boolean enabled) {
33 setEnabledImpl(enabled);
34 onEnabledChanged();
35 }
36
37 private native void setEnabledImpl(boolean enabled);
38
39 public void setEnabledChangedCallback(@Nullable Runnable callback) {
40 mEnabledChangedCallback = callback;
41 }
42
43 private void onEnabledChanged() {
44 if (mEnabledChangedCallback != null) {
45 mEnabledChangedCallback.run();
46 }
47 }
48
49 /**
50 * If the code is valid, returns 0. Otherwise, returns the 1-based index
51 * for the line containing the error.
52 */
53 public static native int isValidGatewayCode(@NonNull String code);
54
55 public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
56 @NonNull String code);
57}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
new file mode 100644
index 000000000..5748162bb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
@@ -0,0 +1,13 @@
1package org.citra.citra_emu.features.cheats.model;
2
3public class CheatEngine {
4 public static native Cheat[] getCheats();
5
6 public static native void addCheat(Cheat cheat);
7
8 public static native void removeCheat(int index);
9
10 public static native void updateCheat(int index, Cheat newCheat);
11
12 public static native void saveCheatFile();
13}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
new file mode 100644
index 000000000..66f4202d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
@@ -0,0 +1,177 @@
1package org.citra.citra_emu.features.cheats.model;
2
3import androidx.lifecycle.LiveData;
4import androidx.lifecycle.MutableLiveData;
5import androidx.lifecycle.ViewModel;
6
7public class CheatsViewModel extends ViewModel {
8 private int mSelectedCheatPosition = -1;
9 private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
10 private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
11 private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
12
13 private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
14 private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
15 private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
16 private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
17
18 private Cheat[] mCheats;
19 private boolean mCheatsNeedSaving = false;
20
21 public void load() {
22 mCheats = CheatEngine.getCheats();
23
24 for (int i = 0; i < mCheats.length; i++) {
25 int position = i;
26 mCheats[i].setEnabledChangedCallback(() -> {
27 mCheatsNeedSaving = true;
28 notifyCheatUpdated(position);
29 });
30 }
31 }
32
33 public void saveIfNeeded() {
34 if (mCheatsNeedSaving) {
35 CheatEngine.saveCheatFile();
36 mCheatsNeedSaving = false;
37 }
38 }
39
40 public Cheat[] getCheats() {
41 return mCheats;
42 }
43
44 public LiveData<Cheat> getSelectedCheat() {
45 return mSelectedCheat;
46 }
47
48 public void setSelectedCheat(Cheat cheat, int position) {
49 if (mIsEditing.getValue()) {
50 setIsEditing(false);
51 }
52
53 mSelectedCheat.setValue(cheat);
54 mSelectedCheatPosition = position;
55 }
56
57 public LiveData<Boolean> getIsAdding() {
58 return mIsAdding;
59 }
60
61 public LiveData<Boolean> getIsEditing() {
62 return mIsEditing;
63 }
64
65 public void setIsEditing(boolean isEditing) {
66 mIsEditing.setValue(isEditing);
67
68 if (mIsAdding.getValue() && !isEditing) {
69 mIsAdding.setValue(false);
70 setSelectedCheat(null, -1);
71 }
72 }
73
74 /**
75 * When a cheat is added, the integer stored in the returned LiveData
76 * changes to the position of that cheat, then changes back to null.
77 */
78 public LiveData<Integer> getCheatAddedEvent() {
79 return mCheatAddedEvent;
80 }
81
82 private void notifyCheatAdded(int position) {
83 mCheatAddedEvent.setValue(position);
84 mCheatAddedEvent.setValue(null);
85 }
86
87 public void startAddingCheat() {
88 mSelectedCheat.setValue(null);
89 mSelectedCheatPosition = -1;
90
91 mIsAdding.setValue(true);
92 mIsEditing.setValue(true);
93 }
94
95 public void finishAddingCheat(Cheat cheat) {
96 if (!mIsAdding.getValue()) {
97 throw new IllegalStateException();
98 }
99
100 mIsAdding.setValue(false);
101 mIsEditing.setValue(false);
102
103 int position = mCheats.length;
104
105 CheatEngine.addCheat(cheat);
106
107 mCheatsNeedSaving = true;
108 load();
109
110 notifyCheatAdded(position);
111 setSelectedCheat(mCheats[position], position);
112 }
113
114 /**
115 * When a cheat is edited, the integer stored in the returned LiveData
116 * changes to the position of that cheat, then changes back to null.
117 */
118 public LiveData<Integer> getCheatUpdatedEvent() {
119 return mCheatChangedEvent;
120 }
121
122 /**
123 * Notifies that an edit has been made to the contents of the cheat at the given position.
124 */
125 private void notifyCheatUpdated(int position) {
126 mCheatChangedEvent.setValue(position);
127 mCheatChangedEvent.setValue(null);
128 }
129
130 public void updateSelectedCheat(Cheat newCheat) {
131 CheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
132
133 mCheatsNeedSaving = true;
134 load();
135
136 notifyCheatUpdated(mSelectedCheatPosition);
137 setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
138 }
139
140 /**
141 * When a cheat is deleted, the integer stored in the returned LiveData
142 * changes to the position of that cheat, then changes back to null.
143 */
144 public LiveData<Integer> getCheatDeletedEvent() {
145 return mCheatDeletedEvent;
146 }
147
148 /**
149 * Notifies that the cheat at the given position has been deleted.
150 */
151 private void notifyCheatDeleted(int position) {
152 mCheatDeletedEvent.setValue(position);
153 mCheatDeletedEvent.setValue(null);
154 }
155
156 public void deleteSelectedCheat() {
157 int position = mSelectedCheatPosition;
158
159 setSelectedCheat(null, -1);
160
161 CheatEngine.removeCheat(position);
162
163 mCheatsNeedSaving = true;
164 load();
165
166 notifyCheatDeleted(position);
167 }
168
169 public LiveData<Boolean> getOpenDetailsViewEvent() {
170 return mOpenDetailsViewEvent;
171 }
172
173 public void openDetailsView() {
174 mOpenDetailsViewEvent.setValue(true);
175 mOpenDetailsViewEvent.setValue(false);
176 }
177}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
new file mode 100644
index 000000000..762cdb80e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
@@ -0,0 +1,174 @@
1package org.citra.citra_emu.features.cheats.ui;
2
3import android.os.Bundle;
4import android.view.LayoutInflater;
5import android.view.View;
6import android.view.ViewGroup;
7import android.widget.Button;
8import android.widget.EditText;
9import android.widget.ScrollView;
10import android.widget.TextView;
11
12import androidx.annotation.NonNull;
13import androidx.annotation.Nullable;
14import androidx.appcompat.app.AlertDialog;
15import androidx.fragment.app.Fragment;
16import androidx.lifecycle.ViewModelProvider;
17
18import org.citra.citra_emu.R;
19import org.citra.citra_emu.features.cheats.model.Cheat;
20import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
21
22public class CheatDetailsFragment extends Fragment {
23 private View mRoot;
24 private ScrollView mScrollView;
25 private TextView mLabelName;
26 private EditText mEditName;
27 private EditText mEditNotes;
28 private EditText mEditCode;
29 private Button mButtonDelete;
30 private Button mButtonEdit;
31 private Button mButtonCancel;
32 private Button mButtonOk;
33
34 private CheatsViewModel mViewModel;
35
36 @Nullable
37 @Override
38 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
39 @Nullable Bundle savedInstanceState) {
40 return inflater.inflate(R.layout.fragment_cheat_details, container, false);
41 }
42
43 @Override
44 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
45 mRoot = view.findViewById(R.id.root);
46 mScrollView = view.findViewById(R.id.scroll_view);
47 mLabelName = view.findViewById(R.id.label_name);
48 mEditName = view.findViewById(R.id.edit_name);
49 mEditNotes = view.findViewById(R.id.edit_notes);
50 mEditCode = view.findViewById(R.id.edit_code);
51 mButtonDelete = view.findViewById(R.id.button_delete);
52 mButtonEdit = view.findViewById(R.id.button_edit);
53 mButtonCancel = view.findViewById(R.id.button_cancel);
54 mButtonOk = view.findViewById(R.id.button_ok);
55
56 CheatsActivity activity = (CheatsActivity) requireActivity();
57 mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
58
59 mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
60 this::onSelectedCheatUpdated);
61 mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
62
63 mButtonDelete.setOnClickListener(this::onDeleteClicked);
64 mButtonEdit.setOnClickListener(this::onEditClicked);
65 mButtonCancel.setOnClickListener(this::onCancelClicked);
66 mButtonOk.setOnClickListener(this::onOkClicked);
67
68 // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
69 // at the same time. If the user is navigating using a d-pad and moves focus to an element
70 // in the currently hidden pane, we need to manually show that pane.
71 CheatsActivity.setOnFocusChangeListenerRecursively(view,
72 (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
73 }
74
75 private void clearEditErrors() {
76 mEditName.setError(null);
77 mEditCode.setError(null);
78 }
79
80 private void onDeleteClicked(View view) {
81 String name = mEditName.getText().toString();
82
83 AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
84 builder.setMessage(getString(R.string.cheats_delete_confirmation, name));
85 builder.setPositiveButton(android.R.string.yes,
86 (dialog, i) -> mViewModel.deleteSelectedCheat());
87 builder.setNegativeButton(android.R.string.no, null);
88 builder.show();
89 }
90
91 private void onEditClicked(View view) {
92 mViewModel.setIsEditing(true);
93 mButtonOk.requestFocus();
94 }
95
96 private void onCancelClicked(View view) {
97 mViewModel.setIsEditing(false);
98 onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
99 mButtonDelete.requestFocus();
100 }
101
102 private void onOkClicked(View view) {
103 clearEditErrors();
104
105 String name = mEditName.getText().toString();
106 String notes = mEditNotes.getText().toString();
107 String code = mEditCode.getText().toString();
108
109 if (name.isEmpty()) {
110 mEditName.setError(getString(R.string.cheats_error_no_name));
111 mScrollView.smoothScrollTo(0, mLabelName.getTop());
112 return;
113 } else if (code.isEmpty()) {
114 mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
115 mScrollView.smoothScrollTo(0, mEditCode.getBottom());
116 return;
117 }
118
119 int validityResult = Cheat.isValidGatewayCode(code);
120
121 if (validityResult != 0) {
122 mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
123 mScrollView.smoothScrollTo(0, mEditCode.getBottom());
124 return;
125 }
126
127 Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
128
129 if (mViewModel.getIsAdding().getValue()) {
130 mViewModel.finishAddingCheat(newCheat);
131 } else {
132 mViewModel.updateSelectedCheat(newCheat);
133 }
134
135 mButtonEdit.requestFocus();
136 }
137
138 private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
139 clearEditErrors();
140
141 boolean isEditing = mViewModel.getIsEditing().getValue();
142
143 mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
144
145 // If the fragment was recreated while editing a cheat, it's vital that we
146 // don't repopulate the fields, otherwise the user's changes will be lost
147 if (!isEditing) {
148 if (cheat == null) {
149 mEditName.setText("");
150 mEditNotes.setText("");
151 mEditCode.setText("");
152 } else {
153 mEditName.setText(cheat.getName());
154 mEditNotes.setText(cheat.getNotes());
155 mEditCode.setText(cheat.getCode());
156 }
157 }
158 }
159
160 private void onIsEditingUpdated(boolean isEditing) {
161 if (isEditing) {
162 mRoot.setVisibility(View.VISIBLE);
163 }
164
165 mEditName.setEnabled(isEditing);
166 mEditNotes.setEnabled(isEditing);
167 mEditCode.setEnabled(isEditing);
168
169 mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
170 mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
171 mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
172 mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
173 }
174}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
new file mode 100644
index 000000000..6c67a31d4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
@@ -0,0 +1,46 @@
1package org.citra.citra_emu.features.cheats.ui;
2
3import android.os.Bundle;
4import android.view.LayoutInflater;
5import android.view.View;
6import android.view.ViewGroup;
7
8import androidx.annotation.NonNull;
9import androidx.annotation.Nullable;
10import androidx.fragment.app.Fragment;
11import androidx.lifecycle.ViewModelProvider;
12import androidx.recyclerview.widget.LinearLayoutManager;
13import androidx.recyclerview.widget.RecyclerView;
14
15import com.google.android.material.floatingactionbutton.FloatingActionButton;
16
17import org.citra.citra_emu.R;
18import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
19import org.citra.citra_emu.ui.DividerItemDecoration;
20
21public class CheatListFragment extends Fragment {
22 @Nullable
23 @Override
24 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
25 @Nullable Bundle savedInstanceState) {
26 return inflater.inflate(R.layout.fragment_cheat_list, container, false);
27 }
28
29 @Override
30 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
31 RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
32 FloatingActionButton fab = view.findViewById(R.id.fab);
33
34 CheatsActivity activity = (CheatsActivity) requireActivity();
35 CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
36
37 recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
38 recyclerView.setLayoutManager(new LinearLayoutManager(activity));
39 recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
40
41 fab.setOnClickListener(v -> {
42 viewModel.startAddingCheat();
43 viewModel.openDetailsView();
44 });
45 }
46}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
new file mode 100644
index 000000000..8ba8f86e7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
@@ -0,0 +1,56 @@
1package org.citra.citra_emu.features.cheats.ui;
2
3import android.view.View;
4import android.widget.CheckBox;
5import android.widget.CompoundButton;
6import android.widget.TextView;
7
8import androidx.annotation.NonNull;
9import androidx.lifecycle.ViewModelProvider;
10import androidx.recyclerview.widget.RecyclerView;
11
12import org.citra.citra_emu.R;
13import org.citra.citra_emu.features.cheats.model.Cheat;
14import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
15
16public class CheatViewHolder extends RecyclerView.ViewHolder
17 implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
18 private final View mRoot;
19 private final TextView mName;
20 private final CheckBox mCheckbox;
21
22 private CheatsViewModel mViewModel;
23 private Cheat mCheat;
24 private int mPosition;
25
26 public CheatViewHolder(@NonNull View itemView) {
27 super(itemView);
28
29 mRoot = itemView.findViewById(R.id.root);
30 mName = itemView.findViewById(R.id.text_name);
31 mCheckbox = itemView.findViewById(R.id.checkbox);
32 }
33
34 public void bind(CheatsActivity activity, Cheat cheat, int position) {
35 mCheckbox.setOnCheckedChangeListener(null);
36
37 mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
38 mCheat = cheat;
39 mPosition = position;
40
41 mName.setText(mCheat.getName());
42 mCheckbox.setChecked(mCheat.getEnabled());
43
44 mRoot.setOnClickListener(this);
45 mCheckbox.setOnCheckedChangeListener(this);
46 }
47
48 public void onClick(View root) {
49 mViewModel.setSelectedCheat(mCheat, mPosition);
50 mViewModel.openDetailsView();
51 }
52
53 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
54 mCheat.setEnabled(isChecked);
55 }
56}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
new file mode 100644
index 000000000..a36bf427c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
@@ -0,0 +1,161 @@
1package org.citra.citra_emu.features.cheats.ui;
2
3import android.content.Context;
4import android.content.Intent;
5import android.os.Bundle;
6import android.view.Menu;
7import android.view.MenuInflater;
8import android.view.View;
9import android.view.ViewGroup;
10
11import androidx.annotation.NonNull;
12import androidx.appcompat.app.AppCompatActivity;
13import androidx.core.view.ViewCompat;
14import androidx.lifecycle.ViewModelProvider;
15import androidx.slidingpanelayout.widget.SlidingPaneLayout;
16
17import org.citra.citra_emu.R;
18import org.citra.citra_emu.features.cheats.model.Cheat;
19import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
20import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
21
22public class CheatsActivity extends AppCompatActivity
23 implements SlidingPaneLayout.PanelSlideListener {
24 private CheatsViewModel mViewModel;
25
26 private SlidingPaneLayout mSlidingPaneLayout;
27 private View mCheatList;
28 private View mCheatDetails;
29
30 private View mCheatListLastFocus;
31 private View mCheatDetailsLastFocus;
32
33 public static void launch(Context context) {
34 Intent intent = new Intent(context, CheatsActivity.class);
35 context.startActivity(intent);
36 }
37
38 @Override
39 protected void onCreate(Bundle savedInstanceState) {
40 super.onCreate(savedInstanceState);
41
42 mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
43 mViewModel.load();
44
45 setContentView(R.layout.activity_cheats);
46
47 mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
48 mCheatList = findViewById(R.id.cheat_list);
49 mCheatDetails = findViewById(R.id.cheat_details);
50
51 mCheatListLastFocus = mCheatList;
52 mCheatDetailsLastFocus = mCheatDetails;
53
54 mSlidingPaneLayout.addPanelSlideListener(this);
55
56 getOnBackPressedDispatcher().addCallback(this,
57 new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
58
59 mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
60 mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
61 onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
62
63 mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
64
65 // Show "Up" button in the action bar for navigation
66 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
67 }
68
69 @Override
70 public boolean onCreateOptionsMenu(Menu menu) {
71 MenuInflater inflater = getMenuInflater();
72 inflater.inflate(R.menu.menu_settings, menu);
73
74 return true;
75 }
76
77 @Override
78 protected void onStop() {
79 super.onStop();
80
81 mViewModel.saveIfNeeded();
82 }
83
84 @Override
85 public void onPanelSlide(@NonNull View panel, float slideOffset) {
86 }
87
88 @Override
89 public void onPanelOpened(@NonNull View panel) {
90 boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
91 mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
92 }
93
94 @Override
95 public void onPanelClosed(@NonNull View panel) {
96 boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
97 mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
98 }
99
100 private void onIsEditingChanged(boolean isEditing) {
101 if (isEditing) {
102 mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
103 }
104 }
105
106 private void onSelectedCheatChanged(Cheat selectedCheat) {
107 boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
108
109 if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
110 mSlidingPaneLayout.close();
111 }
112
113 mSlidingPaneLayout.setLockMode(cheatSelected ?
114 SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
115 }
116
117 public void onListViewFocusChange(boolean hasFocus) {
118 if (hasFocus) {
119 mCheatListLastFocus = mCheatList.findFocus();
120 if (mCheatListLastFocus == null)
121 throw new NullPointerException();
122
123 mSlidingPaneLayout.close();
124 }
125 }
126
127 public void onDetailsViewFocusChange(boolean hasFocus) {
128 if (hasFocus) {
129 mCheatDetailsLastFocus = mCheatDetails.findFocus();
130 if (mCheatDetailsLastFocus == null)
131 throw new NullPointerException();
132
133 mSlidingPaneLayout.open();
134 }
135 }
136
137 @Override
138 public boolean onSupportNavigateUp() {
139 onBackPressed();
140 return true;
141 }
142
143 private void openDetailsView(boolean open) {
144 if (open) {
145 mSlidingPaneLayout.open();
146 }
147 }
148
149 public static void setOnFocusChangeListenerRecursively(@NonNull View view,
150 View.OnFocusChangeListener listener) {
151 view.setOnFocusChangeListener(listener);
152
153 if (view instanceof ViewGroup) {
154 ViewGroup viewGroup = (ViewGroup) view;
155 for (int i = 0; i < viewGroup.getChildCount(); i++) {
156 View child = viewGroup.getChildAt(i);
157 setOnFocusChangeListenerRecursively(child, listener);
158 }
159 }
160 }
161}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
new file mode 100644
index 000000000..9cb2ce8d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
@@ -0,0 +1,72 @@
1package org.citra.citra_emu.features.cheats.ui;
2
3import android.view.LayoutInflater;
4import android.view.View;
5import android.view.ViewGroup;
6
7import androidx.annotation.NonNull;
8import androidx.recyclerview.widget.RecyclerView;
9
10import org.citra.citra_emu.R;
11import org.citra.citra_emu.features.cheats.model.Cheat;
12import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
13
14public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
15 private final CheatsActivity mActivity;
16 private final CheatsViewModel mViewModel;
17
18 public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
19 mActivity = activity;
20 mViewModel = viewModel;
21
22 mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
23 if (position != null) {
24 notifyItemInserted(position);
25 }
26 });
27
28 mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
29 if (position != null) {
30 notifyItemChanged(position);
31 }
32 });
33
34 mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
35 if (position != null) {
36 notifyItemRemoved(position);
37 }
38 });
39 }
40
41 @NonNull
42 @Override
43 public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
44 LayoutInflater inflater = LayoutInflater.from(parent.getContext());
45
46 View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
47 addViewListeners(cheatView);
48 return new CheatViewHolder(cheatView);
49 }
50
51 @Override
52 public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
53 holder.bind(mActivity, getItemAt(position), position);
54 }
55
56 @Override
57 public int getItemCount() {
58 return mViewModel.getCheats().length;
59 }
60
61 private void addViewListeners(View view) {
62 // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
63 // at the same time. If the user is navigating using a d-pad and moves focus to an element
64 // in the currently hidden pane, we need to manually show that pane.
65 CheatsActivity.setOnFocusChangeListenerRecursively(view,
66 (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
67 }
68
69 private Cheat getItemAt(int position) {
70 return mViewModel.getCheats()[position];
71 }
72}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java
new file mode 100644
index 000000000..932dcf1d3
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java
@@ -0,0 +1,23 @@
1package org.citra.citra_emu.features.settings.model;
2
3public final class BooleanSetting extends Setting {
4 private boolean mValue;
5
6 public BooleanSetting(String key, String section, boolean value) {
7 super(key, section);
8 mValue = value;
9 }
10
11 public boolean getValue() {
12 return mValue;
13 }
14
15 public void setValue(boolean value) {
16 mValue = value;
17 }
18
19 @Override
20 public String getValueAsString() {
21 return mValue ? "True" : "False";
22 }
23}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java
new file mode 100644
index 000000000..275f0ecea
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java
@@ -0,0 +1,23 @@
1package org.citra.citra_emu.features.settings.model;
2
3public final class FloatSetting extends Setting {
4 private float mValue;
5
6 public FloatSetting(String key, String section, float value) {
7 super(key, section);
8 mValue = value;
9 }
10
11 public float getValue() {
12 return mValue;
13 }
14
15 public void setValue(float value) {
16 mValue = value;
17 }
18
19 @Override
20 public String getValueAsString() {
21 return Float.toString(mValue);
22 }
23}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java
new file mode 100644
index 000000000..f712e5bfa
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java
@@ -0,0 +1,23 @@
1package org.citra.citra_emu.features.settings.model;
2
3public final class IntSetting extends Setting {
4 private int mValue;
5
6 public IntSetting(String key, String section, int value) {
7 super(key, section);
8 mValue = value;
9 }
10
11 public int getValue() {
12 return mValue;
13 }
14
15 public void setValue(int value) {
16 mValue = value;
17 }
18
19 @Override
20 public String getValueAsString() {
21 return Integer.toString(mValue);
22 }
23}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java
new file mode 100644
index 000000000..b762847c9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java
@@ -0,0 +1,42 @@
1package org.citra.citra_emu.features.settings.model;
2
3/**
4 * Abstraction for a setting item as read from / written to Citra's configuration ini files.
5 * These files generally consist of a key/value pair, though the type of value is ambiguous and
6 * must be inferred at read-time. The type of value determines which child of this class is used
7 * to represent the Setting.
8 */
9public abstract class Setting {
10 private String mKey;
11 private String mSection;
12
13 /**
14 * Base constructor.
15 *
16 * @param key Everything to the left of the = in a line from the ini file.
17 * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets.
18 */
19 public Setting(String key, String section) {
20 mKey = key;
21 mSection = section;
22 }
23
24 /**
25 * @return The identifier used to write this setting to the ini file.
26 */
27 public String getKey() {
28 return mKey;
29 }
30
31 /**
32 * @return The name of the header under which this Setting should be written in the ini file.
33 */
34 public String getSection() {
35 return mSection;
36 }
37
38 /**
39 * @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
40 */
41 public abstract String getValueAsString();
42}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java
new file mode 100644
index 000000000..0a291aa6b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java
@@ -0,0 +1,55 @@
1package org.citra.citra_emu.features.settings.model;
2
3import java.util.HashMap;
4
5/**
6 * A semantically-related group of Settings objects. These Settings are
7 * internally stored as a HashMap.
8 */
9public final class SettingSection {
10 private String mName;
11
12 private HashMap<String, Setting> mSettings = new HashMap<>();
13
14 /**
15 * Create a new SettingSection with no Settings in it.
16 *
17 * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
18 */
19 public SettingSection(String name) {
20 mName = name;
21 }
22
23 public String getName() {
24 return mName;
25 }
26
27 /**
28 * Convenience method; inserts a value directly into the backing HashMap.
29 *
30 * @param setting The Setting to be inserted.
31 */
32 public void putSetting(Setting setting) {
33 mSettings.put(setting.getKey(), setting);
34 }
35
36 /**
37 * Convenience method; gets a value directly from the backing HashMap.
38 *
39 * @param key Used to retrieve the Setting.
40 * @return A Setting object (you should probably cast this before using)
41 */
42 public Setting getSetting(String key) {
43 return mSettings.get(key);
44 }
45
46 public HashMap<String, Setting> getSettings() {
47 return mSettings;
48 }
49
50 public void mergeSection(SettingSection settingSection) {
51 for (Setting setting : settingSection.mSettings.values()) {
52 putSetting(setting);
53 }
54 }
55}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java
new file mode 100644
index 000000000..9684966f2
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java
@@ -0,0 +1,132 @@
1package org.citra.citra_emu.features.settings.model;
2
3import android.text.TextUtils;
4
5import org.citra.citra_emu.CitraApplication;
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
8import org.citra.citra_emu.features.settings.utils.SettingsFile;
9
10import java.util.Arrays;
11import java.util.HashMap;
12import java.util.List;
13import java.util.Map;
14import java.util.TreeMap;
15
16public class Settings {
17 public static final String SECTION_PREMIUM = "Premium";
18 public static final String SECTION_CORE = "Core";
19 public static final String SECTION_SYSTEM = "System";
20 public static final String SECTION_CAMERA = "Camera";
21 public static final String SECTION_CONTROLS = "Controls";
22 public static final String SECTION_RENDERER = "Renderer";
23 public static final String SECTION_LAYOUT = "Layout";
24 public static final String SECTION_UTILITY = "Utility";
25 public static final String SECTION_AUDIO = "Audio";
26 public static final String SECTION_DEBUG = "Debug";
27
28 private String gameId;
29
30 private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
31
32 static {
33 configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
34 }
35
36 /**
37 * A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
38 * when getting a key not already in the map
39 */
40 public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
41 @Override
42 public SettingSection get(Object key) {
43 if (!(key instanceof String)) {
44 return null;
45 }
46
47 String stringKey = (String) key;
48
49 if (!super.containsKey(stringKey)) {
50 SettingSection section = new SettingSection(stringKey);
51 super.put(stringKey, section);
52 return section;
53 }
54 return super.get(key);
55 }
56 }
57
58 private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
59
60 public SettingSection getSection(String sectionName) {
61 return sections.get(sectionName);
62 }
63
64 public boolean isEmpty() {
65 return sections.isEmpty();
66 }
67
68 public HashMap<String, SettingSection> getSections() {
69 return sections;
70 }
71
72 public void loadSettings(SettingsActivityView view) {
73 sections = new Settings.SettingsSectionMap();
74 loadCitraSettings(view);
75
76 if (!TextUtils.isEmpty(gameId)) {
77 loadCustomGameSettings(gameId, view);
78 }
79 }
80
81 private void loadCitraSettings(SettingsActivityView view) {
82 for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
83 String fileName = entry.getKey();
84 sections.putAll(SettingsFile.readFile(fileName, view));
85 }
86 }
87
88 private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
89 // custom game settings
90 mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
91 }
92
93 private void mergeSections(HashMap<String, SettingSection> updatedSections) {
94 for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
95 if (sections.containsKey(entry.getKey())) {
96 SettingSection originalSection = sections.get(entry.getKey());
97 SettingSection updatedSection = entry.getValue();
98 originalSection.mergeSection(updatedSection);
99 } else {
100 sections.put(entry.getKey(), entry.getValue());
101 }
102 }
103 }
104
105 public void loadSettings(String gameId, SettingsActivityView view) {
106 this.gameId = gameId;
107 loadSettings(view);
108 }
109
110 public void saveSettings(SettingsActivityView view) {
111 if (TextUtils.isEmpty(gameId)) {
112 view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false);
113
114 for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
115 String fileName = entry.getKey();
116 List<String> sectionNames = entry.getValue();
117 TreeMap<String, SettingSection> iniSections = new TreeMap<>();
118 for (String section : sectionNames) {
119 iniSections.put(section, sections.get(section));
120 }
121
122 SettingsFile.saveFile(fileName, iniSections, view);
123 }
124 } else {
125 // custom game settings
126 view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false);
127
128 SettingsFile.saveCustomGameSettings(gameId, sections);
129 }
130
131 }
132} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java
new file mode 100644
index 000000000..b906b7010
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java
@@ -0,0 +1,23 @@
1package org.citra.citra_emu.features.settings.model;
2
3public final class StringSetting extends Setting {
4 private String mValue;
5
6 public StringSetting(String key, String section, String value) {
7 super(key, section);
8 mValue = value;
9 }
10
11 public String getValue() {
12 return mValue;
13 }
14
15 public void setValue(String value) {
16 mValue = value;
17 }
18
19 @Override
20 public String getValueAsString() {
21 return mValue;
22 }
23}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java
new file mode 100644
index 000000000..baf40709f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java
@@ -0,0 +1,80 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.CitraApplication;
4import org.citra.citra_emu.R;
5import org.citra.citra_emu.features.settings.model.BooleanSetting;
6import org.citra.citra_emu.features.settings.model.IntSetting;
7import org.citra.citra_emu.features.settings.model.Setting;
8import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
9
10public final class CheckBoxSetting extends SettingsItem {
11 private boolean mDefaultValue;
12 private boolean mShowPerformanceWarning;
13 private SettingsFragmentView mView;
14
15 public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
16 boolean defaultValue, Setting setting) {
17 super(key, section, setting, titleId, descriptionId);
18 mDefaultValue = defaultValue;
19 mShowPerformanceWarning = false;
20 }
21
22 public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
23 boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) {
24 super(key, section, setting, titleId, descriptionId);
25 mDefaultValue = defaultValue;
26 mView = view;
27 mShowPerformanceWarning = show_performance_warning;
28 }
29
30 public boolean isChecked() {
31 if (getSetting() == null) {
32 return mDefaultValue;
33 }
34
35 // Try integer setting
36 try {
37 IntSetting setting = (IntSetting) getSetting();
38 return setting.getValue() == 1;
39 } catch (ClassCastException exception) {
40 }
41
42 // Try boolean setting
43 try {
44 BooleanSetting setting = (BooleanSetting) getSetting();
45 return setting.getValue() == true;
46 } catch (ClassCastException exception) {
47 }
48
49 return mDefaultValue;
50 }
51
52 /**
53 * Write a value to the backing boolean. If that boolean was previously null,
54 * initializes a new one and returns it, so it can be added to the Hashmap.
55 *
56 * @param checked Pretty self explanatory.
57 * @return null if overwritten successfully; otherwise, a newly created BooleanSetting.
58 */
59 public IntSetting setChecked(boolean checked) {
60 // Show a performance warning if the setting has been disabled
61 if (mShowPerformanceWarning && !checked) {
62 mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true);
63 }
64
65 if (getSetting() == null) {
66 IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0);
67 setSetting(setting);
68 return setting;
69 } else {
70 IntSetting setting = (IntSetting) getSetting();
71 setting.setValue(checked ? 1 : 0);
72 return null;
73 }
74 }
75
76 @Override
77 public int getType() {
78 return TYPE_CHECKBOX;
79 }
80}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java
new file mode 100644
index 000000000..afc3352cc
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java
@@ -0,0 +1,40 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.Setting;
4import org.citra.citra_emu.features.settings.model.StringSetting;
5
6public final class DateTimeSetting extends SettingsItem {
7 private String mDefaultValue;
8
9 public DateTimeSetting(String key, String section, int titleId, int descriptionId,
10 String defaultValue, Setting setting) {
11 super(key, section, setting, titleId, descriptionId);
12 mDefaultValue = defaultValue;
13 }
14
15 public String getValue() {
16 if (getSetting() != null) {
17 StringSetting setting = (StringSetting) getSetting();
18 return setting.getValue();
19 } else {
20 return mDefaultValue;
21 }
22 }
23
24 public StringSetting setSelectedValue(String datetime) {
25 if (getSetting() == null) {
26 StringSetting setting = new StringSetting(getKey(), getSection(), datetime);
27 setSetting(setting);
28 return setting;
29 } else {
30 StringSetting setting = (StringSetting) getSetting();
31 setting.setValue(datetime);
32 return null;
33 }
34 }
35
36 @Override
37 public int getType() {
38 return TYPE_DATETIME_SETTING;
39 }
40} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java
new file mode 100644
index 000000000..bac8876cd
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java
@@ -0,0 +1,14 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.Setting;
4
5public final class HeaderSetting extends SettingsItem {
6 public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) {
7 super(key, null, setting, titleId, descriptionId);
8 }
9
10 @Override
11 public int getType() {
12 return SettingsItem.TYPE_HEADER;
13 }
14}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java
new file mode 100644
index 000000000..e9141a208
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java
@@ -0,0 +1,382 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import android.content.SharedPreferences;
4import android.preference.PreferenceManager;
5import android.view.InputDevice;
6import android.view.KeyEvent;
7import android.widget.Toast;
8
9import org.citra.citra_emu.CitraApplication;
10import org.citra.citra_emu.NativeLibrary;
11import org.citra.citra_emu.R;
12import org.citra.citra_emu.features.settings.model.Setting;
13import org.citra.citra_emu.features.settings.model.StringSetting;
14import org.citra.citra_emu.features.settings.utils.SettingsFile;
15
16public final class InputBindingSetting extends SettingsItem {
17 private static final String INPUT_MAPPING_PREFIX = "InputMapping";
18
19 public InputBindingSetting(String key, String section, int titleId, Setting setting) {
20 super(key, section, setting, titleId, 0);
21 }
22
23 public String getValue() {
24 if (getSetting() == null) {
25 return "";
26 }
27
28 StringSetting setting = (StringSetting) getSetting();
29 return setting.getValue();
30 }
31
32 /**
33 * Returns true if this key is for the 3DS Circle Pad
34 */
35 private boolean IsCirclePad() {
36 switch (getKey()) {
37 case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
38 case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
39 return true;
40 }
41 return false;
42 }
43
44 /**
45 * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
46 */
47 public boolean IsHorizontalOrientation() {
48 switch (getKey()) {
49 case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
50 case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
51 case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
52 return true;
53 }
54 return false;
55 }
56
57 /**
58 * Returns true if this key is for the 3DS C-Stick
59 */
60 private boolean IsCStick() {
61 switch (getKey()) {
62 case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
63 case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
64 return true;
65 }
66 return false;
67 }
68
69 /**
70 * Returns true if this key is for the 3DS D-Pad
71 */
72 private boolean IsDPad() {
73 switch (getKey()) {
74 case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
75 case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
76 return true;
77 }
78 return false;
79 }
80
81 /**
82 * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
83 * triggers on the 3DS, but we support them as such on a physical gamepad.
84 */
85 public boolean IsTrigger() {
86 switch (getKey()) {
87 case SettingsFile.KEY_BUTTON_L:
88 case SettingsFile.KEY_BUTTON_R:
89 case SettingsFile.KEY_BUTTON_ZL:
90 case SettingsFile.KEY_BUTTON_ZR:
91 return true;
92 }
93 return false;
94 }
95
96 /**
97 * Returns true if a gamepad axis can be used to map this key.
98 */
99 public boolean IsAxisMappingSupported() {
100 return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
101 }
102
103 /**
104 * Returns true if a gamepad button can be used to map this key.
105 */
106 private boolean IsButtonMappingSupported() {
107 return !IsAxisMappingSupported() || IsTrigger();
108 }
109
110 /**
111 * Returns the Citra button code for the settings key.
112 */
113 private int getButtonCode() {
114 switch (getKey()) {
115 case SettingsFile.KEY_BUTTON_A:
116 return NativeLibrary.ButtonType.BUTTON_A;
117 case SettingsFile.KEY_BUTTON_B:
118 return NativeLibrary.ButtonType.BUTTON_B;
119 case SettingsFile.KEY_BUTTON_X:
120 return NativeLibrary.ButtonType.BUTTON_X;
121 case SettingsFile.KEY_BUTTON_Y:
122 return NativeLibrary.ButtonType.BUTTON_Y;
123 case SettingsFile.KEY_BUTTON_L:
124 return NativeLibrary.ButtonType.TRIGGER_L;
125 case SettingsFile.KEY_BUTTON_R:
126 return NativeLibrary.ButtonType.TRIGGER_R;
127 case SettingsFile.KEY_BUTTON_ZL:
128 return NativeLibrary.ButtonType.BUTTON_ZL;
129 case SettingsFile.KEY_BUTTON_ZR:
130 return NativeLibrary.ButtonType.BUTTON_ZR;
131 case SettingsFile.KEY_BUTTON_SELECT:
132 return NativeLibrary.ButtonType.BUTTON_SELECT;
133 case SettingsFile.KEY_BUTTON_START:
134 return NativeLibrary.ButtonType.BUTTON_START;
135 case SettingsFile.KEY_BUTTON_UP:
136 return NativeLibrary.ButtonType.DPAD_UP;
137 case SettingsFile.KEY_BUTTON_DOWN:
138 return NativeLibrary.ButtonType.DPAD_DOWN;
139 case SettingsFile.KEY_BUTTON_LEFT:
140 return NativeLibrary.ButtonType.DPAD_LEFT;
141 case SettingsFile.KEY_BUTTON_RIGHT:
142 return NativeLibrary.ButtonType.DPAD_RIGHT;
143 }
144 return -1;
145 }
146
147 /**
148 * Returns the settings key for the specified Citra button code.
149 */
150 private static String getButtonKey(int buttonCode) {
151 switch (buttonCode) {
152 case NativeLibrary.ButtonType.BUTTON_A:
153 return SettingsFile.KEY_BUTTON_A;
154 case NativeLibrary.ButtonType.BUTTON_B:
155 return SettingsFile.KEY_BUTTON_B;
156 case NativeLibrary.ButtonType.BUTTON_X:
157 return SettingsFile.KEY_BUTTON_X;
158 case NativeLibrary.ButtonType.BUTTON_Y:
159 return SettingsFile.KEY_BUTTON_Y;
160 case NativeLibrary.ButtonType.TRIGGER_L:
161 return SettingsFile.KEY_BUTTON_L;
162 case NativeLibrary.ButtonType.TRIGGER_R:
163 return SettingsFile.KEY_BUTTON_R;
164 case NativeLibrary.ButtonType.BUTTON_ZL:
165 return SettingsFile.KEY_BUTTON_ZL;
166 case NativeLibrary.ButtonType.BUTTON_ZR:
167 return SettingsFile.KEY_BUTTON_ZR;
168 case NativeLibrary.ButtonType.BUTTON_SELECT:
169 return SettingsFile.KEY_BUTTON_SELECT;
170 case NativeLibrary.ButtonType.BUTTON_START:
171 return SettingsFile.KEY_BUTTON_START;
172 case NativeLibrary.ButtonType.DPAD_UP:
173 return SettingsFile.KEY_BUTTON_UP;
174 case NativeLibrary.ButtonType.DPAD_DOWN:
175 return SettingsFile.KEY_BUTTON_DOWN;
176 case NativeLibrary.ButtonType.DPAD_LEFT:
177 return SettingsFile.KEY_BUTTON_LEFT;
178 case NativeLibrary.ButtonType.DPAD_RIGHT:
179 return SettingsFile.KEY_BUTTON_RIGHT;
180 }
181 return "";
182 }
183
184 /**
185 * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
186 * settings on re-mapping or clearing of a setting.
187 */
188 private String getReverseKey() {
189 String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
190
191 if (IsAxisMappingSupported() && !IsTrigger()) {
192 // Triggers are the only axis-supported mappings without orientation
193 reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
194 }
195
196 return reverseKey;
197 }
198
199 /**
200 * Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
201 */
202 public void removeOldMapping() {
203 // Get preferences editor
204 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
205 SharedPreferences.Editor editor = preferences.edit();
206
207 // Try remove all possible keys we wrote for this setting
208 String oldKey = preferences.getString(getReverseKey(), "");
209 if (!oldKey.equals("")) {
210 editor.remove(getKey()); // Used for ui text
211 editor.remove(oldKey); // Used for button mapping
212 editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
213 editor.remove(oldKey + "_GuestButton"); // Used for axis button
214 }
215
216 // Apply changes
217 editor.apply();
218 }
219
220 /**
221 * Helper function to get the settings key for an gamepad button.
222 */
223 public static String getInputButtonKey(int keyCode) {
224 return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
225 }
226
227 /**
228 * Helper function to get the settings key for an gamepad axis.
229 */
230 public static String getInputAxisKey(int axis) {
231 return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
232 }
233
234 /**
235 * Helper function to get the settings key for an gamepad axis button (stick or trigger).
236 */
237 public static String getInputAxisButtonKey(int axis) {
238 return getInputAxisKey(axis) + "_GuestButton";
239 }
240
241 /**
242 * Helper function to get the settings key for an gamepad axis orientation.
243 */
244 public static String getInputAxisOrientationKey(int axis) {
245 return getInputAxisKey(axis) + "_GuestOrientation";
246 }
247
248 /**
249 * Helper function to write a gamepad button mapping for the setting.
250 */
251 private void WriteButtonMapping(String key) {
252 // Get preferences editor
253 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
254 SharedPreferences.Editor editor = preferences.edit();
255
256 // Remove mapping for another setting using this input
257 int oldButtonCode = preferences.getInt(key, -1);
258 if (oldButtonCode != -1) {
259 String oldKey = getButtonKey(oldButtonCode);
260 editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
261 }
262
263 // Cleanup old mapping for this setting
264 removeOldMapping();
265
266 // Write new mapping
267 editor.putInt(key, getButtonCode());
268
269 // Write next reverse mapping for future cleanup
270 editor.putString(getReverseKey(), key);
271
272 // Apply changes
273 editor.apply();
274 }
275
276 /**
277 * Helper function to write a gamepad axis mapping for the setting.
278 */
279 private void WriteAxisMapping(int axis, int value) {
280 // Get preferences editor
281 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
282 SharedPreferences.Editor editor = preferences.edit();
283
284 // Cleanup old mapping
285 removeOldMapping();
286
287 // Write new mapping
288 editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
289 editor.putInt(getInputAxisButtonKey(axis), value);
290
291 // Write next reverse mapping for future cleanup
292 editor.putString(getReverseKey(), getInputAxisKey(axis));
293
294 // Apply changes
295 editor.apply();
296 }
297
298 /**
299 * Saves the provided key input setting as an Android preference.
300 *
301 * @param keyEvent KeyEvent of this key press.
302 */
303 public void onKeyInput(KeyEvent keyEvent) {
304 if (!IsButtonMappingSupported()) {
305 Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
306 return;
307 }
308
309 InputDevice device = keyEvent.getDevice();
310
311 WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
312
313 String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
314 setUiString(uiString);
315 }
316
317 /**
318 * Saves the provided motion input setting as an Android preference.
319 *
320 * @param device InputDevice from which the input event originated.
321 * @param motionRange MotionRange of the movement
322 * @param axisDir Either '-' or '+' (currently unused)
323 */
324 public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
325 char axisDir) {
326 if (!IsAxisMappingSupported()) {
327 Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
328 return;
329 }
330
331 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
332 SharedPreferences.Editor editor = preferences.edit();
333
334 int button;
335 if (IsCirclePad()) {
336 button = NativeLibrary.ButtonType.STICK_LEFT;
337 } else if (IsCStick()) {
338 button = NativeLibrary.ButtonType.STICK_C;
339 } else if (IsDPad()) {
340 button = NativeLibrary.ButtonType.DPAD;
341 } else {
342 button = getButtonCode();
343 }
344
345 WriteAxisMapping(motionRange.getAxis(), button);
346
347 String uiString = device.getName() + ": Axis " + motionRange.getAxis();
348 setUiString(uiString);
349
350 editor.apply();
351 }
352
353 /**
354 * Sets the string to use in the configuration UI for the gamepad input.
355 */
356 private StringSetting setUiString(String ui) {
357 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
358 SharedPreferences.Editor editor = preferences.edit();
359
360 if (getSetting() == null) {
361 StringSetting setting = new StringSetting(getKey(), getSection(), "");
362 setSetting(setting);
363
364 editor.putString(setting.getKey(), ui);
365 editor.apply();
366
367 return setting;
368 } else {
369 StringSetting setting = (StringSetting) getSetting();
370
371 editor.putString(setting.getKey(), ui);
372 editor.apply();
373
374 return null;
375 }
376 }
377
378 @Override
379 public int getType() {
380 return TYPE_INPUT_BINDING;
381 }
382}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java
new file mode 100644
index 000000000..2b1793d3e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java
@@ -0,0 +1,12 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3public final class PremiumHeader extends SettingsItem {
4 public PremiumHeader() {
5 super(null, null, null, 0, 0);
6 }
7
8 @Override
9 public int getType() {
10 return SettingsItem.TYPE_PREMIUM;
11 }
12}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java
new file mode 100644
index 000000000..c0560d2dc
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java
@@ -0,0 +1,59 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import android.content.SharedPreferences;
4import android.preference.PreferenceManager;
5
6import org.citra.citra_emu.CitraApplication;
7import org.citra.citra_emu.R;
8import org.citra.citra_emu.features.settings.model.Setting;
9import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
10
11public final class PremiumSingleChoiceSetting extends SettingsItem {
12 private int mDefaultValue;
13
14 private int mChoicesId;
15 private int mValuesId;
16 private SettingsFragmentView mView;
17
18 private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
19
20 public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
21 int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) {
22 super(key, section, setting, titleId, descriptionId);
23 mValuesId = valuesId;
24 mChoicesId = choicesId;
25 mDefaultValue = defaultValue;
26 mView = view;
27 }
28
29 public int getChoicesId() {
30 return mChoicesId;
31 }
32
33 public int getValuesId() {
34 return mValuesId;
35 }
36
37 public int getSelectedValue() {
38 return mPreferences.getInt(getKey(), mDefaultValue);
39 }
40
41 /**
42 * Write a value to the backing int. If that int was previously null,
43 * initializes a new one and returns it, so it can be added to the Hashmap.
44 *
45 * @param selection New value of the int.
46 * @return null if overwritten successfully otherwise; a newly created IntSetting.
47 */
48 public void setSelectedValue(int selection) {
49 final SharedPreferences.Editor editor = mPreferences.edit();
50 editor.putInt(getKey(), selection);
51 editor.apply();
52 mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false);
53 }
54
55 @Override
56 public int getType() {
57 return TYPE_SINGLE_CHOICE;
58 }
59}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java
new file mode 100644
index 000000000..305352022
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java
@@ -0,0 +1,107 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.Setting;
4import org.citra.citra_emu.features.settings.model.Settings;
5import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
6
7/**
8 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
9 * Each one corresponds to a {@link Setting} object, so this class's subclasses
10 * should vaguely correspond to those subclasses. There are a few with multiple analogues
11 * and a few with none (Headers, for example, do not correspond to anything in the ini
12 * file.)
13 */
14public abstract class SettingsItem {
15 public static final int TYPE_HEADER = 0;
16 public static final int TYPE_CHECKBOX = 1;
17 public static final int TYPE_SINGLE_CHOICE = 2;
18 public static final int TYPE_SLIDER = 3;
19 public static final int TYPE_SUBMENU = 4;
20 public static final int TYPE_INPUT_BINDING = 5;
21 public static final int TYPE_STRING_SINGLE_CHOICE = 6;
22 public static final int TYPE_DATETIME_SETTING = 7;
23 public static final int TYPE_PREMIUM = 8;
24
25 private String mKey;
26 private String mSection;
27
28 private Setting mSetting;
29
30 private int mNameId;
31 private int mDescriptionId;
32 private boolean mIsPremium;
33
34 /**
35 * Base constructor. Takes a key / section name in case the third parameter, the Setting,
36 * is null; in which case, one can be constructed and saved using the key / section.
37 *
38 * @param key Identifier for the Setting represented by this Item.
39 * @param section Section to which the Setting belongs.
40 * @param setting A possibly-null backing Setting, to be modified on UI events.
41 * @param nameId Resource ID for a text string to be displayed as this setting's name.
42 * @param descriptionId Resource ID for a text string to be displayed as this setting's description.
43 */
44 public SettingsItem(String key, String section, Setting setting, int nameId,
45 int descriptionId) {
46 mKey = key;
47 mSection = section;
48 mSetting = setting;
49 mNameId = nameId;
50 mDescriptionId = descriptionId;
51 mIsPremium = (section == Settings.SECTION_PREMIUM);
52 }
53
54 /**
55 * @return The identifier for the backing Setting.
56 */
57 public String getKey() {
58 return mKey;
59 }
60
61 /**
62 * @return The header under which the backing Setting belongs.
63 */
64 public String getSection() {
65 return mSection;
66 }
67
68 /**
69 * @return The backing Setting, possibly null.
70 */
71 public Setting getSetting() {
72 return mSetting;
73 }
74
75 /**
76 * Replace the backing setting with a new one. Generally used in cases where
77 * the backing setting is null.
78 *
79 * @param setting A non-null Setting.
80 */
81 public void setSetting(Setting setting) {
82 mSetting = setting;
83 }
84
85 /**
86 * @return A resource ID for a text string representing this Setting's name.
87 */
88 public int getNameId() {
89 return mNameId;
90 }
91
92 public int getDescriptionId() {
93 return mDescriptionId;
94 }
95
96 public boolean isPremium() {
97 return mIsPremium;
98 }
99
100 /**
101 * Used by {@link SettingsAdapter}'s onCreateViewHolder()
102 * method to determine which type of ViewHolder should be created.
103 *
104 * @return An integer (ideally, one of the constants defined in this file)
105 */
106 public abstract int getType();
107}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java
new file mode 100644
index 000000000..ee9d225d6
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java
@@ -0,0 +1,60 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.IntSetting;
4import org.citra.citra_emu.features.settings.model.Setting;
5
6public final class SingleChoiceSetting extends SettingsItem {
7 private int mDefaultValue;
8
9 private int mChoicesId;
10 private int mValuesId;
11
12 public SingleChoiceSetting(String key, String section, int titleId, int descriptionId,
13 int choicesId, int valuesId, int defaultValue, Setting setting) {
14 super(key, section, setting, titleId, descriptionId);
15 mValuesId = valuesId;
16 mChoicesId = choicesId;
17 mDefaultValue = defaultValue;
18 }
19
20 public int getChoicesId() {
21 return mChoicesId;
22 }
23
24 public int getValuesId() {
25 return mValuesId;
26 }
27
28 public int getSelectedValue() {
29 if (getSetting() != null) {
30 IntSetting setting = (IntSetting) getSetting();
31 return setting.getValue();
32 } else {
33 return mDefaultValue;
34 }
35 }
36
37 /**
38 * Write a value to the backing int. If that int was previously null,
39 * initializes a new one and returns it, so it can be added to the Hashmap.
40 *
41 * @param selection New value of the int.
42 * @return null if overwritten successfully otherwise; a newly created IntSetting.
43 */
44 public IntSetting setSelectedValue(int selection) {
45 if (getSetting() == null) {
46 IntSetting setting = new IntSetting(getKey(), getSection(), selection);
47 setSetting(setting);
48 return setting;
49 } else {
50 IntSetting setting = (IntSetting) getSetting();
51 setting.setValue(selection);
52 return null;
53 }
54 }
55
56 @Override
57 public int getType() {
58 return TYPE_SINGLE_CHOICE;
59 }
60}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java
new file mode 100644
index 000000000..551b13f99
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java
@@ -0,0 +1,101 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.FloatSetting;
4import org.citra.citra_emu.features.settings.model.IntSetting;
5import org.citra.citra_emu.features.settings.model.Setting;
6import org.citra.citra_emu.utils.Log;
7
8public final class SliderSetting extends SettingsItem {
9 private int mMin;
10 private int mMax;
11 private int mDefaultValue;
12
13 private String mUnits;
14
15 public SliderSetting(String key, String section, int titleId, int descriptionId,
16 int min, int max, String units, int defaultValue, Setting setting) {
17 super(key, section, setting, titleId, descriptionId);
18 mMin = min;
19 mMax = max;
20 mUnits = units;
21 mDefaultValue = defaultValue;
22 }
23
24 public int getMin() {
25 return mMin;
26 }
27
28 public int getMax() {
29 return mMax;
30 }
31
32 public int getDefaultValue() {
33 return mDefaultValue;
34 }
35
36 public int getSelectedValue() {
37 Setting setting = getSetting();
38
39 if (setting == null) {
40 return mDefaultValue;
41 }
42
43 if (setting instanceof IntSetting) {
44 IntSetting intSetting = (IntSetting) setting;
45 return intSetting.getValue();
46 } else if (setting instanceof FloatSetting) {
47 FloatSetting floatSetting = (FloatSetting) setting;
48 return Math.round(floatSetting.getValue());
49 } else {
50 Log.error("[SliderSetting] Error casting setting type.");
51 return -1;
52 }
53 }
54
55 /**
56 * Write a value to the backing int. If that int was previously null,
57 * initializes a new one and returns it, so it can be added to the Hashmap.
58 *
59 * @param selection New value of the int.
60 * @return null if overwritten successfully otherwise; a newly created IntSetting.
61 */
62 public IntSetting setSelectedValue(int selection) {
63 if (getSetting() == null) {
64 IntSetting setting = new IntSetting(getKey(), getSection(), selection);
65 setSetting(setting);
66 return setting;
67 } else {
68 IntSetting setting = (IntSetting) getSetting();
69 setting.setValue(selection);
70 return null;
71 }
72 }
73
74 /**
75 * Write a value to the backing float. If that float was previously null,
76 * initializes a new one and returns it, so it can be added to the Hashmap.
77 *
78 * @param selection New value of the float.
79 * @return null if overwritten successfully otherwise; a newly created FloatSetting.
80 */
81 public FloatSetting setSelectedValue(float selection) {
82 if (getSetting() == null) {
83 FloatSetting setting = new FloatSetting(getKey(), getSection(), selection);
84 setSetting(setting);
85 return setting;
86 } else {
87 FloatSetting setting = (FloatSetting) getSetting();
88 setting.setValue(selection);
89 return null;
90 }
91 }
92
93 public String getUnits() {
94 return mUnits;
95 }
96
97 @Override
98 public int getType() {
99 return TYPE_SLIDER;
100 }
101}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java
new file mode 100644
index 000000000..057145d9d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java
@@ -0,0 +1,82 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.Setting;
4import org.citra.citra_emu.features.settings.model.StringSetting;
5
6public class StringSingleChoiceSetting extends SettingsItem {
7 private String mDefaultValue;
8
9 private String[] mChoicesId;
10 private String[] mValuesId;
11
12 public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
13 String[] choicesId, String[] valuesId, String defaultValue, Setting setting) {
14 super(key, section, setting, titleId, descriptionId);
15 mValuesId = valuesId;
16 mChoicesId = choicesId;
17 mDefaultValue = defaultValue;
18 }
19
20 public String[] getChoicesId() {
21 return mChoicesId;
22 }
23
24 public String[] getValuesId() {
25 return mValuesId;
26 }
27
28 public String getValueAt(int index) {
29 if (mValuesId == null)
30 return null;
31
32 if (index >= 0 && index < mValuesId.length) {
33 return mValuesId[index];
34 }
35
36 return "";
37 }
38
39 public String getSelectedValue() {
40 if (getSetting() != null) {
41 StringSetting setting = (StringSetting) getSetting();
42 return setting.getValue();
43 } else {
44 return mDefaultValue;
45 }
46 }
47
48 public int getSelectValueIndex() {
49 String selectedValue = getSelectedValue();
50 for (int i = 0; i < mValuesId.length; i++) {
51 if (mValuesId[i].equals(selectedValue)) {
52 return i;
53 }
54 }
55
56 return -1;
57 }
58
59 /**
60 * Write a value to the backing int. If that int was previously null,
61 * initializes a new one and returns it, so it can be added to the Hashmap.
62 *
63 * @param selection New value of the int.
64 * @return null if overwritten successfully otherwise; a newly created IntSetting.
65 */
66 public StringSetting setSelectedValue(String selection) {
67 if (getSetting() == null) {
68 StringSetting setting = new StringSetting(getKey(), getSection(), selection);
69 setSetting(setting);
70 return setting;
71 } else {
72 StringSetting setting = (StringSetting) getSetting();
73 setting.setValue(selection);
74 return null;
75 }
76 }
77
78 @Override
79 public int getType() {
80 return TYPE_STRING_SINGLE_CHOICE;
81 }
82}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java
new file mode 100644
index 000000000..9d44a923f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java
@@ -0,0 +1,21 @@
1package org.citra.citra_emu.features.settings.model.view;
2
3import org.citra.citra_emu.features.settings.model.Setting;
4
5public final class SubmenuSetting extends SettingsItem {
6 private String mMenuKey;
7
8 public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) {
9 super(key, null, setting, titleId, descriptionId);
10 mMenuKey = menuKey;
11 }
12
13 public String getMenuKey() {
14 return mMenuKey;
15 }
16
17 @Override
18 public int getType() {
19 return TYPE_SUBMENU;
20 }
21}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java
new file mode 100644
index 000000000..23c3c4c9e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java
@@ -0,0 +1,215 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.app.ProgressDialog;
4import android.content.Context;
5import android.content.Intent;
6import android.content.IntentFilter;
7import android.os.Bundle;
8import android.provider.Settings;
9import android.view.Menu;
10import android.view.MenuInflater;
11import android.widget.Toast;
12
13import androidx.annotation.NonNull;
14import androidx.appcompat.app.AppCompatActivity;
15import androidx.fragment.app.FragmentTransaction;
16import androidx.localbroadcastmanager.content.LocalBroadcastManager;
17
18import org.citra.citra_emu.NativeLibrary;
19import org.citra.citra_emu.R;
20import org.citra.citra_emu.utils.DirectoryInitialization;
21import org.citra.citra_emu.utils.DirectoryStateReceiver;
22import org.citra.citra_emu.utils.EmulationMenuSettings;
23
24public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
25 private static final String ARG_MENU_TAG = "menu_tag";
26 private static final String ARG_GAME_ID = "game_id";
27 private static final String FRAGMENT_TAG = "settings";
28 private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this);
29
30 private ProgressDialog dialog;
31
32 public static void launch(Context context, String menuTag, String gameId) {
33 Intent settings = new Intent(context, SettingsActivity.class);
34 settings.putExtra(ARG_MENU_TAG, menuTag);
35 settings.putExtra(ARG_GAME_ID, gameId);
36 context.startActivity(settings);
37 }
38
39 @Override
40 protected void onCreate(Bundle savedInstanceState) {
41 super.onCreate(savedInstanceState);
42
43 setContentView(R.layout.activity_settings);
44
45 Intent launcher = getIntent();
46 String gameID = launcher.getStringExtra(ARG_GAME_ID);
47 String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
48
49 mPresenter.onCreate(savedInstanceState, menuTag, gameID);
50
51 // Show "Back" button in the action bar for navigation
52 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
53 }
54
55 @Override
56 public boolean onSupportNavigateUp() {
57 onBackPressed();
58
59 return true;
60 }
61
62 @Override
63 public boolean onCreateOptionsMenu(Menu menu) {
64 MenuInflater inflater = getMenuInflater();
65 inflater.inflate(R.menu.menu_settings, menu);
66
67 return true;
68 }
69
70 @Override
71 protected void onSaveInstanceState(@NonNull Bundle outState) {
72 // Critical: If super method is not called, rotations will be busted.
73 super.onSaveInstanceState(outState);
74 mPresenter.saveState(outState);
75 }
76
77 @Override
78 protected void onStart() {
79 super.onStart();
80 mPresenter.onStart();
81 }
82
83 /**
84 * If this is called, the user has left the settings screen (potentially through the
85 * home button) and will expect their changes to be persisted. So we kick off an
86 * IntentService which will do so on a background thread.
87 */
88 @Override
89 protected void onStop() {
90 super.onStop();
91
92 mPresenter.onStop(isFinishing());
93
94 // Update framebuffer layout when closing the settings
95 NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
96 getWindowManager().getDefaultDisplay().getRotation());
97 }
98
99 @Override
100 public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) {
101 if (!addToStack && getFragment() != null) {
102 return;
103 }
104
105 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
106
107 if (addToStack) {
108 if (areSystemAnimationsEnabled()) {
109 transaction.setCustomAnimations(
110 R.animator.settings_enter,
111 R.animator.settings_exit,
112 R.animator.settings_pop_enter,
113 R.animator.setttings_pop_exit);
114 }
115
116 transaction.addToBackStack(null);
117 }
118 transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG);
119
120 transaction.commit();
121 }
122
123 private boolean areSystemAnimationsEnabled() {
124 float duration = Settings.Global.getFloat(
125 getContentResolver(),
126 Settings.Global.ANIMATOR_DURATION_SCALE, 1);
127 float transition = Settings.Global.getFloat(
128 getContentResolver(),
129 Settings.Global.TRANSITION_ANIMATION_SCALE, 1);
130 return duration != 0 && transition != 0;
131 }
132
133 @Override
134 public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) {
135 LocalBroadcastManager.getInstance(this).registerReceiver(
136 receiver,
137 filter);
138 DirectoryInitialization.start(this);
139 }
140
141 @Override
142 public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) {
143 LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
144 }
145
146 @Override
147 public void showLoading() {
148 if (dialog == null) {
149 dialog = new ProgressDialog(this);
150 dialog.setMessage(getString(R.string.load_settings));
151 dialog.setIndeterminate(true);
152 }
153
154 dialog.show();
155 }
156
157 @Override
158 public void hideLoading() {
159 dialog.dismiss();
160 }
161
162 @Override
163 public void showPermissionNeededHint() {
164 Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
165 .show();
166 }
167
168 @Override
169 public void showExternalStorageNotMountedHint() {
170 Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
171 .show();
172 }
173
174 @Override
175 public org.citra.citra_emu.features.settings.model.Settings getSettings() {
176 return mPresenter.getSettings();
177 }
178
179 @Override
180 public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) {
181 mPresenter.setSettings(settings);
182 }
183
184 @Override
185 public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) {
186 SettingsFragmentView fragment = getFragment();
187
188 if (fragment != null) {
189 fragment.onSettingsFileLoaded(settings);
190 }
191 }
192
193 @Override
194 public void onSettingsFileNotFound() {
195 SettingsFragmentView fragment = getFragment();
196
197 if (fragment != null) {
198 fragment.loadDefaultSettings();
199 }
200 }
201
202 @Override
203 public void showToastMessage(String message, boolean is_long) {
204 Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
205 }
206
207 @Override
208 public void onSettingChanged() {
209 mPresenter.onSettingChanged();
210 }
211
212 private SettingsFragment getFragment() {
213 return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
214 }
215}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java
new file mode 100644
index 000000000..0d63873bb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java
@@ -0,0 +1,124 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.content.IntentFilter;
4import android.os.Bundle;
5import android.text.TextUtils;
6
7import org.citra.citra_emu.NativeLibrary;
8import org.citra.citra_emu.features.settings.model.Settings;
9import org.citra.citra_emu.features.settings.utils.SettingsFile;
10import org.citra.citra_emu.utils.DirectoryInitialization;
11import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
12import org.citra.citra_emu.utils.DirectoryStateReceiver;
13import org.citra.citra_emu.utils.Log;
14import org.citra.citra_emu.utils.ThemeUtil;
15
16import java.io.File;
17
18public final class SettingsActivityPresenter {
19 private static final String KEY_SHOULD_SAVE = "should_save";
20
21 private SettingsActivityView mView;
22
23 private Settings mSettings = new Settings();
24
25 private boolean mShouldSave;
26
27 private DirectoryStateReceiver directoryStateReceiver;
28
29 private String menuTag;
30 private String gameId;
31
32 public SettingsActivityPresenter(SettingsActivityView view) {
33 mView = view;
34 }
35
36 public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) {
37 if (savedInstanceState == null) {
38 this.menuTag = menuTag;
39 this.gameId = gameId;
40 } else {
41 mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE);
42 }
43 }
44
45 public void onStart() {
46 prepareCitraDirectoriesIfNeeded();
47 }
48
49 void loadSettingsUI() {
50 if (mSettings.isEmpty()) {
51 if (!TextUtils.isEmpty(gameId)) {
52 mSettings.loadSettings(gameId, mView);
53 } else {
54 mSettings.loadSettings(mView);
55 }
56 }
57
58 mView.showSettingsFragment(menuTag, false, gameId);
59 mView.onSettingsFileLoaded(mSettings);
60 }
61
62 private void prepareCitraDirectoriesIfNeeded() {
63 File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini");
64 if (!configFile.exists()) {
65 Log.error("Citra config file could not be found!");
66 }
67 if (DirectoryInitialization.areCitraDirectoriesReady()) {
68 loadSettingsUI();
69 } else {
70 mView.showLoading();
71 IntentFilter statusIntentFilter = new IntentFilter(
72 DirectoryInitialization.BROADCAST_ACTION);
73
74 directoryStateReceiver =
75 new DirectoryStateReceiver(directoryInitializationState ->
76 {
77 if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
78 mView.hideLoading();
79 loadSettingsUI();
80 } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
81 mView.showPermissionNeededHint();
82 mView.hideLoading();
83 } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
84 mView.showExternalStorageNotMountedHint();
85 mView.hideLoading();
86 }
87 });
88
89 mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter);
90 }
91 }
92
93 public void setSettings(Settings settings) {
94 mSettings = settings;
95 }
96
97 public Settings getSettings() {
98 return mSettings;
99 }
100
101 public void onStop(boolean finishing) {
102 if (directoryStateReceiver != null) {
103 mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
104 directoryStateReceiver = null;
105 }
106
107 if (mSettings != null && finishing && mShouldSave) {
108 Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
109 mSettings.saveSettings(mView);
110 }
111
112 ThemeUtil.applyTheme();
113
114 NativeLibrary.ReloadSettings();
115 }
116
117 public void onSettingChanged() {
118 mShouldSave = true;
119 }
120
121 public void saveState(Bundle outState) {
122 outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
123 }
124}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java
new file mode 100644
index 000000000..0d26d48a7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java
@@ -0,0 +1,103 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.content.IntentFilter;
4
5import org.citra.citra_emu.features.settings.model.Settings;
6import org.citra.citra_emu.utils.DirectoryStateReceiver;
7
8/**
9 * Abstraction for the Activity that manages SettingsFragments.
10 */
11public interface SettingsActivityView {
12 /**
13 * Show a new SettingsFragment.
14 *
15 * @param menuTag Identifier for the settings group that should be displayed.
16 * @param addToStack Whether or not this fragment should replace a previous one.
17 */
18 void showSettingsFragment(String menuTag, boolean addToStack, String gameId);
19
20 /**
21 * Called by a contained Fragment to get access to the Setting HashMap
22 * loaded from disk, so that each Fragment doesn't need to perform its own
23 * read operation.
24 *
25 * @return A possibly null HashMap of Settings.
26 */
27 Settings getSettings();
28
29 /**
30 * Used to provide the Activity with Settings HashMaps if a Fragment already
31 * has one; for example, if a rotation occurs, the Fragment will not be killed,
32 * but the Activity will, so the Activity needs to have its HashMaps resupplied.
33 *
34 * @param settings The ArrayList of all the Settings HashMaps.
35 */
36 void setSettings(Settings settings);
37
38 /**
39 * Called when an asynchronous load operation completes.
40 *
41 * @param settings The (possibly null) result of the ini load operation.
42 */
43 void onSettingsFileLoaded(Settings settings);
44
45 /**
46 * Called when an asynchronous load operation fails.
47 */
48 void onSettingsFileNotFound();
49
50 /**
51 * Display a popup text message on screen.
52 *
53 * @param message The contents of the onscreen message.
54 * @param is_long Whether this should be a long Toast or short one.
55 */
56 void showToastMessage(String message, boolean is_long);
57
58 /**
59 * End the activity.
60 */
61 void finish();
62
63 /**
64 * Called by a containing Fragment to tell the Activity that a setting was changed;
65 * unless this has been called, the Activity will not save to disk.
66 */
67 void onSettingChanged();
68
69 /**
70 * Show loading dialog while loading the settings
71 */
72 void showLoading();
73
74 /**
75 * Hide the loading the dialog
76 */
77 void hideLoading();
78
79 /**
80 * Show a hint to the user that the app needs write to external storage access
81 */
82 void showPermissionNeededHint();
83
84 /**
85 * Show a hint to the user that the app needs the external storage to be mounted
86 */
87 void showExternalStorageNotMountedHint();
88
89 /**
90 * Start the DirectoryInitialization and listen for the result.
91 *
92 * @param receiver the broadcast receiver for the DirectoryInitialization
93 * @param filter the Intent broadcasts to be received.
94 */
95 void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
96
97 /**
98 * Stop listening to the DirectoryInitialization.
99 *
100 * @param receiver The broadcast receiver to unregister.
101 */
102 void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
103}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java
new file mode 100644
index 000000000..bfd7c71a9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java
@@ -0,0 +1,487 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.content.Context;
4import android.content.DialogInterface;
5import android.view.LayoutInflater;
6import android.view.View;
7import android.view.ViewGroup;
8import android.widget.DatePicker;
9import android.widget.SeekBar;
10import android.widget.TextView;
11import android.widget.TimePicker;
12
13import androidx.appcompat.app.AlertDialog;
14import androidx.recyclerview.widget.RecyclerView;
15
16import org.citra.citra_emu.R;
17import org.citra.citra_emu.dialogs.MotionAlertDialog;
18import org.citra.citra_emu.features.settings.model.FloatSetting;
19import org.citra.citra_emu.features.settings.model.IntSetting;
20import org.citra.citra_emu.features.settings.model.StringSetting;
21import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
22import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
23import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
24import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
25import org.citra.citra_emu.features.settings.model.view.SettingsItem;
26import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
27import org.citra.citra_emu.features.settings.model.view.SliderSetting;
28import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
29import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
30import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
31import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
32import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
33import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
34import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
35import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
36import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
37import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
38import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
39import org.citra.citra_emu.ui.main.MainActivity;
40import org.citra.citra_emu.utils.Log;
41
42import java.util.ArrayList;
43
44public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
45 implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener {
46 private SettingsFragmentView mView;
47 private Context mContext;
48 private ArrayList<SettingsItem> mSettings;
49
50 private SettingsItem mClickedItem;
51 private int mClickedPosition;
52 private int mSeekbarProgress;
53
54 private AlertDialog mDialog;
55 private TextView mTextSliderValue;
56
57 public SettingsAdapter(SettingsFragmentView view, Context context) {
58 mView = view;
59 mContext = context;
60 mClickedPosition = -1;
61 }
62
63 @Override
64 public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
65 View view;
66 LayoutInflater inflater = LayoutInflater.from(parent.getContext());
67
68 switch (viewType) {
69 case SettingsItem.TYPE_HEADER:
70 view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
71 return new HeaderViewHolder(view, this);
72
73 case SettingsItem.TYPE_CHECKBOX:
74 view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
75 return new CheckBoxSettingViewHolder(view, this);
76
77 case SettingsItem.TYPE_SINGLE_CHOICE:
78 case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
79 view = inflater.inflate(R.layout.list_item_setting, parent, false);
80 return new SingleChoiceViewHolder(view, this);
81
82 case SettingsItem.TYPE_SLIDER:
83 view = inflater.inflate(R.layout.list_item_setting, parent, false);
84 return new SliderViewHolder(view, this);
85
86 case SettingsItem.TYPE_SUBMENU:
87 view = inflater.inflate(R.layout.list_item_setting, parent, false);
88 return new SubmenuViewHolder(view, this);
89
90 case SettingsItem.TYPE_INPUT_BINDING:
91 view = inflater.inflate(R.layout.list_item_setting, parent, false);
92 return new InputBindingSettingViewHolder(view, this, mContext);
93
94 case SettingsItem.TYPE_DATETIME_SETTING:
95 view = inflater.inflate(R.layout.list_item_setting, parent, false);
96 return new DateTimeViewHolder(view, this);
97
98 case SettingsItem.TYPE_PREMIUM:
99 view = inflater.inflate(R.layout.premium_item_setting, parent, false);
100 return new PremiumViewHolder(view, this, mView);
101
102 default:
103 Log.error("[SettingsAdapter] Invalid view type: " + viewType);
104 return null;
105 }
106 }
107
108 @Override
109 public void onBindViewHolder(SettingViewHolder holder, int position) {
110 holder.bind(getItem(position));
111 }
112
113 private SettingsItem getItem(int position) {
114 return mSettings.get(position);
115 }
116
117 @Override
118 public int getItemCount() {
119 if (mSettings != null) {
120 return mSettings.size();
121 } else {
122 return 0;
123 }
124 }
125
126 @Override
127 public int getItemViewType(int position) {
128 return getItem(position).getType();
129 }
130
131 public void setSettings(ArrayList<SettingsItem> settings) {
132 mSettings = settings;
133 notifyDataSetChanged();
134 }
135
136 public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
137 IntSetting setting = item.setChecked(checked);
138 notifyItemChanged(position);
139
140 if (setting != null) {
141 mView.putSetting(setting);
142 }
143
144 mView.onSettingChanged();
145 }
146
147 public void onSingleChoiceClick(PremiumSingleChoiceSetting item) {
148 mClickedItem = item;
149
150 int value = getSelectionForSingleChoiceValue(item);
151
152 AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
153
154 builder.setTitle(item.getNameId());
155 builder.setSingleChoiceItems(item.getChoicesId(), value, this);
156
157 mDialog = builder.show();
158 }
159
160 public void onSingleChoiceClick(SingleChoiceSetting item) {
161 mClickedItem = item;
162
163 int value = getSelectionForSingleChoiceValue(item);
164
165 AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
166
167 builder.setTitle(item.getNameId());
168 builder.setSingleChoiceItems(item.getChoicesId(), value, this);
169
170 mDialog = builder.show();
171 }
172
173 public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
174 mClickedPosition = position;
175
176 if (!item.isPremium() || MainActivity.isPremiumActive()) {
177 // Setting is either not Premium, or the user has Premium
178 onSingleChoiceClick(item);
179 return;
180 }
181
182 // User needs Premium, invoke the billing flow
183 MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
184 }
185
186 public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) {
187 mClickedPosition = position;
188
189 if (!item.isPremium() || MainActivity.isPremiumActive()) {
190 // Setting is either not Premium, or the user has Premium
191 onSingleChoiceClick(item);
192 return;
193 }
194
195 // User needs Premium, invoke the billing flow
196 MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
197 }
198
199 public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
200 mClickedItem = item;
201
202 AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
203
204 builder.setTitle(item.getNameId());
205 builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
206
207 mDialog = builder.show();
208 }
209
210 public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
211 mClickedPosition = position;
212
213 if (!item.isPremium() || MainActivity.isPremiumActive()) {
214 // Setting is either not Premium, or the user has Premium
215 onStringSingleChoiceClick(item);
216 return;
217 }
218
219 // User needs Premium, invoke the billing flow
220 MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item));
221 }
222
223 DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
224
225 public void onDateTimeClick(DateTimeSetting item, int position) {
226 mClickedItem = item;
227 mClickedPosition = position;
228
229 AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
230
231 LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
232 View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
233
234 DatePicker dp = view.findViewById(R.id.date_picker);
235 TimePicker tp = view.findViewById(R.id.time_picker);
236
237 //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
238 String settingValue = item.getValue();
239 dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
240
241 tp.setIs24HourView(true);
242 tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
243 tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
244
245 DialogInterface.OnClickListener ok = (dialog, which) -> {
246 //set it
247 int year = dp.getYear();
248 if (year < 2000) {
249 year = 2000;
250 }
251 String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
252 String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
253 String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
254 String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
255 String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
256
257 StringSetting setting = item.setSelectedValue(datetime);
258 if (setting != null) {
259 mView.putSetting(setting);
260 }
261
262 mView.onSettingChanged();
263
264 mClickedItem = null;
265 closeDialog();
266 };
267
268 builder.setView(view);
269 builder.setPositiveButton(android.R.string.ok, ok);
270 builder.setNegativeButton(android.R.string.cancel, defaultCancelListener);
271 mDialog = builder.show();
272 }
273
274 public void onSliderClick(SliderSetting item, int position) {
275 mClickedItem = item;
276 mClickedPosition = position;
277 mSeekbarProgress = item.getSelectedValue();
278 AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
279
280 LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
281 View view = inflater.inflate(R.layout.dialog_seekbar, null);
282
283 SeekBar seekbar = view.findViewById(R.id.seekbar);
284
285 builder.setTitle(item.getNameId());
286 builder.setView(view);
287 builder.setPositiveButton(android.R.string.ok, this);
288 builder.setNegativeButton(android.R.string.cancel, defaultCancelListener);
289 builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
290 seekbar.setProgress(item.getDefaultValue());
291 onClick(dialog, which);
292 });
293 mDialog = builder.show();
294
295 mTextSliderValue = view.findViewById(R.id.text_value);
296 mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
297
298 TextView units = view.findViewById(R.id.text_units);
299 units.setText(item.getUnits());
300
301 seekbar.setMin(item.getMin());
302 seekbar.setMax(item.getMax());
303 seekbar.setProgress(mSeekbarProgress);
304
305 seekbar.setOnSeekBarChangeListener(this);
306 }
307
308 public void onSubmenuClick(SubmenuSetting item) {
309 mView.loadSubMenu(item.getMenuKey());
310 }
311
312 public void onInputBindingClick(final InputBindingSetting item, final int position) {
313 final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
314 dialog.setTitle(R.string.input_binding);
315
316 int messageResId = R.string.input_binding_description;
317 if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
318 // Use specialized message for axis left/right or up/down
319 if (item.IsHorizontalOrientation()) {
320 messageResId = R.string.input_binding_description_horizontal_axis;
321 } else {
322 messageResId = R.string.input_binding_description_vertical_axis;
323 }
324 }
325
326 dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
327 dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
328 dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
329 item.removeOldMapping());
330 dialog.setOnDismissListener(dialog1 ->
331 {
332 StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
333 notifyItemChanged(position);
334
335 mView.putSetting(setting);
336
337 mView.onSettingChanged();
338 });
339 dialog.setCanceledOnTouchOutside(false);
340 dialog.show();
341 }
342
343 @Override
344 public void onClick(DialogInterface dialog, int which) {
345 if (mClickedItem instanceof SingleChoiceSetting) {
346 SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
347
348 int value = getValueForSingleChoiceSelection(scSetting, which);
349 if (scSetting.getSelectedValue() != value) {
350 mView.onSettingChanged();
351 }
352
353 // Get the backing Setting, which may be null (if for example it was missing from the file)
354 IntSetting setting = scSetting.setSelectedValue(value);
355 if (setting != null) {
356 mView.putSetting(setting);
357 }
358
359 closeDialog();
360 } else if (mClickedItem instanceof PremiumSingleChoiceSetting) {
361 PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem;
362 scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which));
363 closeDialog();
364 } else if (mClickedItem instanceof StringSingleChoiceSetting) {
365 StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
366 String value = scSetting.getValueAt(which);
367 if (!scSetting.getSelectedValue().equals(value))
368 mView.onSettingChanged();
369
370 StringSetting setting = scSetting.setSelectedValue(value);
371 if (setting != null) {
372 mView.putSetting(setting);
373 }
374
375 closeDialog();
376 } else if (mClickedItem instanceof SliderSetting) {
377 SliderSetting sliderSetting = (SliderSetting) mClickedItem;
378 if (sliderSetting.getSelectedValue() != mSeekbarProgress) {
379 mView.onSettingChanged();
380 }
381
382 if (sliderSetting.getSetting() instanceof FloatSetting) {
383 float value = (float) mSeekbarProgress;
384
385 FloatSetting setting = sliderSetting.setSelectedValue(value);
386 if (setting != null) {
387 mView.putSetting(setting);
388 }
389 } else {
390 IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress);
391 if (setting != null) {
392 mView.putSetting(setting);
393 }
394 }
395
396 closeDialog();
397 }
398
399 mClickedItem = null;
400 mSeekbarProgress = -1;
401 }
402
403 public void closeDialog() {
404 if (mDialog != null) {
405 if (mClickedPosition != -1) {
406 notifyItemChanged(mClickedPosition);
407 mClickedPosition = -1;
408 }
409 mDialog.dismiss();
410 mDialog = null;
411 }
412 }
413
414 @Override
415 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
416 mSeekbarProgress = progress;
417 mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
418 }
419
420 @Override
421 public void onStartTrackingTouch(SeekBar seekBar) {
422 }
423
424 @Override
425 public void onStopTrackingTouch(SeekBar seekBar) {
426 }
427
428 private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
429 int valuesId = item.getValuesId();
430
431 if (valuesId > 0) {
432 int[] valuesArray = mContext.getResources().getIntArray(valuesId);
433 return valuesArray[which];
434 } else {
435 return which;
436 }
437 }
438
439 private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) {
440 int valuesId = item.getValuesId();
441
442 if (valuesId > 0) {
443 int[] valuesArray = mContext.getResources().getIntArray(valuesId);
444 return valuesArray[which];
445 } else {
446 return which;
447 }
448 }
449
450 private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
451 int value = item.getSelectedValue();
452 int valuesId = item.getValuesId();
453
454 if (valuesId > 0) {
455 int[] valuesArray = mContext.getResources().getIntArray(valuesId);
456 for (int index = 0; index < valuesArray.length; index++) {
457 int current = valuesArray[index];
458 if (current == value) {
459 return index;
460 }
461 }
462 } else {
463 return value;
464 }
465
466 return -1;
467 }
468
469 private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) {
470 int value = item.getSelectedValue();
471 int valuesId = item.getValuesId();
472
473 if (valuesId > 0) {
474 int[] valuesArray = mContext.getResources().getIntArray(valuesId);
475 for (int index = 0; index < valuesArray.length; index++) {
476 int current = valuesArray[index];
477 if (current == value) {
478 return index;
479 }
480 }
481 } else {
482 return value;
483 }
484
485 return -1;
486 }
487}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java
new file mode 100644
index 000000000..5799dcb8d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java
@@ -0,0 +1,136 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.content.Context;
4import android.os.Bundle;
5import android.view.LayoutInflater;
6import android.view.View;
7import android.view.ViewGroup;
8
9import androidx.annotation.NonNull;
10import androidx.annotation.Nullable;
11import androidx.fragment.app.Fragment;
12import androidx.recyclerview.widget.LinearLayoutManager;
13import androidx.recyclerview.widget.RecyclerView;
14
15import org.citra.citra_emu.R;
16import org.citra.citra_emu.features.settings.model.Setting;
17import org.citra.citra_emu.features.settings.model.Settings;
18import org.citra.citra_emu.features.settings.model.view.SettingsItem;
19import org.citra.citra_emu.ui.DividerItemDecoration;
20
21import java.util.ArrayList;
22
23public final class SettingsFragment extends Fragment implements SettingsFragmentView {
24 private static final String ARGUMENT_MENU_TAG = "menu_tag";
25 private static final String ARGUMENT_GAME_ID = "game_id";
26
27 private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
28 private SettingsActivityView mActivity;
29
30 private SettingsAdapter mAdapter;
31
32 public static Fragment newInstance(String menuTag, String gameId) {
33 SettingsFragment fragment = new SettingsFragment();
34
35 Bundle arguments = new Bundle();
36 arguments.putString(ARGUMENT_MENU_TAG, menuTag);
37 arguments.putString(ARGUMENT_GAME_ID, gameId);
38
39 fragment.setArguments(arguments);
40 return fragment;
41 }
42
43 @Override
44 public void onAttach(@NonNull Context context) {
45 super.onAttach(context);
46
47 mActivity = (SettingsActivityView) context;
48 mPresenter.onAttach();
49 }
50
51 @Override
52 public void onCreate(Bundle savedInstanceState) {
53 super.onCreate(savedInstanceState);
54
55 setRetainInstance(true);
56 String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
57 String gameId = getArguments().getString(ARGUMENT_GAME_ID);
58
59 mAdapter = new SettingsAdapter(this, getActivity());
60
61 mPresenter.onCreate(menuTag, gameId);
62 }
63
64 @Nullable
65 @Override
66 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
67 return inflater.inflate(R.layout.fragment_settings, container, false);
68 }
69
70 @Override
71 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
72 LinearLayoutManager manager = new LinearLayoutManager(getActivity());
73
74 RecyclerView recyclerView = view.findViewById(R.id.list_settings);
75
76 recyclerView.setAdapter(mAdapter);
77 recyclerView.setLayoutManager(manager);
78 recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
79
80 SettingsActivityView activity = (SettingsActivityView) getActivity();
81
82 mPresenter.onViewCreated(activity.getSettings());
83 }
84
85 @Override
86 public void onDetach() {
87 super.onDetach();
88 mActivity = null;
89
90 if (mAdapter != null) {
91 mAdapter.closeDialog();
92 }
93 }
94
95 @Override
96 public void onSettingsFileLoaded(Settings settings) {
97 mPresenter.setSettings(settings);
98 }
99
100 @Override
101 public void passSettingsToActivity(Settings settings) {
102 if (mActivity != null) {
103 mActivity.setSettings(settings);
104 }
105 }
106
107 @Override
108 public void showSettingsList(ArrayList<SettingsItem> settingsList) {
109 mAdapter.setSettings(settingsList);
110 }
111
112 @Override
113 public void loadDefaultSettings() {
114 mPresenter.loadDefaultSettings();
115 }
116
117 @Override
118 public void loadSubMenu(String menuKey) {
119 mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
120 }
121
122 @Override
123 public void showToastMessage(String message, boolean is_long) {
124 mActivity.showToastMessage(message, is_long);
125 }
126
127 @Override
128 public void putSetting(Setting setting) {
129 mPresenter.putSetting(setting);
130 }
131
132 @Override
133 public void onSettingChanged() {
134 mActivity.onSettingChanged();
135 }
136}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
new file mode 100644
index 000000000..31f3e68eb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
@@ -0,0 +1,416 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.app.Activity;
4import android.content.Context;
5import android.hardware.camera2.CameraAccessException;
6import android.hardware.camera2.CameraCharacteristics;
7import android.hardware.camera2.CameraManager;
8import android.text.TextUtils;
9
10import org.citra.citra_emu.R;
11import org.citra.citra_emu.features.settings.model.Setting;
12import org.citra.citra_emu.features.settings.model.SettingSection;
13import org.citra.citra_emu.features.settings.model.Settings;
14import org.citra.citra_emu.features.settings.model.StringSetting;
15import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
16import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
17import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
18import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
19import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
20import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
21import org.citra.citra_emu.features.settings.model.view.SettingsItem;
22import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
23import org.citra.citra_emu.features.settings.model.view.SliderSetting;
24import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
25import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
26import org.citra.citra_emu.features.settings.utils.SettingsFile;
27import org.citra.citra_emu.utils.Log;
28
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Objects;
32
33public final class SettingsFragmentPresenter {
34 private SettingsFragmentView mView;
35
36 private String mMenuTag;
37 private String mGameID;
38
39 private Settings mSettings;
40 private ArrayList<SettingsItem> mSettingsList;
41
42 public SettingsFragmentPresenter(SettingsFragmentView view) {
43 mView = view;
44 }
45
46 public void onCreate(String menuTag, String gameId) {
47 mGameID = gameId;
48 mMenuTag = menuTag;
49 }
50
51 public void onViewCreated(Settings settings) {
52 setSettings(settings);
53 }
54
55 /**
56 * If the screen is rotated, the Activity will forget the settings map. This fragment
57 * won't, though; so rather than have the Activity reload from disk, have the fragment pass
58 * the settings map back to the Activity.
59 */
60 public void onAttach() {
61 if (mSettings != null) {
62 mView.passSettingsToActivity(mSettings);
63 }
64 }
65
66 public void putSetting(Setting setting) {
67 mSettings.getSection(setting.getSection()).putSetting(setting);
68 }
69
70 private StringSetting asStringSetting(Setting setting) {
71 if (setting == null) {
72 return null;
73 }
74
75 StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
76 putSetting(stringSetting);
77 return stringSetting;
78 }
79
80 public void loadDefaultSettings() {
81 loadSettingsList();
82 }
83
84 public void setSettings(Settings settings) {
85 if (mSettingsList == null && settings != null) {
86 mSettings = settings;
87
88 loadSettingsList();
89 } else {
90 mView.getActivity().setTitle(R.string.preferences_settings);
91 mView.showSettingsList(mSettingsList);
92 }
93 }
94
95 private void loadSettingsList() {
96 if (!TextUtils.isEmpty(mGameID)) {
97 mView.getActivity().setTitle("Game Settings: " + mGameID);
98 }
99 ArrayList<SettingsItem> sl = new ArrayList<>();
100
101 if (mMenuTag == null) {
102 return;
103 }
104
105 switch (mMenuTag) {
106 case SettingsFile.FILE_NAME_CONFIG:
107 addConfigSettings(sl);
108 break;
109 case Settings.SECTION_PREMIUM:
110 addPremiumSettings(sl);
111 break;
112 case Settings.SECTION_CORE:
113 addGeneralSettings(sl);
114 break;
115 case Settings.SECTION_SYSTEM:
116 addSystemSettings(sl);
117 break;
118 case Settings.SECTION_CAMERA:
119 addCameraSettings(sl);
120 break;
121 case Settings.SECTION_CONTROLS:
122 addInputSettings(sl);
123 break;
124 case Settings.SECTION_RENDERER:
125 addGraphicsSettings(sl);
126 break;
127 case Settings.SECTION_AUDIO:
128 addAudioSettings(sl);
129 break;
130 case Settings.SECTION_DEBUG:
131 addDebugSettings(sl);
132 break;
133 default:
134 mView.showToastMessage("Unimplemented menu", false);
135 return;
136 }
137
138 mSettingsList = sl;
139 mView.showSettingsList(mSettingsList);
140 }
141
142 private void addConfigSettings(ArrayList<SettingsItem> sl) {
143 mView.getActivity().setTitle(R.string.preferences_settings);
144
145 sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM));
146 sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
147 sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
148 sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
149 sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
150 sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
151 sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
152 sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
153 }
154
155 private void addPremiumSettings(ArrayList<SettingsItem> sl) {
156 mView.getActivity().setTitle(R.string.preferences_premium);
157
158 SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
159 Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
160
161 sl.add(new PremiumHeader());
162
163 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
164 sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView));
165 } else {
166 // Pre-Android 10 does not support System Default
167 sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView));
168 }
169
170 //Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
171 //sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, R.string.texture_filter_description, textureFilterNames, textureFilterNames, "none", textureFilterName));
172 }
173
174 private void addGeneralSettings(ArrayList<SettingsItem> sl) {
175 mView.getActivity().setTitle(R.string.preferences_general);
176
177 SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
178 Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
179 Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
180
181 sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
182 sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
183 }
184
185 private void addSystemSettings(ArrayList<SettingsItem> sl) {
186 mView.getActivity().setTitle(R.string.preferences_system);
187
188 SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
189 Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
190 Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
191 Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
192 Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
193
194 sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
195 sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
196 sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
197 sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
198 }
199
200 private void addCameraSettings(ArrayList<SettingsItem> sl) {
201 final Activity activity = mView.getActivity();
202 activity.setTitle(R.string.preferences_camera);
203
204 // Get the camera IDs
205 CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
206 ArrayList<String> supportedCameraNameList = new ArrayList<>();
207 ArrayList<String> supportedCameraIdList = new ArrayList<>();
208 if (cameraManager != null) {
209 try {
210 for (String id : cameraManager.getCameraIdList()) {
211 final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
212 if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
213 continue; // Legacy cameras cannot be used with the NDK
214 }
215
216 supportedCameraIdList.add(id);
217
218 final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
219 int stringId = R.string.camera_facing_external;
220 switch (facing) {
221 case CameraCharacteristics.LENS_FACING_FRONT:
222 stringId = R.string.camera_facing_front;
223 break;
224 case CameraCharacteristics.LENS_FACING_BACK:
225 stringId = R.string.camera_facing_back;
226 break;
227 case CameraCharacteristics.LENS_FACING_EXTERNAL:
228 stringId = R.string.camera_facing_external;
229 break;
230 }
231 supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
232 }
233 } catch (CameraAccessException e) {
234 Log.error("Couldn't retrieve camera list");
235 e.printStackTrace();
236 }
237 }
238
239 // Create the names and values for display
240 ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
241 cameraDeviceNameList.addAll(supportedCameraNameList);
242 ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
243 cameraDeviceValueList.addAll(supportedCameraIdList);
244
245 final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
246 final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
247
248 final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
249
250 String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
251 String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
252 if (!haveCameraDevices) {
253 // Remove the last entry (ndk / Device Camera)
254 imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
255 imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
256 }
257
258 final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
259
260 SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
261
262 Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
263 Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
264 Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
265 sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
266 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
267 if (haveCameraDevices)
268 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
269 sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
270
271 Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
272 Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
273 Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
274 sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
275 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
276 if (haveCameraDevices)
277 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
278 sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
279
280 Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
281 Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
282 Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
283 sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
284 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
285 if (haveCameraDevices)
286 sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
287 sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
288 }
289
290 private void addInputSettings(ArrayList<SettingsItem> sl) {
291 mView.getActivity().setTitle(R.string.preferences_controls);
292
293 SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
294 Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
295 Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
296 Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
297 Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
298 Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
299 Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
300 Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
301 Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
302 Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
303 Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
304 Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
305 Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
306 // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
307 // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
308 // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
309 // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
310 Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
311 Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
312 Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
313 Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
314
315 sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
316 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
317 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
318 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
319 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
320 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
321 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
322
323 sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
324 sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
325 sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
326
327 sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
328 sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
329 sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
330
331 sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
332 sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
333 sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
334
335 // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
336 // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
337 // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
338 // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
339 // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
340
341 sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
342 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
343 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
344 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
345 sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
346 }
347
348 private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
349 mView.getActivity().setTitle(R.string.preferences_graphics);
350
351 SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
352 Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
353 Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
354 Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
355 Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
356 Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
357 Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
358 SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
359 Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
360 Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
361 Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT);
362 SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
363 Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
364 Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
365 //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
366
367 sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
368 sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
369 sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
370 sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
371 sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
372
373 sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
374 sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
375 sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d));
376
377 sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0));
378 sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize));
379 sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift));
380 sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift));
381
382 sl.add(new HeaderSetting(null, null, R.string.utility, 0));
383 sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
384 sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
385 //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
386 //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
387 }
388
389 private void addAudioSettings(ArrayList<SettingsItem> sl) {
390 mView.getActivity().setTitle(R.string.preferences_audio);
391
392 SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
393 Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
394 Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE);
395
396 sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
397 sl.add(new SingleChoiceSetting(SettingsFile.KEY_MIC_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 1, micInputType));
398 }
399
400 private void addDebugSettings(ArrayList<SettingsItem> sl) {
401 mView.getActivity().setTitle(R.string.preferences_debug);
402
403 SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
404 SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
405 Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
406 Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER);
407 Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
408 Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
409
410 sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
411 sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
412 sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView));
413 sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
414 sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
415 }
416}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java
new file mode 100644
index 000000000..c36eb55a7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java
@@ -0,0 +1,78 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import androidx.fragment.app.FragmentActivity;
4
5import org.citra.citra_emu.features.settings.model.Setting;
6import org.citra.citra_emu.features.settings.model.Settings;
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8
9import java.util.ArrayList;
10
11/**
12 * Abstraction for a screen showing a list of settings. Instances of
13 * this type of view will each display a layer of the setting hierarchy.
14 */
15public interface SettingsFragmentView {
16 /**
17 * Called by the containing Activity to notify the Fragment that an
18 * asynchronous load operation completed.
19 *
20 * @param settings The (possibly null) result of the ini load operation.
21 */
22 void onSettingsFileLoaded(Settings settings);
23
24 /**
25 * Pass a settings HashMap to the containing activity, so that it can
26 * share the HashMap with other SettingsFragments; useful so that rotations
27 * do not require an additional load operation.
28 *
29 * @param settings An ArrayList containing all the settings HashMaps.
30 */
31 void passSettingsToActivity(Settings settings);
32
33 /**
34 * Pass an ArrayList to the View so that it can be displayed on screen.
35 *
36 * @param settingsList The result of converting the HashMap to an ArrayList
37 */
38 void showSettingsList(ArrayList<SettingsItem> settingsList);
39
40 /**
41 * Called by the containing Activity when an asynchronous load operation fails.
42 * Instructs the Fragment to load the settings screen with defaults selected.
43 */
44 void loadDefaultSettings();
45
46 /**
47 * @return The Fragment's containing activity.
48 */
49 FragmentActivity getActivity();
50
51 /**
52 * Tell the Fragment to tell the containing Activity to show a new
53 * Fragment containing a submenu of settings.
54 *
55 * @param menuKey Identifier for the settings group that should be shown.
56 */
57 void loadSubMenu(String menuKey);
58
59 /**
60 * Tell the Fragment to tell the containing activity to display a toast message.
61 *
62 * @param message Text to be shown in the Toast
63 * @param is_long Whether this should be a long Toast or short one.
64 */
65 void showToastMessage(String message, boolean is_long);
66
67 /**
68 * Have the fragment add a setting to the HashMap.
69 *
70 * @param setting The (possibly previously missing) new setting.
71 */
72 void putSetting(Setting setting);
73
74 /**
75 * Have the fragment tell the containing Activity that a setting was modified.
76 */
77 void onSettingChanged();
78}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java
new file mode 100644
index 000000000..67bde5709
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java
@@ -0,0 +1,48 @@
1package org.citra.citra_emu.features.settings.ui;
2
3import android.content.Context;
4import android.util.AttributeSet;
5import android.widget.FrameLayout;
6
7/**
8 * FrameLayout subclass with few Properties added to simplify animations.
9 * Don't remove the methods appearing as unused, in order not to break the menu animations
10 */
11public final class SettingsFrameLayout extends FrameLayout {
12 private float mVisibleness = 1.0f;
13
14 public SettingsFrameLayout(Context context) {
15 super(context);
16 }
17
18 public SettingsFrameLayout(Context context, AttributeSet attrs) {
19 super(context, attrs);
20 }
21
22 public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
23 super(context, attrs, defStyleAttr);
24 }
25
26 public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
27 super(context, attrs, defStyleAttr, defStyleRes);
28 }
29
30 public float getYFraction() {
31 return getY() / getHeight();
32 }
33
34 public void setYFraction(float yFraction) {
35 final int height = getHeight();
36 setY((height > 0) ? (yFraction * height) : -9999);
37 }
38
39 public float getVisibleness() {
40 return mVisibleness;
41 }
42
43 public void setVisibleness(float visibleness) {
44 setScaleX(visibleness);
45 setScaleY(visibleness);
46 setAlpha(visibleness);
47 }
48}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java
new file mode 100644
index 000000000..d914f7d0b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java
@@ -0,0 +1,54 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.CheckBox;
5import android.widget.TextView;
6
7import org.citra.citra_emu.R;
8import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
9import org.citra.citra_emu.features.settings.model.view.SettingsItem;
10import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
11
12public final class CheckBoxSettingViewHolder extends SettingViewHolder {
13 private CheckBoxSetting mItem;
14
15 private TextView mTextSettingName;
16 private TextView mTextSettingDescription;
17
18 private CheckBox mCheckbox;
19
20 public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) {
21 super(itemView, adapter);
22 }
23
24 @Override
25 protected void findViews(View root) {
26 mTextSettingName = root.findViewById(R.id.text_setting_name);
27 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
28 mCheckbox = root.findViewById(R.id.checkbox);
29 }
30
31 @Override
32 public void bind(SettingsItem item) {
33 mItem = (CheckBoxSetting) item;
34
35 mTextSettingName.setText(item.getNameId());
36
37 if (item.getDescriptionId() > 0) {
38 mTextSettingDescription.setText(item.getDescriptionId());
39 mTextSettingDescription.setVisibility(View.VISIBLE);
40 } else {
41 mTextSettingDescription.setText("");
42 mTextSettingDescription.setVisibility(View.GONE);
43 }
44
45 mCheckbox.setChecked(mItem.isChecked());
46 }
47
48 @Override
49 public void onClick(View clicked) {
50 mCheckbox.toggle();
51
52 getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
53 }
54}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java
new file mode 100644
index 000000000..09ea93010
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java
@@ -0,0 +1,47 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.TextView;
5
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
8import org.citra.citra_emu.features.settings.model.view.SettingsItem;
9import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
10import org.citra.citra_emu.utils.Log;
11
12public final class DateTimeViewHolder extends SettingViewHolder {
13 private DateTimeSetting mItem;
14
15 private TextView mTextSettingName;
16 private TextView mTextSettingDescription;
17
18 public DateTimeViewHolder(View itemView, SettingsAdapter adapter) {
19 super(itemView, adapter);
20 }
21
22 @Override
23 protected void findViews(View root) {
24 mTextSettingName = root.findViewById(R.id.text_setting_name);
25 Log.error("test " + mTextSettingName);
26 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
27 Log.error("test " + mTextSettingDescription);
28 }
29
30 @Override
31 public void bind(SettingsItem item) {
32 mItem = (DateTimeSetting) item;
33 mTextSettingName.setText(item.getNameId());
34
35 if (item.getDescriptionId() > 0) {
36 mTextSettingDescription.setText(item.getDescriptionId());
37 mTextSettingDescription.setVisibility(View.VISIBLE);
38 } else {
39 mTextSettingDescription.setVisibility(View.GONE);
40 }
41 }
42
43 @Override
44 public void onClick(View clicked) {
45 getAdapter().onDateTimeClick(mItem, getAdapterPosition());
46 }
47}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java
new file mode 100644
index 000000000..baf80ed76
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java
@@ -0,0 +1,32 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.TextView;
5
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
9
10public final class HeaderViewHolder extends SettingViewHolder {
11 private TextView mHeaderName;
12
13 public HeaderViewHolder(View itemView, SettingsAdapter adapter) {
14 super(itemView, adapter);
15 itemView.setOnClickListener(null);
16 }
17
18 @Override
19 protected void findViews(View root) {
20 mHeaderName = root.findViewById(R.id.text_header_name);
21 }
22
23 @Override
24 public void bind(SettingsItem item) {
25 mHeaderName.setText(item.getNameId());
26 }
27
28 @Override
29 public void onClick(View clicked) {
30 // no-op
31 }
32} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java
new file mode 100644
index 000000000..7d95c250a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java
@@ -0,0 +1,55 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.content.Context;
4import android.content.SharedPreferences;
5import android.preference.PreferenceManager;
6import android.view.View;
7import android.widget.TextView;
8
9import org.citra.citra_emu.R;
10import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
11import org.citra.citra_emu.features.settings.model.view.SettingsItem;
12import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
13
14public final class InputBindingSettingViewHolder extends SettingViewHolder {
15 private InputBindingSetting mItem;
16
17 private TextView mTextSettingName;
18 private TextView mTextSettingDescription;
19
20 private Context mContext;
21
22 public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) {
23 super(itemView, adapter);
24
25 mContext = context;
26 }
27
28 @Override
29 protected void findViews(View root) {
30 mTextSettingName = root.findViewById(R.id.text_setting_name);
31 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
32 }
33
34 @Override
35 public void bind(SettingsItem item) {
36 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
37
38 mItem = (InputBindingSetting) item;
39
40 mTextSettingName.setText(item.getNameId());
41
42 String key = sharedPreferences.getString(mItem.getKey(), "");
43 if (key != null && !key.isEmpty()) {
44 mTextSettingDescription.setText(key);
45 mTextSettingDescription.setVisibility(View.VISIBLE);
46 } else {
47 mTextSettingDescription.setVisibility(View.GONE);
48 }
49 }
50
51 @Override
52 public void onClick(View clicked) {
53 getAdapter().onInputBindingClick(mItem, getAdapterPosition());
54 }
55}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java
new file mode 100644
index 000000000..be0853ff0
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java
@@ -0,0 +1,57 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.TextView;
5
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
9import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
10import org.citra.citra_emu.ui.main.MainActivity;
11
12public final class PremiumViewHolder extends SettingViewHolder {
13 private TextView mHeaderName;
14 private TextView mTextDescription;
15 private SettingsFragmentView mView;
16
17 public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) {
18 super(itemView, adapter);
19 mView = view;
20 itemView.setOnClickListener(this);
21 }
22
23 @Override
24 protected void findViews(View root) {
25 mHeaderName = root.findViewById(R.id.text_setting_name);
26 mTextDescription = root.findViewById(R.id.text_setting_description);
27 }
28
29 @Override
30 public void bind(SettingsItem item) {
31 updateText();
32 }
33
34 @Override
35 public void onClick(View clicked) {
36 if (MainActivity.isPremiumActive()) {
37 return;
38 }
39
40 // Invoke billing flow if Premium is not already active, then refresh the UI to indicate
41 // the purchase has completed.
42 MainActivity.invokePremiumBilling(() -> updateText());
43 }
44
45 /**
46 * Update the text shown to the user, based on whether Premium is active
47 */
48 private void updateText() {
49 if (MainActivity.isPremiumActive()) {
50 mHeaderName.setText(R.string.premium_settings_welcome);
51 mTextDescription.setText(R.string.premium_settings_welcome_description);
52 } else {
53 mHeaderName.setText(R.string.premium_settings_upsell);
54 mTextDescription.setText(R.string.premium_settings_upsell_description);
55 }
56 }
57}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java
new file mode 100644
index 000000000..2643ea121
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java
@@ -0,0 +1,49 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4
5import androidx.recyclerview.widget.RecyclerView;
6
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
9
10public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
11 private SettingsAdapter mAdapter;
12
13 public SettingViewHolder(View itemView, SettingsAdapter adapter) {
14 super(itemView);
15
16 mAdapter = adapter;
17
18 itemView.setOnClickListener(this);
19
20 findViews(itemView);
21 }
22
23 protected SettingsAdapter getAdapter() {
24 return mAdapter;
25 }
26
27 /**
28 * Gets handles to all this ViewHolder's child views using their XML-defined identifiers.
29 *
30 * @param root The newly inflated top-level view.
31 */
32 protected abstract void findViews(View root);
33
34 /**
35 * Called by the adapter to set this ViewHolder's child views to display the list item
36 * it must now represent.
37 *
38 * @param item The list item that should be represented by this ViewHolder.
39 */
40 public abstract void bind(SettingsItem item);
41
42 /**
43 * Called when this ViewHolder's view is clicked on. Implementations should usually pass
44 * this event up to the adapter.
45 *
46 * @param clicked The view that was clicked on.
47 */
48 public abstract void onClick(View clicked);
49}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java
new file mode 100644
index 000000000..a175af9f8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java
@@ -0,0 +1,76 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.content.res.Resources;
4import android.view.View;
5import android.widget.TextView;
6
7import org.citra.citra_emu.R;
8import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
9import org.citra.citra_emu.features.settings.model.view.SettingsItem;
10import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
11import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
12import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
13
14public final class SingleChoiceViewHolder extends SettingViewHolder {
15 private SettingsItem mItem;
16
17 private TextView mTextSettingName;
18 private TextView mTextSettingDescription;
19
20 public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) {
21 super(itemView, adapter);
22 }
23
24 @Override
25 protected void findViews(View root) {
26 mTextSettingName = root.findViewById(R.id.text_setting_name);
27 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
28 }
29
30 @Override
31 public void bind(SettingsItem item) {
32 mItem = item;
33
34 mTextSettingName.setText(item.getNameId());
35 mTextSettingDescription.setVisibility(View.VISIBLE);
36 if (item.getDescriptionId() > 0) {
37 mTextSettingDescription.setText(item.getDescriptionId());
38 } else if (item instanceof SingleChoiceSetting) {
39 SingleChoiceSetting setting = (SingleChoiceSetting) item;
40 int selected = setting.getSelectedValue();
41 Resources resMgr = mTextSettingDescription.getContext().getResources();
42 String[] choices = resMgr.getStringArray(setting.getChoicesId());
43 int[] values = resMgr.getIntArray(setting.getValuesId());
44 for (int i = 0; i < values.length; ++i) {
45 if (values[i] == selected) {
46 mTextSettingDescription.setText(choices[i]);
47 }
48 }
49 } else if (item instanceof PremiumSingleChoiceSetting) {
50 PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item;
51 int selected = setting.getSelectedValue();
52 Resources resMgr = mTextSettingDescription.getContext().getResources();
53 String[] choices = resMgr.getStringArray(setting.getChoicesId());
54 int[] values = resMgr.getIntArray(setting.getValuesId());
55 for (int i = 0; i < values.length; ++i) {
56 if (values[i] == selected) {
57 mTextSettingDescription.setText(choices[i]);
58 }
59 }
60 } else {
61 mTextSettingDescription.setVisibility(View.GONE);
62 }
63 }
64
65 @Override
66 public void onClick(View clicked) {
67 int position = getAdapterPosition();
68 if (mItem instanceof SingleChoiceSetting) {
69 getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
70 } else if (mItem instanceof PremiumSingleChoiceSetting) {
71 getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position);
72 } else if (mItem instanceof StringSingleChoiceSetting) {
73 getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
74 }
75 }
76}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java
new file mode 100644
index 000000000..3dd048a29
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java
@@ -0,0 +1,45 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.TextView;
5
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8import org.citra.citra_emu.features.settings.model.view.SliderSetting;
9import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
10
11public final class SliderViewHolder extends SettingViewHolder {
12 private SliderSetting mItem;
13
14 private TextView mTextSettingName;
15 private TextView mTextSettingDescription;
16
17 public SliderViewHolder(View itemView, SettingsAdapter adapter) {
18 super(itemView, adapter);
19 }
20
21 @Override
22 protected void findViews(View root) {
23 mTextSettingName = root.findViewById(R.id.text_setting_name);
24 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
25 }
26
27 @Override
28 public void bind(SettingsItem item) {
29 mItem = (SliderSetting) item;
30
31 mTextSettingName.setText(item.getNameId());
32
33 if (item.getDescriptionId() > 0) {
34 mTextSettingDescription.setText(item.getDescriptionId());
35 mTextSettingDescription.setVisibility(View.VISIBLE);
36 } else {
37 mTextSettingDescription.setVisibility(View.GONE);
38 }
39 }
40
41 @Override
42 public void onClick(View clicked) {
43 getAdapter().onSliderClick(mItem, getAdapterPosition());
44 }
45}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java
new file mode 100644
index 000000000..cb8c3e92a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java
@@ -0,0 +1,45 @@
1package org.citra.citra_emu.features.settings.ui.viewholder;
2
3import android.view.View;
4import android.widget.TextView;
5
6import org.citra.citra_emu.R;
7import org.citra.citra_emu.features.settings.model.view.SettingsItem;
8import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
9import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
10
11public final class SubmenuViewHolder extends SettingViewHolder {
12 private SubmenuSetting mItem;
13
14 private TextView mTextSettingName;
15 private TextView mTextSettingDescription;
16
17 public SubmenuViewHolder(View itemView, SettingsAdapter adapter) {
18 super(itemView, adapter);
19 }
20
21 @Override
22 protected void findViews(View root) {
23 mTextSettingName = root.findViewById(R.id.text_setting_name);
24 mTextSettingDescription = root.findViewById(R.id.text_setting_description);
25 }
26
27 @Override
28 public void bind(SettingsItem item) {
29 mItem = (SubmenuSetting) item;
30
31 mTextSettingName.setText(item.getNameId());
32
33 if (item.getDescriptionId() > 0) {
34 mTextSettingDescription.setText(item.getDescriptionId());
35 mTextSettingDescription.setVisibility(View.VISIBLE);
36 } else {
37 mTextSettingDescription.setVisibility(View.GONE);
38 }
39 }
40
41 @Override
42 public void onClick(View clicked) {
43 getAdapter().onSubmenuClick(mItem);
44 }
45} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java
new file mode 100644
index 000000000..8ae6b70d7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java
@@ -0,0 +1,341 @@
1package org.citra.citra_emu.features.settings.utils;
2
3import androidx.annotation.NonNull;
4
5import org.citra.citra_emu.CitraApplication;
6import org.citra.citra_emu.NativeLibrary;
7import org.citra.citra_emu.R;
8import org.citra.citra_emu.features.settings.model.FloatSetting;
9import org.citra.citra_emu.features.settings.model.IntSetting;
10import org.citra.citra_emu.features.settings.model.Setting;
11import org.citra.citra_emu.features.settings.model.SettingSection;
12import org.citra.citra_emu.features.settings.model.Settings;
13import org.citra.citra_emu.features.settings.model.StringSetting;
14import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
15import org.citra.citra_emu.utils.BiMap;
16import org.citra.citra_emu.utils.DirectoryInitialization;
17import org.citra.citra_emu.utils.Log;
18import org.ini4j.Wini;
19
20import java.io.BufferedReader;
21import java.io.File;
22import java.io.FileNotFoundException;
23import java.io.FileReader;
24import java.io.IOException;
25import java.util.HashMap;
26import java.util.Set;
27import java.util.TreeMap;
28import java.util.TreeSet;
29
30/**
31 * Contains static methods for interacting with .ini files in which settings are stored.
32 */
33public final class SettingsFile {
34 public static final String FILE_NAME_CONFIG = "config";
35
36 public static final String KEY_CPU_JIT = "use_cpu_jit";
37
38 public static final String KEY_DESIGN = "design";
39
40 public static final String KEY_PREMIUM = "premium";
41
42 public static final String KEY_HW_RENDERER = "use_hw_renderer";
43 public static final String KEY_HW_SHADER = "use_hw_shader";
44 public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul";
45 public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
46 public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache";
47 public static final String KEY_USE_VSYNC = "use_vsync_new";
48 public static final String KEY_RESOLUTION_FACTOR = "resolution_factor";
49 public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit";
50 public static final String KEY_FRAME_LIMIT = "frame_limit";
51 public static final String KEY_BACKGROUND_RED = "bg_red";
52 public static final String KEY_BACKGROUND_BLUE = "bg_blue";
53 public static final String KEY_BACKGROUND_GREEN = "bg_green";
54 public static final String KEY_RENDER_3D = "render_3d";
55 public static final String KEY_FACTOR_3D = "factor_3d";
56 public static final String KEY_PP_SHADER_NAME = "pp_shader_name";
57 public static final String KEY_FILTER_MODE = "filter_mode";
58 public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name";
59 public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation";
60
61 public static final String KEY_LAYOUT_OPTION = "layout_option";
62 public static final String KEY_SWAP_SCREEN = "swap_screen";
63 public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size";
64 public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift";
65 public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift";
66
67 public static final String KEY_DUMP_TEXTURES = "dump_textures";
68 public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
69 public static final String KEY_PRELOAD_TEXTURES = "preload_textures";
70
71 public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine";
72 public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
73 public static final String KEY_VOLUME = "volume";
74 public static final String KEY_MIC_INPUT_TYPE = "mic_input_type";
75
76 public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
77
78 public static final String KEY_IS_NEW_3DS = "is_new_3ds";
79 public static final String KEY_REGION_VALUE = "region_value";
80 public static final String KEY_LANGUAGE = "language";
81
82 public static final String KEY_INIT_CLOCK = "init_clock";
83 public static final String KEY_INIT_TIME = "init_time";
84
85 public static final String KEY_BUTTON_A = "button_a";
86 public static final String KEY_BUTTON_B = "button_b";
87 public static final String KEY_BUTTON_X = "button_x";
88 public static final String KEY_BUTTON_Y = "button_y";
89 public static final String KEY_BUTTON_SELECT = "button_select";
90 public static final String KEY_BUTTON_START = "button_start";
91 public static final String KEY_BUTTON_UP = "button_up";
92 public static final String KEY_BUTTON_DOWN = "button_down";
93 public static final String KEY_BUTTON_LEFT = "button_left";
94 public static final String KEY_BUTTON_RIGHT = "button_right";
95 public static final String KEY_BUTTON_L = "button_l";
96 public static final String KEY_BUTTON_R = "button_r";
97 public static final String KEY_BUTTON_ZL = "button_zl";
98 public static final String KEY_BUTTON_ZR = "button_zr";
99 public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical";
100 public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal";
101 public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical";
102 public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal";
103 public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical";
104 public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal";
105 public static final String KEY_CIRCLEPAD_UP = "circlepad_up";
106 public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down";
107 public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left";
108 public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right";
109 public static final String KEY_CSTICK_UP = "cstick_up";
110 public static final String KEY_CSTICK_DOWN = "cstick_down";
111 public static final String KEY_CSTICK_LEFT = "cstick_left";
112 public static final String KEY_CSTICK_RIGHT = "cstick_right";
113
114 public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name";
115 public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config";
116 public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip";
117 public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name";
118 public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config";
119 public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip";
120 public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name";
121 public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config";
122 public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip";
123
124 public static final String KEY_LOG_FILTER = "log_filter";
125
126 private static BiMap<String, String> sectionsMap = new BiMap<>();
127
128 static {
129 //TODO: Add members to sectionsMap when game-specific settings are added
130 }
131
132
133 private SettingsFile() {
134 }
135
136 /**
137 * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
138 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
139 * failed.
140 *
141 * @param ini The ini file to load the settings from
142 * @param isCustomGame
143 * @param view The current view.
144 * @return An Observable that emits a HashMap of the file's contents, then completes.
145 */
146 static HashMap<String, SettingSection> readFile(final File ini, boolean isCustomGame, SettingsActivityView view) {
147 HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
148
149 BufferedReader reader = null;
150
151 try {
152 reader = new BufferedReader(new FileReader(ini));
153
154 SettingSection current = null;
155 for (String line; (line = reader.readLine()) != null; ) {
156 if (line.startsWith("[") && line.endsWith("]")) {
157 current = sectionFromLine(line, isCustomGame);
158 sections.put(current.getName(), current);
159 } else if ((current != null)) {
160 Setting setting = settingFromLine(current, line);
161 if (setting != null) {
162 current.putSetting(setting);
163 }
164 }
165 }
166 } catch (FileNotFoundException e) {
167 Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage());
168 if (view != null)
169 view.onSettingsFileNotFound();
170 } catch (IOException e) {
171 Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage());
172 if (view != null)
173 view.onSettingsFileNotFound();
174 } finally {
175 if (reader != null) {
176 try {
177 reader.close();
178 } catch (IOException e) {
179 Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage());
180 }
181 }
182 }
183
184 return sections;
185 }
186
187 public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) {
188 return readFile(getSettingsFile(fileName), false, view);
189 }
190
191 /**
192 * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
193 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
194 * failed.
195 *
196 * @param gameId the id of the game to load it's settings.
197 * @param view The current view.
198 */
199 public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) {
200 return readFile(getCustomGameSettingsFile(gameId), true, view);
201 }
202
203 /**
204 * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
205 * telling why it failed.
206 *
207 * @param fileName The target filename without a path or extension.
208 * @param sections The HashMap containing the Settings we want to serialize.
209 * @param view The current view.
210 */
211 public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
212 SettingsActivityView view) {
213 File ini = getSettingsFile(fileName);
214
215 try {
216 Wini writer = new Wini(ini);
217
218 Set<String> keySet = sections.keySet();
219 for (String key : keySet) {
220 SettingSection section = sections.get(key);
221 writeSection(writer, section);
222 }
223 writer.store();
224 } catch (IOException e) {
225 Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
226 view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
227 }
228 }
229
230
231 public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) {
232 Set<String> sortedSections = new TreeSet<>(sections.keySet());
233
234 for (String sectionKey : sortedSections) {
235 SettingSection section = sections.get(sectionKey);
236
237 HashMap<String, Setting> settings = section.getSettings();
238 Set<String> sortedKeySet = new TreeSet<>(settings.keySet());
239
240 for (String settingKey : sortedKeySet) {
241 Setting setting = settings.get(settingKey);
242 NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString());
243 }
244 }
245 }
246
247 private static String mapSectionNameFromIni(String generalSectionName) {
248 if (sectionsMap.getForward(generalSectionName) != null) {
249 return sectionsMap.getForward(generalSectionName);
250 }
251
252 return generalSectionName;
253 }
254
255 private static String mapSectionNameToIni(String generalSectionName) {
256 if (sectionsMap.getBackward(generalSectionName) != null) {
257 return sectionsMap.getBackward(generalSectionName);
258 }
259
260 return generalSectionName;
261 }
262
263 @NonNull
264 private static File getSettingsFile(String fileName) {
265 return new File(
266 DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini");
267 }
268
269 private static File getCustomGameSettingsFile(String gameId) {
270 return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini");
271 }
272
273 private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
274 String sectionName = line.substring(1, line.length() - 1);
275 if (isCustomGame) {
276 sectionName = mapSectionNameToIni(sectionName);
277 }
278 return new SettingSection(sectionName);
279 }
280
281 /**
282 * For a line of text, determines what type of data is being represented, and returns
283 * a Setting object containing this data.
284 *
285 * @param current The section currently being parsed by the consuming method.
286 * @param line The line of text being parsed.
287 * @return A typed Setting containing the key/value contained in the line.
288 */
289 private static Setting settingFromLine(SettingSection current, String line) {
290 String[] splitLine = line.split("=");
291
292 if (splitLine.length != 2) {
293 Log.warning("Skipping invalid config line \"" + line + "\"");
294 return null;
295 }
296
297 String key = splitLine[0].trim();
298 String value = splitLine[1].trim();
299
300 if (value.isEmpty()) {
301 Log.warning("Skipping null value in config line \"" + line + "\"");
302 return null;
303 }
304
305 try {
306 int valueAsInt = Integer.parseInt(value);
307
308 return new IntSetting(key, current.getName(), valueAsInt);
309 } catch (NumberFormatException ex) {
310 }
311
312 try {
313 float valueAsFloat = Float.parseFloat(value);
314
315 return new FloatSetting(key, current.getName(), valueAsFloat);
316 } catch (NumberFormatException ex) {
317 }
318
319 return new StringSetting(key, current.getName(), value);
320 }
321
322 /**
323 * Writes the contents of a Section HashMap to disk.
324 *
325 * @param parser A Wini pointed at a file on disk.
326 * @param section A section containing settings to be written to the file.
327 */
328 private static void writeSection(Wini parser, SettingSection section) {
329 // Write the section header.
330 String header = section.getName();
331
332 // Write this section's values.
333 HashMap<String, Setting> settings = section.getSettings();
334 Set<String> keySet = settings.keySet();
335
336 for (String key : keySet) {
337 Setting setting = settings.get(key);
338 parser.put(header, setting.getKey(), setting.getValueAsString());
339 }
340 }
341}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java
new file mode 100644
index 000000000..c18ecd4c3
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java
@@ -0,0 +1,120 @@
1package org.citra.citra_emu.fragments;
2
3import android.net.Uri;
4import android.os.Bundle;
5import android.os.Environment;
6import android.view.LayoutInflater;
7import android.view.View;
8import android.view.ViewGroup;
9import android.widget.TextView;
10
11import androidx.annotation.NonNull;
12import androidx.appcompat.widget.Toolbar;
13import androidx.core.content.FileProvider;
14
15import com.nononsenseapps.filepicker.FilePickerFragment;
16
17import org.citra.citra_emu.R;
18
19import java.io.File;
20import java.util.Arrays;
21import java.util.Collections;
22import java.util.List;
23
24public class CustomFilePickerFragment extends FilePickerFragment {
25 private static String ALL_FILES = "*";
26 private int mTitle;
27 private static List<String> extensions = Collections.singletonList(ALL_FILES);
28
29 @NonNull
30 @Override
31 public Uri toUri(@NonNull final File file) {
32 return FileProvider
33 .getUriForFile(getContext(),
34 getContext().getApplicationContext().getPackageName() + ".filesprovider",
35 file);
36 }
37
38 @Override
39 public void onActivityCreated(Bundle savedInstanceState) {
40 super.onActivityCreated(savedInstanceState);
41
42 if (mode == MODE_DIR) {
43 TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
44 ok.setText(R.string.select_dir);
45
46 TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
47 cancel.setVisibility(View.GONE);
48 }
49 }
50
51 @Override
52 protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
53 View view = super.inflateRootView(inflater, container);
54 if (mTitle != 0) {
55 Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
56 ViewGroup parent = (ViewGroup) toolbar.getParent();
57 int index = parent.indexOfChild(toolbar);
58 View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
59 TextView title = newToolbar.findViewById(R.id.filepicker_title);
60 title.setText(mTitle);
61 parent.removeView(toolbar);
62 parent.addView(newToolbar, index);
63 }
64 return view;
65 }
66
67 public void setTitle(int title) {
68 mTitle = title;
69 }
70
71 public void setAllowedExtensions(String allowedExtensions) {
72 if (allowedExtensions == null)
73 return;
74
75 extensions = Arrays.asList(allowedExtensions.split(","));
76 }
77
78 @Override
79 protected boolean isItemVisible(@NonNull final File file) {
80 // Some users jump to the conclusion that Dolphin isn't able to detect their
81 // files if the files don't show up in the file picker when mode == MODE_DIR.
82 // To avoid this, show files even when the user needs to select a directory.
83 return (showHiddenItems || !file.isHidden()) &&
84 (file.isDirectory() || extensions.contains(ALL_FILES) ||
85 extensions.contains(fileExtension(file.getName()).toLowerCase()));
86 }
87
88 @Override
89 public boolean isCheckable(@NonNull final File file) {
90 // We need to make a small correction to the isCheckable logic due to
91 // overriding isItemVisible to show files when mode == MODE_DIR.
92 // AbstractFilePickerFragment always treats files as checkable when
93 // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
94 return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
95 }
96
97 @Override
98 public void goUp() {
99 if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
100 goToDir(new File("/storage/"));
101 return;
102 }
103 if (mCurrentPath.equals(new File("/storage/"))){
104 return;
105 }
106 super.goUp();
107 }
108
109 @Override
110 public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
111 if(viewHolder.file.equals(new File("/storage/emulated/")))
112 viewHolder.file = new File("/storage/emulated/0/");
113 super.onClickDir(view, viewHolder);
114 }
115
116 private static String fileExtension(@NonNull String filename) {
117 int i = filename.lastIndexOf('.');
118 return i < 0 ? "" : filename.substring(i + 1);
119 }
120}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java
new file mode 100644
index 000000000..cdb40d6f8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java
@@ -0,0 +1,380 @@
1package org.citra.citra_emu.fragments;
2
3import android.content.Context;
4import android.content.IntentFilter;
5import android.content.SharedPreferences;
6import android.graphics.Color;
7import android.os.Bundle;
8import android.os.Handler;
9import android.preference.PreferenceManager;
10import android.view.Choreographer;
11import android.view.LayoutInflater;
12import android.view.Surface;
13import android.view.SurfaceHolder;
14import android.view.SurfaceView;
15import android.view.View;
16import android.view.ViewGroup;
17import android.widget.Button;
18import android.widget.TextView;
19import android.widget.Toast;
20
21import androidx.annotation.NonNull;
22import androidx.fragment.app.Fragment;
23import androidx.localbroadcastmanager.content.LocalBroadcastManager;
24
25import org.citra.citra_emu.NativeLibrary;
26import org.citra.citra_emu.R;
27import org.citra.citra_emu.activities.EmulationActivity;
28import org.citra.citra_emu.overlay.InputOverlay;
29import org.citra.citra_emu.utils.DirectoryInitialization;
30import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
31import org.citra.citra_emu.utils.DirectoryStateReceiver;
32import org.citra.citra_emu.utils.EmulationMenuSettings;
33import org.citra.citra_emu.utils.Log;
34
35public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback {
36 private static final String KEY_GAMEPATH = "gamepath";
37
38 private static final Handler perfStatsUpdateHandler = new Handler();
39
40 private SharedPreferences mPreferences;
41
42 private InputOverlay mInputOverlay;
43
44 private EmulationState mEmulationState;
45
46 private DirectoryStateReceiver directoryStateReceiver;
47
48 private EmulationActivity activity;
49
50 private TextView mPerfStats;
51
52 private Runnable perfStatsUpdater;
53
54 public static EmulationFragment newInstance(String gamePath) {
55 Bundle args = new Bundle();
56 args.putString(KEY_GAMEPATH, gamePath);
57
58 EmulationFragment fragment = new EmulationFragment();
59 fragment.setArguments(args);
60 return fragment;
61 }
62
63 @Override
64 public void onAttach(@NonNull Context context) {
65 super.onAttach(context);
66
67 if (context instanceof EmulationActivity) {
68 activity = (EmulationActivity) context;
69 NativeLibrary.setEmulationActivity((EmulationActivity) context);
70 } else {
71 throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
72 }
73 }
74
75 /**
76 * Initialize anything that doesn't depend on the layout / views in here.
77 */
78 @Override
79 public void onCreate(Bundle savedInstanceState) {
80 super.onCreate(savedInstanceState);
81
82 // So this fragment doesn't restart on configuration changes; i.e. rotation.
83 setRetainInstance(true);
84
85 mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
86
87 String gamePath = getArguments().getString(KEY_GAMEPATH);
88 mEmulationState = new EmulationState(gamePath);
89 }
90
91 /**
92 * Initialize the UI and start emulation in here.
93 */
94 @Override
95 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
96 View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
97
98 SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation);
99 surfaceView.getHolder().addCallback(this);
100
101 mInputOverlay = contents.findViewById(R.id.surface_input_overlay);
102 mPerfStats = contents.findViewById(R.id.show_fps_text);
103 mPerfStats.setTextColor(Color.YELLOW);
104
105 Button doneButton = contents.findViewById(R.id.done_control_config);
106 if (doneButton != null) {
107 doneButton.setOnClickListener(v -> stopConfiguringControls());
108 }
109
110 // Show/hide the "Show FPS" overlay
111 updateShowFpsOverlay();
112
113 // The new Surface created here will get passed to the native code via onSurfaceChanged.
114 return contents;
115 }
116
117 @Override
118 public void onResume() {
119 super.onResume();
120 Choreographer.getInstance().postFrameCallback(this);
121 if (DirectoryInitialization.areCitraDirectoriesReady()) {
122 mEmulationState.run(activity.isActivityRecreated());
123 } else {
124 setupCitraDirectoriesThenStartEmulation();
125 }
126 }
127
128 @Override
129 public void onPause() {
130 if (directoryStateReceiver != null) {
131 LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
132 directoryStateReceiver = null;
133 }
134
135 if (mEmulationState.isRunning()) {
136 mEmulationState.pause();
137 }
138
139 Choreographer.getInstance().removeFrameCallback(this);
140 super.onPause();
141 }
142
143 @Override
144 public void onDetach() {
145 NativeLibrary.clearEmulationActivity();
146 super.onDetach();
147 }
148
149 private void setupCitraDirectoriesThenStartEmulation() {
150 IntentFilter statusIntentFilter = new IntentFilter(
151 DirectoryInitialization.BROADCAST_ACTION);
152
153 directoryStateReceiver =
154 new DirectoryStateReceiver(directoryInitializationState ->
155 {
156 if (directoryInitializationState ==
157 DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
158 mEmulationState.run(activity.isActivityRecreated());
159 } else if (directoryInitializationState ==
160 DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
161 Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
162 .show();
163 } else if (directoryInitializationState ==
164 DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
165 Toast.makeText(getContext(), R.string.external_storage_not_mounted,
166 Toast.LENGTH_SHORT)
167 .show();
168 }
169 });
170
171 // Registers the DirectoryStateReceiver and its intent filters
172 LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
173 directoryStateReceiver,
174 statusIntentFilter);
175 DirectoryInitialization.start(getActivity());
176 }
177
178 public void refreshInputOverlay() {
179 mInputOverlay.refreshControls();
180 }
181
182 public void resetInputOverlay() {
183 // Reset button scale
184 SharedPreferences.Editor editor = mPreferences.edit();
185 editor.putInt("controlScale", 50);
186 editor.apply();
187
188 mInputOverlay.resetButtonPlacement();
189 }
190
191 public void updateShowFpsOverlay() {
192 if (true) {
193 final int SYSTEM_FPS = 0;
194 final int FPS = 1;
195 final int FRAMETIME = 2;
196 final int SPEED = 3;
197
198 perfStatsUpdater = () ->
199 {
200 final double[] perfStats = NativeLibrary.GetPerfStats();
201 if (perfStats[FPS] > 0) {
202 mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS]),
203 (int) (perfStats[SPEED] * 100.0)));
204 }
205
206 perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000);
207 };
208 perfStatsUpdateHandler.post(perfStatsUpdater);
209
210 mPerfStats.setVisibility(View.VISIBLE);
211 } else {
212 if (perfStatsUpdater != null) {
213 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
214 }
215
216 mPerfStats.setVisibility(View.GONE);
217 }
218 }
219
220 @Override
221 public void surfaceCreated(SurfaceHolder holder) {
222 // We purposely don't do anything here.
223 // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
224 }
225
226 @Override
227 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
228 Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height);
229 mEmulationState.newSurface(holder.getSurface());
230 }
231
232 @Override
233 public void surfaceDestroyed(SurfaceHolder holder) {
234 mEmulationState.clearSurface();
235 }
236
237 @Override
238 public void doFrame(long frameTimeNanos) {
239 Choreographer.getInstance().postFrameCallback(this);
240 NativeLibrary.DoFrame();
241 }
242
243 public void stopEmulation() {
244 mEmulationState.stop();
245 }
246
247 public void startConfiguringControls() {
248 getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE);
249 mInputOverlay.setIsInEditMode(true);
250 }
251
252 public void stopConfiguringControls() {
253 getView().findViewById(R.id.done_control_config).setVisibility(View.GONE);
254 mInputOverlay.setIsInEditMode(false);
255 }
256
257 public boolean isConfiguringControls() {
258 return mInputOverlay.isInEditMode();
259 }
260
261 private static class EmulationState {
262 private final String mGamePath;
263 private State state;
264 private Surface mSurface;
265 private boolean mRunWhenSurfaceIsValid;
266
267 EmulationState(String gamePath) {
268 mGamePath = gamePath;
269 // Starting state is stopped.
270 state = State.STOPPED;
271 }
272
273 public synchronized boolean isStopped() {
274 return state == State.STOPPED;
275 }
276
277 // Getters for the current state
278
279 public synchronized boolean isPaused() {
280 return state == State.PAUSED;
281 }
282
283 public synchronized boolean isRunning() {
284 return state == State.RUNNING;
285 }
286
287 public synchronized void stop() {
288 if (state != State.STOPPED) {
289 Log.debug("[EmulationFragment] Stopping emulation.");
290 state = State.STOPPED;
291 NativeLibrary.StopEmulation();
292 } else {
293 Log.warning("[EmulationFragment] Stop called while already stopped.");
294 }
295 }
296
297 // State changing methods
298
299 public synchronized void pause() {
300 if (state != State.PAUSED) {
301 state = State.PAUSED;
302 Log.debug("[EmulationFragment] Pausing emulation.");
303
304 // Release the surface before pausing, since emulation has to be running for that.
305 NativeLibrary.SurfaceDestroyed();
306 NativeLibrary.PauseEmulation();
307 } else {
308 Log.warning("[EmulationFragment] Pause called while already paused.");
309 }
310 }
311
312 public synchronized void run(boolean isActivityRecreated) {
313 if (isActivityRecreated) {
314 if (NativeLibrary.IsRunning()) {
315 state = State.PAUSED;
316 }
317 } else {
318 Log.debug("[EmulationFragment] activity resumed or fresh start");
319 }
320
321 // If the surface is set, run now. Otherwise, wait for it to get set.
322 if (mSurface != null) {
323 runWithValidSurface();
324 } else {
325 mRunWhenSurfaceIsValid = true;
326 }
327 }
328
329 // Surface callbacks
330 public synchronized void newSurface(Surface surface) {
331 mSurface = surface;
332 if (mRunWhenSurfaceIsValid) {
333 runWithValidSurface();
334 }
335 }
336
337 public synchronized void clearSurface() {
338 if (mSurface == null) {
339 Log.warning("[EmulationFragment] clearSurface called, but surface already null.");
340 } else {
341 mSurface = null;
342 Log.debug("[EmulationFragment] Surface destroyed.");
343
344 if (state == State.RUNNING) {
345 NativeLibrary.SurfaceDestroyed();
346 state = State.PAUSED;
347 } else if (state == State.PAUSED) {
348 Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
349 } else {
350 Log.warning("[EmulationFragment] Surface cleared while emulation stopped.");
351 }
352 }
353 }
354
355 private void runWithValidSurface() {
356 mRunWhenSurfaceIsValid = false;
357 if (state == State.STOPPED) {
358 NativeLibrary.SurfaceChanged(mSurface);
359 Thread mEmulationThread = new Thread(() ->
360 {
361 Log.debug("[EmulationFragment] Starting emulation thread.");
362 NativeLibrary.Run(mGamePath);
363 }, "NativeEmulation");
364 mEmulationThread.start();
365
366 } else if (state == State.PAUSED) {
367 Log.debug("[EmulationFragment] Resuming emulation.");
368 NativeLibrary.SurfaceChanged(mSurface);
369 NativeLibrary.UnPauseEmulation();
370 } else {
371 Log.debug("[EmulationFragment] Bug, run called while already running.");
372 }
373 state = State.RUNNING;
374 }
375
376 private enum State {
377 STOPPED, RUNNING, PAUSED
378 }
379 }
380}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java
new file mode 100644
index 000000000..a4ffc59c7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java
@@ -0,0 +1,76 @@
1package org.citra.citra_emu.model;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import java.nio.file.Paths;
7
8public final class Game {
9 private String mTitle;
10 private String mDescription;
11 private String mPath;
12 private String mGameId;
13 private String mCompany;
14 private String mRegions;
15
16 public Game(String title, String description, String regions, String path,
17 String gameId, String company) {
18 mTitle = title;
19 mDescription = description;
20 mRegions = regions;
21 mPath = path;
22 mGameId = gameId;
23 mCompany = company;
24 }
25
26 public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) {
27 ContentValues values = new ContentValues();
28
29 if (gameId.isEmpty()) {
30 // Homebrew, etc. may not have a game ID, use filename as a unique identifier
31 gameId = Paths.get(path).getFileName().toString();
32 }
33
34 values.put(GameDatabase.KEY_GAME_TITLE, title);
35 values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
36 values.put(GameDatabase.KEY_GAME_REGIONS, regions);
37 values.put(GameDatabase.KEY_GAME_PATH, path);
38 values.put(GameDatabase.KEY_GAME_ID, gameId);
39 values.put(GameDatabase.KEY_GAME_COMPANY, company);
40
41 return values;
42 }
43
44 public static Game fromCursor(Cursor cursor) {
45 return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
46 cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
47 cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
48 cursor.getString(GameDatabase.GAME_COLUMN_PATH),
49 cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
50 cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
51 }
52
53 public String getTitle() {
54 return mTitle;
55 }
56
57 public String getDescription() {
58 return mDescription;
59 }
60
61 public String getCompany() {
62 return mCompany;
63 }
64
65 public String getRegions() {
66 return mRegions;
67 }
68
69 public String getPath() {
70 return mPath;
71 }
72
73 public String getGameId() {
74 return mGameId;
75 }
76}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
new file mode 100644
index 000000000..8232d0489
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
@@ -0,0 +1,276 @@
1package org.citra.citra_emu.model;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6import android.database.sqlite.SQLiteDatabase;
7import android.database.sqlite.SQLiteOpenHelper;
8
9import org.citra.citra_emu.NativeLibrary;
10import org.citra.citra_emu.utils.Log;
11
12import java.io.File;
13import java.util.Arrays;
14import java.util.HashSet;
15import java.util.Set;
16
17import rx.Observable;
18
19/**
20 * A helper class that provides several utilities simplifying interaction with
21 * the SQLite database.
22 */
23public final class GameDatabase extends SQLiteOpenHelper {
24 public static final int COLUMN_DB_ID = 0;
25 public static final int GAME_COLUMN_PATH = 1;
26 public static final int GAME_COLUMN_TITLE = 2;
27 public static final int GAME_COLUMN_DESCRIPTION = 3;
28 public static final int GAME_COLUMN_REGIONS = 4;
29 public static final int GAME_COLUMN_GAME_ID = 5;
30 public static final int GAME_COLUMN_COMPANY = 6;
31 public static final int FOLDER_COLUMN_PATH = 1;
32 public static final String KEY_DB_ID = "_id";
33 public static final String KEY_GAME_PATH = "path";
34 public static final String KEY_GAME_TITLE = "title";
35 public static final String KEY_GAME_DESCRIPTION = "description";
36 public static final String KEY_GAME_REGIONS = "regions";
37 public static final String KEY_GAME_ID = "game_id";
38 public static final String KEY_GAME_COMPANY = "company";
39 public static final String KEY_FOLDER_PATH = "path";
40 public static final String TABLE_NAME_FOLDERS = "folders";
41 public static final String TABLE_NAME_GAMES = "games";
42 private static final int DB_VERSION = 2;
43 private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
44 private static final String TYPE_INTEGER = " INTEGER";
45 private static final String TYPE_STRING = " TEXT";
46
47 private static final String CONSTRAINT_UNIQUE = " UNIQUE";
48
49 private static final String SEPARATOR = ", ";
50
51 private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
52 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
53 + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
54 + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
55 + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
56 + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
57 + KEY_GAME_ID + TYPE_STRING + SEPARATOR
58 + KEY_GAME_COMPANY + TYPE_STRING + ")";
59
60 private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
61 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
62 + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
63
64 private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
65 private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
66
67 public GameDatabase(Context context) {
68 // Superclass constructor builds a database or uses an existing one.
69 super(context, "games.db", null, DB_VERSION);
70 }
71
72 @Override
73 public void onCreate(SQLiteDatabase database) {
74 Log.debug("[GameDatabase] GameDatabase - Creating database...");
75
76 execSqlAndLog(database, SQL_CREATE_GAMES);
77 execSqlAndLog(database, SQL_CREATE_FOLDERS);
78 }
79
80 @Override
81 public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) {
82 Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..");
83 execSqlAndLog(database, SQL_DELETE_FOLDERS);
84 execSqlAndLog(database, SQL_CREATE_FOLDERS);
85
86 execSqlAndLog(database, SQL_DELETE_GAMES);
87 execSqlAndLog(database, SQL_CREATE_GAMES);
88 }
89
90 @Override
91 public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
92 Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " +
93 newVersion);
94
95 // Delete all the games
96 execSqlAndLog(database, SQL_DELETE_GAMES);
97 execSqlAndLog(database, SQL_CREATE_GAMES);
98 }
99
100 public void resetDatabase(SQLiteDatabase database) {
101 execSqlAndLog(database, SQL_DELETE_FOLDERS);
102 execSqlAndLog(database, SQL_CREATE_FOLDERS);
103
104 execSqlAndLog(database, SQL_DELETE_GAMES);
105 execSqlAndLog(database, SQL_CREATE_GAMES);
106 }
107
108 public void scanLibrary(SQLiteDatabase database) {
109 // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
110 Cursor fileCursor = database.query(TABLE_NAME_GAMES,
111 null, // Get all columns.
112 null, // Get all rows.
113 null,
114 null, // No grouping.
115 null,
116 null); // Order of games is irrelevant.
117
118 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
119 fileCursor.moveToPosition(-1);
120
121 while (fileCursor.moveToNext()) {
122 String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
123 File game = new File(gamePath);
124
125 if (!game.exists()) {
126 Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
127 gamePath);
128 database.delete(TABLE_NAME_GAMES,
129 KEY_DB_ID + " = ?",
130 new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
131 }
132 }
133
134 // Get a cursor listing all the folders the user has added to the library.
135 Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
136 null, // Get all columns.
137 null, // Get all rows.
138 null,
139 null, // No grouping.
140 null,
141 null); // Order of folders is irrelevant.
142
143 Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(
144 ".xci", ".nsp", ".nca", ".nro"));
145
146 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
147 folderCursor.moveToPosition(-1);
148
149 // Iterate through all results of the DB query (i.e. all folders in the library.)
150 while (folderCursor.moveToNext()) {
151 String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
152
153 File folder = new File(folderPath);
154 // If the folder is empty because it no longer exists, remove it from the library.
155 if (!folder.exists()) {
156 Log.error(
157 "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
158 database.delete(TABLE_NAME_FOLDERS,
159 KEY_DB_ID + " = ?",
160 new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
161 }
162
163 addGamesRecursive(database, folder, allowedExtensions, 3);
164 }
165
166 fileCursor.close();
167 folderCursor.close();
168
169 database.close();
170 }
171
172 private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
173 if (depth <= 0) {
174 return;
175 }
176
177 File[] children = parent.listFiles();
178 if (children != null) {
179 for (File file : children) {
180 if (file.isHidden()) {
181 continue;
182 }
183
184 if (file.isDirectory()) {
185 Set<String> newExtensions = new HashSet<>(Arrays.asList(
186 ".xci", ".nsp", ".nca", ".nro"));
187 addGamesRecursive(database, file, newExtensions, depth - 1);
188 } else {
189 String filePath = file.getPath();
190
191 int extensionStart = filePath.lastIndexOf('.');
192 if (extensionStart > 0) {
193 String fileExtension = filePath.substring(extensionStart);
194
195 // Check that the file has an extension we care about before trying to read out of it.
196 if (allowedExtensions.contains(fileExtension.toLowerCase())) {
197 attemptToAddGame(database, filePath);
198 }
199 }
200 }
201 }
202 }
203 }
204
205 private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
206 String name = NativeLibrary.GetTitle(filePath);
207
208 // If the game's title field is empty, use the filename.
209 if (name.isEmpty()) {
210 name = filePath.substring(filePath.lastIndexOf("/") + 1);
211 }
212
213 String gameId = NativeLibrary.GetGameId(filePath);
214
215 // If the game's ID field is empty, use the filename without extension.
216 if (gameId.isEmpty()) {
217 gameId = filePath.substring(filePath.lastIndexOf("/") + 1,
218 filePath.lastIndexOf("."));
219 }
220
221 ContentValues game = Game.asContentValues(name,
222 NativeLibrary.GetDescription(filePath).replace("\n", " "),
223 NativeLibrary.GetRegions(filePath),
224 filePath,
225 gameId,
226 NativeLibrary.GetCompany(filePath));
227
228 // Try to update an existing game first.
229 int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update.
230 game,
231 // The values to fill the row with.
232 KEY_GAME_ID + " = ?",
233 // The WHERE clause used to find the right row.
234 new String[]{game.getAsString(
235 KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this,
236 // which is provided as an array because there
237 // could potentially be more than one argument.
238
239 // If update fails, insert a new game instead.
240 if (rowsMatched == 0) {
241 Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE));
242 database.insert(TABLE_NAME_GAMES, null, game);
243 } else {
244 Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE));
245 }
246 }
247
248 public Observable<Cursor> getGames() {
249 return Observable.create(subscriber ->
250 {
251 Log.info("[GameDatabase] Reading games list...");
252
253 SQLiteDatabase database = getReadableDatabase();
254 Cursor resultCursor = database.query(
255 TABLE_NAME_GAMES,
256 null,
257 null,
258 null,
259 null,
260 null,
261 KEY_GAME_TITLE + " ASC"
262 );
263
264 // Pass the result cursor to the consumer.
265 subscriber.onNext(resultCursor);
266
267 // Tell the consumer we're done; it will unsubscribe implicitly.
268 subscriber.onCompleted();
269 });
270 }
271
272 private void execSqlAndLog(SQLiteDatabase database, String sql) {
273 Log.verbose("[GameDatabase] Executing SQL: " + sql);
274 database.execSQL(sql);
275 }
276}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java
new file mode 100644
index 000000000..33b289fc4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java
@@ -0,0 +1,138 @@
1package org.citra.citra_emu.model;
2
3import android.content.ContentProvider;
4import android.content.ContentValues;
5import android.database.Cursor;
6import android.database.sqlite.SQLiteDatabase;
7import android.net.Uri;
8
9import androidx.annotation.NonNull;
10
11import org.citra.citra_emu.BuildConfig;
12import org.citra.citra_emu.utils.Log;
13
14/**
15 * Provides an interface allowing Activities to interact with the SQLite database.
16 * CRUD methods in this class can be called by Activities using getContentResolver().
17 */
18public final class GameProvider extends ContentProvider {
19 public static final String REFRESH_LIBRARY = "refresh";
20 public static final String RESET_LIBRARY = "reset";
21
22 public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
23 public static final Uri URI_FOLDER =
24 Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
25 public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/");
26 public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/");
27
28 public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder";
29 public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game";
30
31
32 private GameDatabase mDbHelper;
33
34 @Override
35 public boolean onCreate() {
36 Log.info("[GameProvider] Creating Content Provider...");
37
38 mDbHelper = new GameDatabase(getContext());
39
40 return true;
41 }
42
43 @Override
44 public Cursor query(@NonNull Uri uri, String[] projection, String selection,
45 String[] selectionArgs, String sortOrder) {
46 Log.info("[GameProvider] Querying URI: " + uri);
47
48 SQLiteDatabase db = mDbHelper.getReadableDatabase();
49
50 String table = uri.getLastPathSegment();
51
52 if (table == null) {
53 Log.error("[GameProvider] Badly formatted URI: " + uri);
54 return null;
55 }
56
57 Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
58 cursor.setNotificationUri(getContext().getContentResolver(), uri);
59
60 return cursor;
61 }
62
63 @Override
64 public String getType(@NonNull Uri uri) {
65 Log.verbose("[GameProvider] Getting MIME type for URI: " + uri);
66 String lastSegment = uri.getLastPathSegment();
67
68 if (lastSegment == null) {
69 Log.error("[GameProvider] Badly formatted URI: " + uri);
70 return null;
71 }
72
73 if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
74 return MIME_TYPE_FOLDER;
75 } else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) {
76 return MIME_TYPE_GAME;
77 }
78
79 Log.error("[GameProvider] Unknown MIME type for URI: " + uri);
80 return null;
81 }
82
83 @Override
84 public Uri insert(@NonNull Uri uri, ContentValues values) {
85 Log.info("[GameProvider] Inserting row at URI: " + uri);
86
87 SQLiteDatabase database = mDbHelper.getWritableDatabase();
88 String table = uri.getLastPathSegment();
89
90 if (table != null) {
91 if (table.equals(RESET_LIBRARY)) {
92 mDbHelper.resetDatabase(database);
93 return uri;
94 }
95 if (table.equals(REFRESH_LIBRARY)) {
96 Log.info(
97 "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents...");
98 mDbHelper.scanLibrary(database);
99 return uri;
100 }
101
102 long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
103
104 // If insertion was successful...
105 if (id > 0) {
106 // If we just added a folder, add its contents to the game list.
107 if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
108 mDbHelper.scanLibrary(database);
109 }
110
111 // Notify the UI that its contents should be refreshed.
112 getContext().getContentResolver().notifyChange(uri, null);
113 uri = Uri.withAppendedPath(uri, Long.toString(id));
114 } else {
115 Log.error("[GameProvider] Row already exists: " + uri + " id: " + id);
116 }
117 } else {
118 Log.error("[GameProvider] Badly formatted URI: " + uri);
119 }
120
121 database.close();
122
123 return uri;
124 }
125
126 @Override
127 public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
128 Log.error("[GameProvider] Delete operations unsupported. URI: " + uri);
129 return 0;
130 }
131
132 @Override
133 public int update(@NonNull Uri uri, ContentValues values, String selection,
134 String[] selectionArgs) {
135 Log.error("[GameProvider] Update operations unsupported. URI: " + uri);
136 return 0;
137 }
138}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
new file mode 100644
index 000000000..cdb2f7666
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
@@ -0,0 +1,878 @@
1/**
2 * Copyright 2013 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.overlay;
8
9import android.app.Activity;
10import android.content.Context;
11import android.content.SharedPreferences;
12import android.content.res.Configuration;
13import android.content.res.Resources;
14import android.graphics.Bitmap;
15import android.graphics.BitmapFactory;
16import android.graphics.Canvas;
17import android.graphics.Rect;
18import android.graphics.drawable.Drawable;
19import android.preference.PreferenceManager;
20import android.util.AttributeSet;
21import android.util.DisplayMetrics;
22import android.view.Display;
23import android.view.MotionEvent;
24import android.view.SurfaceView;
25import android.view.View;
26import android.view.View.OnTouchListener;
27
28import org.citra.citra_emu.NativeLibrary;
29import org.citra.citra_emu.NativeLibrary.ButtonState;
30import org.citra.citra_emu.NativeLibrary.ButtonType;
31import org.citra.citra_emu.R;
32import org.citra.citra_emu.utils.EmulationMenuSettings;
33
34import java.util.HashSet;
35import java.util.Set;
36
37/**
38 * Draws the interactive input overlay on top of the
39 * {@link SurfaceView} that is rendering emulation.
40 */
41public final class InputOverlay extends SurfaceView implements OnTouchListener {
42 private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
43 private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
44 private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>();
45
46 private boolean mIsInEditMode = false;
47 private InputOverlayDrawableButton mButtonBeingConfigured;
48 private InputOverlayDrawableDpad mDpadBeingConfigured;
49 private InputOverlayDrawableJoystick mJoystickBeingConfigured;
50
51 private SharedPreferences mPreferences;
52
53 // Stores the ID of the pointer that interacted with the 3DS touchscreen.
54 private int mTouchscreenPointerId = -1;
55
56 /**
57 * Constructor
58 *
59 * @param context The current {@link Context}.
60 * @param attrs {@link AttributeSet} for parsing XML attributes.
61 */
62 public InputOverlay(Context context, AttributeSet attrs) {
63 super(context, attrs);
64
65 mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
66 if (!mPreferences.getBoolean("OverlayInit", false)) {
67 defaultOverlay();
68 }
69
70 // Reset 3ds touchscreen pointer ID
71 mTouchscreenPointerId = -1;
72
73 // Load the controls.
74 refreshControls();
75
76 // Set the on touch listener.
77 setOnTouchListener(this);
78
79 // Force draw
80 setWillNotDraw(false);
81
82 // Request focus for the overlay so it has priority on presses.
83 requestFocus();
84 }
85
86 /**
87 * Resizes a {@link Bitmap} by a given scale factor
88 *
89 * @param context The current {@link Context}
90 * @param bitmap The {@link Bitmap} to scale.
91 * @param scale The scale factor for the bitmap.
92 * @return The scaled {@link Bitmap}
93 */
94 public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) {
95 // Determine the button size based on the smaller screen dimension.
96 // This makes sure the buttons are the same size in both portrait and landscape.
97 DisplayMetrics dm = context.getResources().getDisplayMetrics();
98 int minDimension = Math.min(dm.widthPixels, dm.heightPixels);
99
100 return Bitmap.createScaledBitmap(bitmap,
101 (int) (minDimension * scale),
102 (int) (minDimension * scale),
103 true);
104 }
105
106 /**
107 * Initializes an InputOverlayDrawableButton, given by resId, with all of the
108 * parameters set for it to be properly shown on the InputOverlay.
109 * <p>
110 * This works due to the way the X and Y coordinates are stored within
111 * the {@link SharedPreferences}.
112 * <p>
113 * In the input overlay configuration menu,
114 * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
115 * the X and Y coordinates of the button at the END of its touch event
116 * (when you remove your finger/stylus from the touchscreen) are then stored
117 * within a SharedPreferences instance so that those values can be retrieved here.
118 * <p>
119 * This has a few benefits over the conventional way of storing the values
120 * (ie. within the Citra ini file).
121 * <ul>
122 * <li>No native calls</li>
123 * <li>Keeps Android-only values inside the Android environment</li>
124 * </ul>
125 * <p>
126 * Technically no modifications should need to be performed on the returned
127 * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
128 * for Android to call the onDraw method.
129 *
130 * @param context The current {@link Context}.
131 * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State).
132 * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State).
133 * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
134 * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
135 */
136 private static InputOverlayDrawableButton initializeOverlayButton(Context context,
137 int defaultResId, int pressedResId, int buttonId, String orientation) {
138 // Resources handle for fetching the initial Drawable resource.
139 final Resources res = context.getResources();
140
141 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
142 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
143
144 // Decide scale based on button ID and user preference
145 float scale;
146
147 switch (buttonId) {
148 case ButtonType.BUTTON_HOME:
149 case ButtonType.BUTTON_START:
150 case ButtonType.BUTTON_SELECT:
151 scale = 0.08f;
152 break;
153 case ButtonType.TRIGGER_L:
154 case ButtonType.TRIGGER_R:
155 case ButtonType.BUTTON_ZL:
156 case ButtonType.BUTTON_ZR:
157 scale = 0.18f;
158 break;
159 default:
160 scale = 0.11f;
161 break;
162 }
163
164 scale *= (sPrefs.getInt("controlScale", 50) + 50);
165 scale /= 100;
166
167 // Initialize the InputOverlayDrawableButton.
168 final Bitmap defaultStateBitmap =
169 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
170 final Bitmap pressedStateBitmap =
171 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale);
172 final InputOverlayDrawableButton overlayDrawable =
173 new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId);
174
175 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
176 // These were set in the input overlay configuration menu.
177 String xKey;
178 String yKey;
179
180 xKey = buttonId + orientation + "-X";
181 yKey = buttonId + orientation + "-Y";
182
183 int drawableX = (int) sPrefs.getFloat(xKey, 0f);
184 int drawableY = (int) sPrefs.getFloat(yKey, 0f);
185
186 int width = overlayDrawable.getWidth();
187 int height = overlayDrawable.getHeight();
188
189 // Now set the bounds for the InputOverlayDrawableButton.
190 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
191 overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
192
193 // Need to set the image's position
194 overlayDrawable.setPosition(drawableX, drawableY);
195
196 return overlayDrawable;
197 }
198
199 /**
200 * Initializes an {@link InputOverlayDrawableDpad}
201 *
202 * @param context The current {@link Context}.
203 * @param defaultResId The {@link Bitmap} resource ID of the default sate.
204 * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction.
205 * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions.
206 * @param buttonUp Identifier for the up button.
207 * @param buttonDown Identifier for the down button.
208 * @param buttonLeft Identifier for the left button.
209 * @param buttonRight Identifier for the right button.
210 * @return the initialized {@link InputOverlayDrawableDpad}
211 */
212 private static InputOverlayDrawableDpad initializeOverlayDpad(Context context,
213 int defaultResId,
214 int pressedOneDirectionResId,
215 int pressedTwoDirectionsResId,
216 int buttonUp,
217 int buttonDown,
218 int buttonLeft,
219 int buttonRight,
220 String orientation) {
221 // Resources handle for fetching the initial Drawable resource.
222 final Resources res = context.getResources();
223
224 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
225 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
226
227 // Decide scale based on button ID and user preference
228 float scale = 0.22f;
229
230 scale *= (sPrefs.getInt("controlScale", 50) + 50);
231 scale /= 100;
232
233 // Initialize the InputOverlayDrawableDpad.
234 final Bitmap defaultStateBitmap =
235 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
236 final Bitmap pressedOneDirectionStateBitmap =
237 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId),
238 scale);
239 final Bitmap pressedTwoDirectionsStateBitmap =
240 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId),
241 scale);
242 final InputOverlayDrawableDpad overlayDrawable =
243 new InputOverlayDrawableDpad(res, defaultStateBitmap,
244 pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap,
245 buttonUp, buttonDown, buttonLeft, buttonRight);
246
247 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
248 // These were set in the input overlay configuration menu.
249 int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f);
250 int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f);
251
252 int width = overlayDrawable.getWidth();
253 int height = overlayDrawable.getHeight();
254
255 // Now set the bounds for the InputOverlayDrawableDpad.
256 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
257 overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
258
259 // Need to set the image's position
260 overlayDrawable.setPosition(drawableX, drawableY);
261
262 return overlayDrawable;
263 }
264
265 /**
266 * Initializes an {@link InputOverlayDrawableJoystick}
267 *
268 * @param context The current {@link Context}
269 * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
270 * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
271 * @param pressedResInner Resource ID for the pressed inner image of the joystick.
272 * @param joystick Identifier for which joystick this is.
273 * @return the initialized {@link InputOverlayDrawableJoystick}.
274 */
275 private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context,
276 int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) {
277 // Resources handle for fetching the initial Drawable resource.
278 final Resources res = context.getResources();
279
280 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
281 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
282
283 // Decide scale based on user preference
284 float scale = 0.275f;
285 scale *= (sPrefs.getInt("controlScale", 50) + 50);
286 scale /= 100;
287
288 // Initialize the InputOverlayDrawableJoystick.
289 final Bitmap bitmapOuter =
290 resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale);
291 final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner);
292 final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner);
293
294 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
295 // These were set in the input overlay configuration menu.
296 int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f);
297 int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f);
298
299 // Decide inner scale based on joystick ID
300 float outerScale = 1.f;
301 if (joystick == ButtonType.STICK_C) {
302 outerScale = 2.f;
303 }
304
305 // Now set the bounds for the InputOverlayDrawableJoystick.
306 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
307 int outerSize = bitmapOuter.getWidth();
308 Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale));
309 Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale));
310
311 // Send the drawableId to the joystick so it can be referenced when saving control position.
312 final InputOverlayDrawableJoystick overlayDrawable
313 = new InputOverlayDrawableJoystick(res, bitmapOuter,
314 bitmapInnerDefault, bitmapInnerPressed,
315 outerRect, innerRect, joystick);
316
317 // Need to set the image's position
318 overlayDrawable.setPosition(drawableX, drawableY);
319
320 return overlayDrawable;
321 }
322
323 @Override
324 public void draw(Canvas canvas) {
325 super.draw(canvas);
326
327 for (InputOverlayDrawableButton button : overlayButtons) {
328 button.draw(canvas);
329 }
330
331 for (InputOverlayDrawableDpad dpad : overlayDpads) {
332 dpad.draw(canvas);
333 }
334
335 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
336 joystick.draw(canvas);
337 }
338 }
339
340 @Override
341 public boolean onTouch(View v, MotionEvent event) {
342 if (isInEditMode()) {
343 return onTouchWhileEditing(event);
344 }
345
346 int pointerIndex = event.getActionIndex();
347
348 if (mPreferences.getBoolean("isTouchEnabled", true)) {
349 switch (event.getAction() & MotionEvent.ACTION_MASK) {
350 case MotionEvent.ACTION_DOWN:
351 case MotionEvent.ACTION_POINTER_DOWN:
352 if (NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true)) {
353 mTouchscreenPointerId = event.getPointerId(pointerIndex);
354 }
355 break;
356 case MotionEvent.ACTION_UP:
357 case MotionEvent.ACTION_POINTER_UP:
358 if (mTouchscreenPointerId == event.getPointerId(pointerIndex)) {
359 // We don't really care where the touch has been released. We only care whether it has been
360 // released or not.
361 NativeLibrary.onTouchEvent(0, 0, false);
362 mTouchscreenPointerId = -1;
363 }
364 break;
365 }
366
367 for (int i = 0; i < event.getPointerCount(); i++) {
368 if (mTouchscreenPointerId == event.getPointerId(i)) {
369 NativeLibrary.onTouchMoved(event.getX(i), event.getY(i));
370 }
371 }
372 }
373
374 for (InputOverlayDrawableButton button : overlayButtons) {
375 // Determine the button state to apply based on the MotionEvent action flag.
376 switch (event.getAction() & MotionEvent.ACTION_MASK) {
377 case MotionEvent.ACTION_DOWN:
378 case MotionEvent.ACTION_POINTER_DOWN:
379 // If a pointer enters the bounds of a button, press that button.
380 if (button.getBounds()
381 .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
382 button.setPressedState(true);
383 button.setTrackId(event.getPointerId(pointerIndex));
384 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
385 ButtonState.PRESSED);
386 }
387 break;
388 case MotionEvent.ACTION_UP:
389 case MotionEvent.ACTION_POINTER_UP:
390 // If a pointer ends, release the button it was pressing.
391 if (button.getTrackId() == event.getPointerId(pointerIndex)) {
392 button.setPressedState(false);
393 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
394 ButtonState.RELEASED);
395 }
396 break;
397 }
398 }
399
400 for (InputOverlayDrawableDpad dpad : overlayDpads) {
401 // Determine the button state to apply based on the MotionEvent action flag.
402 switch (event.getAction() & MotionEvent.ACTION_MASK) {
403 case MotionEvent.ACTION_DOWN:
404 case MotionEvent.ACTION_POINTER_DOWN:
405 // If a pointer enters the bounds of a button, press that button.
406 if (dpad.getBounds()
407 .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
408 dpad.setTrackId(event.getPointerId(pointerIndex));
409 }
410 break;
411 case MotionEvent.ACTION_UP:
412 case MotionEvent.ACTION_POINTER_UP:
413 // If a pointer ends, release the buttons.
414 if (dpad.getTrackId() == event.getPointerId(pointerIndex)) {
415 for (int i = 0; i < 4; i++) {
416 dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT);
417 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i),
418 NativeLibrary.ButtonState.RELEASED);
419 }
420 dpad.setTrackId(-1);
421 }
422 break;
423 }
424
425 if (dpad.getTrackId() != -1) {
426 for (int i = 0; i < event.getPointerCount(); i++) {
427 if (dpad.getTrackId() == event.getPointerId(i)) {
428 float touchX = event.getX(i);
429 float touchY = event.getY(i);
430 float maxY = dpad.getBounds().bottom;
431 float maxX = dpad.getBounds().right;
432 touchX -= dpad.getBounds().centerX();
433 maxX -= dpad.getBounds().centerX();
434 touchY -= dpad.getBounds().centerY();
435 maxY -= dpad.getBounds().centerY();
436 final float AxisX = touchX / maxX;
437 final float AxisY = touchY / maxY;
438
439 boolean up = false;
440 boolean down = false;
441 boolean left = false;
442 boolean right = false;
443 if (EmulationMenuSettings.getDpadSlideEnable() ||
444 (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN ||
445 (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
446 if (AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) {
447 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0),
448 NativeLibrary.ButtonState.PRESSED);
449 up = true;
450 } else {
451 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0),
452 NativeLibrary.ButtonState.RELEASED);
453 }
454 if (AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) {
455 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1),
456 NativeLibrary.ButtonState.PRESSED);
457 down = true;
458 } else {
459 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1),
460 NativeLibrary.ButtonState.RELEASED);
461 }
462 if (AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) {
463 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2),
464 NativeLibrary.ButtonState.PRESSED);
465 left = true;
466 } else {
467 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2),
468 NativeLibrary.ButtonState.RELEASED);
469 }
470 if (AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) {
471 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3),
472 NativeLibrary.ButtonState.PRESSED);
473 right = true;
474 } else {
475 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3),
476 NativeLibrary.ButtonState.RELEASED);
477 }
478
479 // Set state
480 if (up) {
481 if (left)
482 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT);
483 else if (right)
484 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT);
485 else
486 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP);
487 } else if (down) {
488 if (left)
489 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT);
490 else if (right)
491 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT);
492 else
493 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN);
494 } else if (left) {
495 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT);
496 } else if (right) {
497 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT);
498 } else {
499 dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT);
500 }
501 }
502 }
503 }
504 }
505 }
506
507 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
508 joystick.TrackEvent(event);
509 int axisID = joystick.getId();
510 float[] axises = joystick.getAxisValues();
511
512 NativeLibrary
513 .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]);
514 }
515
516 invalidate();
517
518 return true;
519 }
520
521 public boolean onTouchWhileEditing(MotionEvent event) {
522 int pointerIndex = event.getActionIndex();
523 int fingerPositionX = (int) event.getX(pointerIndex);
524 int fingerPositionY = (int) event.getY(pointerIndex);
525
526 String orientation =
527 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
528 "-Portrait" : "";
529
530 // Maybe combine Button and Joystick as subclasses of the same parent?
531 // Or maybe create an interface like IMoveableHUDControl?
532
533 for (InputOverlayDrawableButton button : overlayButtons) {
534 // Determine the button state to apply based on the MotionEvent action flag.
535 switch (event.getAction() & MotionEvent.ACTION_MASK) {
536 case MotionEvent.ACTION_DOWN:
537 case MotionEvent.ACTION_POINTER_DOWN:
538 // If no button is being moved now, remember the currently touched button to move.
539 if (mButtonBeingConfigured == null &&
540 button.getBounds().contains(fingerPositionX, fingerPositionY)) {
541 mButtonBeingConfigured = button;
542 mButtonBeingConfigured.onConfigureTouch(event);
543 }
544 break;
545 case MotionEvent.ACTION_MOVE:
546 if (mButtonBeingConfigured != null) {
547 mButtonBeingConfigured.onConfigureTouch(event);
548 invalidate();
549 return true;
550 }
551 break;
552
553 case MotionEvent.ACTION_UP:
554 case MotionEvent.ACTION_POINTER_UP:
555 if (mButtonBeingConfigured == button) {
556 // Persist button position by saving new place.
557 saveControlPosition(mButtonBeingConfigured.getId(),
558 mButtonBeingConfigured.getBounds().left,
559 mButtonBeingConfigured.getBounds().top, orientation);
560 mButtonBeingConfigured = null;
561 }
562 break;
563 }
564 }
565
566 for (InputOverlayDrawableDpad dpad : overlayDpads) {
567 // Determine the button state to apply based on the MotionEvent action flag.
568 switch (event.getAction() & MotionEvent.ACTION_MASK) {
569 case MotionEvent.ACTION_DOWN:
570 case MotionEvent.ACTION_POINTER_DOWN:
571 // If no button is being moved now, remember the currently touched button to move.
572 if (mButtonBeingConfigured == null &&
573 dpad.getBounds().contains(fingerPositionX, fingerPositionY)) {
574 mDpadBeingConfigured = dpad;
575 mDpadBeingConfigured.onConfigureTouch(event);
576 }
577 break;
578 case MotionEvent.ACTION_MOVE:
579 if (mDpadBeingConfigured != null) {
580 mDpadBeingConfigured.onConfigureTouch(event);
581 invalidate();
582 return true;
583 }
584 break;
585
586 case MotionEvent.ACTION_UP:
587 case MotionEvent.ACTION_POINTER_UP:
588 if (mDpadBeingConfigured == dpad) {
589 // Persist button position by saving new place.
590 saveControlPosition(mDpadBeingConfigured.getId(0),
591 mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top,
592 orientation);
593 mDpadBeingConfigured = null;
594 }
595 break;
596 }
597 }
598
599 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
600 switch (event.getAction()) {
601 case MotionEvent.ACTION_DOWN:
602 case MotionEvent.ACTION_POINTER_DOWN:
603 if (mJoystickBeingConfigured == null &&
604 joystick.getBounds().contains(fingerPositionX, fingerPositionY)) {
605 mJoystickBeingConfigured = joystick;
606 mJoystickBeingConfigured.onConfigureTouch(event);
607 }
608 break;
609 case MotionEvent.ACTION_MOVE:
610 if (mJoystickBeingConfigured != null) {
611 mJoystickBeingConfigured.onConfigureTouch(event);
612 invalidate();
613 }
614 break;
615 case MotionEvent.ACTION_UP:
616 case MotionEvent.ACTION_POINTER_UP:
617 if (mJoystickBeingConfigured != null) {
618 saveControlPosition(mJoystickBeingConfigured.getId(),
619 mJoystickBeingConfigured.getBounds().left,
620 mJoystickBeingConfigured.getBounds().top, orientation);
621 mJoystickBeingConfigured = null;
622 }
623 break;
624 }
625 }
626
627 return true;
628 }
629
630 private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left,
631 boolean right) {
632 if (up) {
633 if (left)
634 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT);
635 else if (right)
636 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT);
637 else
638 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP);
639 } else if (down) {
640 if (left)
641 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT);
642 else if (right)
643 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT);
644 else
645 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN);
646 } else if (left) {
647 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT);
648 } else if (right) {
649 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT);
650 }
651 }
652
653 private void addOverlayControls(String orientation) {
654 if (mPreferences.getBoolean("buttonToggle0", true)) {
655 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a,
656 R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation));
657 }
658 if (mPreferences.getBoolean("buttonToggle1", true)) {
659 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b,
660 R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation));
661 }
662 if (mPreferences.getBoolean("buttonToggle2", true)) {
663 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x,
664 R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation));
665 }
666 if (mPreferences.getBoolean("buttonToggle3", true)) {
667 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y,
668 R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation));
669 }
670 if (mPreferences.getBoolean("buttonToggle4", true)) {
671 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l,
672 R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation));
673 }
674 if (mPreferences.getBoolean("buttonToggle5", true)) {
675 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r,
676 R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation));
677 }
678 if (mPreferences.getBoolean("buttonToggle6", false)) {
679 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl,
680 R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation));
681 }
682 if (mPreferences.getBoolean("buttonToggle7", false)) {
683 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr,
684 R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation));
685 }
686 if (mPreferences.getBoolean("buttonToggle8", true)) {
687 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start,
688 R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation));
689 }
690 if (mPreferences.getBoolean("buttonToggle9", true)) {
691 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select,
692 R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation));
693 }
694 if (mPreferences.getBoolean("buttonToggle10", true)) {
695 overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad,
696 R.drawable.dpad_pressed_one_direction,
697 R.drawable.dpad_pressed_two_directions,
698 ButtonType.DPAD_UP, ButtonType.DPAD_DOWN,
699 ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation));
700 }
701 if (mPreferences.getBoolean("buttonToggle11", true)) {
702 overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range,
703 R.drawable.stick_main, R.drawable.stick_main_pressed,
704 ButtonType.STICK_LEFT, orientation));
705 }
706 if (mPreferences.getBoolean("buttonToggle12", false)) {
707 overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range,
708 R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation));
709 }
710 }
711
712 public void refreshControls() {
713 // Remove all the overlay buttons from the HashSet.
714 overlayButtons.clear();
715 overlayDpads.clear();
716 overlayJoysticks.clear();
717
718 String orientation =
719 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
720 "-Portrait" : "";
721
722 // Add all the enabled overlay items back to the HashSet.
723 if (EmulationMenuSettings.getShowOverlay()) {
724 addOverlayControls(orientation);
725 }
726
727 invalidate();
728 }
729
730 private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) {
731 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
732 SharedPreferences.Editor sPrefsEditor = sPrefs.edit();
733 sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x);
734 sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y);
735 sPrefsEditor.apply();
736 }
737
738 public void setIsInEditMode(boolean isInEditMode) {
739 mIsInEditMode = isInEditMode;
740 }
741
742 private void defaultOverlay() {
743 if (!mPreferences.getBoolean("OverlayInit", false)) {
744 // It's possible that a user has created their overlay before this was added
745 // Only change the overlay if the 'A' button is not in the upper corner.
746 if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) {
747 defaultOverlayLandscape();
748 }
749 if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) {
750 defaultOverlayPortrait();
751 }
752 }
753
754 SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
755 sPrefsEditor.putBoolean("OverlayInit", true);
756 sPrefsEditor.apply();
757 }
758
759 public void resetButtonPlacement() {
760 boolean isLandscape =
761 getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
762
763 if (isLandscape) {
764 defaultOverlayLandscape();
765 } else {
766 defaultOverlayPortrait();
767 }
768
769 refreshControls();
770 }
771
772 private void defaultOverlayLandscape() {
773 SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
774 // Get screen size
775 Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
776 DisplayMetrics outMetrics = new DisplayMetrics();
777 display.getMetrics(outMetrics);
778 float maxX = outMetrics.heightPixels;
779 float maxY = outMetrics.widthPixels;
780 // Height and width changes depending on orientation. Use the larger value for height.
781 if (maxY > maxX) {
782 float tmp = maxX;
783 maxX = maxY;
784 maxY = tmp;
785 }
786 Resources res = getResources();
787
788 // Each value is a percent from max X/Y stored as an int. Have to bring that value down
789 // to a decimal before multiplying by MAX X/Y.
790 sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX));
791 sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY));
792 sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX));
793 sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY));
794 sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX));
795 sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY));
796 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX));
797 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY));
798 sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX));
799 sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY));
800 sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX));
801 sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY));
802 sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX));
803 sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY));
804 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX));
805 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY));
806 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX));
807 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY));
808 sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX));
809 sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY));
810 sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX));
811 sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY));
812 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX));
813 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY));
814 sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX));
815 sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY));
816 sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX));
817 sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY));
818
819 // We want to commit right away, otherwise the overlay could load before this is saved.
820 sPrefsEditor.commit();
821 }
822
823 private void defaultOverlayPortrait() {
824 SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
825 // Get screen size
826 Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
827 DisplayMetrics outMetrics = new DisplayMetrics();
828 display.getMetrics(outMetrics);
829 float maxX = outMetrics.heightPixels;
830 float maxY = outMetrics.widthPixels;
831 // Height and width changes depending on orientation. Use the larger value for height.
832 if (maxY < maxX) {
833 float tmp = maxX;
834 maxX = maxY;
835 maxY = tmp;
836 }
837 Resources res = getResources();
838 String portrait = "-Portrait";
839
840 // Each value is a percent from max X/Y stored as an int. Have to bring that value down
841 // to a decimal before multiplying by MAX X/Y.
842 sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX));
843 sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY));
844 sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX));
845 sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY));
846 sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX));
847 sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY));
848 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX));
849 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY));
850 sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX));
851 sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY));
852 sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX));
853 sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY));
854 sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX));
855 sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY));
856 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX));
857 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY));
858 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX));
859 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY));
860 sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX));
861 sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY));
862 sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX));
863 sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY));
864 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX));
865 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY));
866 sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX));
867 sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY));
868 sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX));
869 sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY));
870
871 // We want to commit right away, otherwise the overlay could load before this is saved.
872 sPrefsEditor.commit();
873 }
874
875 public boolean isInEditMode() {
876 return mIsInEditMode;
877 }
878}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java
new file mode 100644
index 000000000..81352296c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java
@@ -0,0 +1,122 @@
1/**
2 * Copyright 2013 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.overlay;
8
9import android.content.res.Resources;
10import android.graphics.Bitmap;
11import android.graphics.Canvas;
12import android.graphics.Rect;
13import android.graphics.drawable.BitmapDrawable;
14import android.view.MotionEvent;
15
16/**
17 * Custom {@link BitmapDrawable} that is capable
18 * of storing it's own ID.
19 */
20public final class InputOverlayDrawableButton {
21 // The ID identifying what type of button this Drawable represents.
22 private int mButtonType;
23 private int mTrackId;
24 private int mPreviousTouchX, mPreviousTouchY;
25 private int mControlPositionX, mControlPositionY;
26 private int mWidth;
27 private int mHeight;
28 private BitmapDrawable mDefaultStateBitmap;
29 private BitmapDrawable mPressedStateBitmap;
30 private boolean mPressedState = false;
31
32 /**
33 * Constructor
34 *
35 * @param res {@link Resources} instance.
36 * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable.
37 * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable.
38 * @param buttonType Identifier for this type of button.
39 */
40 public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap,
41 Bitmap pressedStateBitmap, int buttonType) {
42 mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
43 mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap);
44 mButtonType = buttonType;
45
46 mWidth = mDefaultStateBitmap.getIntrinsicWidth();
47 mHeight = mDefaultStateBitmap.getIntrinsicHeight();
48 }
49
50 /**
51 * Gets this InputOverlayDrawableButton's button ID.
52 *
53 * @return this InputOverlayDrawableButton's button ID.
54 */
55 public int getId() {
56 return mButtonType;
57 }
58
59 public int getTrackId() {
60 return mTrackId;
61 }
62
63 public void setTrackId(int trackId) {
64 mTrackId = trackId;
65 }
66
67 public boolean onConfigureTouch(MotionEvent event) {
68 int pointerIndex = event.getActionIndex();
69 int fingerPositionX = (int) event.getX(pointerIndex);
70 int fingerPositionY = (int) event.getY(pointerIndex);
71 switch (event.getAction()) {
72 case MotionEvent.ACTION_DOWN:
73 mPreviousTouchX = fingerPositionX;
74 mPreviousTouchY = fingerPositionY;
75 break;
76 case MotionEvent.ACTION_MOVE:
77 mControlPositionX += fingerPositionX - mPreviousTouchX;
78 mControlPositionY += fingerPositionY - mPreviousTouchY;
79 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
80 getHeight() + mControlPositionY);
81 mPreviousTouchX = fingerPositionX;
82 mPreviousTouchY = fingerPositionY;
83 break;
84
85 }
86 return true;
87 }
88
89 public void setPosition(int x, int y) {
90 mControlPositionX = x;
91 mControlPositionY = y;
92 }
93
94 public void draw(Canvas canvas) {
95 getCurrentStateBitmapDrawable().draw(canvas);
96 }
97
98 private BitmapDrawable getCurrentStateBitmapDrawable() {
99 return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap;
100 }
101
102 public void setBounds(int left, int top, int right, int bottom) {
103 mDefaultStateBitmap.setBounds(left, top, right, bottom);
104 mPressedStateBitmap.setBounds(left, top, right, bottom);
105 }
106
107 public Rect getBounds() {
108 return mDefaultStateBitmap.getBounds();
109 }
110
111 public int getWidth() {
112 return mWidth;
113 }
114
115 public int getHeight() {
116 return mHeight;
117 }
118
119 public void setPressedState(boolean isPressed) {
120 mPressedState = isPressed;
121 }
122}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java
new file mode 100644
index 000000000..87f3b7cd9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java
@@ -0,0 +1,193 @@
1/**
2 * Copyright 2016 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.overlay;
8
9import android.content.res.Resources;
10import android.graphics.Bitmap;
11import android.graphics.Canvas;
12import android.graphics.Rect;
13import android.graphics.drawable.BitmapDrawable;
14import android.view.MotionEvent;
15
16/**
17 * Custom {@link BitmapDrawable} that is capable
18 * of storing it's own ID.
19 */
20public final class InputOverlayDrawableDpad {
21 public static final int STATE_DEFAULT = 0;
22 public static final int STATE_PRESSED_UP = 1;
23 public static final int STATE_PRESSED_DOWN = 2;
24 public static final int STATE_PRESSED_LEFT = 3;
25 public static final int STATE_PRESSED_RIGHT = 4;
26 public static final int STATE_PRESSED_UP_LEFT = 5;
27 public static final int STATE_PRESSED_UP_RIGHT = 6;
28 public static final int STATE_PRESSED_DOWN_LEFT = 7;
29 public static final int STATE_PRESSED_DOWN_RIGHT = 8;
30 public static final float VIRT_AXIS_DEADZONE = 0.5f;
31 // The ID identifying what type of button this Drawable represents.
32 private int[] mButtonType = new int[4];
33 private int mTrackId;
34 private int mPreviousTouchX, mPreviousTouchY;
35 private int mControlPositionX, mControlPositionY;
36 private int mWidth;
37 private int mHeight;
38 private BitmapDrawable mDefaultStateBitmap;
39 private BitmapDrawable mPressedOneDirectionStateBitmap;
40 private BitmapDrawable mPressedTwoDirectionsStateBitmap;
41 private int mPressState = STATE_DEFAULT;
42
43 /**
44 * Constructor
45 *
46 * @param res {@link Resources} instance.
47 * @param defaultStateBitmap {@link Bitmap} of the default state.
48 * @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction.
49 * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction.
50 * @param buttonUp Identifier for the up button.
51 * @param buttonDown Identifier for the down button.
52 * @param buttonLeft Identifier for the left button.
53 * @param buttonRight Identifier for the right button.
54 */
55 public InputOverlayDrawableDpad(Resources res,
56 Bitmap defaultStateBitmap,
57 Bitmap pressedOneDirectionStateBitmap,
58 Bitmap pressedTwoDirectionsStateBitmap,
59 int buttonUp, int buttonDown,
60 int buttonLeft, int buttonRight) {
61 mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
62 mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap);
63 mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap);
64
65 mWidth = mDefaultStateBitmap.getIntrinsicWidth();
66 mHeight = mDefaultStateBitmap.getIntrinsicHeight();
67
68 mButtonType[0] = buttonUp;
69 mButtonType[1] = buttonDown;
70 mButtonType[2] = buttonLeft;
71 mButtonType[3] = buttonRight;
72
73 mTrackId = -1;
74 }
75
76 public void draw(Canvas canvas) {
77 int px = mControlPositionX + (getWidth() / 2);
78 int py = mControlPositionY + (getHeight() / 2);
79 switch (mPressState) {
80 case STATE_DEFAULT:
81 mDefaultStateBitmap.draw(canvas);
82 break;
83 case STATE_PRESSED_UP:
84 mPressedOneDirectionStateBitmap.draw(canvas);
85 break;
86 case STATE_PRESSED_RIGHT:
87 canvas.save();
88 canvas.rotate(90, px, py);
89 mPressedOneDirectionStateBitmap.draw(canvas);
90 canvas.restore();
91 break;
92 case STATE_PRESSED_DOWN:
93 canvas.save();
94 canvas.rotate(180, px, py);
95 mPressedOneDirectionStateBitmap.draw(canvas);
96 canvas.restore();
97 break;
98 case STATE_PRESSED_LEFT:
99 canvas.save();
100 canvas.rotate(270, px, py);
101 mPressedOneDirectionStateBitmap.draw(canvas);
102 canvas.restore();
103 break;
104 case STATE_PRESSED_UP_LEFT:
105 mPressedTwoDirectionsStateBitmap.draw(canvas);
106 break;
107 case STATE_PRESSED_UP_RIGHT:
108 canvas.save();
109 canvas.rotate(90, px, py);
110 mPressedTwoDirectionsStateBitmap.draw(canvas);
111 canvas.restore();
112 break;
113 case STATE_PRESSED_DOWN_RIGHT:
114 canvas.save();
115 canvas.rotate(180, px, py);
116 mPressedTwoDirectionsStateBitmap.draw(canvas);
117 canvas.restore();
118 break;
119 case STATE_PRESSED_DOWN_LEFT:
120 canvas.save();
121 canvas.rotate(270, px, py);
122 mPressedTwoDirectionsStateBitmap.draw(canvas);
123 canvas.restore();
124 break;
125 }
126 }
127
128 /**
129 * Gets one of the InputOverlayDrawableDpad's button IDs.
130 *
131 * @return the requested InputOverlayDrawableDpad's button ID.
132 */
133 public int getId(int direction) {
134 return mButtonType[direction];
135 }
136
137 public int getTrackId() {
138 return mTrackId;
139 }
140
141 public void setTrackId(int trackId) {
142 mTrackId = trackId;
143 }
144
145 public boolean onConfigureTouch(MotionEvent event) {
146 int pointerIndex = event.getActionIndex();
147 int fingerPositionX = (int) event.getX(pointerIndex);
148 int fingerPositionY = (int) event.getY(pointerIndex);
149 switch (event.getAction()) {
150 case MotionEvent.ACTION_DOWN:
151 mPreviousTouchX = fingerPositionX;
152 mPreviousTouchY = fingerPositionY;
153 break;
154 case MotionEvent.ACTION_MOVE:
155 mControlPositionX += fingerPositionX - mPreviousTouchX;
156 mControlPositionY += fingerPositionY - mPreviousTouchY;
157 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
158 getHeight() + mControlPositionY);
159 mPreviousTouchX = fingerPositionX;
160 mPreviousTouchY = fingerPositionY;
161 break;
162
163 }
164 return true;
165 }
166
167 public void setPosition(int x, int y) {
168 mControlPositionX = x;
169 mControlPositionY = y;
170 }
171
172 public void setBounds(int left, int top, int right, int bottom) {
173 mDefaultStateBitmap.setBounds(left, top, right, bottom);
174 mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom);
175 mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom);
176 }
177
178 public Rect getBounds() {
179 return mDefaultStateBitmap.getBounds();
180 }
181
182 public int getWidth() {
183 return mWidth;
184 }
185
186 public int getHeight() {
187 return mHeight;
188 }
189
190 public void setState(int pressState) {
191 mPressState = pressState;
192 }
193}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
new file mode 100644
index 000000000..956a8b1e9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
@@ -0,0 +1,264 @@
1/**
2 * Copyright 2013 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.overlay;
8
9import android.content.res.Resources;
10import android.graphics.Bitmap;
11import android.graphics.Canvas;
12import android.graphics.Rect;
13import android.graphics.drawable.BitmapDrawable;
14import android.view.MotionEvent;
15
16import org.citra.citra_emu.NativeLibrary.ButtonType;
17import org.citra.citra_emu.utils.EmulationMenuSettings;
18
19/**
20 * Custom {@link BitmapDrawable} that is capable
21 * of storing it's own ID.
22 */
23public final class InputOverlayDrawableJoystick {
24 private final int[] axisIDs = {0, 0, 0, 0};
25 private final float[] axises = {0f, 0f};
26 private int trackId = -1;
27 private int mJoystickType;
28 private int mControlPositionX, mControlPositionY;
29 private int mPreviousTouchX, mPreviousTouchY;
30 private int mWidth;
31 private int mHeight;
32 private Rect mVirtBounds;
33 private Rect mOrigBounds;
34 private BitmapDrawable mOuterBitmap;
35 private BitmapDrawable mDefaultStateInnerBitmap;
36 private BitmapDrawable mPressedStateInnerBitmap;
37 private BitmapDrawable mBoundsBoxBitmap;
38 private boolean mPressedState = false;
39
40 /**
41 * Constructor
42 *
43 * @param res {@link Resources} instance.
44 * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick.
45 * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick.
46 * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick.
47 * @param rectOuter {@link Rect} which represents the outer joystick bounds.
48 * @param rectInner {@link Rect} which represents the inner joystick bounds.
49 * @param joystick Identifier for which joystick this is.
50 */
51 public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter,
52 Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed,
53 Rect rectOuter, Rect rectInner, int joystick) {
54 axisIDs[0] = joystick + 1; // Up
55 axisIDs[1] = joystick + 2; // Down
56 axisIDs[2] = joystick + 3; // Left
57 axisIDs[3] = joystick + 4; // Right
58 mJoystickType = joystick;
59
60 mOuterBitmap = new BitmapDrawable(res, bitmapOuter);
61 mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault);
62 mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed);
63 mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter);
64 mWidth = bitmapOuter.getWidth();
65 mHeight = bitmapOuter.getHeight();
66
67 setBounds(rectOuter);
68 mDefaultStateInnerBitmap.setBounds(rectInner);
69 mPressedStateInnerBitmap.setBounds(rectInner);
70 mVirtBounds = getBounds();
71 mOrigBounds = mOuterBitmap.copyBounds();
72 mBoundsBoxBitmap.setAlpha(0);
73 mBoundsBoxBitmap.setBounds(getVirtBounds());
74 SetInnerBounds();
75 }
76
77 /**
78 * Gets this InputOverlayDrawableJoystick's button ID.
79 *
80 * @return this InputOverlayDrawableJoystick's button ID.
81 */
82 public int getId() {
83 return mJoystickType;
84 }
85
86 public void draw(Canvas canvas) {
87 mOuterBitmap.draw(canvas);
88 getCurrentStateBitmapDrawable().draw(canvas);
89 mBoundsBoxBitmap.draw(canvas);
90 }
91
92 public void TrackEvent(MotionEvent event) {
93 int pointerIndex = event.getActionIndex();
94
95 switch (event.getAction() & MotionEvent.ACTION_MASK) {
96 case MotionEvent.ACTION_DOWN:
97 case MotionEvent.ACTION_POINTER_DOWN:
98 if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
99 mPressedState = true;
100 mOuterBitmap.setAlpha(0);
101 mBoundsBoxBitmap.setAlpha(255);
102 if (EmulationMenuSettings.getJoystickRelCenter()) {
103 getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(),
104 (int) event.getY(pointerIndex) - getVirtBounds().centerY());
105 }
106 mBoundsBoxBitmap.setBounds(getVirtBounds());
107 trackId = event.getPointerId(pointerIndex);
108 }
109 break;
110 case MotionEvent.ACTION_UP:
111 case MotionEvent.ACTION_POINTER_UP:
112 if (trackId == event.getPointerId(pointerIndex)) {
113 mPressedState = false;
114 axises[0] = axises[1] = 0.0f;
115 mOuterBitmap.setAlpha(255);
116 mBoundsBoxBitmap.setAlpha(0);
117 setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
118 mOrigBounds.bottom));
119 setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
120 mOrigBounds.bottom));
121 SetInnerBounds();
122 trackId = -1;
123 }
124 break;
125 }
126
127 if (trackId == -1)
128 return;
129
130 for (int i = 0; i < event.getPointerCount(); i++) {
131 if (trackId == event.getPointerId(i)) {
132 float touchX = event.getX(i);
133 float touchY = event.getY(i);
134 float maxY = getVirtBounds().bottom;
135 float maxX = getVirtBounds().right;
136 touchX -= getVirtBounds().centerX();
137 maxX -= getVirtBounds().centerX();
138 touchY -= getVirtBounds().centerY();
139 maxY -= getVirtBounds().centerY();
140 final float AxisX = touchX / maxX;
141 final float AxisY = touchY / maxY;
142
143 // Clamp the circle pad input to a circle
144 final float angle = (float) Math.atan2(AxisY, AxisX);
145 float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY);
146 if(radius > 1.0f)
147 {
148 radius = 1.0f;
149 }
150 axises[0] = ((float)Math.cos(angle) * radius);
151 axises[1] = ((float)Math.sin(angle) * radius);
152 SetInnerBounds();
153 }
154 }
155 }
156
157 public boolean onConfigureTouch(MotionEvent event) {
158 int pointerIndex = event.getActionIndex();
159 int fingerPositionX = (int) event.getX(pointerIndex);
160 int fingerPositionY = (int) event.getY(pointerIndex);
161
162 int scale = 1;
163 if (mJoystickType == ButtonType.STICK_C) {
164 // C-stick is scaled down to be half the size of the circle pad
165 scale = 2;
166 }
167
168 switch (event.getAction()) {
169 case MotionEvent.ACTION_DOWN:
170 mPreviousTouchX = fingerPositionX;
171 mPreviousTouchY = fingerPositionY;
172 break;
173 case MotionEvent.ACTION_MOVE:
174 int deltaX = fingerPositionX - mPreviousTouchX;
175 int deltaY = fingerPositionY - mPreviousTouchY;
176 mControlPositionX += deltaX;
177 mControlPositionY += deltaY;
178 setBounds(new Rect(mControlPositionX, mControlPositionY,
179 mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
180 mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
181 setVirtBounds(new Rect(mControlPositionX, mControlPositionY,
182 mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
183 mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
184 SetInnerBounds();
185 setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY,
186 mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
187 mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)));
188 mPreviousTouchX = fingerPositionX;
189 mPreviousTouchY = fingerPositionY;
190 break;
191 }
192 return true;
193 }
194
195
196 public float[] getAxisValues() {
197 return axises;
198 }
199
200 public int[] getAxisIDs() {
201 return axisIDs;
202 }
203
204 private void SetInnerBounds() {
205 int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2));
206 int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2));
207
208 if (mJoystickType == ButtonType.STICK_LEFT) {
209 X += 1;
210 Y += 1;
211 }
212
213 if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2))
214 X = getVirtBounds().centerX() + (getVirtBounds().width() / 2);
215 if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2))
216 X = getVirtBounds().centerX() - (getVirtBounds().width() / 2);
217 if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2))
218 Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2);
219 if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2))
220 Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2);
221
222 int width = mPressedStateInnerBitmap.getBounds().width() / 2;
223 int height = mPressedStateInnerBitmap.getBounds().height() / 2;
224 mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height);
225 mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds());
226 }
227
228 public void setPosition(int x, int y) {
229 mControlPositionX = x;
230 mControlPositionY = y;
231 }
232
233 private BitmapDrawable getCurrentStateBitmapDrawable() {
234 return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap;
235 }
236
237 public Rect getBounds() {
238 return mOuterBitmap.getBounds();
239 }
240
241 public void setBounds(Rect bounds) {
242 mOuterBitmap.setBounds(bounds);
243 }
244
245 private void setOrigBounds(Rect bounds) {
246 mOrigBounds = bounds;
247 }
248
249 private Rect getVirtBounds() {
250 return mVirtBounds;
251 }
252
253 private void setVirtBounds(Rect bounds) {
254 mVirtBounds = bounds;
255 }
256
257 public int getWidth() {
258 return mWidth;
259 }
260
261 public int getHeight() {
262 return mHeight;
263 }
264}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java
new file mode 100644
index 000000000..96ccc08bb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java
@@ -0,0 +1,130 @@
1package org.citra.citra_emu.ui;
2
3import android.content.Context;
4import android.content.res.TypedArray;
5import android.graphics.Canvas;
6import android.graphics.Rect;
7import android.graphics.drawable.Drawable;
8import android.util.AttributeSet;
9import android.view.View;
10
11import androidx.annotation.NonNull;
12import androidx.recyclerview.widget.LinearLayoutManager;
13import androidx.recyclerview.widget.RecyclerView;
14
15/**
16 * Implementation from:
17 * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36
18 */
19public class DividerItemDecoration extends RecyclerView.ItemDecoration {
20
21 private Drawable mDivider;
22 private boolean mShowFirstDivider = false;
23 private boolean mShowLastDivider = false;
24
25 public DividerItemDecoration(Context context, AttributeSet attrs) {
26 final TypedArray a = context
27 .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider});
28 mDivider = a.getDrawable(0);
29 a.recycle();
30 }
31
32 public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider,
33 boolean showLastDivider) {
34 this(context, attrs);
35 mShowFirstDivider = showFirstDivider;
36 mShowLastDivider = showLastDivider;
37 }
38
39 public DividerItemDecoration(Drawable divider) {
40 mDivider = divider;
41 }
42
43 public DividerItemDecoration(Drawable divider, boolean showFirstDivider,
44 boolean showLastDivider) {
45 this(divider);
46 mShowFirstDivider = showFirstDivider;
47 mShowLastDivider = showLastDivider;
48 }
49
50 @Override
51 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
52 @NonNull RecyclerView.State state) {
53 super.getItemOffsets(outRect, view, parent, state);
54 if (mDivider == null) {
55 return;
56 }
57 if (parent.getChildAdapterPosition(view) < 1) {
58 return;
59 }
60
61 if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
62 outRect.top = mDivider.getIntrinsicHeight();
63 } else {
64 outRect.left = mDivider.getIntrinsicWidth();
65 }
66 }
67
68 @Override
69 public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
70 if (mDivider == null) {
71 super.onDrawOver(c, parent, state);
72 return;
73 }
74
75 // Initialization needed to avoid compiler warning
76 int left = 0, right = 0, top = 0, bottom = 0, size;
77 int orientation = getOrientation(parent);
78 int childCount = parent.getChildCount();
79
80 if (orientation == LinearLayoutManager.VERTICAL) {
81 size = mDivider.getIntrinsicHeight();
82 left = parent.getPaddingLeft();
83 right = parent.getWidth() - parent.getPaddingRight();
84 } else { //horizontal
85 size = mDivider.getIntrinsicWidth();
86 top = parent.getPaddingTop();
87 bottom = parent.getHeight() - parent.getPaddingBottom();
88 }
89
90 for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) {
91 View child = parent.getChildAt(i);
92 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
93
94 if (orientation == LinearLayoutManager.VERTICAL) {
95 top = child.getTop() - params.topMargin;
96 bottom = top + size;
97 } else { //horizontal
98 left = child.getLeft() - params.leftMargin;
99 right = left + size;
100 }
101 mDivider.setBounds(left, top, right, bottom);
102 mDivider.draw(c);
103 }
104
105 // show last divider
106 if (mShowLastDivider && childCount > 0) {
107 View child = parent.getChildAt(childCount - 1);
108 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
109 if (orientation == LinearLayoutManager.VERTICAL) {
110 top = child.getBottom() + params.bottomMargin;
111 bottom = top + size;
112 } else { // horizontal
113 left = child.getRight() + params.rightMargin;
114 right = left + size;
115 }
116 mDivider.setBounds(left, top, right, bottom);
117 mDivider.draw(c);
118 }
119 }
120
121 private int getOrientation(RecyclerView parent) {
122 if (parent.getLayoutManager() instanceof LinearLayoutManager) {
123 LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
124 return layoutManager.getOrientation();
125 } else {
126 throw new IllegalStateException(
127 "DividerItemDecoration can only be used with a LinearLayoutManager.");
128 }
129 }
130}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java
new file mode 100644
index 000000000..d07fe30d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java
@@ -0,0 +1,37 @@
1package org.citra.citra_emu.ui;
2
3import android.view.View;
4
5import androidx.activity.OnBackPressedCallback;
6import androidx.annotation.NonNull;
7import androidx.slidingpanelayout.widget.SlidingPaneLayout;
8
9public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
10 implements SlidingPaneLayout.PanelSlideListener {
11 private final SlidingPaneLayout mSlidingPaneLayout;
12
13 public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
14 super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
15 mSlidingPaneLayout = slidingPaneLayout;
16 slidingPaneLayout.addPanelSlideListener(this);
17 }
18
19 @Override
20 public void handleOnBackPressed() {
21 mSlidingPaneLayout.close();
22 }
23
24 @Override
25 public void onPanelSlide(@NonNull View panel, float slideOffset) {
26 }
27
28 @Override
29 public void onPanelOpened(@NonNull View panel) {
30 setEnabled(true);
31 }
32
33 @Override
34 public void onPanelClosed(@NonNull View panel) {
35 setEnabled(false);
36 }
37}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java
new file mode 100644
index 000000000..4ba419a48
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java
@@ -0,0 +1,267 @@
1package org.citra.citra_emu.ui.main;
2
3import android.content.Intent;
4import android.content.pm.PackageManager;
5import android.os.Bundle;
6import android.view.Menu;
7import android.view.MenuInflater;
8import android.view.MenuItem;
9import android.widget.Toast;
10
11import androidx.annotation.NonNull;
12import androidx.appcompat.app.AppCompatActivity;
13import androidx.appcompat.widget.Toolbar;
14
15import org.citra.citra_emu.R;
16import org.citra.citra_emu.activities.EmulationActivity;
17import org.citra.citra_emu.features.settings.ui.SettingsActivity;
18import org.citra.citra_emu.model.GameProvider;
19import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
20import org.citra.citra_emu.utils.AddDirectoryHelper;
21import org.citra.citra_emu.utils.BillingManager;
22import org.citra.citra_emu.utils.DirectoryInitialization;
23import org.citra.citra_emu.utils.FileBrowserHelper;
24import org.citra.citra_emu.utils.PermissionsHandler;
25import org.citra.citra_emu.utils.PicassoUtils;
26import org.citra.citra_emu.utils.StartupHandler;
27import org.citra.citra_emu.utils.ThemeUtil;
28
29import java.util.Arrays;
30import java.util.Collections;
31
32/**
33 * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
34 * individually display a grid of available games for each Fragment, in a tabbed layout.
35 */
36public final class MainActivity extends AppCompatActivity implements MainView {
37 private Toolbar mToolbar;
38 private int mFrameLayoutId;
39 private PlatformGamesFragment mPlatformGamesFragment;
40
41 private MainPresenter mPresenter = new MainPresenter(this);
42
43 // Singleton to manage user billing state
44 private static BillingManager mBillingManager;
45
46 private static MenuItem mPremiumButton;
47
48 @Override
49 protected void onCreate(Bundle savedInstanceState) {
50 ThemeUtil.applyTheme();
51
52 super.onCreate(savedInstanceState);
53 setContentView(R.layout.activity_main);
54
55 findViews();
56
57 setSupportActionBar(mToolbar);
58
59 mFrameLayoutId = R.id.games_platform_frame;
60 mPresenter.onCreate();
61
62 if (savedInstanceState == null) {
63 StartupHandler.HandleInit(this);
64 if (PermissionsHandler.hasWriteAccess(this)) {
65 mPlatformGamesFragment = new PlatformGamesFragment();
66 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
67 .commit();
68 }
69 } else {
70 mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
71 }
72 PicassoUtils.init();
73
74 // Setup billing manager, so we can globally query for Premium status
75 mBillingManager = new BillingManager(this);
76
77 // Dismiss previous notifications (should not happen unless a crash occurred)
78 EmulationActivity.tryDismissRunningNotification(this);
79 }
80
81 @Override
82 protected void onSaveInstanceState(@NonNull Bundle outState) {
83 super.onSaveInstanceState(outState);
84 if (PermissionsHandler.hasWriteAccess(this)) {
85 if (getSupportFragmentManager() == null) {
86 return;
87 }
88 if (outState == null) {
89 return;
90 }
91 getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
92 }
93 }
94
95 @Override
96 protected void onResume() {
97 super.onResume();
98 mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
99 }
100
101 // TODO: Replace with a ButterKnife injection.
102 private void findViews() {
103 mToolbar = findViewById(R.id.toolbar_main);
104 }
105
106 @Override
107 public boolean onCreateOptionsMenu(Menu menu) {
108 MenuInflater inflater = getMenuInflater();
109 inflater.inflate(R.menu.menu_game_grid, menu);
110 mPremiumButton = menu.findItem(R.id.button_premium);
111
112 if (mBillingManager.isPremiumCached()) {
113 // User had premium in a previous session, hide upsell option
114 setPremiumButtonVisible(false);
115 }
116
117 return true;
118 }
119
120 static public void setPremiumButtonVisible(boolean isVisible) {
121 if (mPremiumButton != null) {
122 mPremiumButton.setVisible(isVisible);
123 }
124 }
125
126 /**
127 * MainView
128 */
129
130 @Override
131 public void setVersionString(String version) {
132 mToolbar.setSubtitle(version);
133 }
134
135 @Override
136 public void refresh() {
137 getContentResolver().insert(GameProvider.URI_REFRESH, null);
138 refreshFragment();
139 }
140
141 @Override
142 public void launchSettingsActivity(String menuTag) {
143 if (PermissionsHandler.hasWriteAccess(this)) {
144 SettingsActivity.launch(this, menuTag, "");
145 } else {
146 PermissionsHandler.checkWritePermission(this);
147 }
148 }
149
150 @Override
151 public void launchFileListActivity(int request) {
152 if (PermissionsHandler.hasWriteAccess(this)) {
153 switch (request) {
154 case MainPresenter.REQUEST_ADD_DIRECTORY:
155 FileBrowserHelper.openDirectoryPicker(this,
156 MainPresenter.REQUEST_ADD_DIRECTORY,
157 R.string.select_game_folder,
158 Arrays.asList("xci", "nsp", "cci", "3ds",
159 "cxi", "app", "3dsx", "cia",
160 "rar", "zip", "7z", "torrent",
161 "tar", "gz", "nro"));
162 break;
163 case MainPresenter.REQUEST_INSTALL_CIA:
164 FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA,
165 R.string.install_cia_title,
166 Collections.singletonList("cia"), true);
167 break;
168 }
169 } else {
170 PermissionsHandler.checkWritePermission(this);
171 }
172 }
173
174 /**
175 * @param requestCode An int describing whether the Activity that is returning did so successfully.
176 * @param resultCode An int describing what Activity is giving us this callback.
177 * @param result The information the returning Activity is providing us.
178 */
179 @Override
180 protected void onActivityResult(int requestCode, int resultCode, Intent result) {
181 super.onActivityResult(requestCode, resultCode, result);
182 switch (requestCode) {
183 case MainPresenter.REQUEST_ADD_DIRECTORY:
184 // If the user picked a file, as opposed to just backing out.
185 if (resultCode == MainActivity.RESULT_OK) {
186 // When a new directory is picked, we currently will reset the existing games
187 // database. This effectively means that only one game directory is supported.
188 // TODO(bunnei): Consider fixing this in the future, or removing code for this.
189 getContentResolver().insert(GameProvider.URI_RESET, null);
190 // Add the new directory
191 mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
192 }
193 break;
194 case MainPresenter.REQUEST_INSTALL_CIA:
195 // If the user picked a file, as opposed to just backing out.
196 if (resultCode == MainActivity.RESULT_OK) {
197 mPresenter.refeshGameList();
198 }
199 break;
200 }
201 }
202
203 @Override
204 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
205 switch (requestCode) {
206 case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
207 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
208 DirectoryInitialization.start(this);
209
210 mPlatformGamesFragment = new PlatformGamesFragment();
211 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
212 .commit();
213
214 // Immediately prompt user to select a game directory on first boot
215 if (mPresenter != null) {
216 mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
217 }
218 } else {
219 Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
220 .show();
221 }
222 break;
223 default:
224 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
225 break;
226 }
227 }
228
229 /**
230 * Called by the framework whenever any actionbar/toolbar icon is clicked.
231 *
232 * @param item The icon that was clicked on.
233 * @return True if the event was handled, false to bubble it up to the OS.
234 */
235 @Override
236 public boolean onOptionsItemSelected(MenuItem item) {
237 return mPresenter.handleOptionSelection(item.getItemId());
238 }
239
240 private void refreshFragment() {
241 if (mPlatformGamesFragment != null) {
242 mPlatformGamesFragment.refresh();
243 }
244 }
245
246 @Override
247 protected void onDestroy() {
248 EmulationActivity.tryDismissRunningNotification(this);
249 super.onDestroy();
250 }
251
252 /**
253 * @return true if Premium subscription is currently active
254 */
255 public static boolean isPremiumActive() {
256 return mBillingManager.isPremiumActive();
257 }
258
259 /**
260 * Invokes the billing flow for Premium
261 *
262 * @param callback Optional callback, called once, on completion of billing
263 */
264 public static void invokePremiumBilling(Runnable callback) {
265 mBillingManager.invokePremiumBilling(callback);
266 }
267}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java
new file mode 100644
index 000000000..4e9994c2a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java
@@ -0,0 +1,82 @@
1package org.citra.citra_emu.ui.main;
2
3import android.os.SystemClock;
4
5import org.citra.citra_emu.BuildConfig;
6import org.citra.citra_emu.CitraApplication;
7import org.citra.citra_emu.R;
8import org.citra.citra_emu.features.settings.model.Settings;
9import org.citra.citra_emu.features.settings.utils.SettingsFile;
10import org.citra.citra_emu.model.GameDatabase;
11import org.citra.citra_emu.utils.AddDirectoryHelper;
12
13public final class MainPresenter {
14 public static final int REQUEST_ADD_DIRECTORY = 1;
15 public static final int REQUEST_INSTALL_CIA = 2;
16
17 private final MainView mView;
18 private String mDirToAdd;
19 private long mLastClickTime = 0;
20
21 public MainPresenter(MainView view) {
22 mView = view;
23 }
24
25 public void onCreate() {
26 String versionName = BuildConfig.VERSION_NAME;
27 mView.setVersionString(versionName);
28 refeshGameList();
29 }
30
31 public void launchFileListActivity(int request) {
32 if (mView != null) {
33 mView.launchFileListActivity(request);
34 }
35 }
36
37 public boolean handleOptionSelection(int itemId) {
38 // Double-click prevention, using threshold of 500 ms
39 if (SystemClock.elapsedRealtime() - mLastClickTime < 500) {
40 return false;
41 }
42 mLastClickTime = SystemClock.elapsedRealtime();
43
44 switch (itemId) {
45 case R.id.menu_settings_core:
46 mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
47 return true;
48
49 case R.id.button_add_directory:
50 launchFileListActivity(REQUEST_ADD_DIRECTORY);
51 return true;
52
53 case R.id.button_install_cia:
54 launchFileListActivity(REQUEST_INSTALL_CIA);
55 return true;
56
57 case R.id.button_premium:
58 mView.launchSettingsActivity(Settings.SECTION_PREMIUM);
59 return true;
60 }
61
62 return false;
63 }
64
65 public void addDirIfNeeded(AddDirectoryHelper helper) {
66 if (mDirToAdd != null) {
67 helper.addDirectory(mDirToAdd, mView::refresh);
68
69 mDirToAdd = null;
70 }
71 }
72
73 public void onDirectorySelected(String dir) {
74 mDirToAdd = dir;
75 }
76
77 public void refeshGameList() {
78 GameDatabase databaseHelper = CitraApplication.databaseHelper;
79 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
80 mView.refresh();
81 }
82}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java
new file mode 100644
index 000000000..de7c04875
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java
@@ -0,0 +1,25 @@
1package org.citra.citra_emu.ui.main;
2
3/**
4 * Abstraction for the screen that shows on application launch.
5 * Implementations will differ primarily to target touch-screen
6 * or non-touch screen devices.
7 */
8public interface MainView {
9 /**
10 * Pass the view the native library's version string. Displaying
11 * it is optional.
12 *
13 * @param version A string pulled from native code.
14 */
15 void setVersionString(String version);
16
17 /**
18 * Tell the view to refresh its contents.
19 */
20 void refresh();
21
22 void launchSettingsActivity(String menuTag);
23
24 void launchFileListActivity(int request);
25}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java
new file mode 100644
index 000000000..9fc30796f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java
@@ -0,0 +1,86 @@
1package org.citra.citra_emu.ui.platform;
2
3import android.database.Cursor;
4import android.os.Bundle;
5import android.view.LayoutInflater;
6import android.view.View;
7import android.view.ViewGroup;
8import android.widget.TextView;
9
10import androidx.core.content.ContextCompat;
11import androidx.fragment.app.Fragment;
12import androidx.recyclerview.widget.GridLayoutManager;
13import androidx.recyclerview.widget.RecyclerView;
14import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
15
16import org.citra.citra_emu.CitraApplication;
17import org.citra.citra_emu.R;
18import org.citra.citra_emu.adapters.GameAdapter;
19import org.citra.citra_emu.model.GameDatabase;
20
21public final class PlatformGamesFragment extends Fragment implements PlatformGamesView {
22 private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this);
23
24 private GameAdapter mAdapter;
25 private RecyclerView mRecyclerView;
26 private TextView mTextView;
27
28 @Override
29 public void onCreate(Bundle savedInstanceState) {
30 super.onCreate(savedInstanceState);
31 }
32
33 @Override
34 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
35 View rootView = inflater.inflate(R.layout.fragment_grid, container, false);
36
37 findViews(rootView);
38
39 mPresenter.onCreateView();
40
41 return rootView;
42 }
43
44 @Override
45 public void onViewCreated(View view, Bundle savedInstanceState) {
46 int columns = getResources().getInteger(R.integer.game_grid_columns);
47 RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
48 mAdapter = new GameAdapter();
49
50 mRecyclerView.setLayoutManager(layoutManager);
51 mRecyclerView.setAdapter(mAdapter);
52 mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1));
53
54 // Add swipe down to refresh gesture
55 final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games);
56 pullToRefresh.setOnRefreshListener(() -> {
57 GameDatabase databaseHelper = CitraApplication.databaseHelper;
58 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
59 refresh();
60 pullToRefresh.setRefreshing(false);
61 });
62 }
63
64 @Override
65 public void refresh() {
66 mPresenter.refresh();
67 updateTextView();
68 }
69
70 @Override
71 public void showGames(Cursor games) {
72 if (mAdapter != null) {
73 mAdapter.swapCursor(games);
74 }
75 updateTextView();
76 }
77
78 private void updateTextView() {
79 mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
80 }
81
82 private void findViews(View root) {
83 mRecyclerView = root.findViewById(R.id.grid_games);
84 mTextView = root.findViewById(R.id.gamelist_empty_text);
85 }
86}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java
new file mode 100644
index 000000000..9d8040e1b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java
@@ -0,0 +1,42 @@
1package org.citra.citra_emu.ui.platform;
2
3
4import org.citra.citra_emu.CitraApplication;
5import org.citra.citra_emu.model.GameDatabase;
6import org.citra.citra_emu.utils.Log;
7
8import rx.android.schedulers.AndroidSchedulers;
9import rx.schedulers.Schedulers;
10
11public final class PlatformGamesPresenter {
12 private final PlatformGamesView mView;
13
14 public PlatformGamesPresenter(PlatformGamesView view) {
15 mView = view;
16 }
17
18 public void onCreateView() {
19 loadGames();
20 }
21
22 public void refresh() {
23 Log.debug("[PlatformGamesPresenter] : Refreshing...");
24 loadGames();
25 }
26
27 private void loadGames() {
28 Log.debug("[PlatformGamesPresenter] : Loading games...");
29
30 GameDatabase databaseHelper = CitraApplication.databaseHelper;
31
32 databaseHelper.getGames()
33 .subscribeOn(Schedulers.io())
34 .observeOn(AndroidSchedulers.mainThread())
35 .subscribe(games ->
36 {
37 Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...");
38
39 mView.showGames(games);
40 });
41 }
42}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java
new file mode 100644
index 000000000..4332121eb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java
@@ -0,0 +1,21 @@
1package org.citra.citra_emu.ui.platform;
2
3import android.database.Cursor;
4
5/**
6 * Abstraction for a screen representing a single platform's games.
7 */
8public interface PlatformGamesView {
9 /**
10 * Tell the view to refresh its contents.
11 */
12 void refresh();
13
14 /**
15 * To be called when an asynchronous database read completes. Passes the
16 * result, in this case a {@link Cursor}, to the view.
17 *
18 * @param games A Cursor containing the games read from the database.
19 */
20 void showGames(Cursor games);
21}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java
new file mode 100644
index 000000000..886846ec5
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java
@@ -0,0 +1,5 @@
1package org.citra.citra_emu.utils;
2
3public interface Action1<T> {
4 void call(T t);
5}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java
new file mode 100644
index 000000000..7578c353f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java
@@ -0,0 +1,38 @@
1package org.citra.citra_emu.utils;
2
3import android.content.AsyncQueryHandler;
4import android.content.ContentValues;
5import android.content.Context;
6import android.net.Uri;
7
8import org.citra.citra_emu.model.GameDatabase;
9import org.citra.citra_emu.model.GameProvider;
10
11public class AddDirectoryHelper {
12 private Context mContext;
13
14 public AddDirectoryHelper(Context context) {
15 this.mContext = context;
16 }
17
18 public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) {
19 AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
20 @Override
21 protected void onInsertComplete(int token, Object cookie, Uri uri) {
22 addDirectoryListener.onDirectoryAdded();
23 }
24 };
25
26 ContentValues file = new ContentValues();
27 file.put(GameDatabase.KEY_FOLDER_PATH, dir);
28
29 handler.startInsert(0, // We don't need to identify this call to the handler
30 null, // We don't need to pass additional data to the handler
31 GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
32 file);
33 }
34
35 public interface AddDirectoryListener {
36 void onDirectoryAdded();
37 }
38}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java
new file mode 100644
index 000000000..dfbab1780
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java
@@ -0,0 +1,22 @@
1package org.citra.citra_emu.utils;
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class BiMap<K, V> {
7 private Map<K, V> forward = new HashMap<K, V>();
8 private Map<V, K> backward = new HashMap<V, K>();
9
10 public synchronized void add(K key, V value) {
11 forward.put(key, value);
12 backward.put(value, key);
13 }
14
15 public synchronized V getForward(K key) {
16 return forward.get(key);
17 }
18
19 public synchronized K getBackward(V key) {
20 return backward.get(key);
21 }
22}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java
new file mode 100644
index 000000000..5dc54c235
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java
@@ -0,0 +1,215 @@
1package org.citra.citra_emu.utils;
2
3import android.app.Activity;
4import android.content.SharedPreferences;
5import android.preference.PreferenceManager;
6import android.widget.Toast;
7
8import com.android.billingclient.api.AcknowledgePurchaseParams;
9import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
10import com.android.billingclient.api.BillingClient;
11import com.android.billingclient.api.BillingClientStateListener;
12import com.android.billingclient.api.BillingFlowParams;
13import com.android.billingclient.api.BillingResult;
14import com.android.billingclient.api.Purchase;
15import com.android.billingclient.api.Purchase.PurchasesResult;
16import com.android.billingclient.api.PurchasesUpdatedListener;
17import com.android.billingclient.api.SkuDetails;
18import com.android.billingclient.api.SkuDetailsParams;
19
20import org.citra.citra_emu.CitraApplication;
21import org.citra.citra_emu.R;
22import org.citra.citra_emu.features.settings.utils.SettingsFile;
23import org.citra.citra_emu.ui.main.MainActivity;
24
25import java.util.ArrayList;
26import java.util.List;
27
28public class BillingManager implements PurchasesUpdatedListener {
29 private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium";
30
31 private final Activity mActivity;
32 private BillingClient mBillingClient;
33 private SkuDetails mSkuPremium;
34 private boolean mIsPremiumActive = false;
35 private boolean mIsServiceConnected = false;
36 private Runnable mUpdateBillingCallback;
37
38 private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
39
40 public BillingManager(Activity activity) {
41 mActivity = activity;
42 mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();
43 querySkuDetails();
44 }
45
46 static public boolean isPremiumCached() {
47 return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false);
48 }
49
50 /**
51 * @return true if Premium subscription is currently active
52 */
53 public boolean isPremiumActive() {
54 return mIsPremiumActive;
55 }
56
57 /**
58 * Invokes the billing flow for Premium
59 *
60 * @param callback Optional callback, called once, on completion of billing
61 */
62 public void invokePremiumBilling(Runnable callback) {
63 if (mSkuPremium == null) {
64 return;
65 }
66
67 // Optional callback to refresh the UI for the caller when billing completes
68 mUpdateBillingCallback = callback;
69
70 // Invoke the billing flow
71 BillingFlowParams flowParams = BillingFlowParams.newBuilder()
72 .setSkuDetails(mSkuPremium)
73 .build();
74 mBillingClient.launchBillingFlow(mActivity, flowParams);
75 }
76
77 private void updatePremiumState(boolean isPremiumActive) {
78 mIsPremiumActive = isPremiumActive;
79
80 // Cache state for synchronous UI
81 SharedPreferences.Editor editor = mPreferences.edit();
82 editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive);
83 editor.apply();
84
85 // No need to show button in action bar if Premium is active
86 MainActivity.setPremiumButtonVisible(!isPremiumActive);
87 }
88
89 @Override
90 public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) {
91 if (purchaseList == null || purchaseList.isEmpty()) {
92 // Premium is not active, or billing is unavailable
93 updatePremiumState(false);
94 return;
95 }
96
97 Purchase premiumPurchase = null;
98 for (Purchase purchase : purchaseList) {
99 if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) {
100 premiumPurchase = purchase;
101 }
102 }
103
104 if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
105 // Premium has been purchased
106 updatePremiumState(true);
107
108 // Acknowledge the purchase if it hasn't already been acknowledged.
109 if (!premiumPurchase.isAcknowledged()) {
110 AcknowledgePurchaseParams acknowledgePurchaseParams =
111 AcknowledgePurchaseParams.newBuilder()
112 .setPurchaseToken(premiumPurchase.getPurchaseToken())
113 .build();
114
115 AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> {
116 Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show();
117 };
118 mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
119 }
120
121 if (mUpdateBillingCallback != null) {
122 try {
123 mUpdateBillingCallback.run();
124 } catch (Exception e) {
125 e.printStackTrace();
126 }
127 mUpdateBillingCallback = null;
128 }
129 }
130 }
131
132 private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) {
133 if (skuDetailsList == null) {
134 // This can happen when no user is signed in
135 return;
136 }
137
138 if (skuDetailsList.isEmpty()) {
139 return;
140 }
141
142 mSkuPremium = skuDetailsList.get(0);
143
144 queryPurchases();
145 }
146
147 private void querySkuDetails() {
148 Runnable queryToExecute = new Runnable() {
149 @Override
150 public void run() {
151 SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
152 List<String> skuList = new ArrayList<>();
153
154 skuList.add(BILLING_SKU_PREMIUM);
155 params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
156
157 mBillingClient.querySkuDetailsAsync(params.build(),
158 (billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList));
159 }
160 };
161
162 executeServiceRequest(queryToExecute);
163 }
164
165 private void onQueryPurchasesFinished(PurchasesResult result) {
166 // Have we been disposed of in the meantime? If so, or bad result code, then quit
167 if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
168 updatePremiumState(false);
169 return;
170 }
171 // Update the UI and purchases inventory with new list of purchases
172 onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
173 }
174
175 private void queryPurchases() {
176 Runnable queryToExecute = new Runnable() {
177 @Override
178 public void run() {
179 final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
180 onQueryPurchasesFinished(purchasesResult);
181 }
182 };
183
184 executeServiceRequest(queryToExecute);
185 }
186
187 private void startServiceConnection(final Runnable executeOnFinish) {
188 mBillingClient.startConnection(new BillingClientStateListener() {
189 @Override
190 public void onBillingSetupFinished(BillingResult billingResult) {
191 if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
192 mIsServiceConnected = true;
193 }
194
195 if (executeOnFinish != null) {
196 executeOnFinish.run();
197 }
198 }
199
200 @Override
201 public void onBillingServiceDisconnected() {
202 mIsServiceConnected = false;
203 }
204 });
205 }
206
207 private void executeServiceRequest(Runnable runnable) {
208 if (mIsServiceConnected) {
209 runnable.run();
210 } else {
211 // If billing service was disconnected, we try to reconnect 1 time.
212 startServiceConnection(runnable);
213 }
214 }
215}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java
new file mode 100644
index 000000000..f801a05f0
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java
@@ -0,0 +1,66 @@
1package org.citra.citra_emu.utils;
2
3import android.view.InputDevice;
4import android.view.KeyEvent;
5import android.view.MotionEvent;
6
7/**
8 * Some controllers have incorrect mappings. This class has special-case fixes for them.
9 */
10public class ControllerMappingHelper {
11 /**
12 * Some controllers report extra button presses that can be ignored.
13 */
14 public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) {
15 if (isDualShock4(inputDevice)) {
16 // The two analog triggers generate analog motion events as well as a keycode.
17 // We always prefer to use the analog values, so throw away the button press
18 return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2;
19 }
20 return false;
21 }
22
23 /**
24 * Scale an axis to be zero-centered with a proper range.
25 */
26 public float scaleAxis(InputDevice inputDevice, int axis, float value) {
27 if (isDualShock4(inputDevice)) {
28 // Android doesn't have correct mappings for this controller's triggers. It reports them
29 // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
30 // Scale them to properly zero-centered with a range of [0.0, 1.0].
31 if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
32 return (value + 1) / 2.0f;
33 }
34 } else if (isXboxOneWireless(inputDevice)) {
35 // Same as the DualShock 4, the mappings are missing.
36 if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
37 return (value + 1) / 2.0f;
38 }
39 if (axis == MotionEvent.AXIS_GENERIC_1) {
40 // This axis is stuck at ~.5. Ignore it.
41 return 0.0f;
42 }
43 } else if (isMogaPro2Hid(inputDevice)) {
44 // This controller has a broken axis that reports a constant value. Ignore it.
45 if (axis == MotionEvent.AXIS_GENERIC_1) {
46 return 0.0f;
47 }
48 }
49 return value;
50 }
51
52 private boolean isDualShock4(InputDevice inputDevice) {
53 // Sony DualShock 4 controller
54 return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc;
55 }
56
57 private boolean isXboxOneWireless(InputDevice inputDevice) {
58 // Microsoft Xbox One controller
59 return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0;
60 }
61
62 private boolean isMogaPro2Hid(InputDevice inputDevice) {
63 // Moga Pro 2 HID
64 return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271;
65 }
66}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java
new file mode 100644
index 000000000..58e552f5e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java
@@ -0,0 +1,186 @@
1/**
2 * Copyright 2014 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.utils;
8
9import android.content.Context;
10import android.content.Intent;
11import android.content.SharedPreferences;
12import android.os.Environment;
13import android.preference.PreferenceManager;
14
15import androidx.localbroadcastmanager.content.LocalBroadcastManager;
16
17import org.citra.citra_emu.NativeLibrary;
18
19import java.io.File;
20import java.io.FileOutputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24import java.util.concurrent.atomic.AtomicBoolean;
25
26/**
27 * A service that spawns its own thread in order to copy several binary and shader files
28 * from the Citra APK to the external file system.
29 */
30public final class DirectoryInitialization {
31 public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST";
32
33 public static final String EXTRA_STATE = "directoryState";
34 private static volatile DirectoryInitializationState directoryState = null;
35 private static String userPath;
36 private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false);
37
38 public static void start(Context context) {
39 // Can take a few seconds to run, so don't block UI thread.
40 //noinspection TrivialFunctionalExpressionUsage
41 ((Runnable) () -> init(context)).run();
42 }
43
44 private static void init(Context context) {
45 if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true))
46 return;
47
48 if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
49 if (PermissionsHandler.hasWriteAccess(context)) {
50 if (setCitraUserDirectory()) {
51 initializeInternalStorage(context);
52 NativeLibrary.CreateConfigFile();
53 directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
54 } else {
55 directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
56 }
57 } else {
58 directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
59 }
60 }
61
62 isCitraDirectoryInitializationRunning.set(false);
63 sendBroadcastState(directoryState, context);
64 }
65
66 private static void deleteDirectoryRecursively(File file) {
67 if (file.isDirectory()) {
68 for (File child : file.listFiles())
69 deleteDirectoryRecursively(child);
70 }
71 file.delete();
72 }
73
74 public static boolean areCitraDirectoriesReady() {
75 return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
76 }
77
78 public static String getUserDirectory() {
79 if (directoryState == null) {
80 throw new IllegalStateException("DirectoryInitialization has to run at least once!");
81 } else if (isCitraDirectoryInitializationRunning.get()) {
82 throw new IllegalStateException(
83 "DirectoryInitialization has to finish running first!");
84 }
85 return userPath;
86 }
87
88 private static native void SetSysDirectory(String path);
89
90 private static boolean setCitraUserDirectory() {
91 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
92 File externalPath = Environment.getExternalStorageDirectory();
93 if (externalPath != null) {
94 userPath = externalPath.getAbsolutePath() + "/citra-emu";
95 Log.debug("[DirectoryInitialization] User Dir: " + userPath);
96 // NativeLibrary.SetUserDirectory(userPath);
97 return true;
98 }
99
100 }
101
102 return false;
103 }
104
105 private static void initializeInternalStorage(Context context) {
106 File sysDirectory = new File(context.getFilesDir(), "Sys");
107
108 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
109 String revision = NativeLibrary.GetGitRevision();
110 if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
111 // There is no extracted Sys directory, or there is a Sys directory from another
112 // version of Citra that might contain outdated files. Let's (re-)extract Sys.
113 deleteDirectoryRecursively(sysDirectory);
114 copyAssetFolder("Sys", sysDirectory, true, context);
115
116 SharedPreferences.Editor editor = preferences.edit();
117 editor.putString("sysDirectoryVersion", revision);
118 editor.apply();
119 }
120
121 // Let the native code know where the Sys directory is.
122 SetSysDirectory(sysDirectory.getPath());
123 }
124
125 private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
126 Intent localIntent =
127 new Intent(BROADCAST_ACTION)
128 .putExtra(EXTRA_STATE, state);
129 LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
130 }
131
132 private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
133 Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
134
135 try {
136 if (!output.exists() || overwrite) {
137 InputStream in = context.getAssets().open(asset);
138 OutputStream out = new FileOutputStream(output);
139 copyFile(in, out);
140 in.close();
141 out.close();
142 }
143 } catch (IOException e) {
144 Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
145 e.getMessage());
146 }
147 }
148
149 private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
150 Context context) {
151 Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
152 outputFolder);
153
154 try {
155 boolean createdFolder = false;
156 for (String file : context.getAssets().list(assetFolder)) {
157 if (!createdFolder) {
158 outputFolder.mkdir();
159 createdFolder = true;
160 }
161 copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
162 overwrite, context);
163 copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
164 context);
165 }
166 } catch (IOException e) {
167 Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
168 e.getMessage());
169 }
170 }
171
172 private static void copyFile(InputStream in, OutputStream out) throws IOException {
173 byte[] buffer = new byte[1024];
174 int read;
175
176 while ((read = in.read(buffer)) != -1) {
177 out.write(buffer, 0, read);
178 }
179 }
180
181 public enum DirectoryInitializationState {
182 CITRA_DIRECTORIES_INITIALIZED,
183 EXTERNAL_STORAGE_PERMISSION_NEEDED,
184 CANT_FIND_EXTERNAL_STORAGE
185 }
186}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java
new file mode 100644
index 000000000..5d1e951ca
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java
@@ -0,0 +1,22 @@
1package org.citra.citra_emu.utils;
2
3import android.content.BroadcastReceiver;
4import android.content.Context;
5import android.content.Intent;
6
7import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
8
9public class DirectoryStateReceiver extends BroadcastReceiver {
10 Action1<DirectoryInitializationState> callback;
11
12 public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) {
13 this.callback = callback;
14 }
15
16 @Override
17 public void onReceive(Context context, Intent intent) {
18 DirectoryInitializationState state = (DirectoryInitializationState) intent
19 .getSerializableExtra(DirectoryInitialization.EXTRA_STATE);
20 callback.call(state);
21 }
22}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java
new file mode 100644
index 000000000..9664f8464
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java
@@ -0,0 +1,78 @@
1package org.citra.citra_emu.utils;
2
3import android.content.SharedPreferences;
4import android.preference.PreferenceManager;
5
6import org.citra.citra_emu.CitraApplication;
7
8public class EmulationMenuSettings {
9 private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
10
11 // These must match what is defined in src/core/settings.h
12 public static final int LayoutOption_Default = 0;
13 public static final int LayoutOption_SingleScreen = 1;
14 public static final int LayoutOption_LargeScreen = 2;
15 public static final int LayoutOption_SideScreen = 3;
16 public static final int LayoutOption_MobilePortrait = 4;
17 public static final int LayoutOption_MobileLandscape = 5;
18
19 public static boolean getJoystickRelCenter() {
20 return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true);
21 }
22
23 public static void setJoystickRelCenter(boolean value) {
24 final SharedPreferences.Editor editor = mPreferences.edit();
25 editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value);
26 editor.apply();
27 }
28
29 public static boolean getDpadSlideEnable() {
30 return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true);
31 }
32
33 public static void setDpadSlideEnable(boolean value) {
34 final SharedPreferences.Editor editor = mPreferences.edit();
35 editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value);
36 editor.apply();
37 }
38
39 public static int getLandscapeScreenLayout() {
40 return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape);
41 }
42
43 public static void setLandscapeScreenLayout(int value) {
44 final SharedPreferences.Editor editor = mPreferences.edit();
45 editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value);
46 editor.apply();
47 }
48
49 public static boolean getShowFps() {
50 return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false);
51 }
52
53 public static void setShowFps(boolean value) {
54 final SharedPreferences.Editor editor = mPreferences.edit();
55 editor.putBoolean("EmulationMenuSettings_ShowFps", value);
56 editor.apply();
57 }
58
59 public static boolean getSwapScreens() {
60 return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false);
61 }
62
63 public static void setSwapScreens(boolean value) {
64 final SharedPreferences.Editor editor = mPreferences.edit();
65 editor.putBoolean("EmulationMenuSettings_SwapScreens", value);
66 editor.apply();
67 }
68
69 public static boolean getShowOverlay() {
70 return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true);
71 }
72
73 public static void setShowOverlay(boolean value) {
74 final SharedPreferences.Editor editor = mPreferences.edit();
75 editor.putBoolean("EmulationMenuSettings_ShowOverylay", value);
76 editor.apply();
77 }
78}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java
new file mode 100644
index 000000000..baf691f5c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java
@@ -0,0 +1,73 @@
1package org.citra.citra_emu.utils;
2
3import android.content.Intent;
4import android.net.Uri;
5import android.os.Environment;
6
7import androidx.annotation.Nullable;
8import androidx.fragment.app.FragmentActivity;
9
10import com.nononsenseapps.filepicker.FilePickerActivity;
11import com.nononsenseapps.filepicker.Utils;
12
13import org.citra.citra_emu.activities.CustomFilePickerActivity;
14
15import java.io.File;
16import java.util.List;
17
18public final class FileBrowserHelper {
19 public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
20 Intent i = new Intent(activity, CustomFilePickerActivity.class);
21
22 i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
23 i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
24 i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
25 i.putExtra(FilePickerActivity.EXTRA_START_PATH,
26 Environment.getExternalStorageDirectory().getPath());
27 i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
28 i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
29
30 activity.startActivityForResult(i, requestCode);
31 }
32
33 public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
34 List<String> extensions, boolean allowMultiple) {
35 Intent i = new Intent(activity, CustomFilePickerActivity.class);
36
37 i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
38 i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
39 i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
40 i.putExtra(FilePickerActivity.EXTRA_START_PATH,
41 Environment.getExternalStorageDirectory().getPath());
42 i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
43 i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
44
45 activity.startActivityForResult(i, requestCode);
46 }
47
48 @Nullable
49 public static String getSelectedDirectory(Intent result) {
50 // Use the provided utility method to parse the result
51 List<Uri> files = Utils.getSelectedFilesFromResult(result);
52 if (!files.isEmpty()) {
53 File file = Utils.getFileForUri(files.get(0));
54 return file.getAbsolutePath();
55 }
56
57 return null;
58 }
59
60 @Nullable
61 public static String[] getSelectedFiles(Intent result) {
62 // Use the provided utility method to parse the result
63 List<Uri> files = Utils.getSelectedFilesFromResult(result);
64 if (!files.isEmpty()) {
65 String[] paths = new String[files.size()];
66 for (int i = 0; i < files.size(); i++)
67 paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
68 return paths;
69 }
70
71 return null;
72 }
73}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java
new file mode 100644
index 000000000..f9025171b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java
@@ -0,0 +1,37 @@
1package org.citra.citra_emu.utils;
2
3import java.io.File;
4import java.io.FileInputStream;
5import java.io.IOException;
6import java.io.InputStream;
7
8public class FileUtil {
9 public static byte[] getBytesFromFile(File file) throws IOException {
10 final long length = file.length();
11
12 // You cannot create an array using a long type.
13 if (length > Integer.MAX_VALUE) {
14 // File is too large
15 throw new IOException("File is too large!");
16 }
17
18 byte[] bytes = new byte[(int) length];
19
20 int offset = 0;
21 int numRead;
22
23 try (InputStream is = new FileInputStream(file)) {
24 while (offset < bytes.length
25 && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
26 offset += numRead;
27 }
28 }
29
30 // Ensure all the bytes have been read in
31 if (offset < bytes.length) {
32 throw new IOException("Could not completely read file " + file.getName());
33 }
34
35 return bytes;
36 }
37}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java
new file mode 100644
index 000000000..31c415779
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java
@@ -0,0 +1,63 @@
1/**
2 * Copyright 2014 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.citra.citra_emu.utils;
8
9import android.app.PendingIntent;
10import android.app.Service;
11import android.content.Intent;
12import android.os.IBinder;
13
14import androidx.core.app.NotificationCompat;
15import androidx.core.app.NotificationManagerCompat;
16
17import org.citra.citra_emu.R;
18import org.citra.citra_emu.activities.EmulationActivity;
19
20/**
21 * A service that shows a permanent notification in the background to avoid the app getting
22 * cleared from memory by the system.
23 */
24public class ForegroundService extends Service {
25 private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
26
27 private void showRunningNotification() {
28 // Intent is used to resume emulation if the notification is clicked
29 PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
30 new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE);
31
32 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
33 .setSmallIcon(R.drawable.ic_stat_notification_logo)
34 .setContentTitle(getString(R.string.app_name))
35 .setContentText(getString(R.string.app_notification_running))
36 .setPriority(NotificationCompat.PRIORITY_LOW)
37 .setOngoing(true)
38 .setVibrate(null)
39 .setSound(null)
40 .setContentIntent(contentIntent);
41 startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build());
42 }
43
44 @Override
45 public IBinder onBind(Intent intent) {
46 return null;
47 }
48
49 @Override
50 public void onCreate() {
51 showRunningNotification();
52 }
53
54 @Override
55 public int onStartCommand(Intent intent, int flags, int startId) {
56 return START_STICKY;
57 }
58
59 @Override
60 public void onDestroy() {
61 NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION);
62 }
63}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java
new file mode 100644
index 000000000..b790c2480
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java
@@ -0,0 +1,27 @@
1package org.citra.citra_emu.utils;
2
3import android.graphics.Bitmap;
4
5import com.squareup.picasso.Picasso;
6import com.squareup.picasso.Request;
7import com.squareup.picasso.RequestHandler;
8
9import org.citra.citra_emu.NativeLibrary;
10
11import java.nio.IntBuffer;
12
13public class GameIconRequestHandler extends RequestHandler {
14 @Override
15 public boolean canHandleRequest(Request data) {
16 return "iso".equals(data.uri.getScheme());
17 }
18
19 @Override
20 public Result load(Request request, int networkPolicy) {
21 String url = request.uri.getHost() + request.uri.getPath();
22 int[] vector = NativeLibrary.GetIcon(url);
23 Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
24 bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
25 return new Result(bitmap, Picasso.LoadedFrom.DISK);
26 }
27}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java
new file mode 100644
index 000000000..070d01eb1
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java
@@ -0,0 +1,39 @@
1package org.citra.citra_emu.utils;
2
3import org.citra.citra_emu.BuildConfig;
4
5/**
6 * Contains methods that call through to {@link android.util.Log}, but
7 * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
8 * levels in release builds.
9 */
10public final class Log {
11 private static final String TAG = "Citra Frontend";
12
13 private Log() {
14 }
15
16 public static void verbose(String message) {
17 if (BuildConfig.DEBUG) {
18 android.util.Log.v(TAG, message);
19 }
20 }
21
22 public static void debug(String message) {
23 if (BuildConfig.DEBUG) {
24 android.util.Log.d(TAG, message);
25 }
26 }
27
28 public static void info(String message) {
29 android.util.Log.i(TAG, message);
30 }
31
32 public static void warning(String message) {
33 android.util.Log.w(TAG, message);
34 }
35
36 public static void error(String message) {
37 android.util.Log.e(TAG, message);
38 }
39}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java
new file mode 100644
index 000000000..a29e23e8d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java
@@ -0,0 +1,35 @@
1package org.citra.citra_emu.utils;
2
3import android.annotation.TargetApi;
4import android.content.Context;
5import android.content.pm.PackageManager;
6import android.os.Build;
7
8import androidx.core.content.ContextCompat;
9import androidx.fragment.app.FragmentActivity;
10
11import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
12
13public class PermissionsHandler {
14 public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
15
16 // We use permissions acceptance as an indicator if this is a first boot for the user.
17 public static boolean isFirstBoot(final FragmentActivity activity) {
18 return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
19 }
20
21 @TargetApi(Build.VERSION_CODES.M)
22 public static boolean checkWritePermission(final FragmentActivity activity) {
23 if (isFirstBoot(activity)) {
24 activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
25 REQUEST_CODE_WRITE_PERMISSION);
26 return false;
27 }
28
29 return true;
30 }
31
32 public static boolean hasWriteAccess(Context context) {
33 return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
34 }
35}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java
new file mode 100644
index 000000000..892b46387
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java
@@ -0,0 +1,45 @@
1package org.citra.citra_emu.utils;
2
3import android.graphics.Bitmap;
4import android.graphics.BitmapShader;
5import android.graphics.Canvas;
6import android.graphics.Paint;
7import android.graphics.Rect;
8import android.graphics.RectF;
9
10import com.squareup.picasso.Transformation;
11
12public class PicassoRoundedCornersTransformation implements Transformation {
13 @Override
14 public Bitmap transform(Bitmap icon) {
15 final int width = icon.getWidth();
16 final int height = icon.getHeight();
17 final Rect rect = new Rect(0, 0, width, height);
18 final int size = Math.min(width, height);
19 final int x = (width - size) / 2;
20 final int y = (height - size) / 2;
21
22 Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size);
23 if (squaredBitmap != icon) {
24 icon.recycle();
25 }
26
27 Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
28 Canvas canvas = new Canvas(output);
29 BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
30 Paint paint = new Paint();
31 paint.setAntiAlias(true);
32 paint.setShader(shader);
33
34 canvas.drawRoundRect(new RectF(rect), 10, 10, paint);
35
36 squaredBitmap.recycle();
37
38 return output;
39 }
40
41 @Override
42 public String key() {
43 return "circle";
44 }
45} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java
new file mode 100644
index 000000000..c99726685
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java
@@ -0,0 +1,57 @@
1package org.citra.citra_emu.utils;
2
3import android.graphics.Bitmap;
4import android.net.Uri;
5import android.widget.ImageView;
6
7import com.squareup.picasso.Picasso;
8
9import org.citra.citra_emu.CitraApplication;
10import org.citra.citra_emu.R;
11
12import java.io.IOException;
13
14import androidx.annotation.Nullable;
15
16public class PicassoUtils {
17 private static boolean mPicassoInitialized = false;
18
19 public static void init() {
20 if (mPicassoInitialized) {
21 return;
22 }
23 Picasso picassoInstance = new Picasso.Builder(CitraApplication.getAppContext())
24 .addRequestHandler(new GameIconRequestHandler())
25 .build();
26
27 Picasso.setSingletonInstance(picassoInstance);
28 mPicassoInitialized = true;
29 }
30
31 public static void loadGameIcon(ImageView imageView, String gamePath) {
32 Picasso
33 .get()
34 .load(Uri.parse("iso:/" + gamePath))
35 .fit()
36 .centerInside()
37 .config(Bitmap.Config.RGB_565)
38 .error(R.drawable.no_icon)
39 .transform(new PicassoRoundedCornersTransformation())
40 .into(imageView);
41 }
42
43 // Blocking call. Load image from file and crop/resize it to fit in width x height.
44 @Nullable
45 public static Bitmap LoadBitmapFromFile(String uri, int width, int height) {
46 try {
47 return Picasso.get()
48 .load(Uri.parse(uri))
49 .config(Bitmap.Config.ARGB_8888)
50 .centerCrop()
51 .resize(width, height)
52 .get();
53 } catch (IOException e) {
54 return null;
55 }
56 }
57}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java
new file mode 100644
index 000000000..9112bf90c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java
@@ -0,0 +1,45 @@
1package org.citra.citra_emu.utils;
2
3import android.content.Intent;
4import android.os.Bundle;
5import android.text.TextUtils;
6
7import androidx.appcompat.app.AlertDialog;
8import androidx.fragment.app.FragmentActivity;
9
10import org.citra.citra_emu.R;
11import org.citra.citra_emu.activities.EmulationActivity;
12
13public final class StartupHandler {
14 private static void handlePermissionsCheck(FragmentActivity parent) {
15 // Ask the user to grant write permission if it's not already granted
16 PermissionsHandler.checkWritePermission(parent);
17
18 String start_file = "";
19 Bundle extras = parent.getIntent().getExtras();
20 if (extras != null) {
21 start_file = extras.getString("AutoStartFile");
22 }
23
24 if (!TextUtils.isEmpty(start_file)) {
25 // Start the emulation activity, send the ISO passed in and finish the main activity
26 Intent emulation_intent = new Intent(parent, EmulationActivity.class);
27 emulation_intent.putExtra("SelectedGame", start_file);
28 parent.startActivity(emulation_intent);
29 parent.finish();
30 }
31 }
32
33 public static void HandleInit(FragmentActivity parent) {
34 if (PermissionsHandler.isFirstBoot(parent)) {
35 // Prompt user with standard first boot disclaimer
36 new AlertDialog.Builder(parent)
37 .setTitle(R.string.app_name)
38 .setIcon(R.mipmap.ic_launcher)
39 .setMessage(parent.getResources().getString(R.string.app_disclaimer))
40 .setPositiveButton(android.R.string.ok, null)
41 .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
42 .show();
43 }
44 }
45}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java
new file mode 100644
index 000000000..74ef3867f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java
@@ -0,0 +1,34 @@
1package org.citra.citra_emu.utils;
2
3import android.content.SharedPreferences;
4import android.os.Build;
5import android.preference.PreferenceManager;
6
7import androidx.appcompat.app.AppCompatDelegate;
8
9import org.citra.citra_emu.CitraApplication;
10import org.citra.citra_emu.features.settings.utils.SettingsFile;
11
12public class ThemeUtil {
13 private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
14
15 private static void applyTheme(int designValue) {
16 switch (designValue) {
17 case 0:
18 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
19 break;
20 case 1:
21 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
22 break;
23 case 2:
24 AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
25 AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM :
26 AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
27 break;
28 }
29 }
30
31 public static void applyTheme() {
32 applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0));
33 }
34}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java
new file mode 100644
index 000000000..50dbcbe18
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java
@@ -0,0 +1,46 @@
1package org.citra.citra_emu.viewholders;
2
3import android.view.View;
4import android.widget.ImageView;
5import android.widget.TextView;
6
7import androidx.recyclerview.widget.RecyclerView;
8
9import org.citra.citra_emu.R;
10
11/**
12 * A simple class that stores references to views so that the GameAdapter doesn't need to
13 * keep calling findViewById(), which is expensive.
14 */
15public class GameViewHolder extends RecyclerView.ViewHolder {
16 private View itemView;
17 public ImageView imageIcon;
18 public TextView textGameTitle;
19 public TextView textCompany;
20 public TextView textFileName;
21
22 public String gameId;
23
24 // TODO Not need any of this stuff. Currently only the properties dialog needs it.
25 public String path;
26 public String title;
27 public String description;
28 public String regions;
29 public String company;
30
31 public GameViewHolder(View itemView) {
32 super(itemView);
33
34 this.itemView = itemView;
35 itemView.setTag(this);
36
37 imageIcon = itemView.findViewById(R.id.image_game_screen);
38 textGameTitle = itemView.findViewById(R.id.text_game_title);
39 textCompany = itemView.findViewById(R.id.text_company);
40 textFileName = itemView.findViewById(R.id.text_filename);
41 }
42
43 public View getItemView() {
44 return itemView;
45 }
46}
diff --git a/src/android/app/src/main/res/animator/settings_enter.xml b/src/android/app/src/main/res/animator/settings_enter.xml
new file mode 100644
index 000000000..3c216a054
--- /dev/null
+++ b/src/android/app/src/main/res/animator/settings_enter.xml
@@ -0,0 +1,28 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:duration="@android:integer/config_mediumAnimTime"
6 android:interpolator="@android:interpolator/decelerate_cubic"
7 android:propertyName="yFraction"
8 android:startOffset="@android:integer/config_shortAnimTime"
9 android:valueFrom="1.0"
10 android:valueTo="0" />
11
12 <objectAnimator
13 android:duration="@android:integer/config_mediumAnimTime"
14 android:interpolator="@android:interpolator/decelerate_cubic"
15 android:propertyName="translationZ"
16 android:startOffset="@android:integer/config_shortAnimTime"
17 android:valueFrom="100.0"
18 android:valueTo="0" />
19
20 <objectAnimator
21 android:duration="@android:integer/config_mediumAnimTime"
22 android:interpolator="@android:interpolator/decelerate_cubic"
23 android:propertyName="elevation"
24 android:startOffset="@android:integer/config_shortAnimTime"
25 android:valueFrom="100.0"
26 android:valueTo="0" />
27
28</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/animator/settings_exit.xml b/src/android/app/src/main/res/animator/settings_exit.xml
new file mode 100644
index 000000000..a233b6757
--- /dev/null
+++ b/src/android/app/src/main/res/animator/settings_exit.xml
@@ -0,0 +1,28 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:duration="@android:integer/config_mediumAnimTime"
6 android:interpolator="@android:interpolator/accelerate_cubic"
7 android:propertyName="visibleness"
8 android:valueFrom="1.0f"
9 android:valueTo="0.6f"
10 android:valueType="floatType" />
11
12 <objectAnimator
13 android:duration="@android:integer/config_mediumAnimTime"
14 android:interpolator="@android:interpolator/decelerate_cubic"
15 android:propertyName="translationZ"
16 android:startOffset="@android:integer/config_shortAnimTime"
17 android:valueFrom="0"
18 android:valueTo="-100.0" />
19
20 <objectAnimator
21 android:duration="@android:integer/config_mediumAnimTime"
22 android:interpolator="@android:interpolator/decelerate_cubic"
23 android:propertyName="elevation"
24 android:startOffset="@android:integer/config_shortAnimTime"
25 android:valueFrom="0"
26 android:valueTo="-100.0" />
27
28</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/animator/settings_pop_enter.xml b/src/android/app/src/main/res/animator/settings_pop_enter.xml
new file mode 100644
index 000000000..080bc27c4
--- /dev/null
+++ b/src/android/app/src/main/res/animator/settings_pop_enter.xml
@@ -0,0 +1,28 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:duration="@android:integer/config_mediumAnimTime"
6 android:interpolator="@android:interpolator/decelerate_cubic"
7 android:propertyName="visibleness"
8 android:valueFrom="0.6f"
9 android:valueTo="1.0f"
10 android:valueType="floatType" />
11
12 <objectAnimator
13 android:duration="@android:integer/config_mediumAnimTime"
14 android:interpolator="@android:interpolator/decelerate_cubic"
15 android:propertyName="translationZ"
16 android:startOffset="@android:integer/config_shortAnimTime"
17 android:valueFrom="-100.0"
18 android:valueTo="0" />
19
20 <objectAnimator
21 android:duration="@android:integer/config_mediumAnimTime"
22 android:interpolator="@android:interpolator/decelerate_cubic"
23 android:propertyName="elevation"
24 android:startOffset="@android:integer/config_shortAnimTime"
25 android:valueFrom="-100.0"
26 android:valueTo="0" />
27
28</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/animator/setttings_pop_exit.xml b/src/android/app/src/main/res/animator/setttings_pop_exit.xml
new file mode 100644
index 000000000..4fccbcca2
--- /dev/null
+++ b/src/android/app/src/main/res/animator/setttings_pop_exit.xml
@@ -0,0 +1,27 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:duration="@android:integer/config_mediumAnimTime"
6 android:interpolator="@android:interpolator/accelerate_cubic"
7 android:propertyName="yFraction"
8 android:valueFrom="0"
9 android:valueTo="1.0" />
10
11 <objectAnimator
12 android:duration="@android:integer/config_mediumAnimTime"
13 android:interpolator="@android:interpolator/decelerate_cubic"
14 android:propertyName="translationZ"
15 android:startOffset="@android:integer/config_shortAnimTime"
16 android:valueFrom="0.0"
17 android:valueTo="100" />
18
19 <objectAnimator
20 android:duration="@android:integer/config_mediumAnimTime"
21 android:interpolator="@android:interpolator/decelerate_cubic"
22 android:propertyName="elevation"
23 android:startOffset="@android:integer/config_shortAnimTime"
24 android:valueFrom="0.0"
25 android:valueTo="100" />
26
27</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_a.png b/src/android/app/src/main/res/drawable-hdpi/button_a.png
new file mode 100644
index 000000000..f96a2061e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_a.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png
new file mode 100644
index 000000000..785a258ee
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_b.png b/src/android/app/src/main/res/drawable-hdpi/button_b.png
new file mode 100644
index 000000000..b15d2b549
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_b.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png
new file mode 100644
index 000000000..b11d5fcee
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_l.png b/src/android/app/src/main/res/drawable-hdpi/button_l.png
new file mode 100644
index 000000000..e19469a7b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_l.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png
new file mode 100644
index 000000000..280857f64
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_r.png b/src/android/app/src/main/res/drawable-hdpi/button_r.png
new file mode 100644
index 000000000..f72cdc1dc
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_r.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png
new file mode 100644
index 000000000..c47d34253
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_select.png b/src/android/app/src/main/res/drawable-hdpi/button_select.png
new file mode 100644
index 000000000..6961b88d2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_select.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png
new file mode 100644
index 000000000..8ee471419
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_start.png b/src/android/app/src/main/res/drawable-hdpi/button_start.png
new file mode 100644
index 000000000..72856cf47
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_start.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png
new file mode 100644
index 000000000..f96cd3359
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_x.png b/src/android/app/src/main/res/drawable-hdpi/button_x.png
new file mode 100644
index 000000000..1a0fd1924
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_x.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png
new file mode 100644
index 000000000..089cb3af1
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_y.png b/src/android/app/src/main/res/drawable-hdpi/button_y.png
new file mode 100644
index 000000000..bc22680c4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_y.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png
new file mode 100644
index 000000000..6e9e89ec9
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zl.png b/src/android/app/src/main/res/drawable-hdpi/button_zl.png
new file mode 100644
index 000000000..dd5d4d5b3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_zl.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png
new file mode 100644
index 000000000..8cd395f3b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zr.png b/src/android/app/src/main/res/drawable-hdpi/button_zr.png
new file mode 100644
index 000000000..728fcf4d1
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_zr.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png
new file mode 100644
index 000000000..121877610
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad.png b/src/android/app/src/main/res/drawable-hdpi/dpad.png
new file mode 100644
index 000000000..921b3902d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/dpad.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png
new file mode 100644
index 000000000..a8ffbb48a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png
new file mode 100644
index 000000000..ceb994a6d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png
new file mode 100644
index 000000000..8c00d8c34
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_folder.png b/src/android/app/src/main/res/drawable-hdpi/ic_folder.png
new file mode 100644
index 000000000..90085252b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_premium.png b/src/android/app/src/main/res/drawable-hdpi/ic_premium.png
new file mode 100644
index 000000000..7dd45a405
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png
new file mode 100644
index 000000000..2e7837020
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..2282f1a3b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c.png b/src/android/app/src/main/res/drawable-hdpi/stick_c.png
new file mode 100644
index 000000000..d4c1d6c97
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_c.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png
new file mode 100644
index 000000000..c8d14c029
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png
new file mode 100644
index 000000000..8263d4b8d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main.png b/src/android/app/src/main/res/drawable-hdpi/stick_main.png
new file mode 100644
index 000000000..ae6d025a5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_main.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png
new file mode 100644
index 000000000..ca469c6a7
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png
new file mode 100644
index 000000000..9b5445edc
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png
new file mode 100644
index 000000000..c6dc232b4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_folder.png b/src/android/app/src/main/res/drawable-mdpi/ic_folder.png
new file mode 100644
index 000000000..1e428dfe3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-mdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_premium.png b/src/android/app/src/main/res/drawable-mdpi/ic_premium.png
new file mode 100644
index 000000000..4dfb62596
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-mdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png
new file mode 100644
index 000000000..cc986c8ac
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png
new file mode 100644
index 000000000..ee688b09f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png
new file mode 100644
index 000000000..6b678d22c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png
new file mode 100644
index 000000000..bc9dc0beb
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png
new file mode 100644
index 000000000..f61d84961
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png
new file mode 100644
index 000000000..05847c34b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png
new file mode 100644
index 000000000..87bac27df
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png
new file mode 100644
index 000000000..1eccbe68d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png
new file mode 100644
index 000000000..ffa1d200e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png
new file mode 100644
index 000000000..23a5cec51
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png
new file mode 100644
index 000000000..9ca7975bb
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png
new file mode 100644
index 000000000..fc3c434b0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png
new file mode 100644
index 000000000..013600d1f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png
new file mode 100644
index 000000000..2a0f1568f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png
new file mode 100644
index 000000000..23706188b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png
new file mode 100644
index 000000000..b4d1b92b7
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png
new file mode 100644
index 000000000..166bd052d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png
new file mode 100644
index 000000000..8d357b228
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png
new file mode 100644
index 000000000..32eb4faff
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-night/no_icon.png b/src/android/app/src/main/res/drawable-night/no_icon.png
new file mode 100644
index 000000000..9a3969709
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-night/no_icon.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_a.png b/src/android/app/src/main/res/drawable-xhdpi/button_a.png
new file mode 100644
index 000000000..4e20f2b0e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_a.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png
new file mode 100644
index 000000000..f18edd07e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_b.png b/src/android/app/src/main/res/drawable-xhdpi/button_b.png
new file mode 100644
index 000000000..deb83a09d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_b.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png
new file mode 100644
index 000000000..f583be028
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_l.png b/src/android/app/src/main/res/drawable-xhdpi/button_l.png
new file mode 100644
index 000000000..d24039fbf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_l.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png
new file mode 100644
index 000000000..378ac8751
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_r.png b/src/android/app/src/main/res/drawable-xhdpi/button_r.png
new file mode 100644
index 000000000..7b01c043e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_r.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png
new file mode 100644
index 000000000..9b3e3e75a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_select.png b/src/android/app/src/main/res/drawable-xhdpi/button_select.png
new file mode 100644
index 000000000..57abf5666
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_select.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png
new file mode 100644
index 000000000..29eda72af
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_start.png b/src/android/app/src/main/res/drawable-xhdpi/button_start.png
new file mode 100644
index 000000000..f9cf0d667
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_start.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png
new file mode 100644
index 000000000..4d690fa7e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_x.png b/src/android/app/src/main/res/drawable-xhdpi/button_x.png
new file mode 100644
index 000000000..93a2ee997
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_x.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png
new file mode 100644
index 000000000..6bbd39646
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_y.png b/src/android/app/src/main/res/drawable-xhdpi/button_y.png
new file mode 100644
index 000000000..d979e98e0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_y.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png
new file mode 100644
index 000000000..a6c9bdb54
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xhdpi/button_zl.png
new file mode 100644
index 000000000..f94474fea
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_zl.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png
new file mode 100644
index 000000000..8f7d5ab7a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xhdpi/button_zr.png
new file mode 100644
index 000000000..a76658351
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_zr.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png
new file mode 100644
index 000000000..bbe4e64ce
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad.png b/src/android/app/src/main/res/drawable-xhdpi/dpad.png
new file mode 100644
index 000000000..94ae84405
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/dpad.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png
new file mode 100644
index 000000000..d6ccb2c4f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png
new file mode 100644
index 000000000..2bba7749e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png
new file mode 100644
index 000000000..839869401
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png
new file mode 100644
index 000000000..02bc3d75a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png
new file mode 100644
index 000000000..ac4b19ff4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png
new file mode 100644
index 000000000..8cff45f84
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..5e2787ba3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c.png
new file mode 100644
index 000000000..7819f220a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_c.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png
new file mode 100644
index 000000000..a111c2ac7
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png
new file mode 100644
index 000000000..774c54292
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main.png
new file mode 100644
index 000000000..3f80cdf6c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_main.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png
new file mode 100644
index 000000000..2a7675ef7
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png
new file mode 100644
index 000000000..ca1672caf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_a.png b/src/android/app/src/main/res/drawable-xxhdpi/button_a.png
new file mode 100644
index 000000000..999b4c01e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_a.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png
new file mode 100644
index 000000000..bb4de9bd9
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_b.png b/src/android/app/src/main/res/drawable-xxhdpi/button_b.png
new file mode 100644
index 000000000..8ed042e7e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_b.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png
new file mode 100644
index 000000000..86f5d535e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_l.png b/src/android/app/src/main/res/drawable-xxhdpi/button_l.png
new file mode 100644
index 000000000..9572c66f8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_l.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png
new file mode 100644
index 000000000..64bedc326
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_r.png b/src/android/app/src/main/res/drawable-xxhdpi/button_r.png
new file mode 100644
index 000000000..abbcadede
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_r.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png
new file mode 100644
index 000000000..07421767f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_select.png b/src/android/app/src/main/res/drawable-xxhdpi/button_select.png
new file mode 100644
index 000000000..42c3b7c43
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_select.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png
new file mode 100644
index 000000000..0d1e56f6a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_start.png b/src/android/app/src/main/res/drawable-xxhdpi/button_start.png
new file mode 100644
index 000000000..4e9585bb4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_start.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png
new file mode 100644
index 000000000..8c089e237
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_x.png b/src/android/app/src/main/res/drawable-xxhdpi/button_x.png
new file mode 100644
index 000000000..0500f964f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_x.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png
new file mode 100644
index 000000000..56db5843d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_y.png b/src/android/app/src/main/res/drawable-xxhdpi/button_y.png
new file mode 100644
index 000000000..53c5ca084
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_y.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png
new file mode 100644
index 000000000..5d91cbfb0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png
new file mode 100644
index 000000000..f8ce9a0c6
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png
new file mode 100644
index 000000000..981c8b0c8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png
new file mode 100644
index 000000000..82065e126
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png
new file mode 100644
index 000000000..b30b2e799
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad.png
new file mode 100644
index 000000000..36b7ea183
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/dpad.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png
new file mode 100644
index 000000000..3715e1c11
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png
new file mode 100644
index 000000000..fb0d7fc5c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png
new file mode 100644
index 000000000..e6812f0d4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png
new file mode 100644
index 000000000..05f429614
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png
new file mode 100644
index 000000000..63f162e52
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png
new file mode 100644
index 000000000..0b9049f46
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..06cef9de3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png
new file mode 100644
index 000000000..e950c5b15
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png
new file mode 100644
index 000000000..3ac88ed9b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png
new file mode 100644
index 000000000..a3491c80f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png
new file mode 100644
index 000000000..16ca58c0f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png
new file mode 100644
index 000000000..e7fe0c2d5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png
new file mode 100644
index 000000000..8c47b2ba3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png
new file mode 100644
index 000000000..e364fae1e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png
new file mode 100644
index 000000000..08d65cc99
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png
new file mode 100644
index 000000000..faae9b6f7
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png
new file mode 100644
index 000000000..669780f28
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png
new file mode 100644
index 000000000..888b147de
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png
new file mode 100644
index 000000000..605493e3e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png
new file mode 100644
index 000000000..90a93af8d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png
new file mode 100644
index 000000000..4500cd2be
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png
new file mode 100644
index 000000000..b18b2fd59
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png
new file mode 100644
index 000000000..53ed400e0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png
new file mode 100644
index 000000000..c55e56852
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png
new file mode 100644
index 000000000..1507cc365
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png
new file mode 100644
index 000000000..7ef2b883e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png
new file mode 100644
index 000000000..f3f11ede2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png
new file mode 100644
index 000000000..4ce679c69
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png
new file mode 100644
index 000000000..926f5e269
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png
new file mode 100644
index 000000000..7faf8db3b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png
new file mode 100644
index 000000000..cc56a749c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png
new file mode 100644
index 000000000..ed1b6b683
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png
new file mode 100644
index 000000000..892fa74f1
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png
new file mode 100644
index 000000000..6272f39e6
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png
new file mode 100644
index 000000000..0cccd3a30
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png
new file mode 100644
index 000000000..18a99ad2d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png
new file mode 100644
index 000000000..69ae32dc3
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png
new file mode 100644
index 000000000..c85074c60
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png
new file mode 100644
index 000000000..6f1550a10
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png
new file mode 100644
index 000000000..2827a1777
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png
new file mode 100644
index 000000000..88e09b8a0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png
new file mode 100644
index 000000000..edc920e8e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png
new file mode 100644
index 000000000..a8b693494
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png
new file mode 100644
index 000000000..d157edca2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png
new file mode 100644
index 000000000..2ac2440be
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png
new file mode 100644
index 000000000..71e67e02a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable/gamelist_divider.xml b/src/android/app/src/main/res/drawable/gamelist_divider.xml
new file mode 100644
index 000000000..7da9dccce
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/gamelist_divider.xml
@@ -0,0 +1,11 @@
1<?xml version="1.0" encoding="utf-8"?>
2<shape xmlns:android="http://schemas.android.com/apk/res/android"
3 android:shape="rectangle">
4
5 <size
6 android:width="1dp"
7 android:height="1dp" />
8
9 <solid android:color="@color/gamelist_divider" />
10
11</shape>
diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..bdd99f48d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="@android:color/white"
8 android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
9</vector>
diff --git a/src/android/app/src/main/res/drawable/no_icon.png b/src/android/app/src/main/res/drawable/no_icon.png
new file mode 100644
index 000000000..1ce8fdc76
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/no_icon.png
Binary files differ
diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml
new file mode 100644
index 000000000..9bcf883e1
--- /dev/null
+++ b/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml
@@ -0,0 +1,38 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/root"
7 android:layout_width="match_parent"
8 android:layout_height="wrap_content"
9 android:focusable="true"
10 android:nextFocusLeft="@id/checkbox">
11
12 <TextView
13 android:id="@+id/text_name"
14 android:layout_width="0dp"
15 android:layout_height="wrap_content"
16 android:textColor="@color/header_text"
17 android:textSize="16sp"
18 android:layout_margin="@dimen/spacing_large"
19 style="@style/TextAppearance.AppCompat.Headline"
20 app:layout_constraintStart_toStartOf="parent"
21 app:layout_constraintEnd_toStartOf="@id/checkbox"
22 app:layout_constraintTop_toTopOf="parent"
23 app:layout_constraintBottom_toBottomOf="parent"
24 tools:text="Max Lives after losing 1" />
25
26 <CheckBox
27 android:id="@+id/checkbox"
28 android:layout_width="48dp"
29 android:layout_height="64dp"
30 android:focusable="true"
31 android:gravity="center"
32 android:nextFocusRight="@id/root"
33 app:layout_constraintBottom_toBottomOf="parent"
34 app:layout_constraintEnd_toEndOf="parent"
35 app:layout_constraintStart_toEndOf="@id/text_name"
36 app:layout_constraintTop_toTopOf="parent" />
37
38</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/activity_cheats.xml b/src/android/app/src/main/res/layout/activity_cheats.xml
new file mode 100644
index 000000000..b9414ab6d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_cheats.xml
@@ -0,0 +1,22 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.slidingpanelayout.widget.SlidingPaneLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:id="@+id/sliding_pane_layout"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <androidx.fragment.app.FragmentContainerView
9 android:layout_width="320dp"
10 android:layout_height="match_parent"
11 android:layout_weight="1"
12 android:id="@+id/cheat_list"
13 android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment" />
14
15 <androidx.fragment.app.FragmentContainerView
16 android:layout_width="320dp"
17 android:layout_height="match_parent"
18 android:layout_weight="1"
19 android:id="@+id/cheat_details"
20 android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment" />
21
22</androidx.slidingpanelayout.widget.SlidingPaneLayout>
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
new file mode 100644
index 000000000..7d7f36925
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -0,0 +1,17 @@
1<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 android:id="@+id/frame_content"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent">
5
6 <FrameLayout
7 android:id="@+id/frame_emulation_fragment"
8 android:layout_width="match_parent"
9 android:layout_height="match_parent" />
10
11 <ImageView
12 android:id="@+id/image_icon"
13 android:layout_width="match_parent"
14 android:layout_height="match_parent"
15 android:transitionName="image_game_icon" />
16
17</FrameLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..cea0922a7
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,27 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_main"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <FrameLayout
9 android:id="@+id/games_platform_frame"
10 android:layout_width="match_parent"
11 android:layout_height="match_parent"
12 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
13
14 <com.google.android.material.appbar.AppBarLayout
15 android:id="@+id/appbar"
16 android:layout_width="match_parent"
17 android:layout_height="wrap_content">
18
19 <androidx.appcompat.widget.Toolbar
20 android:id="@+id/toolbar_main"
21 android:layout_width="match_parent"
22 android:layout_height="?attr/actionBarSize"
23 android:background="?colorPrimary"/>
24
25 </com.google.android.material.appbar.AppBarLayout>
26
27</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 000000000..11b91c45f
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:id="@+id/frame_content" />
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
new file mode 100644
index 000000000..217f02d34
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -0,0 +1,81 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="wrap_content"
7 android:clickable="true"
8 android:focusable="true"
9 android:foreground="?android:attr/selectableItemBackground"
10 android:transitionName="card_game"
11 tools:layout_width="match_parent">
12
13 <androidx.constraintlayout.widget.ConstraintLayout
14 android:id="@+id/linearLayout"
15 android:layout_width="match_parent"
16 android:layout_height="wrap_content"
17 android:padding="8dp">
18
19 <ImageView
20 android:id="@+id/image_game_screen"
21 android:layout_width="56dp"
22 android:layout_height="56dp"
23 android:adjustViewBounds="false"
24 android:cropToPadding="false"
25 android:scaleType="fitCenter"
26 app:layout_constraintBottom_toBottomOf="parent"
27 app:layout_constraintStart_toStartOf="parent"
28 app:layout_constraintTop_toTopOf="parent"
29 tools:scaleType="fitCenter" />
30
31 <TextView
32 android:id="@+id/text_game_title"
33 style="@android:style/TextAppearance.Material.Subhead"
34 android:layout_width="0dp"
35 android:layout_height="wrap_content"
36 android:layout_marginStart="8dp"
37 android:baselineAligned="false"
38 android:ellipsize="end"
39 android:gravity="center_vertical"
40 android:lines="1"
41 android:maxLines="1"
42 android:textAlignment="viewStart"
43 app:layout_constraintEnd_toEndOf="parent"
44 app:layout_constraintStart_toEndOf="@+id/image_game_screen"
45 app:layout_constraintTop_toTopOf="parent"
46 tools:text="The Legend of Zelda\nOcarina of Time 3D"
47 android:textColor="@color/header_text" />
48
49 <TextView
50 android:id="@+id/text_company"
51 style="@android:style/TextAppearance.Material.Caption"
52 android:layout_width="wrap_content"
53 android:layout_height="wrap_content"
54 android:ellipsize="end"
55 android:lines="1"
56 android:maxLines="1"
57 app:layout_constraintBottom_toBottomOf="@+id/image_game_screen"
58 app:layout_constraintStart_toStartOf="@+id/text_game_title"
59 app:layout_constraintTop_toBottomOf="@+id/text_game_title"
60 app:layout_constraintVertical_bias="0.842"
61 tools:text="Nintendo"
62 android:textColor="@color/header_subtext" />
63
64 <TextView
65 android:id="@+id/text_filename"
66 style="@android:style/TextAppearance.Material.Caption"
67 android:layout_width="wrap_content"
68 android:layout_height="wrap_content"
69 android:ellipsize="end"
70 android:lines="1"
71 android:maxLines="1"
72 app:layout_constraintBottom_toBottomOf="@+id/image_game_screen"
73 app:layout_constraintStart_toStartOf="@+id/text_game_title"
74 app:layout_constraintTop_toBottomOf="@+id/text_game_title"
75 app:layout_constraintVertical_bias="0.0"
76 tools:text="Pilotwings_Resort.cxi"
77 android:textColor="@color/header_subtext" />
78
79 </androidx.constraintlayout.widget.ConstraintLayout>
80
81</androidx.cardview.widget.CardView>
diff --git a/src/android/app/src/main/res/layout/dialog_checkbox.xml b/src/android/app/src/main/res/layout/dialog_checkbox.xml
new file mode 100644
index 000000000..c0f307117
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_checkbox.xml
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:orientation="vertical"
4 android:paddingTop="5dp"
5 android:paddingLeft="20dp"
6 android:paddingRight="20dp"
7 android:paddingBottom="0dp"
8 android:layout_width="match_parent"
9 android:layout_height="match_parent">
10
11 <CheckBox
12 android:id="@+id/checkBox"
13 android:layout_width="match_parent"
14 android:layout_height="wrap_content"
15 android:text="@string/do_not_show_this_again" />
16</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
new file mode 100644
index 000000000..a81157a29
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout
3 xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:orientation="vertical">
6
7 <ProgressBar
8 android:id="@+id/progress_bar"
9 style="?android:attr/progressBarStyleHorizontal"
10 android:layout_width="match_parent"
11 android:layout_height="wrap_content"
12 android:layout_marginLeft="@dimen/spacing_large"
13 android:layout_marginRight="@dimen/spacing_large"
14 android:layout_alignParentEnd="true"
15 android:layout_below="@+id/progress_text"
16 android:layout_alignParentStart="true"/>
17
18 <TextView
19 android:id="@+id/progress_text"
20 android:layout_width="match_parent"
21 android:layout_height="wrap_content"
22 android:layout_marginLeft="@dimen/spacing_large"
23 android:layout_marginRight="@dimen/spacing_large"
24 android:gravity="right"
25 android:text="1/100" />
26</LinearLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/dialog_seekbar.xml b/src/android/app/src/main/res/layout/dialog_seekbar.xml
new file mode 100644
index 000000000..35abecfcb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_seekbar.xml
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:orientation="vertical"
5 android:layout_width="match_parent"
6 android:layout_height="wrap_content">
7
8 <SeekBar
9 android:id="@+id/seekbar"
10 android:layout_width="match_parent"
11 android:layout_height="wrap_content"
12 android:layout_marginLeft="@dimen/spacing_large"
13 android:layout_marginRight="@dimen/spacing_large"
14 android:layout_alignParentEnd="true"
15 android:layout_alignParentStart="true"
16 android:layout_below="@+id/text_value"
17 android:layout_marginBottom="@dimen/spacing_medlarge" />
18
19 <TextView
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 tools:text="75"
23 android:id="@+id/text_value"
24 android:layout_alignParentTop="true"
25 android:layout_centerHorizontal="true"
26 android:layout_marginTop="@dimen/spacing_medlarge"
27 android:layout_marginBottom="@dimen/spacing_medlarge" />
28
29 <TextView
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 tools:text="%"
33 android:id="@+id/text_units"
34 android:layout_alignTop="@+id/text_value"
35 android:layout_toEndOf="@+id/text_value" />
36
37</RelativeLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/filepicker_toolbar.xml b/src/android/app/src/main/res/layout/filepicker_toolbar.xml
new file mode 100644
index 000000000..644934171
--- /dev/null
+++ b/src/android/app/src/main/res/layout/filepicker_toolbar.xml
@@ -0,0 +1,32 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/nnf_picker_toolbar"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:layout_alignParentTop="true"
7 android:background="?attr/colorPrimary"
8 android:minHeight="?attr/actionBarSize"
9 android:theme="?nnf_toolbarTheme">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="match_parent"
14 android:orientation="vertical">
15
16 <TextView
17 android:id="@+id/filepicker_title"
18 android:layout_width="match_parent"
19 android:layout_height="wrap_content"
20 android:ellipsize="start"
21 android:singleLine="true"
22 android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
23
24 <TextView
25 android:id="@+id/nnf_current_dir"
26 android:layout_width="match_parent"
27 android:layout_height="wrap_content"
28 android:ellipsize="start"
29 android:singleLine="true"
30 android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
31 </LinearLayout>
32</androidx.appcompat.widget.Toolbar>
diff --git a/src/android/app/src/main/res/layout/fragment_cheat_details.xml b/src/android/app/src/main/res/layout/fragment_cheat_details.xml
new file mode 100644
index 000000000..25b1a268a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_cheat_details.xml
@@ -0,0 +1,163 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/root"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent">
9
10 <ScrollView
11 android:id="@+id/scroll_view"
12 android:layout_width="match_parent"
13 android:layout_height="0dp"
14 app:layout_constraintStart_toStartOf="parent"
15 app:layout_constraintEnd_toEndOf="parent"
16 app:layout_constraintTop_toTopOf="parent"
17 app:layout_constraintBottom_toTopOf="@id/barrier">
18
19 <androidx.constraintlayout.widget.ConstraintLayout
20 android:layout_width="match_parent"
21 android:layout_height="wrap_content">
22
23 <TextView
24 android:id="@+id/label_name"
25 android:layout_width="match_parent"
26 android:layout_height="wrap_content"
27 style="@style/TextAppearance.MaterialComponents.Headline5"
28 android:textSize="18sp"
29 android:text="@string/cheats_name"
30 android:layout_margin="@dimen/spacing_large"
31 android:labelFor="@id/edit_name"
32 app:layout_constraintStart_toStartOf="parent"
33 app:layout_constraintEnd_toEndOf="parent"
34 app:layout_constraintTop_toTopOf="parent"
35 app:layout_constraintBottom_toTopOf="@id/edit_name" />
36
37 <EditText
38 android:id="@+id/edit_name"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:minHeight="48dp"
42 android:layout_marginHorizontal="@dimen/spacing_large"
43 android:importantForAutofill="no"
44 android:inputType="text"
45 app:layout_constraintStart_toStartOf="parent"
46 app:layout_constraintEnd_toEndOf="parent"
47 app:layout_constraintTop_toBottomOf="@id/label_name"
48 app:layout_constraintBottom_toTopOf="@id/label_notes"
49 tools:text="Max Lives after losing 1" />
50
51 <TextView
52 android:id="@+id/label_notes"
53 android:layout_width="match_parent"
54 android:layout_height="wrap_content"
55 style="@style/TextAppearance.MaterialComponents.Headline5"
56 android:textSize="18sp"
57 android:text="@string/cheats_notes"
58 android:layout_margin="@dimen/spacing_large"
59 android:labelFor="@id/edit_notes"
60 app:layout_constraintStart_toStartOf="parent"
61 app:layout_constraintEnd_toEndOf="parent"
62 app:layout_constraintTop_toBottomOf="@id/edit_name"
63 app:layout_constraintBottom_toTopOf="@id/edit_notes" />
64
65 <EditText
66 android:id="@+id/edit_notes"
67 android:layout_width="match_parent"
68 android:layout_height="wrap_content"
69 android:minHeight="48dp"
70 android:layout_marginHorizontal="@dimen/spacing_large"
71 android:importantForAutofill="no"
72 android:inputType="textMultiLine"
73 app:layout_constraintStart_toStartOf="parent"
74 app:layout_constraintEnd_toEndOf="parent"
75 app:layout_constraintTop_toBottomOf="@id/label_notes"
76 app:layout_constraintBottom_toTopOf="@id/label_code" />
77
78 <TextView
79 android:id="@+id/label_code"
80 android:layout_width="match_parent"
81 android:layout_height="wrap_content"
82 style="@style/TextAppearance.MaterialComponents.Headline5"
83 android:textSize="18sp"
84 android:text="@string/cheats_code"
85 android:layout_margin="@dimen/spacing_large"
86 android:labelFor="@id/edit_code"
87 app:layout_constraintStart_toStartOf="parent"
88 app:layout_constraintEnd_toEndOf="parent"
89 app:layout_constraintTop_toBottomOf="@id/edit_notes"
90 app:layout_constraintBottom_toTopOf="@id/edit_code" />
91
92 <EditText
93 android:id="@+id/edit_code"
94 android:layout_width="match_parent"
95 android:layout_height="wrap_content"
96 android:minHeight="108sp"
97 android:layout_marginHorizontal="@dimen/spacing_large"
98 android:importantForAutofill="no"
99 android:inputType="textMultiLine"
100 android:typeface="monospace"
101 android:gravity="start"
102 app:layout_constraintStart_toStartOf="parent"
103 app:layout_constraintEnd_toEndOf="parent"
104 app:layout_constraintTop_toBottomOf="@id/label_code"
105 app:layout_constraintBottom_toBottomOf="parent"
106 tools:text="D3000000 00000000\n00138C78 E1C023BE" />
107
108 </androidx.constraintlayout.widget.ConstraintLayout>
109
110 </ScrollView>
111
112 <androidx.constraintlayout.widget.Barrier
113 android:id="@+id/barrier"
114 android:layout_width="wrap_content"
115 android:layout_height="wrap_content"
116 app:barrierDirection="top"
117 app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" />
118
119 <Button
120 android:id="@+id/button_delete"
121 android:layout_width="0dp"
122 android:layout_height="wrap_content"
123 android:layout_margin="@dimen/spacing_large"
124 android:text="@string/cheats_delete"
125 app:layout_constraintStart_toStartOf="parent"
126 app:layout_constraintEnd_toStartOf="@id/button_edit"
127 app:layout_constraintTop_toBottomOf="@id/barrier"
128 app:layout_constraintBottom_toBottomOf="parent" />
129
130 <Button
131 android:id="@+id/button_edit"
132 android:layout_width="0dp"
133 android:layout_height="wrap_content"
134 android:layout_margin="@dimen/spacing_large"
135 android:text="@string/cheats_edit"
136 app:layout_constraintStart_toEndOf="@id/button_delete"
137 app:layout_constraintEnd_toStartOf="@id/button_cancel"
138 app:layout_constraintTop_toBottomOf="@id/barrier"
139 app:layout_constraintBottom_toBottomOf="parent" />
140
141 <Button
142 android:id="@+id/button_cancel"
143 android:layout_width="0dp"
144 android:layout_height="wrap_content"
145 android:layout_margin="@dimen/spacing_large"
146 android:text="@android:string/cancel"
147 app:layout_constraintStart_toEndOf="@id/button_edit"
148 app:layout_constraintEnd_toStartOf="@id/button_ok"
149 app:layout_constraintTop_toBottomOf="@id/barrier"
150 app:layout_constraintBottom_toBottomOf="parent" />
151
152 <Button
153 android:id="@+id/button_ok"
154 android:layout_width="0dp"
155 android:layout_height="wrap_content"
156 android:layout_margin="@dimen/spacing_large"
157 android:text="@android:string/ok"
158 app:layout_constraintStart_toEndOf="@id/button_cancel"
159 app:layout_constraintEnd_toEndOf="parent"
160 app:layout_constraintTop_toBottomOf="@id/barrier"
161 app:layout_constraintBottom_toBottomOf="parent" />
162
163</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_cheat_list.xml b/src/android/app/src/main/res/layout/fragment_cheat_list.xml
new file mode 100644
index 000000000..679a49c28
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_cheat_list.xml
@@ -0,0 +1,27 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <androidx.recyclerview.widget.RecyclerView
9 android:id="@+id/cheat_list"
10 android:layout_width="match_parent"
11 android:layout_height="0dp"
12 app:layout_constraintStart_toStartOf="parent"
13 app:layout_constraintEnd_toEndOf="parent"
14 app:layout_constraintTop_toTopOf="parent"
15 app:layout_constraintBottom_toBottomOf="parent" />
16
17 <com.google.android.material.floatingactionbutton.FloatingActionButton
18 android:id="@+id/fab"
19 android:layout_width="wrap_content"
20 android:layout_height="wrap_content"
21 android:src="@drawable/ic_add"
22 android:contentDescription="@string/cheats_add"
23 android:layout_margin="16dp"
24 app:layout_constraintStart_toStartOf="parent"
25 app:layout_constraintBottom_toBottomOf="parent" />
26
27</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
new file mode 100644
index 000000000..d6e47e1e4
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -0,0 +1,47 @@
1<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:tools="http://schemas.android.com/tools"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:keepScreenOn="true"
6 tools:context="org.citra.citra_emu.fragments.EmulationFragment">
7
8 <!-- This is what everything is rendered to during emulation -->
9 <SurfaceView
10 android:id="@+id/surface_emulation"
11 android:layout_width="match_parent"
12 android:layout_height="match_parent"
13 android:focusable="false"
14 android:focusableInTouchMode="false" />
15
16 <!-- This is the onscreen input overlay -->
17 <org.citra.citra_emu.overlay.InputOverlay
18 android:id="@+id/surface_input_overlay"
19 android:layout_height="match_parent"
20 android:layout_width="match_parent"
21 android:focusable="true"
22 android:focusableInTouchMode="true" />
23
24 <TextView
25 android:id="@+id/show_fps_text"
26 android:layout_marginStart="8dp"
27 android:layout_marginTop="2dp"
28 android:layout_width="wrap_content"
29 android:layout_height="wrap_content"
30 android:clickable="false"
31 android:linksClickable="false"
32 android:longClickable="false"
33 android:shadowColor="@android:color/black"
34 android:textColor="@android:color/white"
35 android:textSize="12sp" />
36
37 <Button
38 android:id="@+id/done_control_config"
39 android:layout_width="wrap_content"
40 android:layout_height="wrap_content"
41 android:layout_gravity="center"
42 android:padding="@dimen/spacing_small"
43 android:background="@color/citra_orange"
44 android:text="@string/emulation_done"
45 android:visibility="gone" />
46
47</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml
new file mode 100644
index 000000000..f5b6c2e19
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_grid.xml
@@ -0,0 +1,33 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent">
6
7 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
8 android:id="@+id/refresh_grid_games"
9 android:layout_width="match_parent"
10 android:layout_height="wrap_content">
11
12 <RelativeLayout
13 android:layout_width="match_parent"
14 android:layout_height="match_parent">
15
16 <TextView
17 android:id="@+id/gamelist_empty_text"
18 android:layout_width="match_parent"
19 android:layout_height="match_parent"
20 android:text="@string/empty_gamelist"
21 android:visibility="gone"
22 android:textSize="18sp"
23 android:gravity="center" />
24
25 <androidx.recyclerview.widget.RecyclerView
26 android:id="@+id/grid_games"
27 android:layout_width="match_parent"
28 android:layout_height="match_parent"
29 tools:listitem="@layout/card_game" />
30 </RelativeLayout>
31
32 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
33</FrameLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 000000000..4c5d597c1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,12 @@
1<?xml version="1.0" encoding="utf-8"?>
2<org.citra.citra_emu.features.settings.ui.SettingsFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent">
5
6 <androidx.recyclerview.widget.RecyclerView
7 android:id="@+id/list_settings"
8 android:layout_width="match_parent"
9 android:layout_height="match_parent"
10 android:background="@color/card_view_background" />
11
12</org.citra.citra_emu.features.settings.ui.SettingsFrameLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/list_item_cheat.xml b/src/android/app/src/main/res/layout/list_item_cheat.xml
new file mode 100644
index 000000000..c0b5f982f
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_cheat.xml
@@ -0,0 +1,38 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/root"
7 android:layout_width="match_parent"
8 android:layout_height="wrap_content"
9 android:focusable="true"
10 android:nextFocusRight="@id/checkbox">
11
12 <TextView
13 android:id="@+id/text_name"
14 android:layout_width="0dp"
15 android:layout_height="wrap_content"
16 android:textColor="@color/header_text"
17 android:textSize="16sp"
18 android:layout_margin="@dimen/spacing_large"
19 style="@style/TextAppearance.AppCompat.Headline"
20 app:layout_constraintStart_toStartOf="parent"
21 app:layout_constraintEnd_toStartOf="@id/checkbox"
22 app:layout_constraintTop_toTopOf="parent"
23 app:layout_constraintBottom_toBottomOf="parent"
24 tools:text="Max Lives after losing 1" />
25
26 <CheckBox
27 android:id="@+id/checkbox"
28 android:layout_width="48dp"
29 android:layout_height="64dp"
30 android:focusable="true"
31 android:gravity="center"
32 android:nextFocusLeft="@id/root"
33 app:layout_constraintBottom_toBottomOf="parent"
34 app:layout_constraintEnd_toEndOf="parent"
35 app:layout_constraintStart_toEndOf="@id/text_name"
36 app:layout_constraintTop_toTopOf="parent" />
37
38</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
new file mode 100644
index 000000000..df83684f7
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -0,0 +1,43 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:background="?android:attr/selectableItemBackground"
7 android:clickable="true"
8 android:focusable="true"
9 android:gravity="center_vertical"
10 android:minHeight="72dp"
11 android:paddingTop="@dimen/spacing_large"
12 android:paddingBottom="@dimen/spacing_large">
13
14 <TextView
15 android:id="@+id/text_setting_name"
16 style="@style/TextAppearance.AppCompat.Headline"
17 android:layout_width="0dp"
18 android:layout_height="wrap_content"
19 android:layout_alignParentStart="true"
20 android:layout_alignParentTop="true"
21 android:layout_alignParentEnd="true"
22 android:layout_marginStart="@dimen/spacing_large"
23 android:layout_marginEnd="@dimen/spacing_large"
24 android:textColor="@color/header_text"
25 android:textSize="16sp"
26 tools:text="Setting Name" />
27
28 <TextView
29 android:id="@+id/text_setting_description"
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 android:layout_below="@+id/text_setting_name"
33 android:layout_alignStart="@+id/text_setting_name"
34 android:layout_alignParentStart="true"
35 android:layout_alignParentEnd="true"
36 android:layout_marginStart="@dimen/spacing_large"
37 android:layout_marginTop="@dimen/spacing_small"
38 android:layout_marginEnd="@dimen/spacing_large"
39 android:visibility="visible"
40 tools:text="@string/app_disclaimer"
41 android:textColor="@color/header_subtext" />
42
43</RelativeLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml
new file mode 100644
index 000000000..86ba83f11
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml
@@ -0,0 +1,52 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:minHeight="72dp"
7 android:background="?android:attr/selectableItemBackground"
8 android:focusable="true"
9 android:clickable="true">
10
11 <TextView
12 android:id="@+id/text_setting_name"
13 style="@style/TextAppearance.AppCompat.Headline"
14 android:layout_width="0dp"
15 android:layout_height="wrap_content"
16 android:layout_alignParentStart="true"
17 android:layout_alignParentTop="true"
18 android:layout_marginEnd="@dimen/spacing_large"
19 android:layout_marginStart="@dimen/spacing_large"
20 android:layout_marginTop="@dimen/spacing_large"
21 android:layout_toStartOf="@+id/checkbox"
22 android:textColor="@color/header_text"
23 android:textSize="16sp"
24 tools:text="@string/frame_limit_enable" />
25
26 <TextView
27 android:id="@+id/text_setting_description"
28 android:layout_width="wrap_content"
29 android:layout_height="wrap_content"
30 android:layout_alignParentStart="true"
31 android:layout_alignStart="@+id/text_setting_name"
32 android:layout_below="@+id/text_setting_name"
33 android:layout_marginBottom="@dimen/spacing_large"
34 android:layout_marginEnd="@dimen/spacing_large"
35 android:layout_marginStart="@dimen/spacing_large"
36 android:layout_marginTop="@dimen/spacing_small"
37 android:layout_toStartOf="@+id/checkbox"
38 android:textAlignment="textStart"
39 android:textColor="@color/header_subtext"
40 tools:text="@string/frame_limit_enable_description" />
41
42 <CheckBox
43 android:id="@+id/checkbox"
44 android:layout_width="wrap_content"
45 android:layout_height="wrap_content"
46 android:layout_alignParentEnd="true"
47 android:layout_centerVertical="true"
48 android:layout_marginEnd="@dimen/spacing_large"
49 android:focusable="false"
50 android:clickable="false" />
51
52</RelativeLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml
new file mode 100644
index 000000000..d220dfd61
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="48dp">
6
7 <TextView
8 android:id="@+id/text_header_name"
9 android:layout_width="wrap_content"
10 android:layout_height="wrap_content"
11 android:layout_gravity="center_vertical"
12 android:layout_marginStart="@dimen/spacing_large"
13 android:layout_marginBottom="@dimen/spacing_small"
14 android:layout_marginTop="@dimen/spacing_small"
15 android:textColor="?android:colorAccent"
16 android:textStyle="bold"
17 tools:text="CPU Settings" />
18
19</FrameLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/premium_item_setting.xml b/src/android/app/src/main/res/layout/premium_item_setting.xml
new file mode 100644
index 000000000..17d5a13b2
--- /dev/null
+++ b/src/android/app/src/main/res/layout/premium_item_setting.xml
@@ -0,0 +1,43 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:background="?android:attr/selectableItemBackground"
7 android:clickable="true"
8 android:focusable="true"
9 android:gravity="center_vertical"
10 android:minHeight="72dp"
11 android:paddingTop="@dimen/spacing_large"
12 android:paddingBottom="@dimen/spacing_large">
13
14 <TextView
15 android:id="@+id/text_setting_name"
16 style="@style/TextAppearance.AppCompat.Headline"
17 android:layout_width="0dp"
18 android:layout_height="wrap_content"
19 android:layout_alignParentStart="true"
20 android:layout_alignParentTop="true"
21 android:layout_alignParentEnd="true"
22 android:layout_marginStart="@dimen/spacing_large"
23 android:layout_marginEnd="@dimen/spacing_large"
24 android:textColor="?android:colorAccent"
25 android:textStyle="bold"
26 tools:text="Setting Name" />
27
28 <TextView
29 android:id="@+id/text_setting_description"
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 android:layout_below="@+id/text_setting_name"
33 android:layout_alignStart="@+id/text_setting_name"
34 android:layout_alignParentStart="true"
35 android:layout_alignParentEnd="true"
36 android:layout_marginStart="@dimen/spacing_large"
37 android:layout_marginTop="@dimen/spacing_small"
38 android:layout_marginEnd="@dimen/spacing_large"
39 android:visibility="visible"
40 tools:text="@string/app_disclaimer"
41 android:textColor="@color/header_subtext" />
42
43</RelativeLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml
new file mode 100644
index 000000000..d082f5283
--- /dev/null
+++ b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml
@@ -0,0 +1,22 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:orientation="vertical"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:padding="8dp"
7 android:gravity="center">
8
9 <DatePicker
10 android:id="@+id/date_picker"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:calendarViewShown="false"
14 android:datePickerMode="spinner"
15 android:spinnersShown="true" />
16
17 <TimePicker
18 android:id="@+id/time_picker"
19 android:layout_width="match_parent"
20 android:layout_height="wrap_content"
21 android:timePickerMode="spinner" />
22</LinearLayout>
diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml
new file mode 100644
index 000000000..b6c0d7cc4
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_emulation.xml
@@ -0,0 +1,118 @@
1<menu xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:tools="http://schemas.android.com/tools"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 tools:context="org.citra.citra_emu.activities.EmulationActivity">
5
6 <item
7 android:id="@+id/menu_emulation_save_state"
8 android:title="@string/emulation_save_state">
9 <menu/>
10 </item>
11
12 <item
13 android:id="@+id/menu_emulation_load_state"
14 android:title="@string/emulation_load_state">
15 <menu/>
16 </item>
17
18 <item
19 android:id="@+id/menu_emulation_configure_controls"
20 android:title="@string/emulation_configure_controls">
21 <menu>
22 <item
23 android:id="@+id/menu_emulation_edit_layout"
24 android:title="@string/emulation_edit_layout" />
25
26 <item
27 android:id="@+id/menu_emulation_toggle_controls"
28 android:title="@string/emulation_toggle_controls" />
29
30 <item
31 android:id="@+id/menu_emulation_adjust_scale"
32 android:title="@string/emulation_control_scale" />
33
34 <group android:checkableBehavior="all">
35 <item
36 android:id="@+id/menu_emulation_joystick_rel_center"
37 android:checkable="true"
38 android:title="@string/emulation_control_joystick_rel_center"/>
39 <item
40 android:id="@+id/menu_emulation_dpad_slide_enable"
41 android:checkable="true"
42 android:title="@string/emulation_control_dpad_slide_enable" />
43 </group>
44
45 <item
46 android:id="@+id/menu_emulation_reset_overlay"
47 android:title="@string/emulation_touch_overlay_reset" />
48 </menu>
49 </item>
50
51 <item
52 android:id="@+id/menu_emulation_amiibo"
53 android:title="@string/menu_emulation_amiibo">
54 <menu>
55 <item
56 android:id="@+id/menu_emulation_amiibo_load"
57 android:title="@string/menu_emulation_amiibo_load" />
58
59 <item
60 android:id="@+id/menu_emulation_amiibo_remove"
61 android:title="@string/menu_emulation_amiibo_remove" />
62 </menu>
63 </item>
64
65 <item
66 android:id="@+id/menu_emulation_switch_screen_layout"
67 app:showAsAction="never"
68 android:title="@string/emulation_switch_screen_layout">
69 <menu>
70 <group android:checkableBehavior="single">
71 <item
72 android:id="@+id/menu_screen_layout_landscape"
73 android:title="@string/emulation_screen_layout_landscape" />
74
75 <item
76 android:id="@+id/menu_screen_layout_portrait"
77 android:title="@string/emulation_screen_layout_portrait" />
78
79 <item
80 android:id="@+id/menu_screen_layout_single"
81 android:title="@string/emulation_screen_layout_single" />
82
83 <item
84 android:id="@+id/menu_screen_layout_sidebyside"
85 android:title="@string/emulation_screen_layout_sidebyside" />
86 </group>
87 </menu>
88 </item>
89
90 <item
91 android:id="@+id/menu_emulation_swap_screens"
92 app:showAsAction="never"
93 android:title="@string/emulation_swap_screens"
94 android:checkable="true" />
95
96 <item
97 android:id="@+id/menu_emulation_show_fps"
98 app:showAsAction="never"
99 android:title="@string/emulation_show_fps"
100 android:checkable="true" />
101
102 <item
103 android:id="@+id/menu_emulation_show_overlay"
104 app:showAsAction="never"
105 android:title="@string/emulation_show_overlay"
106 android:checkable="true" />
107
108 <item
109 android:id="@+id/menu_emulation_open_cheats"
110 app:showAsAction="never"
111 android:title="@string/emulation_open_cheats" />
112
113 <item
114 android:id="@+id/menu_emulation_open_settings"
115 app:showAsAction="never"
116 android:title="@string/emulation_open_settings" />
117
118</menu>
diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml
new file mode 100644
index 000000000..9cdcc7f08
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_game_grid.xml
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto">
4
5 <item
6 android:id="@+id/button_premium"
7 android:icon="@drawable/ic_premium"
8 android:title="@string/premium_text"
9 app:showAsAction="ifRoom" />
10 <item
11 android:id="@+id/button_file_menu"
12 android:icon="@drawable/ic_folder"
13 android:title="@string/select_game_folder"
14 app:showAsAction="ifRoom">
15 <menu>
16 <item
17 android:id="@+id/button_add_directory"
18 android:icon="@drawable/ic_folder"
19 android:title="@string/select_game_folder"
20 app:showAsAction="ifRoom" />
21 <item
22 android:id="@+id/button_install_cia"
23 android:icon="@drawable/ic_cia_install"
24 android:title="@string/install_cia_title"
25 app:showAsAction="ifRoom" />
26 </menu>
27 </item>
28 <item
29 android:id="@+id/menu_settings_core"
30 android:icon="@drawable/ic_settings_core"
31 android:title="@string/grid_menu_core_settings"
32 app:showAsAction="ifRoom" />
33
34</menu>
diff --git a/src/android/app/src/main/res/menu/menu_settings.xml b/src/android/app/src/main/res/menu/menu_settings.xml
new file mode 100644
index 000000000..1fe7aa6d4
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_settings.xml
@@ -0,0 +1,2 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu /> \ No newline at end of file
diff --git a/src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..c9ad5f98f
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3 <background android:drawable="@color/ic_launcher_background" />
4 <foreground android:drawable="@mipmap/ic_launcher_foreground" />
5</adaptive-icon> \ No newline at end of file
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..57ea32d88
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..18cc694d1
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..7052f4077
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0e7cdeed6
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..4d3e9fc41
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..b57a8d623
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d2c6d0692
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..22f6eb36f
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1aa7f3ae2
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..b57c8f75b
--- /dev/null
+++ b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/src/android/app/src/main/res/values-night/colors.xml b/src/android/app/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..43b948021
--- /dev/null
+++ b/src/android/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <color name="citra_orange">#272727</color>
5 <color name="citra_orange_dark">#121212</color>
6 <color name="citra_accent">#FEC303</color>
7
8 <color name="card_view_background">#121212</color>
9 <color name="card_view_disabled">#3D3D3D</color>
10
11 <color name="gamelist_divider">#404040</color>
12
13 <color name="header_text">#E0E0E0</color>
14 <color name="header_subtext">#A0A0A0</color>
15
16 <color name="citra_logo_text_color">@color/citra_accent</color>
17</resources>
diff --git a/src/android/app/src/main/res/values-night/styles_filepicker.xml b/src/android/app/src/main/res/values-night/styles_filepicker.xml
new file mode 100644
index 000000000..1a175cdcf
--- /dev/null
+++ b/src/android/app/src/main/res/values-night/styles_filepicker.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="FilePickerBaseTheme" parent="NNF_BaseTheme" />
5</resources>
diff --git a/src/android/app/src/main/res/values-w1000dp/integers.xml b/src/android/app/src/main/res/values-w1000dp/integers.xml
new file mode 100644
index 000000000..5cd4e24f3
--- /dev/null
+++ b/src/android/app/src/main/res/values-w1000dp/integers.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <integer name="game_grid_columns">4</integer>
4</resources> \ No newline at end of file
diff --git a/src/android/app/src/main/res/values-w1050dp/dimens.xml b/src/android/app/src/main/res/values-w1050dp/dimens.xml
new file mode 100644
index 000000000..92fcb2b66
--- /dev/null
+++ b/src/android/app/src/main/res/values-w1050dp/dimens.xml
@@ -0,0 +1,6 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <!-- Example customization of dimensions originally defined in res/values/dimens.xml
4 (such as screen margins) for screens with more than 1024dp of available width. -->
5 <dimen name="activity_horizontal_margin">96dp</dimen>
6</resources>
diff --git a/src/android/app/src/main/res/values-w500dp/integers.xml b/src/android/app/src/main/res/values-w500dp/integers.xml
new file mode 100644
index 000000000..d2955c0ae
--- /dev/null
+++ b/src/android/app/src/main/res/values-w500dp/integers.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <integer name="game_grid_columns">2</integer>
4</resources> \ No newline at end of file
diff --git a/src/android/app/src/main/res/values-w750dp/integers.xml b/src/android/app/src/main/res/values-w750dp/integers.xml
new file mode 100644
index 000000000..f049d8b44
--- /dev/null
+++ b/src/android/app/src/main/res/values-w750dp/integers.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <integer name="game_grid_columns">3</integer>
4</resources> \ No newline at end of file
diff --git a/src/android/app/src/main/res/values-w820dp/dimens.xml b/src/android/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..d27181e85
--- /dev/null
+++ b/src/android/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,5 @@
1<resources>
2 <!-- Example customization of dimensions originally defined in res/values/dimens.xml
3 (such as screen margins) for screens with more than 820dp of available width. -->
4 <dimen name="activity_horizontal_margin">64dp</dimen>
5</resources>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..c948e6a8b
--- /dev/null
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -0,0 +1,174 @@
1<?xml version="1.0" encoding="utf-8"?>
2
3<!-- All lists for ListPreference keys/values are placed here -->
4<resources>
5 <string-array name="systemClockNames" translatable="true">
6 <item>Device clock</item>
7 <item>Simulated clock</item>
8 </string-array>
9
10 <integer-array name="systemClockValues" translatable="false">
11 <item>0</item>
12 <item>1</item>
13 </integer-array>
14
15 <string-array name="designNames" translatable="true">
16 <item>Light</item>
17 <item>Dark</item>
18 <item>System default</item>
19 </string-array>
20
21 <integer-array name="designValues" translatable="false">
22 <item>0</item>
23 <item>1</item>
24 <item>2</item>
25 </integer-array>
26
27 <!-- Pre-Android 10 does not support System Default -->
28 <string-array name="designNamesOld" translatable="true">
29 <item>Light</item>
30 <item>Dark</item>
31 </string-array>
32
33 <!-- Pre-Android 10 does not support System Default -->
34 <integer-array name="designValuesOld" translatable="false">
35 <item>0</item>
36 <item>1</item>
37 </integer-array>
38
39 <string-array name="regionNames">
40 <item>Auto-select</item>
41 <item>Japan</item>
42 <item>USA</item>
43 <item>Europe</item>
44 <item>Australia</item>
45 <item>China</item>
46 <item>Korea</item>
47 <item>Taiwan</item>
48 </string-array>
49
50 <integer-array name="regionValues">
51 <item>-1</item>
52 <item>0</item>
53 <item>1</item>
54 <item>2</item>
55 <item>3</item>
56 <item>4</item>
57 <item>5</item>
58 <item>6</item>
59 </integer-array>
60
61 <string-array name="languageNames">
62 <item>Japanese (日本語)</item>
63 <item>English</item>
64 <item>French (français)</item>
65 <item>German (Deutsch)</item>
66 <item>Italian (italiano)</item>
67 <item>Spanish (español)</item>
68 <item>Simplified Chinese (简体中文)</item>
69 <item>Korean (한국어)</item>
70 <item>Dutch (Nederlands)</item>
71 <item>Portuguese (português)</item>
72 <item>Russian (Русский)</item>
73 <item>Traditional Chinese (正體中文)</item>
74 </string-array>
75
76 <integer-array name="languageValues">
77 <item>0</item>
78 <item>1</item>
79 <item>2</item>
80 <item>3</item>
81 <item>4</item>
82 <item>5</item>
83 <item>6</item>
84 <item>7</item>
85 <item>8</item>
86 <item>9</item>
87 <item>10</item>
88 <item>11</item>
89 </integer-array>
90
91 <string-array name="n3dsButtons">
92 <item>a</item>
93 <item>b</item>
94 <item>x</item>
95 <item>y</item>
96 <item>L</item>
97 <item>R</item>
98 <item>ZL</item>
99 <item>ZR</item>
100 <item>Start</item>
101 <item>Select</item>
102 <item>D-Pad</item>
103 <item>Circle Pad</item>
104 <item>C Stick</item>
105 </string-array>
106
107 <string-array name="cameraImageSourceNames">
108 <item>Blank</item>
109 <item>Still Image</item>
110 <item>Device Camera</item>
111 </string-array>
112
113 <string-array name="cameraImageSourceValues">
114 <item>blank</item>
115 <item>image</item>
116 <item>ndk</item>
117 </string-array>
118
119 <string-array name="cameraDeviceNames">
120 <item>Default</item>
121 <item>Any Front Camera</item>
122 <item>Any Back Camera</item>
123 </string-array>
124
125 <string-array name="cameraDeviceValues">
126 <item />
127 <item>_front</item>
128 <item>_back</item>
129 </string-array>
130
131 <string-array name="cameraFlipNames">
132 <item>None</item>
133 <item>Horizontal</item>
134 <item>Vertical</item>
135 <item>Reverse</item>
136 </string-array>
137
138 <integer-array name="cameraFlipValues">
139 <item>0</item>
140 <item>1</item>
141 <item>2</item>
142 <item>3</item>
143 </integer-array>
144
145 <string-array name="audioInputTypeNames">
146 <item>None</item>
147 <item>Real Device</item>
148 <item>Static Noise</item>
149 </string-array>
150
151 <integer-array name="audioInputTypeValues">
152 <item>0</item>
153 <item>1</item>
154 <item>2</item>
155 </integer-array>
156
157 <string-array name="render3dModes">
158 <item>Off</item>
159 <item>Side by Side</item>
160 <item>Anaglyph</item>
161 <item>Interlaced</item>
162 <item>Reverse Interlaced</item>
163 <item>Cardboard VR</item>
164 </string-array>
165
166 <integer-array name="render3dValues">
167 <item>0</item>
168 <item>1</item>
169 <item>2</item>
170 <item>3</item>
171 <item>4</item>
172 <item>5</item>
173 </integer-array>
174</resources>
diff --git a/src/android/app/src/main/res/values/colors.xml b/src/android/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..6668288a7
--- /dev/null
+++ b/src/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <color name="citra_orange">#FFC303</color>
5 <color name="citra_orange_dark">#FF8D03</color>
6 <color name="citra_accent">#CC7102</color>
7
8 <color name="card_view_background">#ffffff</color>
9 <color name="card_view_disabled">#D5D5D5</color>
10
11 <color name="gamelist_divider">#ffffff</color>
12
13 <color name="header_text">#1C1424</color>
14 <color name="header_subtext">#5C5661</color>
15
16 <color name="citra_logo_text_color">@color/header_text</color>
17</resources>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..b3d186d88
--- /dev/null
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
1<resources>
2 <!-- Default screen margins, per the Android Design guidelines. -->
3 <dimen name="activity_horizontal_margin">16dp</dimen>
4
5 <dimen name="spacing_small">4dp</dimen>
6 <dimen name="spacing_medlarge">12dp</dimen>
7 <dimen name="spacing_large">16dp</dimen>
8
9 <dimen name="dialog_margin">20dp</dimen>
10</resources>
diff --git a/src/android/app/src/main/res/values/ic_launcher_background.xml b/src/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..c5d5899fd
--- /dev/null
+++ b/src/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <color name="ic_launcher_background">#FFFFFF</color>
4</resources> \ No newline at end of file
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
new file mode 100644
index 000000000..9f6d8492e
--- /dev/null
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -0,0 +1,65 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <integer name="game_grid_columns">1</integer>
4
5 <!-- Default N3DS landscape layout -->
6 <integer name="N3DS_BUTTON_A_X">930</integer>
7 <integer name="N3DS_BUTTON_A_Y">620</integer>
8 <integer name="N3DS_BUTTON_B_X">870</integer>
9 <integer name="N3DS_BUTTON_B_Y">720</integer>
10 <integer name="N3DS_BUTTON_X_X">870</integer>
11 <integer name="N3DS_BUTTON_X_Y">520</integer>
12 <integer name="N3DS_BUTTON_Y_X">810</integer>
13 <integer name="N3DS_BUTTON_Y_Y">620</integer>
14 <integer name="N3DS_BUTTON_UP_X">15</integer>
15 <integer name="N3DS_BUTTON_UP_Y">470</integer>
16 <integer name="N3DS_TRIGGER_L_X">13</integer>
17 <integer name="N3DS_TRIGGER_L_Y">0</integer>
18 <integer name="N3DS_BUTTON_ZL_X">13</integer>
19 <integer name="N3DS_BUTTON_ZL_Y">110</integer>
20 <integer name="N3DS_TRIGGER_R_X">895</integer>
21 <integer name="N3DS_TRIGGER_R_Y">0</integer>
22 <integer name="N3DS_BUTTON_ZR_X">895</integer>
23 <integer name="N3DS_BUTTON_ZR_Y">110</integer>
24 <integer name="N3DS_STICK_C_X">740</integer>
25 <integer name="N3DS_STICK_C_Y">770</integer>
26 <integer name="N3DS_STICK_MAIN_X">100</integer>
27 <integer name="N3DS_STICK_MAIN_Y">670</integer>
28 <integer name="N3DS_BUTTON_SELECT_X">470</integer>
29 <integer name="N3DS_BUTTON_SELECT_Y">850</integer>
30 <integer name="N3DS_BUTTON_START_X">550</integer>
31 <integer name="N3DS_BUTTON_START_Y">850</integer>
32 <integer name="N3DS_BUTTON_HOME_X">450</integer>
33 <integer name="N3DS_BUTTON_HOME_Y">850</integer>
34
35 <!-- Default N3DS portrait layout -->
36 <integer name="N3DS_BUTTON_A_PORTRAIT_X">810</integer>
37 <integer name="N3DS_BUTTON_A_PORTRAIT_Y">870</integer>
38 <integer name="N3DS_BUTTON_B_PORTRAIT_X">710</integer>
39 <integer name="N3DS_BUTTON_B_PORTRAIT_Y">925</integer>
40 <integer name="N3DS_BUTTON_X_PORTRAIT_X">710</integer>
41 <integer name="N3DS_BUTTON_X_PORTRAIT_Y">815</integer>
42 <integer name="N3DS_BUTTON_Y_PORTRAIT_X">610</integer>
43 <integer name="N3DS_BUTTON_Y_PORTRAIT_Y">870</integer>
44 <integer name="N3DS_BUTTON_UP_PORTRAIT_X">10</integer>
45 <integer name="N3DS_BUTTON_UP_PORTRAIT_Y">680</integer>
46 <integer name="N3DS_TRIGGER_L_PORTRAIT_X">10</integer>
47 <integer name="N3DS_TRIGGER_L_PORTRAIT_Y">0</integer>
48 <integer name="N3DS_BUTTON_ZL_PORTRAIT_X">10</integer>
49 <integer name="N3DS_BUTTON_ZL_PORTRAIT_Y">70</integer>
50 <integer name="N3DS_TRIGGER_R_PORTRAIT_X">810</integer>
51 <integer name="N3DS_TRIGGER_R_PORTRAIT_Y">0</integer>
52 <integer name="N3DS_BUTTON_ZR_PORTRAIT_X">810</integer>
53 <integer name="N3DS_BUTTON_ZR_PORTRAIT_Y">70</integer>
54 <integer name="N3DS_STICK_C_PORTRAIT_X">800</integer>
55 <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer>
56 <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</integer>
57 <integer name="N3DS_STICK_MAIN_PORTRAIT_Y">840</integer>
58 <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">360</integer>
59 <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">794</integer>
60 <integer name="N3DS_BUTTON_SELECT_PORTRAIT_X">400</integer>
61 <integer name="N3DS_BUTTON_SELECT_PORTRAIT_Y">794</integer>
62 <integer name="N3DS_BUTTON_START_PORTRAIT_X">520</integer>
63 <integer name="N3DS_BUTTON_START_PORTRAIT_Y">794</integer>
64
65</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..85422e849
--- /dev/null
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,246 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <!-- General application strings -->
5 <string name="app_name" translatable="false">yuzu</string>
6 <string name="app_disclaimer">This software will run games for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you run, please place your rightfully owned 3DS game files onto your device storage.</string>
7 <string name="app_notification_channel_name" translatable="false">yuzu</string>
8 <string name="app_notification_channel_id" translatable="false">yuzu</string>
9 <string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
10 <string name="app_notification_running">yuzu is running</string>
11
12 <!-- Input related strings -->
13 <string name="controller_circlepad">Circle Pad</string>
14 <string name="controller_c">C-Stick</string>
15 <string name="controller_triggers">Triggers</string>
16 <string name="controller_dpad">D-Pad</string>
17 <string name="controller_axis_vertical">Up/Down Axis</string>
18 <string name="controller_axis_horizontal">Left/Right Axis</string>
19 <string name="input_binding">Input Binding</string>
20 <string name="input_binding_description">Press or move an input to bind it to %1$s.</string>
21 <string name="input_binding_description_vertical_axis">Move your joystick up or down.</string>
22 <string name="input_binding_description_horizontal_axis">Move your joystick left or right.</string>
23 <string name="button_a" translatable="false">A</string>
24 <string name="button_b" translatable="false">B</string>
25 <string name="button_select" translatable="false">SELECT</string>
26 <string name="button_start" translatable="false">START</string>
27 <string name="button_x" translatable="false">X</string>
28 <string name="button_y" translatable="false">Y</string>
29 <string name="button_l" translatable="false">L</string>
30 <string name="button_r" translatable="false">R</string>
31 <string name="button_zl" translatable="false">ZL</string>
32 <string name="button_zr" translatable="false">ZR</string>
33 <string name="input_message_analog_only">This control must be bound to a gamepad analog stick or D-pad axis!</string>
34 <string name="input_message_button_only">This control must be bound to a gamepad button!</string>
35
36 <!-- Generic buttons (Shared with lots of stuff) -->
37 <string name="generic_buttons">Buttons</string>
38
39 <!-- Premium settings strings -->
40 <string name="design">Change Theme (Light, Dark)</string>
41 <string name="design_updated">Theme will update when exiting Settings</string>
42
43 <!-- Core settings strings -->
44 <string name="cpu_jit">Enable CPU JIT</string>
45 <string name="cpu_jit_description">Uses the Just-in-Time (JIT) compiler for CPU emulation. When enabled, game performance will be significantly improved.</string>
46 <string name="init_clock">System clock type</string>
47 <string name="init_clock_description">Set the emulated 3DS clock to either reflect that of your device or start at a simulated date and time.</string>
48
49 <!-- System settings strings -->
50 <string name="init_time">System clock starting time override</string>
51 <string name="init_time_description">If the \"System clock type\" setting is set to \"Simulated clock\", this changes the fixed date and time to start at.</string>
52 <string name="emulated_region">Emulated region</string>
53 <string name="emulated_language">Emulated language</string>
54
55 <!-- Camera settings strings -->
56 <string name="inner_camera">Inner Camera</string>
57 <string name="outer_left_camera">Outer Left Camera</string>
58 <string name="outer_right_camera">Outer Right Camera</string>
59 <string name="image_source">Image Source</string>
60 <string name="image_source_description">Sets the image source of the virtual camera. You can use an image file, or a device camera when supported.</string>
61 <string name="camera_device">Camera Device</string>
62 <string name="camera_device_description">If the \"Image Source\" setting is set to \"Device Camera\", this sets the physical camera to use.</string>
63 <string name="camera_facing_front">Front</string>
64 <string name="camera_facing_back">Back</string>
65 <string name="camera_facing_external">External</string>
66 <string name="image_flip">Image Flip</string>
67
68 <!-- Graphics settings strings -->
69 <string name="renderer">Renderer</string>
70 <string name="vsync">Enable V-Sync</string>
71 <string name="vsync_description">Synchronizes the game frame rate to the refresh rate of your device.</string>
72 <string name="linear_filtering">Enable linear filtering</string>
73 <string name="linear_filtering_description">Enables linear filtering, which causes game visuals to appear smoother.</string>
74 <string name="texture_filter_name">Texture Filter</string>
75 <string name="texture_filter_description">Enhances the visuals of games by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, and xBRZ freescale.</string>
76 <string name="hw_renderer">Enable hardware renderer</string>
77 <string name="hw_renderer_description">Uses hardware to emulate 3DS graphics. When enabled, game performance will be significantly improved.</string>
78 <string name="hw_shaders">Enable hardware shader</string>
79 <string name="hw_shaders_description">Uses hardware to emulate 3DS shaders. When enabled, game performance will be significantly improved.</string>
80 <string name="shaders_accurate_mul">Enable accurate shader multiplication</string>
81 <string name="shaders_accurate_mul_description">Uses more accurate multiplication in hardware shaders, which may fix some graphical bugs. When enabled, performance will be reduced.</string>
82 <string name="asynchronous_gpu">Enable asynchronous GPU emulation</string>
83 <string name="asynchronous_gpu_description">Uses a separate thread to emulate the GPU asynchronously. When enabled, performance will be improved.</string>
84 <string name="frame_limit_enable">Enable limit speed</string>
85 <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string>
86 <string name="frame_limit_slider">Limit speed percent</string>
87 <string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. With the default of 100% emulation will be limited to normal speed. Values higher or lower will increase or decrease the speed limit.</string>
88 <string name="internal_resolution">Internal resolution</string>
89 <string name="internal_resolution_description">Specifies the resolution used to render at. A high resolution will improve visual quality a lot but is also quite heavy on performance and might cause glitches in certain games.</string>
90 <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string>
91 <string name="debug_warning">Warning: Modifying these settings will slow emulation</string>
92 <string name="stereoscopy">Stereoscopy</string>
93 <string name="render3d">Stereoscopic 3D Mode</string>
94 <string name="factor3d">Depth</string>
95 <string name="factor3d_description">Specifies the value of the 3D slider. This should be set to higher than 0% when Stereoscopic 3D is enabled.</string>
96 <string name="cardboard_vr">Cardboard VR</string>
97 <string name="cardboard_screen_size">Cardboard Screen size</string>
98 <string name="cardboard_screen_size_description">Scales the screen to a percentage of its original size.</string>
99 <string name="cardboard_x_shift">Horizontal shift</string>
100 <string name="cardboard_x_shift_description">Specifies the percentage of empty space to shift the screens horizontally. Positive values move the two eyes closer to the middle, while negative values move them away.</string>
101 <string name="cardboard_y_shift">Vertical shift</string>
102 <string name="cardboard_y_shift_description">Specifies the percentage of empty space to shift the screens vertically. Positive values move the two eyes towards the bottom, while negative values move them towards the top.</string>
103 <string name="use_shader_jit">Use shader JIT</string>
104 <string name="use_disk_shader_cache">Use disk shader cache</string>
105 <string name="use_disk_shader_cache_description">Reduce stuttering by storing and loading generated shaders to disk. It cannot be used without Enabling Hardware Shader.</string>
106 <string name="utility">Utility</string>
107 <string name="dump_textures">Dump textures</string>
108 <string name="dump_textures_description">Dumps textures to dump/textures/[GAME ID]</string>
109 <string name="custom_textures">Use custom textures</string>
110 <string name="custom_textures_description">Uses custom textures found in load/textures/[GAME ID]</string>
111 <string name="preload_textures">Preload custom textures</string>
112 <string name="preload_textures_description">Loads all custom textures into memory. This feature can use a lot of memory.</string>
113 <!-- Premium strings -->
114 <string name="premium_text">Premium</string>
115 <string name="premium_settings_upsell">Upgrade to Premium and support yuzu!</string>
116 <string name="premium_settings_upsell_description">With Premium, you will support the developers to continue improving yuzu, and gain access to these exclusive features!</string>
117 <string name="premium_settings_welcome">Welcome to Premium.</string>
118 <string name="premium_settings_welcome_description">Thank you for your support!</string>
119
120 <!-- Audio settings strings -->
121 <string name="audio_stretch">Enable audio stretching</string>
122 <string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string>
123 <string name="audio_input_type">Audio Input Device</string>
124
125 <!-- Miscellaneous -->
126 <string name="clear">Clear</string>
127 <string name="slider_default">Default</string>
128 <string name="ini_saved">Saved settings</string>
129 <string name="gameid_saved">Saved settings for %1$s</string>
130 <string name="error_saving">Error saving %1$s.ini: %2$s</string>
131 <string name="loading">Loading...</string>
132
133 <!-- Game Grid Screen-->
134 <string name="grid_menu_core_settings">Settings</string>
135
136 <!-- Add Directory Screen-->
137 <string name="select_game_folder">Select Game Folder</string>
138 <string name="install_cia_title">Install CIA</string>
139
140 <!-- Preferences Screen -->
141 <string name="preferences_settings">Settings</string>
142 <string name="preferences_premium">Premium</string>
143 <string name="preferences_general">General</string>
144 <string name="preferences_system">System</string>
145 <string name="preferences_camera">Camera</string>
146 <string name="preferences_controls">Gamepad</string>
147 <string name="preferences_graphics">Graphics</string>
148 <string name="preferences_audio">Audio</string>
149 <string name="preferences_debug">Debug</string>
150
151 <!-- ROM loading errors -->
152 <string name="loader_error_encrypted">Your ROM is encrypted</string>
153 <string name="loader_error_invalid_format">Invalid ROM format</string>
154
155 <!-- Emulation Menu -->
156 <string name="emulation_save_state">Save State</string>
157 <string name="emulation_load_state">Load State</string>
158 <string name="emulation_empty_state_slot">Slot %1$d</string>
159 <string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</string>
160 <string name="emulation_show_fps">Show FPS</string>
161 <string name="emulation_configure_controls">Configure Controls</string>
162 <string name="emulation_edit_layout">Edit Layout</string>
163 <string name="emulation_done">Done</string>
164 <string name="emulation_toggle_controls">Toggle Controls</string>
165 <string name="emulation_control_scale">Adjust Scale</string>
166 <string name="emulation_control_joystick_rel_center">Relative Stick Center</string>
167 <string name="emulation_control_dpad_slide_enable">Enable D-Pad Sliding</string>
168 <string name="emulation_open_settings">Open Settings</string>
169 <string name="emulation_open_cheats">Open Cheats</string>
170 <string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
171 <string name="emulation_screen_layout_landscape">Default</string>
172 <string name="emulation_screen_layout_portrait">Portrait</string>
173 <string name="emulation_screen_layout_single">Single Screen</string>
174 <string name="emulation_screen_layout_sidebyside">Side by Side Screens</string>
175 <string name="emulation_swap_screens">Swap Screens</string>
176 <string name="emulation_touch_overlay_reset">Reset Overlay</string>
177 <string name="emulation_show_overlay">Show Overlay</string>
178 <string name="emulation_close_game">Close Game</string>
179 <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
180 <string name="menu_emulation_amiibo">Amiibo</string>
181 <string name="menu_emulation_amiibo_load">Load</string>
182 <string name="menu_emulation_amiibo_remove">Remove</string>
183 <string name="select_amiibo">Select Amiibo file</string>
184 <string name="amiibo_load_error">Error loading Amiibo</string>
185 <string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string>
186
187 <string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
188 <string name="load_settings">Loading Settings...</string>
189
190 <string name="external_storage_not_mounted">The external storage needs to be available in order to use yuzu</string>
191
192 <string name="select_dir">Select This Directory</string>
193 <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
194
195 <string name="do_not_show_this_again">Do not show this again</string>
196 <string name="savestate_warning_title">Savestates</string>
197 <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>
198
199 <!-- Software Keyboard -->
200 <string name="software_keyboard">Software Keyboard</string>
201 <string name="i_forgot">I Forgot</string>
202 <string name="fixed_length_required">Text length is not correct (should be %d characters)</string>
203 <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string>
204 <string name="blank_input_not_allowed">Blank input is not allowed</string>
205 <string name="empty_input_not_allowed">Empty input is not allowed</string>
206
207 <!-- Mii Selector -->
208 <string name="mii_selector">Mii Selector</string>
209 <string name="standard_mii">Standard Mii</string>
210
211 <!-- Camera -->
212 <string name="camera_select_image">Select Image</string>
213 <string name="camera">Camera</string>
214 <string name="camera_permission_needed">yuzu needs to access your camera to emulate the 3DS\'s cameras.\n\nAlternatively, you can also set \"Image Source\" to \"Still Image\" in Camera Settings.</string>
215
216 <!-- Microphone -->
217 <string name="microphone">Microphone</string>
218 <string name="microphone_permission_needed">yuzu needs to access your microphone to emulate the 3DS\'s microphone.\n\nAlternatively, you can also change \"Audio Input Device\" in Audio Settings.</string>
219
220 <!-- Core Errors -->
221 <string name="abort_button">Abort</string>
222 <string name="continue_button">Continue</string>
223 <string name="system_archive_not_found">System Archive Not Found</string>
224 <string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string>
225 <string name="system_archive_general">A system archive</string>
226 <string name="save_load_error">Save/Load Error</string>
227 <string name="fatal_error">Fatal Error</string>
228 <string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string>
229
230 <!-- Disk shader cache -->
231 <string name="preparing_shaders">Preparing shaders</string>
232 <string name="building_shaders">Building shaders</string>
233
234 <!-- Cheats -->
235 <string name="cheats">Cheats</string>
236 <string name="cheats_add">Add Cheat</string>
237 <string name="cheats_name">Name</string>
238 <string name="cheats_notes">Notes</string>
239 <string name="cheats_code">Code</string>
240 <string name="cheats_edit">Edit</string>
241 <string name="cheats_delete">Delete</string>
242 <string name="cheats_delete_confirmation">Are you sure you want to delete \"%1$s\"?</string>
243 <string name="cheats_error_no_name">Name can\'t be empty</string>
244 <string name="cheats_error_no_code_lines">Code can\'t be empty</string>
245 <string name="cheats_error_on_line">Error on line %1$d</string>
246</resources>
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..47fe6f6ea
--- /dev/null
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,65 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <!-- Inherit from the material theme -->
5 <style name="CitraBase" parent="Theme.AppCompat.DayNight.NoActionBar">
6 <!-- Main theme colors -->
7 <!-- Branding color for the app bar -->
8 <item name="colorPrimary">@color/citra_orange</item>
9 <!-- Darker variant for the status bar and contextual app bars -->
10 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
11 <item name="colorAccent">@color/citra_accent</item>
12
13 <item name="titleTextColor">@color/citra_logo_text_color</item>
14
15 <!-- Enable window content transitions -->
16 <item name="android:windowContentTransitions">true</item>
17 <item name="android:windowAllowEnterTransitionOverlap">true</item>
18 <item name="android:windowAllowReturnTransitionOverlap">true</item>
19
20 <item name="android:colorControlHighlight">?attr/colorAccent</item>
21 </style>
22
23 <!-- Same as above, but use default action bar, and mandate margins. -->
24 <style name="CitraSettingsBase" parent="Theme.AppCompat.DayNight">
25 <item name="colorPrimary">@color/citra_orange</item>
26 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
27 <item name="colorAccent">@color/citra_accent</item>
28 </style>
29
30 <!-- Inherit from the Base Citra Dialog Theme -->
31 <style name="CitraEmulationBase" parent="Theme.AppCompat.DayNight">
32 <item name="colorPrimary">@color/citra_orange</item>
33 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
34 <item name="colorAccent">@color/citra_accent</item>
35 <item name="android:windowTranslucentNavigation">true</item>
36
37 <item name="android:windowBackground">@android:color/black</item>
38
39 <!-- Enable window content transitions -->
40 <item name="android:windowContentTransitions">true</item>
41 <item name="android:windowAllowEnterTransitionOverlap">true</item>
42 <item name="android:windowAllowReturnTransitionOverlap">true</item>
43 </style>
44
45 <!-- Inherit from a base file picker theme that handles day/night -->
46 <style name="FilePickerTheme" parent="FilePickerBaseTheme">
47 <item name="colorPrimary">@color/citra_orange</item>
48 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
49 <item name="colorAccent">@color/citra_accent</item>
50 <item name="android:windowBackground">@color/card_view_background</item>
51
52 <!-- Need to set this also to style create folder dialog -->
53 <item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
54
55 <item name="nnf_list_item_divider">@drawable/gamelist_divider</item>
56 <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
57 </style>
58
59 <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
60 <item name="colorPrimary">@color/citra_orange</item>
61 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
62 <item name="colorAccent">@color/citra_accent</item>
63 </style>
64
65</resources>
diff --git a/src/android/app/src/main/res/values/styles_filepicker.xml b/src/android/app/src/main/res/values/styles_filepicker.xml
new file mode 100644
index 000000000..0b0c3fe1a
--- /dev/null
+++ b/src/android/app/src/main/res/values/styles_filepicker.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" />
5</resources>
diff --git a/src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java b/src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java
new file mode 100644
index 000000000..4e4bb317f
--- /dev/null
+++ b/src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java
@@ -0,0 +1,17 @@
1package org.citra.citra_emu;
2
3import org.junit.Test;
4
5import static org.junit.Assert.*;
6
7/**
8 * Example local unit test, which will execute on the development machine (host).
9 *
10 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
11 */
12public class ExampleUnitTest {
13 @Test
14 public void addition_isCorrect() {
15 assertEquals(4, 2 + 2);
16 }
17} \ No newline at end of file
diff --git a/src/android/build.gradle b/src/android/build.gradle
new file mode 100644
index 000000000..9d7571a4e
--- /dev/null
+++ b/src/android/build.gradle
@@ -0,0 +1,26 @@
1// Top-level build file where you can add configuration options common to all sub-projects/modules.
2
3buildscript {
4
5 repositories {
6 google()
7 jcenter()
8 }
9 dependencies {
10 classpath 'com.android.tools.build:gradle:7.3.1'
11
12 // NOTE: Do not place your application dependencies here; they belong
13 // in the individual module build.gradle files
14 }
15}
16
17allprojects {
18 repositories {
19 google()
20 jcenter()
21 }
22}
23
24task clean(type: Delete) {
25 delete rootProject.buildDir
26}
diff --git a/src/android/code-style-java.xml b/src/android/code-style-java.xml
new file mode 100644
index 000000000..a8ed003c8
--- /dev/null
+++ b/src/android/code-style-java.xml
@@ -0,0 +1,240 @@
1<code_scheme name="Citra-Java" version="173">
2 <option name="OTHER_INDENT_OPTIONS">
3 <value>
4 <option name="INDENT_SIZE" value="2" />
5 <option name="TAB_SIZE" value="2" />
6 </value>
7 </option>
8 <option name="RIGHT_MARGIN" value="100" />
9 <AndroidXmlCodeStyleSettings>
10 <option name="USE_CUSTOM_SETTINGS" value="true" />
11 </AndroidXmlCodeStyleSettings>
12 <JavaCodeStyleSettings>
13 <option name="FIELD_NAME_PREFIX" value="m_" />
14 <option name="STATIC_FIELD_NAME_PREFIX" value="s_" />
15 <option name="ANNOTATION_PARAMETER_WRAP" value="1" />
16 <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
17 <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
18 <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
19 <value />
20 </option>
21 <option name="IMPORT_LAYOUT_TABLE">
22 <value>
23 <package name="android" withSubpackages="true" static="false" />
24 <emptyLine />
25 <package name="com" withSubpackages="true" static="false" />
26 <emptyLine />
27 <package name="junit" withSubpackages="true" static="false" />
28 <emptyLine />
29 <package name="net" withSubpackages="true" static="false" />
30 <emptyLine />
31 <package name="org" withSubpackages="true" static="false" />
32 <emptyLine />
33 <package name="java" withSubpackages="true" static="false" />
34 <emptyLine />
35 <package name="javax" withSubpackages="true" static="false" />
36 <emptyLine />
37 <package name="" withSubpackages="true" static="false" />
38 <emptyLine />
39 <package name="" withSubpackages="true" static="true" />
40 <emptyLine />
41 </value>
42 </option>
43 <option name="JD_P_AT_EMPTY_LINES" value="false" />
44 </JavaCodeStyleSettings>
45 <XML>
46 <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
47 </XML>
48 <codeStyleSettings language="JAVA">
49 <option name="BRACE_STYLE" value="2" />
50 <option name="CLASS_BRACE_STYLE" value="2" />
51 <option name="METHOD_BRACE_STYLE" value="2" />
52 <option name="LAMBDA_BRACE_STYLE" value="2" />
53 <option name="ELSE_ON_NEW_LINE" value="true" />
54 <option name="WHILE_ON_NEW_LINE" value="true" />
55 <option name="CATCH_ON_NEW_LINE" value="true" />
56 <option name="FINALLY_ON_NEW_LINE" value="true" />
57 <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
58 <option name="CALL_PARAMETERS_WRAP" value="1" />
59 <option name="METHOD_PARAMETERS_WRAP" value="1" />
60 <option name="RESOURCE_LIST_WRAP" value="1" />
61 <option name="EXTENDS_LIST_WRAP" value="1" />
62 <option name="THROWS_LIST_WRAP" value="1" />
63 <option name="EXTENDS_KEYWORD_WRAP" value="1" />
64 <option name="THROWS_KEYWORD_WRAP" value="1" />
65 <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
66 <option name="BINARY_OPERATION_WRAP" value="1" />
67 <option name="TERNARY_OPERATION_WRAP" value="1" />
68 <option name="FOR_STATEMENT_WRAP" value="1" />
69 <option name="ASSIGNMENT_WRAP" value="1" />
70 <option name="ASSERT_STATEMENT_WRAP" value="1" />
71 <option name="DOWHILE_BRACE_FORCE" value="3" />
72 <option name="METHOD_ANNOTATION_WRAP" value="1" />
73 <option name="CLASS_ANNOTATION_WRAP" value="1" />
74 <option name="FIELD_ANNOTATION_WRAP" value="1" />
75 <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
76 <option name="VARIABLE_ANNOTATION_WRAP" value="1" />
77 <option name="ENUM_CONSTANTS_WRAP" value="1" />
78 <indentOptions>
79 <option name="INDENT_SIZE" value="2" />
80 <option name="TAB_SIZE" value="2" />
81 </indentOptions>
82 </codeStyleSettings>
83 <codeStyleSettings language="XML">
84 <option name="FORCE_REARRANGE_MODE" value="1" />
85 <indentOptions>
86 <option name="CONTINUATION_INDENT_SIZE" value="4" />
87 </indentOptions>
88 <arrangement>
89 <rules>
90 <section>
91 <rule>
92 <match>
93 <AND>
94 <NAME>xmlns:android</NAME>
95 <XML_NAMESPACE>Namespace:</XML_NAMESPACE>
96 </AND>
97 </match>
98 </rule>
99 </section>
100 <section>
101 <rule>
102 <match>
103 <AND>
104 <NAME>xmlns:.*</NAME>
105 <XML_NAMESPACE>Namespace:</XML_NAMESPACE>
106 </AND>
107 </match>
108 <order>BY_NAME</order>
109 </rule>
110 </section>
111 <section>
112 <rule>
113 <match>
114 <AND>
115 <NAME>.*:id</NAME>
116 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
117 </AND>
118 </match>
119 </rule>
120 </section>
121 <section>
122 <rule>
123 <match>
124 <AND>
125 <NAME>.*:name</NAME>
126 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
127 </AND>
128 </match>
129 </rule>
130 </section>
131 <section>
132 <rule>
133 <match>
134 <AND>
135 <NAME>name</NAME>
136 <XML_NAMESPACE>^$</XML_NAMESPACE>
137 </AND>
138 </match>
139 </rule>
140 </section>
141 <section>
142 <rule>
143 <match>
144 <AND>
145 <NAME>style</NAME>
146 <XML_NAMESPACE>^$</XML_NAMESPACE>
147 </AND>
148 </match>
149 </rule>
150 </section>
151 <section>
152 <rule>
153 <match>
154 <AND>
155 <NAME>.*</NAME>
156 <XML_NAMESPACE>^$</XML_NAMESPACE>
157 </AND>
158 </match>
159 <order>BY_NAME</order>
160 </rule>
161 </section>
162 <section>
163 <rule>
164 <match>
165 <AND>
166 <NAME>.*:layout_width</NAME>
167 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
168 </AND>
169 </match>
170 </rule>
171 </section>
172 <section>
173 <rule>
174 <match>
175 <AND>
176 <NAME>.*:layout_height</NAME>
177 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
178 </AND>
179 </match>
180 </rule>
181 </section>
182 <section>
183 <rule>
184 <match>
185 <AND>
186 <NAME>.*:layout_.*</NAME>
187 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
188 </AND>
189 </match>
190 <order>BY_NAME</order>
191 </rule>
192 </section>
193 <section>
194 <rule>
195 <match>
196 <AND>
197 <NAME>.*:width</NAME>
198 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
199 </AND>
200 </match>
201 <order>BY_NAME</order>
202 </rule>
203 </section>
204 <section>
205 <rule>
206 <match>
207 <AND>
208 <NAME>.*:height</NAME>
209 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
210 </AND>
211 </match>
212 <order>BY_NAME</order>
213 </rule>
214 </section>
215 <section>
216 <rule>
217 <match>
218 <AND>
219 <NAME>.*</NAME>
220 <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
221 </AND>
222 </match>
223 <order>BY_NAME</order>
224 </rule>
225 </section>
226 <section>
227 <rule>
228 <match>
229 <AND>
230 <NAME>.*</NAME>
231 <XML_NAMESPACE>.*</XML_NAMESPACE>
232 </AND>
233 </match>
234 <order>BY_NAME</order>
235 </rule>
236 </section>
237 </rules>
238 </arrangement>
239 </codeStyleSettings>
240</code_scheme>
diff --git a/src/android/gradle.properties b/src/android/gradle.properties
new file mode 100644
index 000000000..8de505811
--- /dev/null
+++ b/src/android/gradle.properties
@@ -0,0 +1,15 @@
1# Project-wide Gradle settings.
2# IDE (e.g. Android Studio) users:
3# Gradle settings configured through the IDE *will override*
4# any settings specified in this file.
5# For more details on how to configure your build environment visit
6# http://www.gradle.org/docs/current/userguide/build_environment.html
7# Specifies the JVM arguments used for the daemon process.
8# The setting is particularly useful for tweaking memory settings.
9android.enableJetifier=true
10android.useAndroidX=true
11org.gradle.jvmargs=-Xmx1536m
12# When configured, Gradle will run in incubating parallel mode.
13# This option should only be used with decoupled projects. More details, visit
14# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15# org.gradle.parallel=true
diff --git a/src/android/gradle/wrapper/gradle-wrapper.jar b/src/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..7a3265ee9
--- /dev/null
+++ b/src/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..bd08889e5
--- /dev/null
+++ b/src/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
1#Sun Feb 21 18:16:59 EST 2021
2distributionBase=GRADLE_USER_HOME
3distributionPath=wrapper/dists
4zipStoreBase=GRADLE_USER_HOME
5zipStorePath=wrapper/dists
6distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
diff --git a/src/android/gradlew b/src/android/gradlew
new file mode 100644
index 000000000..cccdd3d51
--- /dev/null
+++ b/src/android/gradlew
@@ -0,0 +1,172 @@
1#!/usr/bin/env sh
2
3##############################################################################
4##
5## Gradle start up script for UN*X
6##
7##############################################################################
8
9# Attempt to set APP_HOME
10# Resolve links: $0 may be a link
11PRG="$0"
12# Need this for relative symlinks.
13while [ -h "$PRG" ] ; do
14 ls=`ls -ld "$PRG"`
15 link=`expr "$ls" : '.*-> \(.*\)$'`
16 if expr "$link" : '/.*' > /dev/null; then
17 PRG="$link"
18 else
19 PRG=`dirname "$PRG"`"/$link"
20 fi
21done
22SAVED="`pwd`"
23cd "`dirname \"$PRG\"`/" >/dev/null
24APP_HOME="`pwd -P`"
25cd "$SAVED" >/dev/null
26
27APP_NAME="Gradle"
28APP_BASE_NAME=`basename "$0"`
29
30# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31DEFAULT_JVM_OPTS=""
32
33# Use the maximum available, or set MAX_FD != -1 to use that value.
34MAX_FD="maximum"
35
36warn () {
37 echo "$*"
38}
39
40die () {
41 echo
42 echo "$*"
43 echo
44 exit 1
45}
46
47# OS specific support (must be 'true' or 'false').
48cygwin=false
49msys=false
50darwin=false
51nonstop=false
52case "`uname`" in
53 CYGWIN* )
54 cygwin=true
55 ;;
56 Darwin* )
57 darwin=true
58 ;;
59 MINGW* )
60 msys=true
61 ;;
62 NONSTOP* )
63 nonstop=true
64 ;;
65esac
66
67CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68
69# Determine the Java command to use to start the JVM.
70if [ -n "$JAVA_HOME" ] ; then
71 if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 # IBM's JDK on AIX uses strange locations for the executables
73 JAVACMD="$JAVA_HOME/jre/sh/java"
74 else
75 JAVACMD="$JAVA_HOME/bin/java"
76 fi
77 if [ ! -x "$JAVACMD" ] ; then
78 die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79
80Please set the JAVA_HOME variable in your environment to match the
81location of your Java installation."
82 fi
83else
84 JAVACMD="java"
85 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86
87Please set the JAVA_HOME variable in your environment to match the
88location of your Java installation."
89fi
90
91# Increase the maximum file descriptors if we can.
92if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 MAX_FD_LIMIT=`ulimit -H -n`
94 if [ $? -eq 0 ] ; then
95 if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 MAX_FD="$MAX_FD_LIMIT"
97 fi
98 ulimit -n $MAX_FD
99 if [ $? -ne 0 ] ; then
100 warn "Could not set maximum file descriptor limit: $MAX_FD"
101 fi
102 else
103 warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 fi
105fi
106
107# For Darwin, add options to specify how the application appears in the dock
108if $darwin; then
109 GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110fi
111
112# For Cygwin, switch paths to Windows format before running java
113if $cygwin ; then
114 APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 JAVACMD=`cygpath --unix "$JAVACMD"`
117
118 # We build the pattern for arguments to be converted via cygpath
119 ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 SEP=""
121 for dir in $ROOTDIRSRAW ; do
122 ROOTDIRS="$ROOTDIRS$SEP$dir"
123 SEP="|"
124 done
125 OURCYGPATTERN="(^($ROOTDIRS))"
126 # Add a user-defined pattern to the cygpath arguments
127 if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 fi
130 # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 i=0
132 for arg in "$@" ; do
133 CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135
136 if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 else
139 eval `echo args$i`="\"$arg\""
140 fi
141 i=$((i+1))
142 done
143 case $i in
144 (0) set -- ;;
145 (1) set -- "$args0" ;;
146 (2) set -- "$args0" "$args1" ;;
147 (3) set -- "$args0" "$args1" "$args2" ;;
148 (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 esac
155fi
156
157# Escape application args
158save () {
159 for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 echo " "
161}
162APP_ARGS=$(save "$@")
163
164# Collect all arguments for the java command, following the shell quoting and substitution rules
165eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166
167# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 cd "$(dirname "$0")"
170fi
171
172exec "$JAVACMD" "$@"
diff --git a/src/android/gradlew.bat b/src/android/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/src/android/gradlew.bat
@@ -0,0 +1,84 @@
1@if "%DEBUG%" == "" @echo off
2@rem ##########################################################################
3@rem
4@rem Gradle startup script for Windows
5@rem
6@rem ##########################################################################
7
8@rem Set local scope for the variables with windows NT shell
9if "%OS%"=="Windows_NT" setlocal
10
11set DIRNAME=%~dp0
12if "%DIRNAME%" == "" set DIRNAME=.
13set APP_BASE_NAME=%~n0
14set APP_HOME=%DIRNAME%
15
16@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17set DEFAULT_JVM_OPTS=
18
19@rem Find java.exe
20if defined JAVA_HOME goto findJavaFromJavaHome
21
22set JAVA_EXE=java.exe
23%JAVA_EXE% -version >NUL 2>&1
24if "%ERRORLEVEL%" == "0" goto init
25
26echo.
27echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28echo.
29echo Please set the JAVA_HOME variable in your environment to match the
30echo location of your Java installation.
31
32goto fail
33
34:findJavaFromJavaHome
35set JAVA_HOME=%JAVA_HOME:"=%
36set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
38if exist "%JAVA_EXE%" goto init
39
40echo.
41echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42echo.
43echo Please set the JAVA_HOME variable in your environment to match the
44echo location of your Java installation.
45
46goto fail
47
48:init
49@rem Get command-line arguments, handling Windows variants
50
51if not "%OS%" == "Windows_NT" goto win9xME_args
52
53:win9xME_args
54@rem Slurp the command line arguments.
55set CMD_LINE_ARGS=
56set _SKIP=2
57
58:win9xME_args_slurp
59if "x%~1" == "x" goto execute
60
61set CMD_LINE_ARGS=%*
62
63:execute
64@rem Setup the command line
65
66set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67
68@rem Execute Gradle
69"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70
71:end
72@rem End local scope for the variables with windows NT shell
73if "%ERRORLEVEL%"=="0" goto mainEnd
74
75:fail
76rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77rem the _cmd.exe /c_ return code!
78if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79exit /b 1
80
81:mainEnd
82if "%OS%"=="Windows_NT" endlocal
83
84:omega
diff --git a/src/android/settings.gradle b/src/android/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/src/android/settings.gradle
@@ -0,0 +1 @@
include ':app'