From 5e198d1421372f7673f181666df76acc8ff1607d Mon Sep 17 00:00:00 2001 From: bunnei Date: Fri, 30 Dec 2022 18:09:03 -0800 Subject: android: Minimize frontend & convert to yuzu. --- src/android/app/build.gradle | 2 +- .../citra/citra_emu/ExampleInstrumentedTest.java | 3 - .../org/yuzu/yuzu_emu/ExampleInstrumentedTest.java | 3 + src/android/app/src/main/AndroidManifest.xml | 34 +- .../java/org/citra/citra_emu/CitraApplication.java | 56 -- .../java/org/citra/citra_emu/NativeLibrary.java | 631 --------------- .../activities/CustomFilePickerActivity.java | 38 - .../citra_emu/activities/EmulationActivity.java | 755 ------------------ .../org/citra/citra_emu/adapters/GameAdapter.java | 247 ------ .../org/citra/citra_emu/applets/MiiSelector.java | 122 --- .../citra/citra_emu/applets/SoftwareKeyboard.java | 264 ------- .../citra_emu/camera/StillImageCameraHelper.java | 65 -- .../citra/citra_emu/dialogs/MotionAlertDialog.java | 140 ---- .../disk_shader_cache/DiskShaderCacheProgress.java | 138 ---- .../citra_emu/features/cheats/model/Cheat.java | 57 -- .../features/cheats/model/CheatEngine.java | 13 - .../features/cheats/model/CheatsViewModel.java | 177 ----- .../features/cheats/ui/CheatDetailsFragment.java | 174 ---- .../features/cheats/ui/CheatListFragment.java | 46 -- .../features/cheats/ui/CheatViewHolder.java | 56 -- .../features/cheats/ui/CheatsActivity.java | 161 ---- .../features/cheats/ui/CheatsAdapter.java | 72 -- .../features/settings/model/BooleanSetting.java | 23 - .../features/settings/model/FloatSetting.java | 23 - .../features/settings/model/IntSetting.java | 23 - .../citra_emu/features/settings/model/Setting.java | 42 - .../features/settings/model/SettingSection.java | 55 -- .../features/settings/model/Settings.java | 132 ---- .../features/settings/model/StringSetting.java | 23 - .../settings/model/view/CheckBoxSetting.java | 80 -- .../settings/model/view/DateTimeSetting.java | 40 - .../settings/model/view/HeaderSetting.java | 14 - .../settings/model/view/InputBindingSetting.java | 382 --------- .../settings/model/view/PremiumHeader.java | 12 - .../model/view/PremiumSingleChoiceSetting.java | 59 -- .../features/settings/model/view/SettingsItem.java | 107 --- .../settings/model/view/SingleChoiceSetting.java | 60 -- .../settings/model/view/SliderSetting.java | 101 --- .../model/view/StringSingleChoiceSetting.java | 82 -- .../settings/model/view/SubmenuSetting.java | 21 - .../features/settings/ui/SettingsActivity.java | 215 ----- .../settings/ui/SettingsActivityPresenter.java | 124 --- .../features/settings/ui/SettingsActivityView.java | 103 --- .../features/settings/ui/SettingsAdapter.java | 487 ------------ .../features/settings/ui/SettingsFragment.java | 136 ---- .../settings/ui/SettingsFragmentPresenter.java | 416 ---------- .../features/settings/ui/SettingsFragmentView.java | 78 -- .../features/settings/ui/SettingsFrameLayout.java | 48 -- .../ui/viewholder/CheckBoxSettingViewHolder.java | 54 -- .../settings/ui/viewholder/DateTimeViewHolder.java | 47 -- .../settings/ui/viewholder/HeaderViewHolder.java | 32 - .../viewholder/InputBindingSettingViewHolder.java | 55 -- .../settings/ui/viewholder/PremiumViewHolder.java | 57 -- .../settings/ui/viewholder/SettingViewHolder.java | 49 -- .../ui/viewholder/SingleChoiceViewHolder.java | 76 -- .../settings/ui/viewholder/SliderViewHolder.java | 45 -- .../settings/ui/viewholder/SubmenuViewHolder.java | 45 -- .../features/settings/utils/SettingsFile.java | 341 -------- .../fragments/CustomFilePickerFragment.java | 120 --- .../citra_emu/fragments/EmulationFragment.java | 380 --------- .../main/java/org/citra/citra_emu/model/Game.java | 76 -- .../org/citra/citra_emu/model/GameDatabase.java | 276 ------- .../org/citra/citra_emu/model/GameProvider.java | 138 ---- .../org/citra/citra_emu/overlay/InputOverlay.java | 878 --------------------- .../overlay/InputOverlayDrawableButton.java | 122 --- .../overlay/InputOverlayDrawableDpad.java | 193 ----- .../overlay/InputOverlayDrawableJoystick.java | 264 ------- .../citra/citra_emu/ui/DividerItemDecoration.java | 130 --- .../citra_emu/ui/TwoPaneOnBackPressedCallback.java | 37 - .../org/citra/citra_emu/ui/main/MainActivity.java | 267 ------- .../org/citra/citra_emu/ui/main/MainPresenter.java | 82 -- .../java/org/citra/citra_emu/ui/main/MainView.java | 25 - .../ui/platform/PlatformGamesFragment.java | 86 -- .../ui/platform/PlatformGamesPresenter.java | 42 - .../citra_emu/ui/platform/PlatformGamesView.java | 21 - .../java/org/citra/citra_emu/utils/Action1.java | 5 - .../citra/citra_emu/utils/AddDirectoryHelper.java | 38 - .../main/java/org/citra/citra_emu/utils/BiMap.java | 22 - .../org/citra/citra_emu/utils/BillingManager.java | 215 ----- .../citra_emu/utils/ControllerMappingHelper.java | 66 -- .../citra_emu/utils/DirectoryInitialization.java | 186 ----- .../citra_emu/utils/DirectoryStateReceiver.java | 22 - .../citra_emu/utils/EmulationMenuSettings.java | 78 -- .../citra/citra_emu/utils/FileBrowserHelper.java | 73 -- .../java/org/citra/citra_emu/utils/FileUtil.java | 37 - .../citra/citra_emu/utils/ForegroundService.java | 63 -- .../citra_emu/utils/GameIconRequestHandler.java | 27 - .../main/java/org/citra/citra_emu/utils/Log.java | 39 - .../citra/citra_emu/utils/PermissionsHandler.java | 35 - .../utils/PicassoRoundedCornersTransformation.java | 45 -- .../org/citra/citra_emu/utils/PicassoUtils.java | 57 -- .../org/citra/citra_emu/utils/StartupHandler.java | 45 -- .../java/org/citra/citra_emu/utils/ThemeUtil.java | 34 - .../citra_emu/viewholders/GameViewHolder.java | 46 -- .../main/java/org/yuzu/yuzu_emu/NativeLibrary.java | 631 +++++++++++++++ .../java/org/yuzu/yuzu_emu/YuzuApplication.java | 56 ++ .../activities/CustomFilePickerActivity.java | 38 + .../yuzu_emu/activities/EmulationActivity.java | 537 +++++++++++++ .../org/yuzu/yuzu_emu/adapters/GameAdapter.java | 247 ++++++ .../yuzu/yuzu_emu/applets/SoftwareKeyboard.java | 264 +++++++ .../yuzu/yuzu_emu/dialogs/MotionAlertDialog.java | 140 ++++ .../features/settings/model/BooleanSetting.java | 23 + .../features/settings/model/FloatSetting.java | 23 + .../features/settings/model/IntSetting.java | 23 + .../yuzu_emu/features/settings/model/Setting.java | 42 + .../features/settings/model/SettingSection.java | 55 ++ .../yuzu_emu/features/settings/model/Settings.java | 131 +++ .../features/settings/model/StringSetting.java | 23 + .../settings/model/view/CheckBoxSetting.java | 80 ++ .../settings/model/view/DateTimeSetting.java | 40 + .../settings/model/view/HeaderSetting.java | 14 + .../settings/model/view/InputBindingSetting.java | 382 +++++++++ .../settings/model/view/PremiumHeader.java | 12 + .../model/view/PremiumSingleChoiceSetting.java | 59 ++ .../features/settings/model/view/SettingsItem.java | 107 +++ .../settings/model/view/SingleChoiceSetting.java | 60 ++ .../settings/model/view/SliderSetting.java | 101 +++ .../model/view/StringSingleChoiceSetting.java | 82 ++ .../settings/model/view/SubmenuSetting.java | 21 + .../features/settings/ui/SettingsActivity.java | 215 +++++ .../settings/ui/SettingsActivityPresenter.java | 125 +++ .../features/settings/ui/SettingsActivityView.java | 103 +++ .../features/settings/ui/SettingsAdapter.java | 487 ++++++++++++ .../features/settings/ui/SettingsFragment.java | 136 ++++ .../settings/ui/SettingsFragmentPresenter.java | 322 ++++++++ .../features/settings/ui/SettingsFragmentView.java | 78 ++ .../features/settings/ui/SettingsFrameLayout.java | 48 ++ .../ui/viewholder/CheckBoxSettingViewHolder.java | 54 ++ .../settings/ui/viewholder/DateTimeViewHolder.java | 47 ++ .../settings/ui/viewholder/HeaderViewHolder.java | 32 + .../viewholder/InputBindingSettingViewHolder.java | 55 ++ .../settings/ui/viewholder/PremiumViewHolder.java | 57 ++ .../settings/ui/viewholder/SettingViewHolder.java | 49 ++ .../ui/viewholder/SingleChoiceViewHolder.java | 76 ++ .../settings/ui/viewholder/SliderViewHolder.java | 45 ++ .../settings/ui/viewholder/SubmenuViewHolder.java | 45 ++ .../features/settings/utils/SettingsFile.java | 341 ++++++++ .../fragments/CustomFilePickerFragment.java | 120 +++ .../yuzu/yuzu_emu/fragments/EmulationFragment.java | 380 +++++++++ .../org/yuzu/yuzu_emu/fragments/MenuFragment.java | 129 +++ .../main/java/org/yuzu/yuzu_emu/model/Game.java | 76 ++ .../java/org/yuzu/yuzu_emu/model/GameDatabase.java | 276 +++++++ .../java/org/yuzu/yuzu_emu/model/GameProvider.java | 138 ++++ .../org/yuzu/yuzu_emu/overlay/InputOverlay.java | 878 +++++++++++++++++++++ .../overlay/InputOverlayDrawableButton.java | 122 +++ .../yuzu_emu/overlay/InputOverlayDrawableDpad.java | 193 +++++ .../overlay/InputOverlayDrawableJoystick.java | 264 +++++++ .../yuzu/yuzu_emu/ui/DividerItemDecoration.java | 130 +++ .../yuzu_emu/ui/TwoPaneOnBackPressedCallback.java | 37 + .../org/yuzu/yuzu_emu/ui/main/MainActivity.java | 267 +++++++ .../org/yuzu/yuzu_emu/ui/main/MainPresenter.java | 82 ++ .../java/org/yuzu/yuzu_emu/ui/main/MainView.java | 25 + .../ui/platform/PlatformGamesFragment.java | 86 ++ .../ui/platform/PlatformGamesPresenter.java | 42 + .../yuzu_emu/ui/platform/PlatformGamesView.java | 21 + .../main/java/org/yuzu/yuzu_emu/utils/Action1.java | 5 + .../yuzu/yuzu_emu/utils/AddDirectoryHelper.java | 38 + .../main/java/org/yuzu/yuzu_emu/utils/BiMap.java | 22 + .../org/yuzu/yuzu_emu/utils/BillingManager.java | 215 +++++ .../yuzu_emu/utils/ControllerMappingHelper.java | 66 ++ .../yuzu_emu/utils/DirectoryInitialization.java | 186 +++++ .../yuzu_emu/utils/DirectoryStateReceiver.java | 22 + .../yuzu/yuzu_emu/utils/EmulationMenuSettings.java | 78 ++ .../org/yuzu/yuzu_emu/utils/FileBrowserHelper.java | 73 ++ .../java/org/yuzu/yuzu_emu/utils/FileUtil.java | 37 + .../org/yuzu/yuzu_emu/utils/ForegroundService.java | 63 ++ .../yuzu_emu/utils/GameIconRequestHandler.java | 27 + .../src/main/java/org/yuzu/yuzu_emu/utils/Log.java | 39 + .../yuzu/yuzu_emu/utils/PermissionsHandler.java | 35 + .../utils/PicassoRoundedCornersTransformation.java | 45 ++ .../java/org/yuzu/yuzu_emu/utils/PicassoUtils.java | 57 ++ .../org/yuzu/yuzu_emu/utils/StartupHandler.java | 45 ++ .../java/org/yuzu/yuzu_emu/utils/ThemeUtil.java | 34 + .../yuzu/yuzu_emu/viewholders/GameViewHolder.java | 46 ++ src/android/app/src/main/jni/native.cpp | 64 +- src/android/app/src/main/jni/native.h | 68 +- .../main/res/animator/menu_slide_in_from_end.xml | 20 + .../main/res/animator/menu_slide_in_from_start.xml | 20 + .../main/res/animator/menu_slide_out_to_end.xml | 21 + .../main/res/animator/menu_slide_out_to_start.xml | 21 + .../src/main/res/layout-ldrtl/list_item_cheat.xml | 38 - .../app/src/main/res/layout/activity_cheats.xml | 22 - .../app/src/main/res/layout/activity_emulation.xml | 31 +- .../src/main/res/layout/fragment_cheat_details.xml | 163 ---- .../src/main/res/layout/fragment_cheat_list.xml | 27 - .../app/src/main/res/layout/fragment_emulation.xml | 5 +- .../src/main/res/layout/fragment_ingame_menu.xml | 56 ++ .../app/src/main/res/layout/fragment_settings.xml | 6 +- .../app/src/main/res/layout/list_item_cheat.xml | 38 - .../app/src/main/res/menu/menu_emulation.xml | 118 --- .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 5899 -> 17950 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 7416 -> 48880 bytes .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 3377 -> 15249 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 4413 -> 47388 bytes .../app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 8742 -> 17663 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 10530 -> 49903 bytes .../app/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 14300 -> 20413 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 17511 -> 57817 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 20804 -> 23487 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 24886 -> 58695 bytes .../app/src/main/res/values-night/colors.xml | 7 +- src/android/app/src/main/res/values/colors.xml | 7 +- src/android/app/src/main/res/values/dimens.xml | 9 +- src/android/app/src/main/res/values/integers.xml | 1 + src/android/app/src/main/res/values/strings.xml | 74 +- src/android/app/src/main/res/values/styles.xml | 39 +- .../java/org/citra/citra_emu/ExampleUnitTest.java | 17 - src/android/code-style-java.xml | 3 +- 208 files changed, 10208 insertions(+), 11783 deletions(-) delete mode 100644 src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java create mode 100644 src/android/app/src/androidTest/java/org/yuzu/yuzu_emu/ExampleInstrumentedTest.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/Game.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/MotionAlertDialog.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Setting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/CheckBoxSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputBindingSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumHeader.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumSingleChoiceSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFrameLayout.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/PremiumViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MenuFragment.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/DividerItemDecoration.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/TwoPaneOnBackPressedCallback.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BillingManager.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryStateReceiver.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoRoundedCornersTransformation.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeUtil.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java create mode 100644 src/android/app/src/main/res/animator/menu_slide_in_from_end.xml create mode 100644 src/android/app/src/main/res/animator/menu_slide_in_from_start.xml create mode 100644 src/android/app/src/main/res/animator/menu_slide_out_to_end.xml create mode 100644 src/android/app/src/main/res/animator/menu_slide_out_to_start.xml delete mode 100644 src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml delete mode 100644 src/android/app/src/main/res/layout/activity_cheats.xml delete mode 100644 src/android/app/src/main/res/layout/fragment_cheat_details.xml delete mode 100644 src/android/app/src/main/res/layout/fragment_cheat_list.xml create mode 100644 src/android/app/src/main/res/layout/fragment_ingame_menu.xml delete mode 100644 src/android/app/src/main/res/layout/list_item_cheat.xml delete mode 100644 src/android/app/src/main/res/menu/menu_emulation.xml delete mode 100644 src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java (limited to 'src') diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index 5a108743b..ffbadce14 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -30,7 +30,7 @@ android { defaultConfig { // TODO If this is ever modified, change application_id in strings.xml - applicationId "org.citra.citra_emu" + applicationId "org.yuzu.yuzu_emu" minSdkVersion 28 targetSdkVersion 29 versionCode autoVersion 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 deleted file mode 100644 index 6a25f2ce6..000000000 --- a/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.citra.citra_emu; - -import android.content.Context; diff --git a/src/android/app/src/androidTest/java/org/yuzu/yuzu_emu/ExampleInstrumentedTest.java b/src/android/app/src/androidTest/java/org/yuzu/yuzu_emu/ExampleInstrumentedTest.java new file mode 100644 index 000000000..0cea19827 --- /dev/null +++ b/src/android/app/src/androidTest/java/org/yuzu/yuzu_emu/ExampleInstrumentedTest.java @@ -0,0 +1,3 @@ +package org.yuzu.yuzu_emu; + +import android.content.Context; diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c2463e079..0d7e3f7ad 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="org.yuzu.yuzu_emu"> @@ -11,9 +11,6 @@ - @@ -23,7 +20,7 @@ @@ -46,22 +43,22 @@ - + @@ -70,16 +67,10 @@ - - - + @@ -97,3 +88,4 @@ + 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 deleted file mode 100644 index 41ac7e27c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu; - -import android.app.Application; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.os.Build; - -import org.citra.citra_emu.model.GameDatabase; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.PermissionsHandler; - -public class CitraApplication extends Application { - public static GameDatabase databaseHelper; - private static CitraApplication application; - - private void createNotificationChannel() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = getString(R.string.app_notification_channel_name); - String description = getString(R.string.app_notification_channel_description); - NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW); - channel.setDescription(description); - channel.setSound(null, null); - channel.setVibrationPattern(null); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - } - - @Override - public void onCreate() { - super.onCreate(); - application = this; - - if (PermissionsHandler.hasWriteAccess(getApplicationContext())) { - DirectoryInitialization.start(getApplicationContext()); - } - - NativeLibrary.LogDeviceInfo(); - createNotificationChannel(); - - databaseHelper = new GameDatabase(this); - } - - public static Context getAppContext() { - return application.getApplicationContext(); - } -} 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 deleted file mode 100644 index baff99dc8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ /dev/null @@ -1,631 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu; - -import android.app.Activity; -import android.app.Dialog; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.Surface; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; - -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.utils.EmulationMenuSettings; -import org.citra.citra_emu.utils.Log; - -import java.lang.ref.WeakReference; -import java.util.Objects; - -import static android.Manifest.permission.CAMERA; -import static android.Manifest.permission.RECORD_AUDIO; - -/** - * Class which contains methods that interact - * with the native side of the Citra code. - */ -public final class NativeLibrary { - /** - * Default touchscreen device - */ - public static final String TouchScreenDevice = "Touchscreen"; - public static WeakReference sEmulationActivity = new WeakReference<>(null); - - private static boolean alertResult = false; - private static String alertPromptResult = ""; - private static int alertPromptButton = 0; - private static final Object alertPromptLock = new Object(); - private static boolean alertPromptInProgress = false; - private static String alertPromptCaption = ""; - private static int alertPromptButtonConfig = 0; - private static EditText alertPromptEditText = null; - - static { - try { - System.loadLibrary("yuzu-android"); - } catch (UnsatisfiedLinkError ex) { - Log.error("[NativeLibrary] " + ex.toString()); - } - } - - private NativeLibrary() { - // Disallows instantiation. - } - - /** - * Handles button press events for a gamepad. - * - * @param Device The input descriptor of the gamepad. - * @param Button Key code identifying which button was pressed. - * @param Action Mask identifying which action is happening (button pressed down, or button released). - * @return If we handled the button press. - */ - public static native boolean onGamePadEvent(String Device, int Button, int Action); - - /** - * Handles gamepad movement events. - * - * @param Device The device ID of the gamepad. - * @param Axis The axis ID - * @param x_axis The value of the x-axis represented by the given ID. - * @param y_axis The value of the y-axis represented by the given ID - */ - public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis); - - /** - * Handles gamepad movement events. - * - * @param Device The device ID of the gamepad. - * @param Axis_id The axis ID - * @param axis_val The value of the axis represented by the given ID. - */ - public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val); - - /** - * Handles touch events. - * - * @param x_axis The value of the x-axis. - * @param y_axis The value of the y-axis - * @param pressed To identify if the touch held down or released. - * @return true if the pointer is within the touchscreen - */ - public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed); - - /** - * Handles touch movement. - * - * @param x_axis The value of the instantaneous x-axis. - * @param y_axis The value of the instantaneous y-axis. - */ - public static native void onTouchMoved(float x_axis, float y_axis); - - public static native void ReloadSettings(); - - public static native String GetUserSetting(String gameID, String Section, String Key); - - public static native void SetUserSetting(String gameID, String Section, String Key, String Value); - - public static native void InitGameIni(String gameID); - - /** - * Gets the embedded icon within the given ROM. - * - * @param filename the file path to the ROM. - * @return an integer array containing the color data for the icon. - */ - public static native int[] GetIcon(String filename); - - /** - * Gets the embedded title of the given ISO/ROM. - * - * @param filename The file path to the ISO/ROM. - * @return the embedded title of the ISO/ROM. - */ - public static native String GetTitle(String filename); - - public static native String GetDescription(String filename); - - public static native String GetGameId(String filename); - - public static native String GetRegions(String filename); - - public static native String GetCompany(String filename); - - public static native String GetGitRevision(); - - /** - * Sets the current working user directory - * If not set, it auto-detects a location - */ - public static native void SetUserDirectory(String directory); - - // Create the config.ini file. - public static native void CreateConfigFile(); - - public static native int DefaultCPUCore(); - - /** - * Begins emulation. - */ - public static native void Run(String path); - - /** - * Begins emulation from the specified savestate. - */ - public static native void Run(String path, String savestatePath, boolean deleteSavestate); - - // Surface Handling - public static native void SurfaceChanged(Surface surf); - - public static native void SurfaceDestroyed(); - - public static native void DoFrame(); - - /** - * Unpauses emulation from a paused state. - */ - public static native void UnPauseEmulation(); - - /** - * Pauses emulation. - */ - public static native void PauseEmulation(); - - /** - * Stops emulation. - */ - public static native void StopEmulation(); - - /** - * Returns true if emulation is running (or is paused). - */ - public static native boolean IsRunning(); - - /** - * Returns the performance stats for the current game - **/ - public static native double[] GetPerfStats(); - - /** - * Notifies the core emulation that the orientation has changed. - */ - public static native void NotifyOrientationChange(int layout_option, int rotation); - - public enum CoreError { - ErrorSystemFiles, - ErrorSavestate, - ErrorUnknown, - } - - private static boolean coreErrorAlertResult = false; - private static final Object coreErrorAlertLock = new Object(); - - public static class CoreErrorDialogFragment extends DialogFragment { - static CoreErrorDialogFragment newInstance(String title, String message) { - CoreErrorDialogFragment frag = new CoreErrorDialogFragment(); - Bundle args = new Bundle(); - args.putString("title", title); - args.putString("message", message); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity emulationActivity = Objects.requireNonNull(getActivity()); - - final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); - final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); - - return new AlertDialog.Builder(emulationActivity) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.continue_button, (dialog, which) -> { - coreErrorAlertResult = true; - synchronized (coreErrorAlertLock) { - coreErrorAlertLock.notify(); - } - }) - .setNegativeButton(R.string.abort_button, (dialog, which) -> { - coreErrorAlertResult = false; - synchronized (coreErrorAlertLock) { - coreErrorAlertLock.notify(); - } - }).setOnDismissListener(dialog -> { - coreErrorAlertResult = true; - synchronized (coreErrorAlertLock) { - coreErrorAlertLock.notify(); - } - }).create(); - } - } - - private static void OnCoreErrorImpl(String title, String message) { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) { - Log.error("[NativeLibrary] EmulationActivity not present"); - return; - } - - CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message); - fragment.show(emulationActivity.getSupportFragmentManager(), "coreError"); - } - - /** - * Handles a core error. - * @return true: continue; false: abort - */ - public static boolean OnCoreError(CoreError error, String details) { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) { - Log.error("[NativeLibrary] EmulationActivity not present"); - return false; - } - - String title, message; - switch (error) { - case ErrorSystemFiles: { - title = emulationActivity.getString(R.string.system_archive_not_found); - message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details); - break; - } - case ErrorSavestate: { - title = emulationActivity.getString(R.string.save_load_error); - message = details; - break; - } - case ErrorUnknown: { - title = emulationActivity.getString(R.string.fatal_error); - message = emulationActivity.getString(R.string.fatal_error_message); - break; - } - default: { - return true; - } - } - - // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message)); - - // Wait for the lock to notify that it is complete. - synchronized (coreErrorAlertLock) { - try { - coreErrorAlertLock.wait(); - } catch (Exception ignored) { - } - } - - return coreErrorAlertResult; - } - - public static boolean isPortraitMode() { - return CitraApplication.getAppContext().getResources().getConfiguration().orientation == - Configuration.ORIENTATION_PORTRAIT; - } - - public static int landscapeScreenLayout() { - return EmulationMenuSettings.getLandscapeScreenLayout(); - } - - public static boolean displayAlertMsg(final String caption, final String text, - final boolean yesNo) { - Log.error("[NativeLibrary] Alert: " + text); - final EmulationActivity emulationActivity = sEmulationActivity.get(); - boolean result = false; - if (emulationActivity == null) { - Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert."); - } else { - // Create object used for waiting. - final Object lock = new Object(); - AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) - .setTitle(caption) - .setMessage(text); - - // If not yes/no dialog just have one button that dismisses modal, - // otherwise have a yes and no button that sets alertResult accordingly. - if (!yesNo) { - builder - .setCancelable(false) - .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> - { - dialog.dismiss(); - synchronized (lock) { - lock.notify(); - } - }); - } else { - alertResult = false; - - builder - .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> - { - alertResult = true; - dialog.dismiss(); - synchronized (lock) { - lock.notify(); - } - }) - .setNegativeButton(android.R.string.no, (dialog, whichButton) -> - { - alertResult = false; - dialog.dismiss(); - synchronized (lock) { - lock.notify(); - } - }); - } - - // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(builder::show); - - // Wait for the lock to notify that it is complete. - synchronized (lock) { - try { - lock.wait(); - } catch (Exception e) { - } - } - - if (yesNo) - result = alertResult; - } - return result; - } - - public static void retryDisplayAlertPrompt() { - if (!alertPromptInProgress) { - return; - } - displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show(); - } - - public static String displayAlertPrompt(String caption, String text, int buttonConfig) { - alertPromptCaption = caption; - alertPromptButtonConfig = buttonConfig; - alertPromptInProgress = true; - - // Show the AlertDialog on the main thread - sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show()); - - // Wait for the lock to notify that it is complete - synchronized (alertPromptLock) { - try { - alertPromptLock.wait(); - } catch (Exception e) { - } - } - alertPromptInProgress = false; - - return alertPromptResult; - } - - public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - alertPromptResult = ""; - alertPromptButton = 0; - - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin); - - // Set up the input - alertPromptEditText = new EditText(CitraApplication.getAppContext()); - alertPromptEditText.setText(text); - alertPromptEditText.setSingleLine(); - alertPromptEditText.setLayoutParams(params); - - FrameLayout container = new FrameLayout(emulationActivity); - container.addView(alertPromptEditText); - - AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) - .setTitle(caption) - .setView(container) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> - { - alertPromptButton = buttonConfig; - alertPromptResult = alertPromptEditText.getText().toString(); - synchronized (alertPromptLock) { - alertPromptLock.notifyAll(); - } - }) - .setOnDismissListener(dialogInterface -> - { - alertPromptResult = ""; - synchronized (alertPromptLock) { - alertPromptLock.notifyAll(); - } - }); - - if (buttonConfig > 0) { - builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> - { - alertPromptResult = ""; - synchronized (alertPromptLock) { - alertPromptLock.notifyAll(); - } - }); - } - - return builder; - } - - public static int alertPromptButton() { - return alertPromptButton; - } - - public static void exitEmulationActivity(int resultCode) { - final int Success = 0; - final int ErrorNotInitialized = 1; - final int ErrorGetLoader = 2; - final int ErrorSystemMode = 3; - final int ErrorLoader = 4; - final int ErrorLoader_ErrorEncrypted = 5; - final int ErrorLoader_ErrorInvalidFormat = 6; - final int ErrorSystemFiles = 7; - final int ErrorVideoCore = 8; - final int ErrorVideoCore_ErrorGenericDrivers = 9; - final int ErrorVideoCore_ErrorBelowGL33 = 10; - final int ShutdownRequested = 11; - final int ErrorUnknown = 12; - - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) { - Log.warning("[NativeLibrary] EmulationActivity is null, can't exit."); - return; - } - - int captionId = R.string.loader_error_invalid_format; - if (resultCode == ErrorLoader_ErrorEncrypted) { - captionId = R.string.loader_error_encrypted; - } - - AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) - .setTitle(captionId) - .setMessage(Html.fromHtml("Please follow the guides to redump your game cartidges or installed titles.", Html.FROM_HTML_MODE_LEGACY)) - .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish()) - .setOnDismissListener(dialogInterface -> emulationActivity.finish()); - emulationActivity.runOnUiThread(() -> { - AlertDialog alert = builder.create(); - alert.show(); - ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); - }); - } - - public static void setEmulationActivity(EmulationActivity emulationActivity) { - Log.verbose("[NativeLibrary] Registering EmulationActivity."); - sEmulationActivity = new WeakReference<>(emulationActivity); - } - - public static void clearEmulationActivity() { - Log.verbose("[NativeLibrary] Unregistering EmulationActivity."); - - sEmulationActivity.clear(); - } - - private static final Object cameraPermissionLock = new Object(); - private static boolean cameraPermissionGranted = false; - public static final int REQUEST_CODE_NATIVE_CAMERA = 800; - - public static boolean RequestCameraPermission() { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) { - Log.error("[NativeLibrary] EmulationActivity not present"); - return false; - } - if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) { - // Permission already granted - return true; - } - emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA); - - // Wait until result is returned - synchronized (cameraPermissionLock) { - try { - cameraPermissionLock.wait(); - } catch (InterruptedException ignored) { - } - } - return cameraPermissionGranted; - } - - public static void CameraPermissionResult(boolean granted) { - cameraPermissionGranted = granted; - synchronized (cameraPermissionLock) { - cameraPermissionLock.notify(); - } - } - - private static final Object micPermissionLock = new Object(); - private static boolean micPermissionGranted = false; - public static final int REQUEST_CODE_NATIVE_MIC = 900; - - public static boolean RequestMicPermission() { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) { - Log.error("[NativeLibrary] EmulationActivity not present"); - return false; - } - if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { - // Permission already granted - return true; - } - emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC); - - // Wait until result is returned - synchronized (micPermissionLock) { - try { - micPermissionLock.wait(); - } catch (InterruptedException ignored) { - } - } - return micPermissionGranted; - } - - public static void MicPermissionResult(boolean granted) { - micPermissionGranted = granted; - synchronized (micPermissionLock) { - micPermissionLock.notify(); - } - } - - /** - * Logs the Citra version, Android version and, CPU. - */ - public static native void LogDeviceInfo(); - - /** - * Button type for use in onTouchEvent - */ - public static final class ButtonType { - public static final int BUTTON_A = 0; - public static final int BUTTON_B = 1; - public static final int BUTTON_X = 2; - public static final int BUTTON_Y = 3; - public static final int BUTTON_START = 11; - public static final int BUTTON_SELECT = 12; - public static final int BUTTON_HOME = 19; - public static final int BUTTON_ZL = 9; - public static final int BUTTON_ZR = 10; - public static final int DPAD_UP = 14; - public static final int DPAD_DOWN = 16; - public static final int DPAD_LEFT = 13; - public static final int DPAD_RIGHT = 15; - public static final int STICK_LEFT = 5; - public static final int STICK_LEFT_UP = 714; - public static final int STICK_LEFT_DOWN = 715; - public static final int STICK_LEFT_LEFT = 716; - public static final int STICK_LEFT_RIGHT = 717; - public static final int STICK_C = 6; - public static final int STICK_C_UP = 719; - public static final int STICK_C_DOWN = 720; - public static final int STICK_C_LEFT = 771; - public static final int STICK_C_RIGHT = 772; - public static final int TRIGGER_L = 7; - public static final int TRIGGER_R = 8; - public static final int DPAD = 780; - public static final int BUTTON_DEBUG = 781; - public static final int BUTTON_GPIO14 = 782; - } - - /** - * Button states - */ - public static final class ButtonState { - public static final int RELEASED = 0; - public static final int PRESSED = 1; - } -} 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 deleted file mode 100644 index 3083286e2..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.citra.citra_emu.activities; - -import android.content.Intent; -import android.os.Environment; - -import androidx.annotation.Nullable; - -import com.nononsenseapps.filepicker.AbstractFilePickerFragment; -import com.nononsenseapps.filepicker.FilePickerActivity; - -import org.citra.citra_emu.fragments.CustomFilePickerFragment; - -import java.io.File; - -public class CustomFilePickerActivity extends FilePickerActivity { - public static final String EXTRA_TITLE = "filepicker.intent.TITLE"; - public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS"; - - @Override - protected AbstractFilePickerFragment getFragment( - @Nullable final String startPath, final int mode, final boolean allowMultiple, - final boolean allowCreateDir, final boolean allowExistingFile, - final boolean singleClick) { - CustomFilePickerFragment fragment = new CustomFilePickerFragment(); - // startPath is allowed to be null. In that case, default folder should be SD-card and not "/" - fragment.setArgs( - startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), - mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); - - Intent intent = getIntent(); - int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0); - fragment.setTitle(title); - String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS); - fragment.setAllowedExtensions(allowedExtensions); - - return fragment; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java deleted file mode 100644 index 47ef0fd23..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ /dev/null @@ -1,755 +0,0 @@ -package org.citra.citra_emu.activities; - -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.util.SparseIntArray; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.widget.CheckBox; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.NotificationManagerCompat; -import androidx.fragment.app.FragmentActivity; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.ui.CheatsActivity; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.ui.SettingsActivity; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.camera.StillImageCameraHelper; -import org.citra.citra_emu.fragments.EmulationFragment; -import org.citra.citra_emu.ui.main.MainActivity; -import org.citra.citra_emu.utils.ControllerMappingHelper; -import org.citra.citra_emu.utils.EmulationMenuSettings; -import org.citra.citra_emu.utils.FileBrowserHelper; -import org.citra.citra_emu.utils.FileUtil; -import org.citra.citra_emu.utils.ForegroundService; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.util.Collections; -import java.util.List; - -import static android.Manifest.permission.CAMERA; -import static android.Manifest.permission.RECORD_AUDIO; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -public final class EmulationActivity extends AppCompatActivity { - public static final String EXTRA_SELECTED_GAME = "SelectedGame"; - public static final String EXTRA_SELECTED_TITLE = "SelectedTitle"; - public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0; - public static final int MENU_ACTION_TOGGLE_CONTROLS = 1; - public static final int MENU_ACTION_ADJUST_SCALE = 2; - public static final int MENU_ACTION_EXIT = 3; - public static final int MENU_ACTION_SHOW_FPS = 4; - public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5; - public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6; - public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7; - public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8; - public static final int MENU_ACTION_SWAP_SCREENS = 9; - public static final int MENU_ACTION_RESET_OVERLAY = 10; - public static final int MENU_ACTION_SHOW_OVERLAY = 11; - public static final int MENU_ACTION_OPEN_SETTINGS = 12; - public static final int MENU_ACTION_LOAD_AMIIBO = 13; - public static final int MENU_ACTION_REMOVE_AMIIBO = 14; - public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15; - public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16; - public static final int MENU_ACTION_OPEN_CHEATS = 17; - - public static final int REQUEST_SELECT_AMIIBO = 2; - private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; - private static SparseIntArray buttonsActionsMap = new SparseIntArray(); - - static { - buttonsActionsMap.append(R.id.menu_emulation_edit_layout, - EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT); - buttonsActionsMap.append(R.id.menu_emulation_toggle_controls, - EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS); - buttonsActionsMap - .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE); - buttonsActionsMap.append(R.id.menu_emulation_show_fps, - EmulationActivity.MENU_ACTION_SHOW_FPS); - buttonsActionsMap.append(R.id.menu_screen_layout_landscape, - EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE); - buttonsActionsMap.append(R.id.menu_screen_layout_portrait, - EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT); - buttonsActionsMap.append(R.id.menu_screen_layout_single, - EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE); - buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside, - EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE); - buttonsActionsMap.append(R.id.menu_emulation_swap_screens, - EmulationActivity.MENU_ACTION_SWAP_SCREENS); - buttonsActionsMap - .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY); - buttonsActionsMap - .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY); - buttonsActionsMap - .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS); - buttonsActionsMap - .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO); - buttonsActionsMap - .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO); - buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center, - EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER); - buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable, - EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE); - buttonsActionsMap - .append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS); - } - - private View mDecorView; - private EmulationFragment mEmulationFragment; - private SharedPreferences mPreferences; - private ControllerMappingHelper mControllerMappingHelper; - private Intent foregroundService; - private boolean activityRecreated; - private String mSelectedTitle; - private String mPath; - - public static void launch(FragmentActivity activity, String path, String title) { - Intent launcher = new Intent(activity, EmulationActivity.class); - - launcher.putExtra(EXTRA_SELECTED_GAME, path); - launcher.putExtra(EXTRA_SELECTED_TITLE, title); - activity.startActivity(launcher); - } - - public static void tryDismissRunningNotification(Activity activity) { - NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION); - } - - @Override - protected void onDestroy() { - stopService(foregroundService); - super.onDestroy(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState == null) { - // Get params we were passed - Intent gameToEmulate = getIntent(); - mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME); - mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE); - activityRecreated = false; - } else { - activityRecreated = true; - restoreState(savedInstanceState); - } - - mControllerMappingHelper = new ControllerMappingHelper(); - - // Get a handle to the Window containing the UI. - mDecorView = getWindow().getDecorView(); - mDecorView.setOnSystemUiVisibilityChangeListener(visibility -> - { - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - // Go back to immersive fullscreen mode in 3s - Handler handler = new Handler(getMainLooper()); - handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */); - } - }); - // Set these options now so that the SurfaceView the game renders into is the right size. - enableFullscreenImmersive(); - - setTheme(R.style.CitraEmulationBase); - - setContentView(R.layout.activity_emulation); - - // Find or create the EmulationFragment - mEmulationFragment = (EmulationFragment) getSupportFragmentManager() - .findFragmentById(R.id.frame_emulation_fragment); - if (mEmulationFragment == null) { - mEmulationFragment = EmulationFragment.newInstance(mPath); - getSupportFragmentManager().beginTransaction() - .add(R.id.frame_emulation_fragment, mEmulationFragment) - .commit(); - } - - setTitle(mSelectedTitle); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(this); - - // Start a foreground service to prevent the app from getting killed in the background - foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); - startForegroundService(foregroundService); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - outState.putString(EXTRA_SELECTED_GAME, mPath); - outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle); - super.onSaveInstanceState(outState); - } - - protected void restoreState(Bundle savedInstanceState) { - mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME); - mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE); - - // If an alert prompt was in progress when state was restored, retry displaying it - NativeLibrary.retryDisplayAlertPrompt(); - } - - @Override - public void onRestart() { - super.onRestart(); - } - - @Override - public void onBackPressed() { - NativeLibrary.PauseEmulation(); - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_close_game) - .setMessage(R.string.emulation_close_game_message) - .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> - { - mEmulationFragment.stopEmulation(); - finish(); - }) - .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> - NativeLibrary.UnPauseEmulation()) - .setOnCancelListener(dialogInterface -> - NativeLibrary.UnPauseEmulation()) - .create() - .show(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA: - if (grantResults[0] != PackageManager.PERMISSION_GRANTED && - shouldShowRequestPermissionRationale(CAMERA)) { - new AlertDialog.Builder(this) - .setTitle(R.string.camera) - .setMessage(R.string.camera_permission_needed) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); - break; - case NativeLibrary.REQUEST_CODE_NATIVE_MIC: - if (grantResults[0] != PackageManager.PERMISSION_GRANTED && - shouldShowRequestPermissionRationale(RECORD_AUDIO)) { - new AlertDialog.Builder(this) - .setTitle(R.string.microphone) - .setMessage(R.string.microphone_permission_needed) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); - break; - default: - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - break; - } - } - - private void enableFullscreenImmersive() { - // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. - mDecorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_emulation, menu); - - int layoutOptionMenuItem = R.id.menu_screen_layout_landscape; - switch (EmulationMenuSettings.getLandscapeScreenLayout()) { - case EmulationMenuSettings.LayoutOption_SingleScreen: - layoutOptionMenuItem = R.id.menu_screen_layout_single; - break; - case EmulationMenuSettings.LayoutOption_SideScreen: - layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside; - break; - case EmulationMenuSettings.LayoutOption_MobilePortrait: - layoutOptionMenuItem = R.id.menu_screen_layout_portrait; - break; - } - - menu.findItem(layoutOptionMenuItem).setChecked(true); - menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter()); - menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable()); - menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps()); - menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens()); - menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay()); - - return true; - } - - private void DisplaySavestateWarning() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - if (preferences.getBoolean("savestateWarningShown", false)) { - return; - } - - LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.dialog_checkbox, null); - CheckBox checkBox = view.findViewById(R.id.checkBox); - - new AlertDialog.Builder(this) - .setTitle(R.string.savestate_warning_title) - .setMessage(R.string.savestate_warning_message) - .setView(view) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply(); - }) - .show(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_emulation_save_state).setVisible(false); - menu.findItem(R.id.menu_emulation_load_state).setVisible(false); - return true; - } - - @SuppressWarnings("WrongConstant") - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int action = buttonsActionsMap.get(item.getItemId(), -1); - - switch (action) { - // Edit the placement of the controls - case MENU_ACTION_EDIT_CONTROLS_PLACEMENT: - editControlsPlacement(); - break; - - // Enable/Disable specific buttons or the entire input overlay. - case MENU_ACTION_TOGGLE_CONTROLS: - toggleControls(); - break; - - // Adjust the scale of the overlay controls. - case MENU_ACTION_ADJUST_SCALE: - adjustScale(); - break; - - // Toggle the visibility of the Performance stats TextView - case MENU_ACTION_SHOW_FPS: { - final boolean isEnabled = !EmulationMenuSettings.getShowFps(); - EmulationMenuSettings.setShowFps(isEnabled); - item.setChecked(isEnabled); - - mEmulationFragment.updateShowFpsOverlay(); - break; - } - // Sets the screen layout to Landscape - case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE: - changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item); - break; - - // Sets the screen layout to Portrait - case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT: - changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item); - break; - - // Sets the screen layout to Single - case MENU_ACTION_SCREEN_LAYOUT_SINGLE: - changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item); - break; - - // Sets the screen layout to Side by Side - case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE: - changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item); - break; - - // Swap the top and bottom screen locations - case MENU_ACTION_SWAP_SCREENS: { - final boolean isEnabled = !EmulationMenuSettings.getSwapScreens(); - EmulationMenuSettings.setSwapScreens(isEnabled); - item.setChecked(isEnabled); - break; - } - - // Reset overlay placement - case MENU_ACTION_RESET_OVERLAY: - resetOverlay(); - break; - - // Show or hide overlay - case MENU_ACTION_SHOW_OVERLAY: { - final boolean isEnabled = !EmulationMenuSettings.getShowOverlay(); - EmulationMenuSettings.setShowOverlay(isEnabled); - item.setChecked(isEnabled); - - mEmulationFragment.refreshInputOverlay(); - break; - } - - case MENU_ACTION_EXIT: - mEmulationFragment.stopEmulation(); - finish(); - break; - - case MENU_ACTION_OPEN_SETTINGS: - SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); - break; - - case MENU_ACTION_LOAD_AMIIBO: - FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, - R.string.select_amiibo, - Collections.singletonList("bin"), false); - break; - - case MENU_ACTION_REMOVE_AMIIBO: - RemoveAmiibo(); - break; - - case MENU_ACTION_JOYSTICK_REL_CENTER: - final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter(); - EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled); - item.setChecked(isJoystickRelCenterEnabled); - break; - - case MENU_ACTION_DPAD_SLIDE_ENABLE: - final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable(); - EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled); - item.setChecked(isDpadSlideEnabled); - break; - - case MENU_ACTION_OPEN_CHEATS: - CheatsActivity.launch(this); - break; - } - - return true; - } - - private void changeScreenOrientation(int layoutOption, MenuItem item) { - item.setChecked(true); - NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay() - .getRotation()); - EmulationMenuSettings.setLandscapeScreenLayout(layoutOption); - } - - private void editControlsPlacement() { - if (mEmulationFragment.isConfiguringControls()) { - mEmulationFragment.stopConfiguringControls(); - } else { - mEmulationFragment.startConfiguringControls(); - } - } - - // Gets button presses - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int action; - int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); - - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - // Handling the case where the back button is pressed. - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - onBackPressed(); - return true; - } - - // Normal key events. - action = NativeLibrary.ButtonState.PRESSED; - break; - case KeyEvent.ACTION_UP: - action = NativeLibrary.ButtonState.RELEASED; - break; - default: - return false; - } - InputDevice input = event.getDevice(); - - if (input == null) { - // Controller was disconnected - return false; - } - - return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) { - super.onActivityResult(requestCode, resultCode, result); - switch (requestCode) { - case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER: - StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null); - break; - case REQUEST_SELECT_AMIIBO: - // If the user picked a file, as opposed to just backing out. - if (resultCode == MainActivity.RESULT_OK) { - String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result); - if (selectedFiles == null) - return; - - onAmiiboSelected(selectedFiles[0]); - } - break; - } - } - - private void onAmiiboSelected(String selectedFile) { - File file = new File(selectedFile); - boolean success = false; - try { - byte[] bytes = FileUtil.getBytesFromFile(file); - } catch (IOException e) { - e.printStackTrace(); - } - - if (!success) { - new AlertDialog.Builder(this) - .setTitle(R.string.amiibo_load_error) - .setMessage(R.string.amiibo_load_error_message) - .setPositiveButton(android.R.string.ok, null) - .create() - .show(); - } - } - - private void RemoveAmiibo() { - - } - - private void toggleControls() { - final SharedPreferences.Editor editor = mPreferences.edit(); - boolean[] enabledButtons = new boolean[14]; - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.emulation_toggle_controls); - - for (int i = 0; i < enabledButtons.length; i++) { - // Buttons that are disabled by default - boolean defaultValue = true; - switch (i) { - case 6: // ZL - case 7: // ZR - case 12: // C-stick - defaultValue = false; - break; - } - - enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue); - } - builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons, - (dialog, indexSelected, isChecked) -> editor - .putBoolean("buttonToggle" + indexSelected, isChecked)); - builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> - { - editor.apply(); - - mEmulationFragment.refreshInputOverlay(); - }); - - AlertDialog alertDialog = builder.create(); - alertDialog.show(); - } - - private void adjustScale() { - LayoutInflater inflater = LayoutInflater.from(this); - View view = inflater.inflate(R.layout.dialog_seekbar, null); - - final SeekBar seekbar = view.findViewById(R.id.seekbar); - final TextView value = view.findViewById(R.id.text_value); - final TextView units = view.findViewById(R.id.text_units); - - seekbar.setMax(150); - seekbar.setProgress(mPreferences.getInt("controlScale", 50)); - seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - public void onStartTrackingTouch(SeekBar seekBar) { - } - - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - value.setText(String.valueOf(progress + 50)); - } - - public void onStopTrackingTouch(SeekBar seekBar) { - setControlScale(seekbar.getProgress()); - } - }); - - value.setText(String.valueOf(seekbar.getProgress() + 50)); - units.setText("%"); - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.emulation_control_scale); - builder.setView(view); - final int previousProgress = seekbar.getProgress(); - builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { - setControlScale(previousProgress); - }); - builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> - { - setControlScale(seekbar.getProgress()); - }); - builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> { - setControlScale(50); - }); - - AlertDialog alertDialog = builder.create(); - alertDialog.show(); - } - - private void setControlScale(int scale) { - SharedPreferences.Editor editor = mPreferences.edit(); - editor.putInt("controlScale", scale); - editor.apply(); - mEmulationFragment.refreshInputOverlay(); - } - - private void resetOverlay() { - new AlertDialog.Builder(this) - .setTitle(getString(R.string.emulation_touch_overlay_reset)) - .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay()) - .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { - }) - .create() - .show(); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) { - return super.dispatchGenericMotionEvent(event); - } - - // Don't attempt to do anything if we are disconnecting a device. - if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { - return true; - } - - InputDevice input = event.getDevice(); - List motions = input.getMotionRanges(); - - float[] axisValuesCirclePad = {0.0f, 0.0f}; - float[] axisValuesCStick = {0.0f, 0.0f}; - float[] axisValuesDPad = {0.0f, 0.0f}; - boolean isTriggerPressedLMapped = false; - boolean isTriggerPressedRMapped = false; - boolean isTriggerPressedZLMapped = false; - boolean isTriggerPressedZRMapped = false; - boolean isTriggerPressedL = false; - boolean isTriggerPressedR = false; - boolean isTriggerPressedZL = false; - boolean isTriggerPressedZR = false; - - for (InputDevice.MotionRange range : motions) { - int axis = range.getAxis(); - float origValue = event.getAxisValue(axis); - float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); - int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); - int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); - - if (nextMapping == -1 || guestOrientation == -1) { - // Axis is unmapped - continue; - } - - if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) { - // Skip joystick wobble - value = 0.f; - } - - if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) { - axisValuesCirclePad[guestOrientation] = value; - } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) { - axisValuesCStick[guestOrientation] = value; - } else if (nextMapping == NativeLibrary.ButtonType.DPAD) { - axisValuesDPad[guestOrientation] = value; - } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) { - isTriggerPressedLMapped = true; - isTriggerPressedL = value != 0.f; - } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) { - isTriggerPressedRMapped = true; - isTriggerPressedR = value != 0.f; - } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) { - isTriggerPressedZLMapped = true; - isTriggerPressedZL = value != 0.f; - } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) { - isTriggerPressedZRMapped = true; - isTriggerPressedZR = value != 0.f; - } - } - - // Circle-Pad and C-Stick status - NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]); - NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]); - - // Triggers L/R and ZL/ZR - if (isTriggerPressedLMapped) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); - } - if (isTriggerPressedRMapped) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); - } - if (isTriggerPressedZLMapped) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); - } - if (isTriggerPressedZRMapped) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); - } - - // Work-around to allow D-pad axis to be bound to emulated buttons - if (axisValuesDPad[0] == 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); - } - if (axisValuesDPad[0] < 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); - } - if (axisValuesDPad[0] > 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED); - } - if (axisValuesDPad[1] == 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); - } - if (axisValuesDPad[1] < 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); - } - if (axisValuesDPad[1] > 0.f) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED); - } - - return true; - } - - public boolean isActivityRecreated() { - return activityRecreated; - } - - @Retention(SOURCE) - @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE, - MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE, - MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE, - MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS}) - public @interface MenuAction { - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java deleted file mode 100644 index bc791638a..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java +++ /dev/null @@ -1,247 +0,0 @@ -package org.citra.citra_emu.adapters; - -import android.database.Cursor; -import android.database.DataSetObserver; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.SystemClock; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.model.GameDatabase; -import org.citra.citra_emu.ui.DividerItemDecoration; -import org.citra.citra_emu.utils.Log; -import org.citra.citra_emu.utils.PicassoUtils; -import org.citra.citra_emu.viewholders.GameViewHolder; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; - -/** - * This adapter gets its information from a database Cursor. This fact, paired with the usage of - * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) - * large dataset. - */ -public final class GameAdapter extends RecyclerView.Adapter implements - View.OnClickListener { - private Cursor mCursor; - private GameDataSetObserver mObserver; - - private boolean mDatasetValid; - private long mLastClickTime = 0; - - /** - * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will - * display no data until a Cursor is supplied by a CursorLoader. - */ - public GameAdapter() { - mDatasetValid = false; - mObserver = new GameDataSetObserver(); - } - - /** - * Called by the LayoutManager when it is necessary to create a new view. - * - * @param parent The RecyclerView (I think?) the created view will be thrown into. - * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. - * @return The created ViewHolder with references to all the child view's members. - */ - @Override - public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - // Create a new view. - View gameCard = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.card_game, parent, false); - - gameCard.setOnClickListener(this); - - // Use that view to create a ViewHolder. - return new GameViewHolder(gameCard); - } - - /** - * Called by the LayoutManager when a new view is not necessary because we can recycle - * an existing one (for example, if a view just scrolled onto the screen from the bottom, we - * can use the view that just scrolled off the top instead of inflating a new one.) - * - * @param holder A ViewHolder representing the view we're recycling. - * @param position The position of the 'new' view in the dataset. - */ - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public void onBindViewHolder(@NonNull GameViewHolder holder, int position) { - if (mDatasetValid) { - if (mCursor.moveToPosition(position)) { - PicassoUtils.loadGameIcon(holder.imageIcon, - mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); - - holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); - holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); - - final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); - holder.textFileName.setText(gamePath.getFileName().toString()); - - // TODO These shouldn't be necessary once the move to a DB-based model is complete. - holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); - holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH); - holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); - holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); - holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); - holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); - - final int backgroundColorId = isValidGame(holder.path) ? R.color.card_view_background : R.color.card_view_disabled; - View itemView = holder.getItemView(); - itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId)); - } else { - Log.error("[GameAdapter] Can't bind view; Cursor is not valid."); - } - } else { - Log.error("[GameAdapter] Can't bind view; dataset is not valid."); - } - } - - /** - * Called by the LayoutManager to find out how much data we have. - * - * @return Size of the dataset. - */ - @Override - public int getItemCount() { - if (mDatasetValid && mCursor != null) { - return mCursor.getCount(); - } - Log.error("[GameAdapter] Dataset is not valid."); - return 0; - } - - /** - * Return the contents of the _id column for a given row. - * - * @param position The row for which Android wants an ID. - * @return A valid ID from the database, or 0 if not available. - */ - @Override - public long getItemId(int position) { - if (mDatasetValid && mCursor != null) { - if (mCursor.moveToPosition(position)) { - return mCursor.getLong(GameDatabase.COLUMN_DB_ID); - } - } - - Log.error("[GameAdapter] Dataset is not valid."); - return 0; - } - - /** - * Tell Android whether or not each item in the dataset has a stable identifier. - * Which it does, because it's a database, so always tell Android 'true'. - * - * @param hasStableIds ignored. - */ - @Override - public void setHasStableIds(boolean hasStableIds) { - super.setHasStableIds(true); - } - - /** - * When a load is finished, call this to replace the existing data with the newly-loaded - * data. - * - * @param cursor The newly-loaded Cursor. - */ - public void swapCursor(Cursor cursor) { - // Sanity check. - if (cursor == mCursor) { - return; - } - - // Before getting rid of the old cursor, disassociate it from the Observer. - final Cursor oldCursor = mCursor; - if (oldCursor != null && mObserver != null) { - oldCursor.unregisterDataSetObserver(mObserver); - } - - mCursor = cursor; - if (mCursor != null) { - // Attempt to associate the new Cursor with the Observer. - if (mObserver != null) { - mCursor.registerDataSetObserver(mObserver); - } - - mDatasetValid = true; - } else { - mDatasetValid = false; - } - - notifyDataSetChanged(); - } - - /** - * Launches the game that was clicked on. - * - * @param view The card representing the game the user wants to play. - */ - @Override - public void onClick(View view) { - // Double-click prevention, using threshold of 1000 ms - if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) { - return; - } - mLastClickTime = SystemClock.elapsedRealtime(); - - GameViewHolder holder = (GameViewHolder) view.getTag(); - - EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title); - } - - public static class SpacesItemDecoration extends DividerItemDecoration { - private int space; - - public SpacesItemDecoration(Drawable divider, int space) { - super(divider); - this.space = space; - } - - @Override - public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent, - @NonNull RecyclerView.State state) { - outRect.left = 0; - outRect.right = 0; - outRect.bottom = space; - outRect.top = 0; - } - } - - private boolean isValidGame(String path) { - return Stream.of( - ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix)); - } - - private final class GameDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - super.onChanged(); - - mDatasetValid = true; - notifyDataSetChanged(); - } - - @Override - public void onInvalidated() { - super.onInvalidated(); - - mDatasetValid = false; - notifyDataSetChanged(); - } - } -} 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 deleted file mode 100644 index 3586a9b34..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.applets; - -import android.app.Activity; -import android.app.Dialog; -import android.os.Bundle; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -public final class MiiSelector { - public static class MiiSelectorConfig implements java.io.Serializable { - public boolean enable_cancel_button; - public String title; - public long initially_selected_mii_index; - // List of Miis to display - public String[] mii_names; - } - - public static class MiiSelectorData { - public long return_code; - public int index; - - private MiiSelectorData(long return_code, int index) { - this.return_code = return_code; - this.index = index; - } - } - - public static class MiiSelectorDialogFragment extends DialogFragment { - static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) { - MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment(); - Bundle args = new Bundle(); - args.putSerializable("config", config); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity emulationActivity = Objects.requireNonNull(getActivity()); - - MiiSelectorConfig config = - Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments()) - .getSerializable("config")); - - // Note: we intentionally leave out the Standard Mii in the native code so that - // the string can get translated - ArrayList list = new ArrayList<>(); - list.add(emulationActivity.getString(R.string.standard_mii)); - list.addAll(Arrays.asList(config.mii_names)); - - final int initialIndex = config.initially_selected_mii_index < list.size() - ? (int) config.initially_selected_mii_index - : 0; - data.index = initialIndex; - AlertDialog.Builder builder = - new AlertDialog.Builder(emulationActivity) - .setTitle(config.title.isEmpty() - ? emulationActivity.getString(R.string.mii_selector) - : config.title) - .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex, - (dialog, which) -> { - data.index = which; - }) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - data.return_code = 0; - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - if (config.enable_cancel_button) { - builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { - data.return_code = 1; - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - setCancelable(false); - return builder.create(); - } - } - - private static MiiSelectorData data; - private static final Object finishLock = new Object(); - - private static void ExecuteImpl(MiiSelectorConfig config) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - - data = new MiiSelectorData(0, 0); - - MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config); - fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector"); - } - - public static MiiSelectorData Execute(MiiSelectorConfig config) { - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); - - synchronized (finishLock) { - try { - finishLock.wait(); - } catch (Exception ignored) { - } - } - - return data; - } -} 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 deleted file mode 100644 index 7be5f6d97..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.applets; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.InputFilter; -import android.text.Spanned; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.utils.Log; - -import java.util.Objects; - -public final class SoftwareKeyboard { - /// Corresponds to Frontend::ButtonConfig - private interface ButtonConfig { - int Single = 0; /// Ok button - int Dual = 1; /// Cancel | Ok buttons - int Triple = 2; /// Cancel | I Forgot | Ok buttons - int None = 3; /// No button (returned by swkbdInputText in special cases) - } - - /// Corresponds to Frontend::ValidationError - public enum ValidationError { - None, - // Button Selection - ButtonOutOfRange, - // Configured Filters - MaxDigitsExceeded, - AtSignNotAllowed, - PercentNotAllowed, - BackslashNotAllowed, - ProfanityNotAllowed, - CallbackFailed, - // Allowed Input Type - FixedLengthRequired, - MaxLengthExceeded, - BlankInputNotAllowed, - EmptyInputNotAllowed, - } - - public static class KeyboardConfig implements java.io.Serializable { - public int button_config; - public int max_text_length; - public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input - public String hint_text; /// Displayed in the field as a hint before - @Nullable - public String[] button_text; /// Contains the button text that the caller provides - } - - /// Corresponds to Frontend::KeyboardData - public static class KeyboardData { - public int button; - public String text; - - private KeyboardData(int button, String text) { - this.button = button; - this.text = text; - } - } - - private static class Filter implements InputFilter { - @Override - public CharSequence filter(CharSequence source, int start, int end, Spanned dest, - int dstart, int dend) { - String text = new StringBuilder(dest) - .replace(dstart, dend, source.subSequence(start, end).toString()) - .toString(); - if (ValidateFilters(text) == ValidationError.None) { - return null; // Accept replacement - } - return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged - } - } - - public static class KeyboardDialogFragment extends DialogFragment { - static KeyboardDialogFragment newInstance(KeyboardConfig config) { - KeyboardDialogFragment frag = new KeyboardDialogFragment(); - Bundle args = new Bundle(); - args.putSerializable("config", config); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity emulationActivity = getActivity(); - assert emulationActivity != null; - - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.leftMargin = params.rightMargin = - CitraApplication.getAppContext().getResources().getDimensionPixelSize( - R.dimen.dialog_margin); - - KeyboardConfig config = Objects.requireNonNull( - (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); - - // Set up the input - EditText editText = new EditText(CitraApplication.getAppContext()); - editText.setHint(config.hint_text); - editText.setSingleLine(!config.multiline_mode); - editText.setLayoutParams(params); - editText.setFilters(new InputFilter[]{ - new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); - - FrameLayout container = new FrameLayout(emulationActivity); - container.addView(editText); - - AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) - .setTitle(R.string.software_keyboard) - .setView(container); - setCancelable(false); - - switch (config.button_config) { - case ButtonConfig.Triple: { - final String text = config.button_text[1].isEmpty() - ? emulationActivity.getString(R.string.i_forgot) - : config.button_text[1]; - builder.setNeutralButton(text, null); - } - // fallthrough - case ButtonConfig.Dual: { - final String text = config.button_text[0].isEmpty() - ? emulationActivity.getString(android.R.string.cancel) - : config.button_text[0]; - builder.setNegativeButton(text, null); - } - // fallthrough - case ButtonConfig.Single: { - final String text = config.button_text[2].isEmpty() - ? emulationActivity.getString(android.R.string.ok) - : config.button_text[2]; - builder.setPositiveButton(text, null); - break; - } - } - - final AlertDialog dialog = builder.create(); - dialog.create(); - if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { - data.button = config.button_config; - data.text = editText.getText().toString(); - final ValidationError error = ValidateInput(data.text); - if (error != ValidationError.None) { - HandleValidationError(config, error); - return; - } - - dialog.dismiss(); - - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { - dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { - data.button = 1; - dialog.dismiss(); - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { - data.button = 0; - dialog.dismiss(); - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - - return dialog; - } - } - - private static KeyboardData data; - private static final Object finishLock = new Object(); - - private static void ExecuteImpl(KeyboardConfig config) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - - data = new KeyboardData(0, ""); - - KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); - fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); - } - - private static void HandleValidationError(KeyboardConfig config, ValidationError error) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - String message = ""; - switch (error) { - case FixedLengthRequired: - message = - emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); - break; - case MaxLengthExceeded: - message = - emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); - break; - case BlankInputNotAllowed: - message = emulationActivity.getString(R.string.blank_input_not_allowed); - break; - case EmptyInputNotAllowed: - message = emulationActivity.getString(R.string.empty_input_not_allowed); - break; - } - - new AlertDialog.Builder(emulationActivity) - .setTitle(R.string.software_keyboard) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - - public static KeyboardData Execute(KeyboardConfig config) { - if (config.button_config == ButtonConfig.None) { - Log.error("Unexpected button config None"); - return new KeyboardData(0, ""); - } - - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); - - synchronized (finishLock) { - try { - finishLock.wait(); - } catch (Exception ignored) { - } - } - - return data; - } - - public static void ShowError(String error) { - NativeLibrary.displayAlertMsg( - CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard), - error, false); - } - - private static native ValidationError ValidateFilters(String text); - - private static native ValidationError ValidateInput(String text); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java deleted file mode 100644 index 701cb0710..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.camera; - -import android.content.Intent; -import android.graphics.Bitmap; -import android.provider.MediaStore; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.utils.PicassoUtils; - -import androidx.annotation.Nullable; - -// Used in native code. -public final class StillImageCameraHelper { - public static final int REQUEST_CAMERA_FILE_PICKER = 1; - private static final Object filePickerLock = new Object(); - private static @Nullable - String filePickerPath; - - // Opens file picker for camera. - public static @Nullable - String OpenFilePicker() { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - - // At this point, we are assuming that we already have permissions as they are - // needed to launch a game - emulationActivity.runOnUiThread(() -> { - Intent intent = new Intent(Intent.ACTION_PICK); - intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); - emulationActivity.startActivityForResult( - Intent.createChooser(intent, - emulationActivity.getString(R.string.camera_select_image)), - REQUEST_CAMERA_FILE_PICKER); - }); - - synchronized (filePickerLock) { - try { - filePickerLock.wait(); - } catch (InterruptedException ignored) { - } - } - - return filePickerPath; - } - - // Called from EmulationActivity. - public static void OnFilePickerResult(Intent result) { - filePickerPath = result == null ? null : result.getDataString(); - - synchronized (filePickerLock) { - filePickerLock.notifyAll(); - } - } - - // Blocking call. Load image from file and crop/resize it to fit in width x height. - @Nullable - public static Bitmap LoadImageFromFile(String uri, int width, int height) { - return PicassoUtils.LoadBitmapFromFile(uri, width, height); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java deleted file mode 100644 index 0f10f1858..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.citra.citra_emu.dialogs; - -import android.content.Context; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@link AlertDialog} derivative that listens for - * motion events from controllers and joysticks. - */ -public final class MotionAlertDialog extends AlertDialog { - // The selected input preference - private final InputBindingSetting setting; - private final ArrayList mPreviousValues = new ArrayList<>(); - private int mPrevDeviceId = 0; - private boolean mWaitingForEvent = true; - - /** - * Constructor - * - * @param context The current {@link Context}. - * @param setting The Preference to show this dialog for. - */ - public MotionAlertDialog(Context context, InputBindingSetting setting) { - super(context); - - this.setting = setting; - } - - public boolean onKeyEvent(int keyCode, KeyEvent event) { - Log.debug("[MotionAlertDialog] Received key event: " + event.getAction()); - switch (event.getAction()) { - case KeyEvent.ACTION_UP: - setting.onKeyInput(event); - dismiss(); - // Even if we ignore the key, we still consume it. Thus return true regardless. - return true; - - default: - return false; - } - } - - @Override - public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { - return super.onKeyLongPress(keyCode, event); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // Handle this key if we care about it, otherwise pass it down the framework - return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) { - // Handle this event if we care about it, otherwise pass it down the framework - return onMotionEvent(event) || super.dispatchGenericMotionEvent(event); - } - - private boolean onMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) - return false; - if (event.getAction() != MotionEvent.ACTION_MOVE) - return false; - - InputDevice input = event.getDevice(); - - List motionRanges = input.getMotionRanges(); - - if (input.getId() != mPrevDeviceId) { - mPreviousValues.clear(); - } - mPrevDeviceId = input.getId(); - boolean firstEvent = mPreviousValues.isEmpty(); - - int numMovedAxis = 0; - float axisMoveValue = 0.0f; - InputDevice.MotionRange lastMovedRange = null; - char lastMovedDir = '?'; - if (mWaitingForEvent) { - for (int i = 0; i < motionRanges.size(); i++) { - InputDevice.MotionRange range = motionRanges.get(i); - int axis = range.getAxis(); - float origValue = event.getAxisValue(axis); - float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue); - if (firstEvent) { - mPreviousValues.add(value); - } else { - float previousValue = mPreviousValues.get(i); - - // Only handle the axes that are not neutral (more than 0.5) - // but ignore any axis that has a constant value (e.g. always 1) - if (Math.abs(value) > 0.5f && value != previousValue) { - // It is common to have multiple axes with the same physical input. For example, - // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. - // To handle this, we ignore an axis motion that's the exact same as a motion - // we already saw. This way, we ignore axes with two names, but catch the case - // where a joystick is moved in two directions. - // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html - if (value != axisMoveValue) { - axisMoveValue = value; - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = value < 0.0f ? '-' : '+'; - } - } - // Special case for d-pads (axis value jumps between 0 and 1 without any values - // in between). Without this, the user would need to press the d-pad twice - // due to the first press being caught by the "if (firstEvent)" case further up. - else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) { - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = previousValue < 0.0f ? '-' : '+'; - } - } - - mPreviousValues.set(i, value); - } - - // If only one axis moved, that's the winner. - if (numMovedAxis == 1) { - mWaitingForEvent = false; - setting.onMotionInput(input, lastMovedRange, lastMovedDir); - dismiss(); - } - } - return true; - } -} \ 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 deleted file mode 100644 index d6d14cc5f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2021 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.disk_shader_cache; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.utils.Log; - -import java.util.Objects; - -public class DiskShaderCacheProgress { - - // Equivalent to VideoCore::LoadCallbackStage - public enum LoadCallbackStage { - Prepare, - Decompile, - Build, - Complete, - } - - private static final Object finishLock = new Object(); - private static ProgressDialogFragment fragment; - - public static class ProgressDialogFragment extends DialogFragment { - ProgressBar progressBar; - TextView progressText; - AlertDialog dialog; - - static ProgressDialogFragment newInstance(String title, String message) { - ProgressDialogFragment frag = new ProgressDialogFragment(); - Bundle args = new Bundle(); - args.putString("title", title); - args.putString("message", message); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity emulationActivity = Objects.requireNonNull(getActivity()); - - final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); - final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); - - LayoutInflater inflater = LayoutInflater.from(emulationActivity); - View view = inflater.inflate(R.layout.dialog_progress_bar, null); - - progressBar = view.findViewById(R.id.progress_bar); - progressText = view.findViewById(R.id.progress_text); - progressText.setText(""); - - setCancelable(false); - setRetainInstance(true); - - AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity); - builder.setTitle(title); - builder.setMessage(message); - builder.setView(view); - builder.setNegativeButton(android.R.string.cancel, null); - - dialog = builder.create(); - dialog.create(); - - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v) -> emulationActivity.onBackPressed()); - - synchronized (finishLock) { - finishLock.notifyAll(); - } - - return dialog; - } - - private void onUpdateProgress(String msg, int progress, int max) { - Objects.requireNonNull(getActivity()).runOnUiThread(() -> { - progressBar.setProgress(progress); - progressBar.setMax(max); - progressText.setText(String.format("%d/%d", progress, max)); - dialog.setMessage(msg); - }); - } - } - - private static void prepareDialog() { - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders)); - fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders"); - }); - - synchronized (finishLock) { - try { - finishLock.wait(); - } catch (Exception ignored) { - } - } - } - - public static void loadProgress(LoadCallbackStage stage, int progress, int max) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - if (emulationActivity == null) { - Log.error("[DiskShaderCacheProgress] EmulationActivity not present"); - return; - } - - switch (stage) { - case Prepare: - prepareDialog(); - break; - case Decompile: - fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max); - break; - case Build: - fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max); - break; - case Complete: - // Workaround for when dialog is dismissed when the app is in the background - fragment.dismissAllowingStateLoss(); - break; - } - } -} 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 deleted file mode 100644 index 93b026364..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.citra.citra_emu.features.cheats.model; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class Cheat { - @Keep - private final long mPointer; - - private Runnable mEnabledChangedCallback = null; - - @Keep - private Cheat(long pointer) { - mPointer = pointer; - } - - @Override - protected native void finalize(); - - @NonNull - public native String getName(); - - @NonNull - public native String getNotes(); - - @NonNull - public native String getCode(); - - public native boolean getEnabled(); - - public void setEnabled(boolean enabled) { - setEnabledImpl(enabled); - onEnabledChanged(); - } - - private native void setEnabledImpl(boolean enabled); - - public void setEnabledChangedCallback(@Nullable Runnable callback) { - mEnabledChangedCallback = callback; - } - - private void onEnabledChanged() { - if (mEnabledChangedCallback != null) { - mEnabledChangedCallback.run(); - } - } - - /** - * If the code is valid, returns 0. Otherwise, returns the 1-based index - * for the line containing the error. - */ - public static native int isValidGatewayCode(@NonNull String code); - - public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes, - @NonNull String code); -} 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 deleted file mode 100644 index 5748162bb..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.citra.citra_emu.features.cheats.model; - -public class CheatEngine { - public static native Cheat[] getCheats(); - - public static native void addCheat(Cheat cheat); - - public static native void removeCheat(int index); - - public static native void updateCheat(int index, Cheat newCheat); - - public static native void saveCheatFile(); -} 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 deleted file mode 100644 index 66f4202d8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.citra.citra_emu.features.cheats.model; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; - -public class CheatsViewModel extends ViewModel { - private int mSelectedCheatPosition = -1; - private final MutableLiveData mSelectedCheat = new MutableLiveData<>(null); - private final MutableLiveData mIsAdding = new MutableLiveData<>(false); - private final MutableLiveData mIsEditing = new MutableLiveData<>(false); - - private final MutableLiveData mCheatAddedEvent = new MutableLiveData<>(null); - private final MutableLiveData mCheatChangedEvent = new MutableLiveData<>(null); - private final MutableLiveData mCheatDeletedEvent = new MutableLiveData<>(null); - private final MutableLiveData mOpenDetailsViewEvent = new MutableLiveData<>(false); - - private Cheat[] mCheats; - private boolean mCheatsNeedSaving = false; - - public void load() { - mCheats = CheatEngine.getCheats(); - - for (int i = 0; i < mCheats.length; i++) { - int position = i; - mCheats[i].setEnabledChangedCallback(() -> { - mCheatsNeedSaving = true; - notifyCheatUpdated(position); - }); - } - } - - public void saveIfNeeded() { - if (mCheatsNeedSaving) { - CheatEngine.saveCheatFile(); - mCheatsNeedSaving = false; - } - } - - public Cheat[] getCheats() { - return mCheats; - } - - public LiveData getSelectedCheat() { - return mSelectedCheat; - } - - public void setSelectedCheat(Cheat cheat, int position) { - if (mIsEditing.getValue()) { - setIsEditing(false); - } - - mSelectedCheat.setValue(cheat); - mSelectedCheatPosition = position; - } - - public LiveData getIsAdding() { - return mIsAdding; - } - - public LiveData getIsEditing() { - return mIsEditing; - } - - public void setIsEditing(boolean isEditing) { - mIsEditing.setValue(isEditing); - - if (mIsAdding.getValue() && !isEditing) { - mIsAdding.setValue(false); - setSelectedCheat(null, -1); - } - } - - /** - * When a cheat is added, the integer stored in the returned LiveData - * changes to the position of that cheat, then changes back to null. - */ - public LiveData getCheatAddedEvent() { - return mCheatAddedEvent; - } - - private void notifyCheatAdded(int position) { - mCheatAddedEvent.setValue(position); - mCheatAddedEvent.setValue(null); - } - - public void startAddingCheat() { - mSelectedCheat.setValue(null); - mSelectedCheatPosition = -1; - - mIsAdding.setValue(true); - mIsEditing.setValue(true); - } - - public void finishAddingCheat(Cheat cheat) { - if (!mIsAdding.getValue()) { - throw new IllegalStateException(); - } - - mIsAdding.setValue(false); - mIsEditing.setValue(false); - - int position = mCheats.length; - - CheatEngine.addCheat(cheat); - - mCheatsNeedSaving = true; - load(); - - notifyCheatAdded(position); - setSelectedCheat(mCheats[position], position); - } - - /** - * When a cheat is edited, the integer stored in the returned LiveData - * changes to the position of that cheat, then changes back to null. - */ - public LiveData getCheatUpdatedEvent() { - return mCheatChangedEvent; - } - - /** - * Notifies that an edit has been made to the contents of the cheat at the given position. - */ - private void notifyCheatUpdated(int position) { - mCheatChangedEvent.setValue(position); - mCheatChangedEvent.setValue(null); - } - - public void updateSelectedCheat(Cheat newCheat) { - CheatEngine.updateCheat(mSelectedCheatPosition, newCheat); - - mCheatsNeedSaving = true; - load(); - - notifyCheatUpdated(mSelectedCheatPosition); - setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition); - } - - /** - * When a cheat is deleted, the integer stored in the returned LiveData - * changes to the position of that cheat, then changes back to null. - */ - public LiveData getCheatDeletedEvent() { - return mCheatDeletedEvent; - } - - /** - * Notifies that the cheat at the given position has been deleted. - */ - private void notifyCheatDeleted(int position) { - mCheatDeletedEvent.setValue(position); - mCheatDeletedEvent.setValue(null); - } - - public void deleteSelectedCheat() { - int position = mSelectedCheatPosition; - - setSelectedCheat(null, -1); - - CheatEngine.removeCheat(position); - - mCheatsNeedSaving = true; - load(); - - notifyCheatDeleted(position); - } - - public LiveData getOpenDetailsViewEvent() { - return mOpenDetailsViewEvent; - } - - public void openDetailsView() { - mOpenDetailsViewEvent.setValue(true); - mOpenDetailsViewEvent.setValue(false); - } -} 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 deleted file mode 100644 index 762cdb80e..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java +++ /dev/null @@ -1,174 +0,0 @@ -package org.citra.citra_emu.features.cheats.ui; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.model.Cheat; -import org.citra.citra_emu.features.cheats.model.CheatsViewModel; - -public class CheatDetailsFragment extends Fragment { - private View mRoot; - private ScrollView mScrollView; - private TextView mLabelName; - private EditText mEditName; - private EditText mEditNotes; - private EditText mEditCode; - private Button mButtonDelete; - private Button mButtonEdit; - private Button mButtonCancel; - private Button mButtonOk; - - private CheatsViewModel mViewModel; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_cheat_details, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - mRoot = view.findViewById(R.id.root); - mScrollView = view.findViewById(R.id.scroll_view); - mLabelName = view.findViewById(R.id.label_name); - mEditName = view.findViewById(R.id.edit_name); - mEditNotes = view.findViewById(R.id.edit_notes); - mEditCode = view.findViewById(R.id.edit_code); - mButtonDelete = view.findViewById(R.id.button_delete); - mButtonEdit = view.findViewById(R.id.button_edit); - mButtonCancel = view.findViewById(R.id.button_cancel); - mButtonOk = view.findViewById(R.id.button_ok); - - CheatsActivity activity = (CheatsActivity) requireActivity(); - mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); - - mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(), - this::onSelectedCheatUpdated); - mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated); - - mButtonDelete.setOnClickListener(this::onDeleteClicked); - mButtonEdit.setOnClickListener(this::onEditClicked); - mButtonCancel.setOnClickListener(this::onCancelClicked); - mButtonOk.setOnClickListener(this::onOkClicked); - - // On a portrait phone screen (or other narrow screen), only one of the two panes are shown - // at the same time. If the user is navigating using a d-pad and moves focus to an element - // in the currently hidden pane, we need to manually show that pane. - CheatsActivity.setOnFocusChangeListenerRecursively(view, - (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus)); - } - - private void clearEditErrors() { - mEditName.setError(null); - mEditCode.setError(null); - } - - private void onDeleteClicked(View view) { - String name = mEditName.getText().toString(); - - AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setMessage(getString(R.string.cheats_delete_confirmation, name)); - builder.setPositiveButton(android.R.string.yes, - (dialog, i) -> mViewModel.deleteSelectedCheat()); - builder.setNegativeButton(android.R.string.no, null); - builder.show(); - } - - private void onEditClicked(View view) { - mViewModel.setIsEditing(true); - mButtonOk.requestFocus(); - } - - private void onCancelClicked(View view) { - mViewModel.setIsEditing(false); - onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue()); - mButtonDelete.requestFocus(); - } - - private void onOkClicked(View view) { - clearEditErrors(); - - String name = mEditName.getText().toString(); - String notes = mEditNotes.getText().toString(); - String code = mEditCode.getText().toString(); - - if (name.isEmpty()) { - mEditName.setError(getString(R.string.cheats_error_no_name)); - mScrollView.smoothScrollTo(0, mLabelName.getTop()); - return; - } else if (code.isEmpty()) { - mEditCode.setError(getString(R.string.cheats_error_no_code_lines)); - mScrollView.smoothScrollTo(0, mEditCode.getBottom()); - return; - } - - int validityResult = Cheat.isValidGatewayCode(code); - - if (validityResult != 0) { - mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult)); - mScrollView.smoothScrollTo(0, mEditCode.getBottom()); - return; - } - - Cheat newCheat = Cheat.createGatewayCode(name, notes, code); - - if (mViewModel.getIsAdding().getValue()) { - mViewModel.finishAddingCheat(newCheat); - } else { - mViewModel.updateSelectedCheat(newCheat); - } - - mButtonEdit.requestFocus(); - } - - private void onSelectedCheatUpdated(@Nullable Cheat cheat) { - clearEditErrors(); - - boolean isEditing = mViewModel.getIsEditing().getValue(); - - mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE); - - // If the fragment was recreated while editing a cheat, it's vital that we - // don't repopulate the fields, otherwise the user's changes will be lost - if (!isEditing) { - if (cheat == null) { - mEditName.setText(""); - mEditNotes.setText(""); - mEditCode.setText(""); - } else { - mEditName.setText(cheat.getName()); - mEditNotes.setText(cheat.getNotes()); - mEditCode.setText(cheat.getCode()); - } - } - } - - private void onIsEditingUpdated(boolean isEditing) { - if (isEditing) { - mRoot.setVisibility(View.VISIBLE); - } - - mEditName.setEnabled(isEditing); - mEditNotes.setEnabled(isEditing); - mEditCode.setEnabled(isEditing); - - mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE); - mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE); - mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE); - mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE); - } -} 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 deleted file mode 100644 index 6c67a31d4..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.citra.citra_emu.features.cheats.ui; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.model.CheatsViewModel; -import org.citra.citra_emu.ui.DividerItemDecoration; - -public class CheatListFragment extends Fragment { - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_cheat_list, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - RecyclerView recyclerView = view.findViewById(R.id.cheat_list); - FloatingActionButton fab = view.findViewById(R.id.fab); - - CheatsActivity activity = (CheatsActivity) requireActivity(); - CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); - - recyclerView.setAdapter(new CheatsAdapter(activity, viewModel)); - recyclerView.setLayoutManager(new LinearLayoutManager(activity)); - recyclerView.addItemDecoration(new DividerItemDecoration(activity, null)); - - fab.setOnClickListener(v -> { - viewModel.startAddingCheat(); - viewModel.openDetailsView(); - }); - } -} 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 deleted file mode 100644 index 8ba8f86e7..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.citra.citra_emu.features.cheats.ui; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.model.Cheat; -import org.citra.citra_emu.features.cheats.model.CheatsViewModel; - -public class CheatViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { - private final View mRoot; - private final TextView mName; - private final CheckBox mCheckbox; - - private CheatsViewModel mViewModel; - private Cheat mCheat; - private int mPosition; - - public CheatViewHolder(@NonNull View itemView) { - super(itemView); - - mRoot = itemView.findViewById(R.id.root); - mName = itemView.findViewById(R.id.text_name); - mCheckbox = itemView.findViewById(R.id.checkbox); - } - - public void bind(CheatsActivity activity, Cheat cheat, int position) { - mCheckbox.setOnCheckedChangeListener(null); - - mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); - mCheat = cheat; - mPosition = position; - - mName.setText(mCheat.getName()); - mCheckbox.setChecked(mCheat.getEnabled()); - - mRoot.setOnClickListener(this); - mCheckbox.setOnCheckedChangeListener(this); - } - - public void onClick(View root) { - mViewModel.setSelectedCheat(mCheat, mPosition); - mViewModel.openDetailsView(); - } - - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mCheat.setEnabled(isChecked); - } -} 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 deleted file mode 100644 index a36bf427c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.citra.citra_emu.features.cheats.ui; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.view.ViewCompat; -import androidx.lifecycle.ViewModelProvider; -import androidx.slidingpanelayout.widget.SlidingPaneLayout; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.model.Cheat; -import org.citra.citra_emu.features.cheats.model.CheatsViewModel; -import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback; - -public class CheatsActivity extends AppCompatActivity - implements SlidingPaneLayout.PanelSlideListener { - private CheatsViewModel mViewModel; - - private SlidingPaneLayout mSlidingPaneLayout; - private View mCheatList; - private View mCheatDetails; - - private View mCheatListLastFocus; - private View mCheatDetailsLastFocus; - - public static void launch(Context context) { - Intent intent = new Intent(context, CheatsActivity.class); - context.startActivity(intent); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class); - mViewModel.load(); - - setContentView(R.layout.activity_cheats); - - mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout); - mCheatList = findViewById(R.id.cheat_list); - mCheatDetails = findViewById(R.id.cheat_details); - - mCheatListLastFocus = mCheatList; - mCheatDetailsLastFocus = mCheatDetails; - - mSlidingPaneLayout.addPanelSlideListener(this); - - getOnBackPressedDispatcher().addCallback(this, - new TwoPaneOnBackPressedCallback(mSlidingPaneLayout)); - - mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged); - mViewModel.getIsEditing().observe(this, this::onIsEditingChanged); - onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue()); - - mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView); - - // Show "Up" button in the action bar for navigation - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_settings, menu); - - return true; - } - - @Override - protected void onStop() { - super.onStop(); - - mViewModel.saveIfNeeded(); - } - - @Override - public void onPanelSlide(@NonNull View panel, float slideOffset) { - } - - @Override - public void onPanelOpened(@NonNull View panel) { - boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; - mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT); - } - - @Override - public void onPanelClosed(@NonNull View panel) { - boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; - mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT); - } - - private void onIsEditingChanged(boolean isEditing) { - if (isEditing) { - mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED); - } - } - - private void onSelectedCheatChanged(Cheat selectedCheat) { - boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue(); - - if (!cheatSelected && mSlidingPaneLayout.isOpen()) { - mSlidingPaneLayout.close(); - } - - mSlidingPaneLayout.setLockMode(cheatSelected ? - SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED); - } - - public void onListViewFocusChange(boolean hasFocus) { - if (hasFocus) { - mCheatListLastFocus = mCheatList.findFocus(); - if (mCheatListLastFocus == null) - throw new NullPointerException(); - - mSlidingPaneLayout.close(); - } - } - - public void onDetailsViewFocusChange(boolean hasFocus) { - if (hasFocus) { - mCheatDetailsLastFocus = mCheatDetails.findFocus(); - if (mCheatDetailsLastFocus == null) - throw new NullPointerException(); - - mSlidingPaneLayout.open(); - } - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - private void openDetailsView(boolean open) { - if (open) { - mSlidingPaneLayout.open(); - } - } - - public static void setOnFocusChangeListenerRecursively(@NonNull View view, - View.OnFocusChangeListener listener) { - view.setOnFocusChangeListener(listener); - - if (view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View child = viewGroup.getChildAt(i); - setOnFocusChangeListenerRecursively(child, listener); - } - } - } -} 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 deleted file mode 100644 index 9cb2ce8d8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.citra.citra_emu.features.cheats.ui; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.cheats.model.Cheat; -import org.citra.citra_emu.features.cheats.model.CheatsViewModel; - -public class CheatsAdapter extends RecyclerView.Adapter { - private final CheatsActivity mActivity; - private final CheatsViewModel mViewModel; - - public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) { - mActivity = activity; - mViewModel = viewModel; - - mViewModel.getCheatAddedEvent().observe(activity, (position) -> { - if (position != null) { - notifyItemInserted(position); - } - }); - - mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> { - if (position != null) { - notifyItemChanged(position); - } - }); - - mViewModel.getCheatDeletedEvent().observe(activity, (position) -> { - if (position != null) { - notifyItemRemoved(position); - } - }); - } - - @NonNull - @Override - public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false); - addViewListeners(cheatView); - return new CheatViewHolder(cheatView); - } - - @Override - public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) { - holder.bind(mActivity, getItemAt(position), position); - } - - @Override - public int getItemCount() { - return mViewModel.getCheats().length; - } - - private void addViewListeners(View view) { - // On a portrait phone screen (or other narrow screen), only one of the two panes are shown - // at the same time. If the user is navigating using a d-pad and moves focus to an element - // in the currently hidden pane, we need to manually show that pane. - CheatsActivity.setOnFocusChangeListenerRecursively(view, - (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus)); - } - - private Cheat getItemAt(int position) { - return mViewModel.getCheats()[position]; - } -} 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 deleted file mode 100644 index 932dcf1d3..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class BooleanSetting extends Setting { - private boolean mValue; - - public BooleanSetting(String key, String section, boolean value) { - super(key, section); - mValue = value; - } - - public boolean getValue() { - return mValue; - } - - public void setValue(boolean value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return mValue ? "True" : "False"; - } -} 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 deleted file mode 100644 index 275f0ecea..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class FloatSetting extends Setting { - private float mValue; - - public FloatSetting(String key, String section, float value) { - super(key, section); - mValue = value; - } - - public float getValue() { - return mValue; - } - - public void setValue(float value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return Float.toString(mValue); - } -} 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 deleted file mode 100644 index f712e5bfa..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class IntSetting extends Setting { - private int mValue; - - public IntSetting(String key, String section, int value) { - super(key, section); - mValue = value; - } - - public int getValue() { - return mValue; - } - - public void setValue(int value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return Integer.toString(mValue); - } -} 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 deleted file mode 100644 index b762847c9..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -/** - * Abstraction for a setting item as read from / written to Citra's configuration ini files. - * These files generally consist of a key/value pair, though the type of value is ambiguous and - * must be inferred at read-time. The type of value determines which child of this class is used - * to represent the Setting. - */ -public abstract class Setting { - private String mKey; - private String mSection; - - /** - * Base constructor. - * - * @param key Everything to the left of the = in a line from the ini file. - * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets. - */ - public Setting(String key, String section) { - mKey = key; - mSection = section; - } - - /** - * @return The identifier used to write this setting to the ini file. - */ - public String getKey() { - return mKey; - } - - /** - * @return The name of the header under which this Setting should be written in the ini file. - */ - public String getSection() { - return mSection; - } - - /** - * @return A representation of this Setting's backing value converted to a String (e.g. for serialization). - */ - public abstract String getValueAsString(); -} 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 deleted file mode 100644 index 0a291aa6b..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -import java.util.HashMap; - -/** - * A semantically-related group of Settings objects. These Settings are - * internally stored as a HashMap. - */ -public final class SettingSection { - private String mName; - - private HashMap mSettings = new HashMap<>(); - - /** - * Create a new SettingSection with no Settings in it. - * - * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets. - */ - public SettingSection(String name) { - mName = name; - } - - public String getName() { - return mName; - } - - /** - * Convenience method; inserts a value directly into the backing HashMap. - * - * @param setting The Setting to be inserted. - */ - public void putSetting(Setting setting) { - mSettings.put(setting.getKey(), setting); - } - - /** - * Convenience method; gets a value directly from the backing HashMap. - * - * @param key Used to retrieve the Setting. - * @return A Setting object (you should probably cast this before using) - */ - public Setting getSetting(String key) { - return mSettings.get(key); - } - - public HashMap getSettings() { - return mSettings; - } - - public void mergeSection(SettingSection settingSection) { - for (Setting setting : settingSection.mSettings.values()) { - putSetting(setting); - } - } -} 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 deleted file mode 100644 index 9684966f2..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -import android.text.TextUtils; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.ui.SettingsActivityView; -import org.citra.citra_emu.features.settings.utils.SettingsFile; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class Settings { - public static final String SECTION_PREMIUM = "Premium"; - public static final String SECTION_CORE = "Core"; - public static final String SECTION_SYSTEM = "System"; - public static final String SECTION_CAMERA = "Camera"; - public static final String SECTION_CONTROLS = "Controls"; - public static final String SECTION_RENDERER = "Renderer"; - public static final String SECTION_LAYOUT = "Layout"; - public static final String SECTION_UTILITY = "Utility"; - public static final String SECTION_AUDIO = "Audio"; - public static final String SECTION_DEBUG = "Debug"; - - private String gameId; - - private static final Map> configFileSectionsMap = new HashMap<>(); - - static { - 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)); - } - - /** - * A HashMap that constructs a new SettingSection instead of returning null - * when getting a key not already in the map - */ - public static final class SettingsSectionMap extends HashMap { - @Override - public SettingSection get(Object key) { - if (!(key instanceof String)) { - return null; - } - - String stringKey = (String) key; - - if (!super.containsKey(stringKey)) { - SettingSection section = new SettingSection(stringKey); - super.put(stringKey, section); - return section; - } - return super.get(key); - } - } - - private HashMap sections = new Settings.SettingsSectionMap(); - - public SettingSection getSection(String sectionName) { - return sections.get(sectionName); - } - - public boolean isEmpty() { - return sections.isEmpty(); - } - - public HashMap getSections() { - return sections; - } - - public void loadSettings(SettingsActivityView view) { - sections = new Settings.SettingsSectionMap(); - loadCitraSettings(view); - - if (!TextUtils.isEmpty(gameId)) { - loadCustomGameSettings(gameId, view); - } - } - - private void loadCitraSettings(SettingsActivityView view) { - for (Map.Entry> entry : configFileSectionsMap.entrySet()) { - String fileName = entry.getKey(); - sections.putAll(SettingsFile.readFile(fileName, view)); - } - } - - private void loadCustomGameSettings(String gameId, SettingsActivityView view) { - // custom game settings - mergeSections(SettingsFile.readCustomGameSettings(gameId, view)); - } - - private void mergeSections(HashMap updatedSections) { - for (Map.Entry entry : updatedSections.entrySet()) { - if (sections.containsKey(entry.getKey())) { - SettingSection originalSection = sections.get(entry.getKey()); - SettingSection updatedSection = entry.getValue(); - originalSection.mergeSection(updatedSection); - } else { - sections.put(entry.getKey(), entry.getValue()); - } - } - } - - public void loadSettings(String gameId, SettingsActivityView view) { - this.gameId = gameId; - loadSettings(view); - } - - public void saveSettings(SettingsActivityView view) { - if (TextUtils.isEmpty(gameId)) { - view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false); - - for (Map.Entry> entry : configFileSectionsMap.entrySet()) { - String fileName = entry.getKey(); - List sectionNames = entry.getValue(); - TreeMap iniSections = new TreeMap<>(); - for (String section : sectionNames) { - iniSections.put(section, sections.get(section)); - } - - SettingsFile.saveFile(fileName, iniSections, view); - } - } else { - // custom game settings - view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false); - - SettingsFile.saveCustomGameSettings(gameId, sections); - } - - } -} \ 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 deleted file mode 100644 index b906b7010..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class StringSetting extends Setting { - private String mValue; - - public StringSetting(String key, String section, String value) { - super(key, section); - mValue = value; - } - - public String getValue() { - return mValue; - } - - public void setValue(String value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return mValue; - } -} 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 deleted file mode 100644 index baf40709f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.BooleanSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; - -public final class CheckBoxSetting extends SettingsItem { - private boolean mDefaultValue; - private boolean mShowPerformanceWarning; - private SettingsFragmentView mView; - - public CheckBoxSetting(String key, String section, int titleId, int descriptionId, - boolean defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - mShowPerformanceWarning = false; - } - - public CheckBoxSetting(String key, String section, int titleId, int descriptionId, - boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - mView = view; - mShowPerformanceWarning = show_performance_warning; - } - - public boolean isChecked() { - if (getSetting() == null) { - return mDefaultValue; - } - - // Try integer setting - try { - IntSetting setting = (IntSetting) getSetting(); - return setting.getValue() == 1; - } catch (ClassCastException exception) { - } - - // Try boolean setting - try { - BooleanSetting setting = (BooleanSetting) getSetting(); - return setting.getValue() == true; - } catch (ClassCastException exception) { - } - - return mDefaultValue; - } - - /** - * Write a value to the backing boolean. If that boolean was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param checked Pretty self explanatory. - * @return null if overwritten successfully; otherwise, a newly created BooleanSetting. - */ - public IntSetting setChecked(boolean checked) { - // Show a performance warning if the setting has been disabled - if (mShowPerformanceWarning && !checked) { - mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true); - } - - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(checked ? 1 : 0); - return null; - } - } - - @Override - public int getType() { - return TYPE_CHECKBOX; - } -} 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 deleted file mode 100644 index afc3352cc..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; - -public final class DateTimeSetting extends SettingsItem { - private String mDefaultValue; - - public DateTimeSetting(String key, String section, int titleId, int descriptionId, - String defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - } - - public String getValue() { - if (getSetting() != null) { - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - public StringSetting setSelectedValue(String datetime) { - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), datetime); - setSetting(setting); - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - setting.setValue(datetime); - return null; - } - } - - @Override - public int getType() { - return TYPE_DATETIME_SETTING; - } -} \ 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 deleted file mode 100644 index bac8876cd..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; - -public final class HeaderSetting extends SettingsItem { - public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) { - super(key, null, setting, titleId, descriptionId); - } - - @Override - public int getType() { - return SettingsItem.TYPE_HEADER; - } -} 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 deleted file mode 100644 index e9141a208..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java +++ /dev/null @@ -1,382 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.widget.Toast; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.utils.SettingsFile; - -public final class InputBindingSetting extends SettingsItem { - private static final String INPUT_MAPPING_PREFIX = "InputMapping"; - - public InputBindingSetting(String key, String section, int titleId, Setting setting) { - super(key, section, setting, titleId, 0); - } - - public String getValue() { - if (getSetting() == null) { - return ""; - } - - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } - - /** - * Returns true if this key is for the 3DS Circle Pad - */ - private boolean IsCirclePad() { - switch (getKey()) { - case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad - */ - public boolean IsHorizontalOrientation() { - switch (getKey()) { - case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: - case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS C-Stick - */ - private boolean IsCStick() { - switch (getKey()) { - case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: - case SettingsFile.KEY_CSTICK_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS D-Pad - */ - private boolean IsDPad() { - switch (getKey()) { - case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_DPAD_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real - * triggers on the 3DS, but we support them as such on a physical gamepad. - */ - public boolean IsTrigger() { - switch (getKey()) { - case SettingsFile.KEY_BUTTON_L: - case SettingsFile.KEY_BUTTON_R: - case SettingsFile.KEY_BUTTON_ZL: - case SettingsFile.KEY_BUTTON_ZR: - return true; - } - return false; - } - - /** - * Returns true if a gamepad axis can be used to map this key. - */ - public boolean IsAxisMappingSupported() { - return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger(); - } - - /** - * Returns true if a gamepad button can be used to map this key. - */ - private boolean IsButtonMappingSupported() { - return !IsAxisMappingSupported() || IsTrigger(); - } - - /** - * Returns the Citra button code for the settings key. - */ - private int getButtonCode() { - switch (getKey()) { - case SettingsFile.KEY_BUTTON_A: - return NativeLibrary.ButtonType.BUTTON_A; - case SettingsFile.KEY_BUTTON_B: - return NativeLibrary.ButtonType.BUTTON_B; - case SettingsFile.KEY_BUTTON_X: - return NativeLibrary.ButtonType.BUTTON_X; - case SettingsFile.KEY_BUTTON_Y: - return NativeLibrary.ButtonType.BUTTON_Y; - case SettingsFile.KEY_BUTTON_L: - return NativeLibrary.ButtonType.TRIGGER_L; - case SettingsFile.KEY_BUTTON_R: - return NativeLibrary.ButtonType.TRIGGER_R; - case SettingsFile.KEY_BUTTON_ZL: - return NativeLibrary.ButtonType.BUTTON_ZL; - case SettingsFile.KEY_BUTTON_ZR: - return NativeLibrary.ButtonType.BUTTON_ZR; - case SettingsFile.KEY_BUTTON_SELECT: - return NativeLibrary.ButtonType.BUTTON_SELECT; - case SettingsFile.KEY_BUTTON_START: - return NativeLibrary.ButtonType.BUTTON_START; - case SettingsFile.KEY_BUTTON_UP: - return NativeLibrary.ButtonType.DPAD_UP; - case SettingsFile.KEY_BUTTON_DOWN: - return NativeLibrary.ButtonType.DPAD_DOWN; - case SettingsFile.KEY_BUTTON_LEFT: - return NativeLibrary.ButtonType.DPAD_LEFT; - case SettingsFile.KEY_BUTTON_RIGHT: - return NativeLibrary.ButtonType.DPAD_RIGHT; - } - return -1; - } - - /** - * Returns the settings key for the specified Citra button code. - */ - private static String getButtonKey(int buttonCode) { - switch (buttonCode) { - case NativeLibrary.ButtonType.BUTTON_A: - return SettingsFile.KEY_BUTTON_A; - case NativeLibrary.ButtonType.BUTTON_B: - return SettingsFile.KEY_BUTTON_B; - case NativeLibrary.ButtonType.BUTTON_X: - return SettingsFile.KEY_BUTTON_X; - case NativeLibrary.ButtonType.BUTTON_Y: - return SettingsFile.KEY_BUTTON_Y; - case NativeLibrary.ButtonType.TRIGGER_L: - return SettingsFile.KEY_BUTTON_L; - case NativeLibrary.ButtonType.TRIGGER_R: - return SettingsFile.KEY_BUTTON_R; - case NativeLibrary.ButtonType.BUTTON_ZL: - return SettingsFile.KEY_BUTTON_ZL; - case NativeLibrary.ButtonType.BUTTON_ZR: - return SettingsFile.KEY_BUTTON_ZR; - case NativeLibrary.ButtonType.BUTTON_SELECT: - return SettingsFile.KEY_BUTTON_SELECT; - case NativeLibrary.ButtonType.BUTTON_START: - return SettingsFile.KEY_BUTTON_START; - case NativeLibrary.ButtonType.DPAD_UP: - return SettingsFile.KEY_BUTTON_UP; - case NativeLibrary.ButtonType.DPAD_DOWN: - return SettingsFile.KEY_BUTTON_DOWN; - case NativeLibrary.ButtonType.DPAD_LEFT: - return SettingsFile.KEY_BUTTON_LEFT; - case NativeLibrary.ButtonType.DPAD_RIGHT: - return SettingsFile.KEY_BUTTON_RIGHT; - } - return ""; - } - - /** - * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old - * settings on re-mapping or clearing of a setting. - */ - private String getReverseKey() { - String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey(); - - if (IsAxisMappingSupported() && !IsTrigger()) { - // Triggers are the only axis-supported mappings without orientation - reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1); - } - - return reverseKey; - } - - /** - * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. - */ - public void removeOldMapping() { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Try remove all possible keys we wrote for this setting - String oldKey = preferences.getString(getReverseKey(), ""); - if (!oldKey.equals("")) { - editor.remove(getKey()); // Used for ui text - editor.remove(oldKey); // Used for button mapping - editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation - editor.remove(oldKey + "_GuestButton"); // Used for axis button - } - - // Apply changes - editor.apply(); - } - - /** - * Helper function to get the settings key for an gamepad button. - */ - public static String getInputButtonKey(int keyCode) { - return INPUT_MAPPING_PREFIX + "_Button_" + keyCode; - } - - /** - * Helper function to get the settings key for an gamepad axis. - */ - public static String getInputAxisKey(int axis) { - return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis; - } - - /** - * Helper function to get the settings key for an gamepad axis button (stick or trigger). - */ - public static String getInputAxisButtonKey(int axis) { - return getInputAxisKey(axis) + "_GuestButton"; - } - - /** - * Helper function to get the settings key for an gamepad axis orientation. - */ - public static String getInputAxisOrientationKey(int axis) { - return getInputAxisKey(axis) + "_GuestOrientation"; - } - - /** - * Helper function to write a gamepad button mapping for the setting. - */ - private void WriteButtonMapping(String key) { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Remove mapping for another setting using this input - int oldButtonCode = preferences.getInt(key, -1); - if (oldButtonCode != -1) { - String oldKey = getButtonKey(oldButtonCode); - editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten - } - - // Cleanup old mapping for this setting - removeOldMapping(); - - // Write new mapping - editor.putInt(key, getButtonCode()); - - // Write next reverse mapping for future cleanup - editor.putString(getReverseKey(), key); - - // Apply changes - editor.apply(); - } - - /** - * Helper function to write a gamepad axis mapping for the setting. - */ - private void WriteAxisMapping(int axis, int value) { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Cleanup old mapping - removeOldMapping(); - - // Write new mapping - editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1); - editor.putInt(getInputAxisButtonKey(axis), value); - - // Write next reverse mapping for future cleanup - editor.putString(getReverseKey(), getInputAxisKey(axis)); - - // Apply changes - editor.apply(); - } - - /** - * Saves the provided key input setting as an Android preference. - * - * @param keyEvent KeyEvent of this key press. - */ - public void onKeyInput(KeyEvent keyEvent) { - if (!IsButtonMappingSupported()) { - Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show(); - return; - } - - InputDevice device = keyEvent.getDevice(); - - WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode())); - - String uiString = device.getName() + ": Button " + keyEvent.getKeyCode(); - setUiString(uiString); - } - - /** - * Saves the provided motion input setting as an Android preference. - * - * @param device InputDevice from which the input event originated. - * @param motionRange MotionRange of the movement - * @param axisDir Either '-' or '+' (currently unused) - */ - public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, - char axisDir) { - if (!IsAxisMappingSupported()) { - Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show(); - return; - } - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - int button; - if (IsCirclePad()) { - button = NativeLibrary.ButtonType.STICK_LEFT; - } else if (IsCStick()) { - button = NativeLibrary.ButtonType.STICK_C; - } else if (IsDPad()) { - button = NativeLibrary.ButtonType.DPAD; - } else { - button = getButtonCode(); - } - - WriteAxisMapping(motionRange.getAxis(), button); - - String uiString = device.getName() + ": Axis " + motionRange.getAxis(); - setUiString(uiString); - - editor.apply(); - } - - /** - * Sets the string to use in the configuration UI for the gamepad input. - */ - private StringSetting setUiString(String ui) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), ""); - setSetting(setting); - - editor.putString(setting.getKey(), ui); - editor.apply(); - - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - - editor.putString(setting.getKey(), ui); - editor.apply(); - - return null; - } - } - - @Override - public int getType() { - return TYPE_INPUT_BINDING; - } -} 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 deleted file mode 100644 index 2b1793d3e..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -public final class PremiumHeader extends SettingsItem { - public PremiumHeader() { - super(null, null, null, 0, 0); - } - - @Override - public int getType() { - return SettingsItem.TYPE_PREMIUM; - } -} 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 deleted file mode 100644 index c0560d2dc..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; - -public final class PremiumSingleChoiceSetting extends SettingsItem { - private int mDefaultValue; - - private int mChoicesId; - private int mValuesId; - private SettingsFragmentView mView; - - private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - - public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId, - int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) { - super(key, section, setting, titleId, descriptionId); - mValuesId = valuesId; - mChoicesId = choicesId; - mDefaultValue = defaultValue; - mView = view; - } - - public int getChoicesId() { - return mChoicesId; - } - - public int getValuesId() { - return mValuesId; - } - - public int getSelectedValue() { - return mPreferences.getInt(getKey(), mDefaultValue); - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public void setSelectedValue(int selection) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putInt(getKey(), selection); - editor.apply(); - mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false); - } - - @Override - public int getType() { - return TYPE_SINGLE_CHOICE; - } -} 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 deleted file mode 100644 index 305352022..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -/** - * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. - * Each one corresponds to a {@link Setting} object, so this class's subclasses - * should vaguely correspond to those subclasses. There are a few with multiple analogues - * and a few with none (Headers, for example, do not correspond to anything in the ini - * file.) - */ -public abstract class SettingsItem { - public static final int TYPE_HEADER = 0; - public static final int TYPE_CHECKBOX = 1; - public static final int TYPE_SINGLE_CHOICE = 2; - public static final int TYPE_SLIDER = 3; - public static final int TYPE_SUBMENU = 4; - public static final int TYPE_INPUT_BINDING = 5; - public static final int TYPE_STRING_SINGLE_CHOICE = 6; - public static final int TYPE_DATETIME_SETTING = 7; - public static final int TYPE_PREMIUM = 8; - - private String mKey; - private String mSection; - - private Setting mSetting; - - private int mNameId; - private int mDescriptionId; - private boolean mIsPremium; - - /** - * Base constructor. Takes a key / section name in case the third parameter, the Setting, - * is null; in which case, one can be constructed and saved using the key / section. - * - * @param key Identifier for the Setting represented by this Item. - * @param section Section to which the Setting belongs. - * @param setting A possibly-null backing Setting, to be modified on UI events. - * @param nameId Resource ID for a text string to be displayed as this setting's name. - * @param descriptionId Resource ID for a text string to be displayed as this setting's description. - */ - public SettingsItem(String key, String section, Setting setting, int nameId, - int descriptionId) { - mKey = key; - mSection = section; - mSetting = setting; - mNameId = nameId; - mDescriptionId = descriptionId; - mIsPremium = (section == Settings.SECTION_PREMIUM); - } - - /** - * @return The identifier for the backing Setting. - */ - public String getKey() { - return mKey; - } - - /** - * @return The header under which the backing Setting belongs. - */ - public String getSection() { - return mSection; - } - - /** - * @return The backing Setting, possibly null. - */ - public Setting getSetting() { - return mSetting; - } - - /** - * Replace the backing setting with a new one. Generally used in cases where - * the backing setting is null. - * - * @param setting A non-null Setting. - */ - public void setSetting(Setting setting) { - mSetting = setting; - } - - /** - * @return A resource ID for a text string representing this Setting's name. - */ - public int getNameId() { - return mNameId; - } - - public int getDescriptionId() { - return mDescriptionId; - } - - public boolean isPremium() { - return mIsPremium; - } - - /** - * Used by {@link SettingsAdapter}'s onCreateViewHolder() - * method to determine which type of ViewHolder should be created. - * - * @return An integer (ideally, one of the constants defined in this file) - */ - public abstract int getType(); -} 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 deleted file mode 100644 index ee9d225d6..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; - -public final class SingleChoiceSetting extends SettingsItem { - private int mDefaultValue; - - private int mChoicesId; - private int mValuesId; - - public SingleChoiceSetting(String key, String section, int titleId, int descriptionId, - int choicesId, int valuesId, int defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mValuesId = valuesId; - mChoicesId = choicesId; - mDefaultValue = defaultValue; - } - - public int getChoicesId() { - return mChoicesId; - } - - public int getValuesId() { - return mValuesId; - } - - public int getSelectedValue() { - if (getSetting() != null) { - IntSetting setting = (IntSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public IntSetting setSelectedValue(int selection) { - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - @Override - public int getType() { - return TYPE_SINGLE_CHOICE; - } -} 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 deleted file mode 100644 index 551b13f99..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.utils.Log; - -public final class SliderSetting extends SettingsItem { - private int mMin; - private int mMax; - private int mDefaultValue; - - private String mUnits; - - public SliderSetting(String key, String section, int titleId, int descriptionId, - int min, int max, String units, int defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mMin = min; - mMax = max; - mUnits = units; - mDefaultValue = defaultValue; - } - - public int getMin() { - return mMin; - } - - public int getMax() { - return mMax; - } - - public int getDefaultValue() { - return mDefaultValue; - } - - public int getSelectedValue() { - Setting setting = getSetting(); - - if (setting == null) { - return mDefaultValue; - } - - if (setting instanceof IntSetting) { - IntSetting intSetting = (IntSetting) setting; - return intSetting.getValue(); - } else if (setting instanceof FloatSetting) { - FloatSetting floatSetting = (FloatSetting) setting; - return Math.round(floatSetting.getValue()); - } else { - Log.error("[SliderSetting] Error casting setting type."); - return -1; - } - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public IntSetting setSelectedValue(int selection) { - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - /** - * Write a value to the backing float. If that float was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the float. - * @return null if overwritten successfully otherwise; a newly created FloatSetting. - */ - public FloatSetting setSelectedValue(float selection) { - if (getSetting() == null) { - FloatSetting setting = new FloatSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - FloatSetting setting = (FloatSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - public String getUnits() { - return mUnits; - } - - @Override - public int getType() { - return TYPE_SLIDER; - } -} 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 deleted file mode 100644 index 057145d9d..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; - -public class StringSingleChoiceSetting extends SettingsItem { - private String mDefaultValue; - - private String[] mChoicesId; - private String[] mValuesId; - - public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId, - String[] choicesId, String[] valuesId, String defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mValuesId = valuesId; - mChoicesId = choicesId; - mDefaultValue = defaultValue; - } - - public String[] getChoicesId() { - return mChoicesId; - } - - public String[] getValuesId() { - return mValuesId; - } - - public String getValueAt(int index) { - if (mValuesId == null) - return null; - - if (index >= 0 && index < mValuesId.length) { - return mValuesId[index]; - } - - return ""; - } - - public String getSelectedValue() { - if (getSetting() != null) { - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - public int getSelectValueIndex() { - String selectedValue = getSelectedValue(); - for (int i = 0; i < mValuesId.length; i++) { - if (mValuesId[i].equals(selectedValue)) { - return i; - } - } - - return -1; - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public StringSetting setSelectedValue(String selection) { - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - @Override - public int getType() { - return TYPE_STRING_SINGLE_CHOICE; - } -} 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 deleted file mode 100644 index 9d44a923f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; - -public final class SubmenuSetting extends SettingsItem { - private String mMenuKey; - - public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) { - super(key, null, setting, titleId, descriptionId); - mMenuKey = menuKey; - } - - public String getMenuKey() { - return mMenuKey; - } - - @Override - public int getType() { - return TYPE_SUBMENU; - } -} 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 deleted file mode 100644 index 23c3c4c9e..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java +++ /dev/null @@ -1,215 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.provider.Settings; -import android.view.Menu; -import android.view.MenuInflater; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.FragmentTransaction; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.DirectoryStateReceiver; -import org.citra.citra_emu.utils.EmulationMenuSettings; - -public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { - private static final String ARG_MENU_TAG = "menu_tag"; - private static final String ARG_GAME_ID = "game_id"; - private static final String FRAGMENT_TAG = "settings"; - private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this); - - private ProgressDialog dialog; - - public static void launch(Context context, String menuTag, String gameId) { - Intent settings = new Intent(context, SettingsActivity.class); - settings.putExtra(ARG_MENU_TAG, menuTag); - settings.putExtra(ARG_GAME_ID, gameId); - context.startActivity(settings); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_settings); - - Intent launcher = getIntent(); - String gameID = launcher.getStringExtra(ARG_GAME_ID); - String menuTag = launcher.getStringExtra(ARG_MENU_TAG); - - mPresenter.onCreate(savedInstanceState, menuTag, gameID); - - // Show "Back" button in the action bar for navigation - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - - return true; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_settings, menu); - - return true; - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - // Critical: If super method is not called, rotations will be busted. - super.onSaveInstanceState(outState); - mPresenter.saveState(outState); - } - - @Override - protected void onStart() { - super.onStart(); - mPresenter.onStart(); - } - - /** - * If this is called, the user has left the settings screen (potentially through the - * home button) and will expect their changes to be persisted. So we kick off an - * IntentService which will do so on a background thread. - */ - @Override - protected void onStop() { - super.onStop(); - - mPresenter.onStop(isFinishing()); - - // Update framebuffer layout when closing the settings - NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(), - getWindowManager().getDefaultDisplay().getRotation()); - } - - @Override - public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) { - if (!addToStack && getFragment() != null) { - return; - } - - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - - if (addToStack) { - if (areSystemAnimationsEnabled()) { - transaction.setCustomAnimations( - R.animator.settings_enter, - R.animator.settings_exit, - R.animator.settings_pop_enter, - R.animator.setttings_pop_exit); - } - - transaction.addToBackStack(null); - } - transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG); - - transaction.commit(); - } - - private boolean areSystemAnimationsEnabled() { - float duration = Settings.Global.getFloat( - getContentResolver(), - Settings.Global.ANIMATOR_DURATION_SCALE, 1); - float transition = Settings.Global.getFloat( - getContentResolver(), - Settings.Global.TRANSITION_ANIMATION_SCALE, 1); - return duration != 0 && transition != 0; - } - - @Override - public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) { - LocalBroadcastManager.getInstance(this).registerReceiver( - receiver, - filter); - DirectoryInitialization.start(this); - } - - @Override - public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) { - LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); - } - - @Override - public void showLoading() { - if (dialog == null) { - dialog = new ProgressDialog(this); - dialog.setMessage(getString(R.string.load_settings)); - dialog.setIndeterminate(true); - } - - dialog.show(); - } - - @Override - public void hideLoading() { - dialog.dismiss(); - } - - @Override - public void showPermissionNeededHint() { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) - .show(); - } - - @Override - public void showExternalStorageNotMountedHint() { - Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) - .show(); - } - - @Override - public org.citra.citra_emu.features.settings.model.Settings getSettings() { - return mPresenter.getSettings(); - } - - @Override - public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) { - mPresenter.setSettings(settings); - } - - @Override - public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) { - SettingsFragmentView fragment = getFragment(); - - if (fragment != null) { - fragment.onSettingsFileLoaded(settings); - } - } - - @Override - public void onSettingsFileNotFound() { - SettingsFragmentView fragment = getFragment(); - - if (fragment != null) { - fragment.loadDefaultSettings(); - } - } - - @Override - public void showToastMessage(String message, boolean is_long) { - Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); - } - - @Override - public void onSettingChanged() { - mPresenter.onSettingChanged(); - } - - private SettingsFragment getFragment() { - return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); - } -} 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 deleted file mode 100644 index 0d63873bb..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.IntentFilter; -import android.os.Bundle; -import android.text.TextUtils; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; -import org.citra.citra_emu.utils.DirectoryStateReceiver; -import org.citra.citra_emu.utils.Log; -import org.citra.citra_emu.utils.ThemeUtil; - -import java.io.File; - -public final class SettingsActivityPresenter { - private static final String KEY_SHOULD_SAVE = "should_save"; - - private SettingsActivityView mView; - - private Settings mSettings = new Settings(); - - private boolean mShouldSave; - - private DirectoryStateReceiver directoryStateReceiver; - - private String menuTag; - private String gameId; - - public SettingsActivityPresenter(SettingsActivityView view) { - mView = view; - } - - public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) { - if (savedInstanceState == null) { - this.menuTag = menuTag; - this.gameId = gameId; - } else { - mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); - } - } - - public void onStart() { - prepareCitraDirectoriesIfNeeded(); - } - - void loadSettingsUI() { - if (mSettings.isEmpty()) { - if (!TextUtils.isEmpty(gameId)) { - mSettings.loadSettings(gameId, mView); - } else { - mSettings.loadSettings(mView); - } - } - - mView.showSettingsFragment(menuTag, false, gameId); - mView.onSettingsFileLoaded(mSettings); - } - - private void prepareCitraDirectoriesIfNeeded() { - File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini"); - if (!configFile.exists()) { - Log.error("Citra config file could not be found!"); - } - if (DirectoryInitialization.areCitraDirectoriesReady()) { - loadSettingsUI(); - } else { - mView.showLoading(); - IntentFilter statusIntentFilter = new IntentFilter( - DirectoryInitialization.BROADCAST_ACTION); - - directoryStateReceiver = - new DirectoryStateReceiver(directoryInitializationState -> - { - if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { - mView.hideLoading(); - loadSettingsUI(); - } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { - mView.showPermissionNeededHint(); - mView.hideLoading(); - } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { - mView.showExternalStorageNotMountedHint(); - mView.hideLoading(); - } - }); - - mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter); - } - } - - public void setSettings(Settings settings) { - mSettings = settings; - } - - public Settings getSettings() { - return mSettings; - } - - public void onStop(boolean finishing) { - if (directoryStateReceiver != null) { - mView.stopListeningToDirectoryInitializationService(directoryStateReceiver); - directoryStateReceiver = null; - } - - if (mSettings != null && finishing && mShouldSave) { - Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI..."); - mSettings.saveSettings(mView); - } - - ThemeUtil.applyTheme(); - - NativeLibrary.ReloadSettings(); - } - - public void onSettingChanged() { - mShouldSave = true; - } - - public void saveState(Bundle outState) { - outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); - } -} 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 deleted file mode 100644 index 0d26d48a7..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.IntentFilter; - -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.utils.DirectoryStateReceiver; - -/** - * Abstraction for the Activity that manages SettingsFragments. - */ -public interface SettingsActivityView { - /** - * Show a new SettingsFragment. - * - * @param menuTag Identifier for the settings group that should be displayed. - * @param addToStack Whether or not this fragment should replace a previous one. - */ - void showSettingsFragment(String menuTag, boolean addToStack, String gameId); - - /** - * Called by a contained Fragment to get access to the Setting HashMap - * loaded from disk, so that each Fragment doesn't need to perform its own - * read operation. - * - * @return A possibly null HashMap of Settings. - */ - Settings getSettings(); - - /** - * Used to provide the Activity with Settings HashMaps if a Fragment already - * has one; for example, if a rotation occurs, the Fragment will not be killed, - * but the Activity will, so the Activity needs to have its HashMaps resupplied. - * - * @param settings The ArrayList of all the Settings HashMaps. - */ - void setSettings(Settings settings); - - /** - * Called when an asynchronous load operation completes. - * - * @param settings The (possibly null) result of the ini load operation. - */ - void onSettingsFileLoaded(Settings settings); - - /** - * Called when an asynchronous load operation fails. - */ - void onSettingsFileNotFound(); - - /** - * Display a popup text message on screen. - * - * @param message The contents of the onscreen message. - * @param is_long Whether this should be a long Toast or short one. - */ - void showToastMessage(String message, boolean is_long); - - /** - * End the activity. - */ - void finish(); - - /** - * Called by a containing Fragment to tell the Activity that a setting was changed; - * unless this has been called, the Activity will not save to disk. - */ - void onSettingChanged(); - - /** - * Show loading dialog while loading the settings - */ - void showLoading(); - - /** - * Hide the loading the dialog - */ - void hideLoading(); - - /** - * Show a hint to the user that the app needs write to external storage access - */ - void showPermissionNeededHint(); - - /** - * Show a hint to the user that the app needs the external storage to be mounted - */ - void showExternalStorageNotMountedHint(); - - /** - * Start the DirectoryInitialization and listen for the result. - * - * @param receiver the broadcast receiver for the DirectoryInitialization - * @param filter the Intent broadcasts to be received. - */ - void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter); - - /** - * Stop listening to the DirectoryInitialization. - * - * @param receiver The broadcast receiver to unregister. - */ - void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver); -} 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 deleted file mode 100644 index bfd7c71a9..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java +++ /dev/null @@ -1,487 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.Context; -import android.content.DialogInterface; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.DatePicker; -import android.widget.SeekBar; -import android.widget.TextView; -import android.widget.TimePicker; - -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.dialogs.MotionAlertDialog; -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder; -import org.citra.citra_emu.ui.main.MainActivity; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; - -public final class SettingsAdapter extends RecyclerView.Adapter - implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener { - private SettingsFragmentView mView; - private Context mContext; - private ArrayList mSettings; - - private SettingsItem mClickedItem; - private int mClickedPosition; - private int mSeekbarProgress; - - private AlertDialog mDialog; - private TextView mTextSliderValue; - - public SettingsAdapter(SettingsFragmentView view, Context context) { - mView = view; - mContext = context; - mClickedPosition = -1; - } - - @Override - public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - switch (viewType) { - case SettingsItem.TYPE_HEADER: - view = inflater.inflate(R.layout.list_item_settings_header, parent, false); - return new HeaderViewHolder(view, this); - - case SettingsItem.TYPE_CHECKBOX: - view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false); - return new CheckBoxSettingViewHolder(view, this); - - case SettingsItem.TYPE_SINGLE_CHOICE: - case SettingsItem.TYPE_STRING_SINGLE_CHOICE: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SingleChoiceViewHolder(view, this); - - case SettingsItem.TYPE_SLIDER: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SliderViewHolder(view, this); - - case SettingsItem.TYPE_SUBMENU: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SubmenuViewHolder(view, this); - - case SettingsItem.TYPE_INPUT_BINDING: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new InputBindingSettingViewHolder(view, this, mContext); - - case SettingsItem.TYPE_DATETIME_SETTING: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new DateTimeViewHolder(view, this); - - case SettingsItem.TYPE_PREMIUM: - view = inflater.inflate(R.layout.premium_item_setting, parent, false); - return new PremiumViewHolder(view, this, mView); - - default: - Log.error("[SettingsAdapter] Invalid view type: " + viewType); - return null; - } - } - - @Override - public void onBindViewHolder(SettingViewHolder holder, int position) { - holder.bind(getItem(position)); - } - - private SettingsItem getItem(int position) { - return mSettings.get(position); - } - - @Override - public int getItemCount() { - if (mSettings != null) { - return mSettings.size(); - } else { - return 0; - } - } - - @Override - public int getItemViewType(int position) { - return getItem(position).getType(); - } - - public void setSettings(ArrayList settings) { - mSettings = settings; - notifyDataSetChanged(); - } - - public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { - IntSetting setting = item.setChecked(checked); - notifyItemChanged(position); - - if (setting != null) { - mView.putSetting(setting); - } - - mView.onSettingChanged(); - } - - public void onSingleChoiceClick(PremiumSingleChoiceSetting item) { - mClickedItem = item; - - int value = getSelectionForSingleChoiceValue(item); - - AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); - - builder.setTitle(item.getNameId()); - builder.setSingleChoiceItems(item.getChoicesId(), value, this); - - mDialog = builder.show(); - } - - public void onSingleChoiceClick(SingleChoiceSetting item) { - mClickedItem = item; - - int value = getSelectionForSingleChoiceValue(item); - - AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); - - builder.setTitle(item.getNameId()); - builder.setSingleChoiceItems(item.getChoicesId(), value, this); - - mDialog = builder.show(); - } - - public void onSingleChoiceClick(SingleChoiceSetting item, int position) { - mClickedPosition = position; - - if (!item.isPremium() || MainActivity.isPremiumActive()) { - // Setting is either not Premium, or the user has Premium - onSingleChoiceClick(item); - return; - } - - // User needs Premium, invoke the billing flow - MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); - } - - public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) { - mClickedPosition = position; - - if (!item.isPremium() || MainActivity.isPremiumActive()) { - // Setting is either not Premium, or the user has Premium - onSingleChoiceClick(item); - return; - } - - // User needs Premium, invoke the billing flow - MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); - } - - public void onStringSingleChoiceClick(StringSingleChoiceSetting item) { - mClickedItem = item; - - AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); - - builder.setTitle(item.getNameId()); - builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); - - mDialog = builder.show(); - } - - public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { - mClickedPosition = position; - - if (!item.isPremium() || MainActivity.isPremiumActive()) { - // Setting is either not Premium, or the user has Premium - onStringSingleChoiceClick(item); - return; - } - - // User needs Premium, invoke the billing flow - MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item)); - } - - DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog(); - - public void onDateTimeClick(DateTimeSetting item, int position) { - mClickedItem = item; - mClickedPosition = position; - - AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); - - LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); - View view = inflater.inflate(R.layout.sysclock_datetime_picker, null); - - DatePicker dp = view.findViewById(R.id.date_picker); - TimePicker tp = view.findViewById(R.id.time_picker); - - //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69) - String settingValue = item.getValue(); - dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10))); - - tp.setIs24HourView(true); - tp.setHour(Integer.parseInt(settingValue.substring(11, 13))); - tp.setMinute(Integer.parseInt(settingValue.substring(14, 16))); - - DialogInterface.OnClickListener ok = (dialog, which) -> { - //set it - int year = dp.getYear(); - if (year < 2000) { - year = 2000; - } - String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length()); - String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length()); - String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length()); - String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length()); - String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01"; - - StringSetting setting = item.setSelectedValue(datetime); - if (setting != null) { - mView.putSetting(setting); - } - - mView.onSettingChanged(); - - mClickedItem = null; - closeDialog(); - }; - - builder.setView(view); - builder.setPositiveButton(android.R.string.ok, ok); - builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); - mDialog = builder.show(); - } - - public void onSliderClick(SliderSetting item, int position) { - mClickedItem = item; - mClickedPosition = position; - mSeekbarProgress = item.getSelectedValue(); - AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); - - LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); - View view = inflater.inflate(R.layout.dialog_seekbar, null); - - SeekBar seekbar = view.findViewById(R.id.seekbar); - - builder.setTitle(item.getNameId()); - builder.setView(view); - builder.setPositiveButton(android.R.string.ok, this); - builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); - builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> { - seekbar.setProgress(item.getDefaultValue()); - onClick(dialog, which); - }); - mDialog = builder.show(); - - mTextSliderValue = view.findViewById(R.id.text_value); - mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); - - TextView units = view.findViewById(R.id.text_units); - units.setText(item.getUnits()); - - seekbar.setMin(item.getMin()); - seekbar.setMax(item.getMax()); - seekbar.setProgress(mSeekbarProgress); - - seekbar.setOnSeekBarChangeListener(this); - } - - public void onSubmenuClick(SubmenuSetting item) { - mView.loadSubMenu(item.getMenuKey()); - } - - public void onInputBindingClick(final InputBindingSetting item, final int position) { - final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item); - dialog.setTitle(R.string.input_binding); - - int messageResId = R.string.input_binding_description; - if (item.IsAxisMappingSupported() && !item.IsTrigger()) { - // Use specialized message for axis left/right or up/down - if (item.IsHorizontalOrientation()) { - messageResId = R.string.input_binding_description_horizontal_axis; - } else { - messageResId = R.string.input_binding_description_vertical_axis; - } - } - - dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId()))); - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this); - dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) -> - item.removeOldMapping()); - dialog.setOnDismissListener(dialog1 -> - { - StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue()); - notifyItemChanged(position); - - mView.putSetting(setting); - - mView.onSettingChanged(); - }); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (mClickedItem instanceof SingleChoiceSetting) { - SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; - - int value = getValueForSingleChoiceSelection(scSetting, which); - if (scSetting.getSelectedValue() != value) { - mView.onSettingChanged(); - } - - // Get the backing Setting, which may be null (if for example it was missing from the file) - IntSetting setting = scSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - - closeDialog(); - } else if (mClickedItem instanceof PremiumSingleChoiceSetting) { - PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem; - scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which)); - closeDialog(); - } else if (mClickedItem instanceof StringSingleChoiceSetting) { - StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; - String value = scSetting.getValueAt(which); - if (!scSetting.getSelectedValue().equals(value)) - mView.onSettingChanged(); - - StringSetting setting = scSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - - closeDialog(); - } else if (mClickedItem instanceof SliderSetting) { - SliderSetting sliderSetting = (SliderSetting) mClickedItem; - if (sliderSetting.getSelectedValue() != mSeekbarProgress) { - mView.onSettingChanged(); - } - - if (sliderSetting.getSetting() instanceof FloatSetting) { - float value = (float) mSeekbarProgress; - - FloatSetting setting = sliderSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - } else { - IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress); - if (setting != null) { - mView.putSetting(setting); - } - } - - closeDialog(); - } - - mClickedItem = null; - mSeekbarProgress = -1; - } - - public void closeDialog() { - if (mDialog != null) { - if (mClickedPosition != -1) { - notifyItemChanged(mClickedPosition); - mClickedPosition = -1; - } - mDialog.dismiss(); - mDialog = null; - } - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - mSeekbarProgress = progress; - mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - - private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) { - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - return valuesArray[which]; - } else { - return which; - } - } - - private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) { - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - return valuesArray[which]; - } else { - return which; - } - } - - private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) { - int value = item.getSelectedValue(); - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - for (int index = 0; index < valuesArray.length; index++) { - int current = valuesArray[index]; - if (current == value) { - return index; - } - } - } else { - return value; - } - - return -1; - } - - private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) { - int value = item.getSelectedValue(); - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - for (int index = 0; index < valuesArray.length; index++) { - int current = valuesArray[index]; - if (current == value) { - return index; - } - } - } else { - return value; - } - - return -1; - } -} 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 deleted file mode 100644 index 5799dcb8d..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.ui.DividerItemDecoration; - -import java.util.ArrayList; - -public final class SettingsFragment extends Fragment implements SettingsFragmentView { - private static final String ARGUMENT_MENU_TAG = "menu_tag"; - private static final String ARGUMENT_GAME_ID = "game_id"; - - private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this); - private SettingsActivityView mActivity; - - private SettingsAdapter mAdapter; - - public static Fragment newInstance(String menuTag, String gameId) { - SettingsFragment fragment = new SettingsFragment(); - - Bundle arguments = new Bundle(); - arguments.putString(ARGUMENT_MENU_TAG, menuTag); - arguments.putString(ARGUMENT_GAME_ID, gameId); - - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - mActivity = (SettingsActivityView) context; - mPresenter.onAttach(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setRetainInstance(true); - String menuTag = getArguments().getString(ARGUMENT_MENU_TAG); - String gameId = getArguments().getString(ARGUMENT_GAME_ID); - - mAdapter = new SettingsAdapter(this, getActivity()); - - mPresenter.onCreate(menuTag, gameId); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_settings, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - LinearLayoutManager manager = new LinearLayoutManager(getActivity()); - - RecyclerView recyclerView = view.findViewById(R.id.list_settings); - - recyclerView.setAdapter(mAdapter); - recyclerView.setLayoutManager(manager); - recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); - - SettingsActivityView activity = (SettingsActivityView) getActivity(); - - mPresenter.onViewCreated(activity.getSettings()); - } - - @Override - public void onDetach() { - super.onDetach(); - mActivity = null; - - if (mAdapter != null) { - mAdapter.closeDialog(); - } - } - - @Override - public void onSettingsFileLoaded(Settings settings) { - mPresenter.setSettings(settings); - } - - @Override - public void passSettingsToActivity(Settings settings) { - if (mActivity != null) { - mActivity.setSettings(settings); - } - } - - @Override - public void showSettingsList(ArrayList settingsList) { - mAdapter.setSettings(settingsList); - } - - @Override - public void loadDefaultSettings() { - mPresenter.loadDefaultSettings(); - } - - @Override - public void loadSubMenu(String menuKey) { - mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID)); - } - - @Override - public void showToastMessage(String message, boolean is_long) { - mActivity.showToastMessage(message, is_long); - } - - @Override - public void putSetting(Setting setting) { - mPresenter.putSetting(setting); - } - - @Override - public void onSettingChanged() { - mActivity.onSettingChanged(); - } -} 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 deleted file mode 100644 index 31f3e68eb..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java +++ /dev/null @@ -1,416 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.app.Activity; -import android.content.Context; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.text.TextUtils; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.SettingSection; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.HeaderSetting; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.PremiumHeader; -import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; - -public final class SettingsFragmentPresenter { - private SettingsFragmentView mView; - - private String mMenuTag; - private String mGameID; - - private Settings mSettings; - private ArrayList mSettingsList; - - public SettingsFragmentPresenter(SettingsFragmentView view) { - mView = view; - } - - public void onCreate(String menuTag, String gameId) { - mGameID = gameId; - mMenuTag = menuTag; - } - - public void onViewCreated(Settings settings) { - setSettings(settings); - } - - /** - * If the screen is rotated, the Activity will forget the settings map. This fragment - * won't, though; so rather than have the Activity reload from disk, have the fragment pass - * the settings map back to the Activity. - */ - public void onAttach() { - if (mSettings != null) { - mView.passSettingsToActivity(mSettings); - } - } - - public void putSetting(Setting setting) { - mSettings.getSection(setting.getSection()).putSetting(setting); - } - - private StringSetting asStringSetting(Setting setting) { - if (setting == null) { - return null; - } - - StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString()); - putSetting(stringSetting); - return stringSetting; - } - - public void loadDefaultSettings() { - loadSettingsList(); - } - - public void setSettings(Settings settings) { - if (mSettingsList == null && settings != null) { - mSettings = settings; - - loadSettingsList(); - } else { - mView.getActivity().setTitle(R.string.preferences_settings); - mView.showSettingsList(mSettingsList); - } - } - - private void loadSettingsList() { - if (!TextUtils.isEmpty(mGameID)) { - mView.getActivity().setTitle("Game Settings: " + mGameID); - } - ArrayList sl = new ArrayList<>(); - - if (mMenuTag == null) { - return; - } - - switch (mMenuTag) { - case SettingsFile.FILE_NAME_CONFIG: - addConfigSettings(sl); - break; - case Settings.SECTION_PREMIUM: - addPremiumSettings(sl); - break; - case Settings.SECTION_CORE: - addGeneralSettings(sl); - break; - case Settings.SECTION_SYSTEM: - addSystemSettings(sl); - break; - case Settings.SECTION_CAMERA: - addCameraSettings(sl); - break; - case Settings.SECTION_CONTROLS: - addInputSettings(sl); - break; - case Settings.SECTION_RENDERER: - addGraphicsSettings(sl); - break; - case Settings.SECTION_AUDIO: - addAudioSettings(sl); - break; - case Settings.SECTION_DEBUG: - addDebugSettings(sl); - break; - default: - mView.showToastMessage("Unimplemented menu", false); - return; - } - - mSettingsList = sl; - mView.showSettingsList(mSettingsList); - } - - private void addConfigSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_settings); - - sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG)); - } - - private void addPremiumSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_premium); - - SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM); - Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN); - - sl.add(new PremiumHeader()); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView)); - } else { - // Pre-Android 10 does not support System Default - sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView)); - } - - //Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME); - //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)); - } - - private void addGeneralSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_general); - - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED); - Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT); - - 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)); - 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)); - } - - private void addSystemSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_system); - - SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM); - Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE); - Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE); - Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK); - Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME); - - sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language)); - 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)); - 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)); - } - - private void addCameraSettings(ArrayList sl) { - final Activity activity = mView.getActivity(); - activity.setTitle(R.string.preferences_camera); - - // Get the camera IDs - CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - ArrayList supportedCameraNameList = new ArrayList<>(); - ArrayList supportedCameraIdList = new ArrayList<>(); - if (cameraManager != null) { - try { - for (String id : cameraManager.getCameraIdList()) { - final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); - if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { - continue; // Legacy cameras cannot be used with the NDK - } - - supportedCameraIdList.add(id); - - final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING)); - int stringId = R.string.camera_facing_external; - switch (facing) { - case CameraCharacteristics.LENS_FACING_FRONT: - stringId = R.string.camera_facing_front; - break; - case CameraCharacteristics.LENS_FACING_BACK: - stringId = R.string.camera_facing_back; - break; - case CameraCharacteristics.LENS_FACING_EXTERNAL: - stringId = R.string.camera_facing_external; - break; - } - supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId))); - } - } catch (CameraAccessException e) { - Log.error("Couldn't retrieve camera list"); - e.printStackTrace(); - } - } - - // Create the names and values for display - ArrayList cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); - cameraDeviceNameList.addAll(supportedCameraNameList); - ArrayList cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues))); - cameraDeviceValueList.addAll(supportedCameraIdList); - - final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{}); - final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{}); - - final boolean haveCameraDevices = !supportedCameraIdList.isEmpty(); - - String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames); - String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues); - if (!haveCameraDevices) { - // Remove the last entry (ndk / Device Camera) - imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1); - imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1); - } - - final String defaultImageSource = haveCameraDevices ? "ndk" : "image"; - - SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA); - - Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); - Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG)); - Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP); - sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); - 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)); - if (haveCameraDevices) - 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)); - 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)); - - Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME); - Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG)); - Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP); - sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0)); - 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)); - if (haveCameraDevices) - 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)); - 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)); - - Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME); - Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG)); - Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP); - sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0)); - 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)); - if (haveCameraDevices) - 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)); - 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)); - } - - private void addInputSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_controls); - - SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS); - Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A); - Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B); - Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X); - Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y); - Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); - Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START); - Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL); - Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL); - Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL); - Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL); - Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL); - Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL); - // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP); - // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); - // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); - // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); - Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L); - Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R); - Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL); - Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR); - - sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart)); - - sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz)); - - sl.add(new HeaderSetting(null, null, R.string.controller_c, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz)); - - sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz)); - - // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing. - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight)); - - sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR)); - } - - private void addGraphicsSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_graphics); - - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); - Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE); - Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); - Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D); - Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D); - Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE); - SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT); - Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE); - Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT); - Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT); - SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY); - Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES); - Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES); - //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES); - - sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); - 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)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode)); - 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)); - 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)); - - sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode)); - sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d)); - - sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0)); - 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)); - 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)); - 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)); - - sl.add(new HeaderSetting(null, null, R.string.utility, 0)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures)); - //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra. - //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures)); - } - - private void addAudioSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_audio); - - SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO); - Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING); - Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE); - - sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch)); - 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)); - } - - private void addDebugSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_debug); - - SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE); - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT); - Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER); - Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER); - Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC); - - sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable)); - } -} 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 deleted file mode 100644 index c36eb55a7..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import androidx.fragment.app.FragmentActivity; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; - -import java.util.ArrayList; - -/** - * Abstraction for a screen showing a list of settings. Instances of - * this type of view will each display a layer of the setting hierarchy. - */ -public interface SettingsFragmentView { - /** - * Called by the containing Activity to notify the Fragment that an - * asynchronous load operation completed. - * - * @param settings The (possibly null) result of the ini load operation. - */ - void onSettingsFileLoaded(Settings settings); - - /** - * Pass a settings HashMap to the containing activity, so that it can - * share the HashMap with other SettingsFragments; useful so that rotations - * do not require an additional load operation. - * - * @param settings An ArrayList containing all the settings HashMaps. - */ - void passSettingsToActivity(Settings settings); - - /** - * Pass an ArrayList to the View so that it can be displayed on screen. - * - * @param settingsList The result of converting the HashMap to an ArrayList - */ - void showSettingsList(ArrayList settingsList); - - /** - * Called by the containing Activity when an asynchronous load operation fails. - * Instructs the Fragment to load the settings screen with defaults selected. - */ - void loadDefaultSettings(); - - /** - * @return The Fragment's containing activity. - */ - FragmentActivity getActivity(); - - /** - * Tell the Fragment to tell the containing Activity to show a new - * Fragment containing a submenu of settings. - * - * @param menuKey Identifier for the settings group that should be shown. - */ - void loadSubMenu(String menuKey); - - /** - * Tell the Fragment to tell the containing activity to display a toast message. - * - * @param message Text to be shown in the Toast - * @param is_long Whether this should be a long Toast or short one. - */ - void showToastMessage(String message, boolean is_long); - - /** - * Have the fragment add a setting to the HashMap. - * - * @param setting The (possibly previously missing) new setting. - */ - void putSetting(Setting setting); - - /** - * Have the fragment tell the containing Activity that a setting was modified. - */ - void onSettingChanged(); -} 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 deleted file mode 100644 index 67bde5709..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -/** - * FrameLayout subclass with few Properties added to simplify animations. - * Don't remove the methods appearing as unused, in order not to break the menu animations - */ -public final class SettingsFrameLayout extends FrameLayout { - private float mVisibleness = 1.0f; - - public SettingsFrameLayout(Context context) { - super(context); - } - - public SettingsFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public float getYFraction() { - return getY() / getHeight(); - } - - public void setYFraction(float yFraction) { - final int height = getHeight(); - setY((height > 0) ? (yFraction * height) : -9999); - } - - public float getVisibleness() { - return mVisibleness; - } - - public void setVisibleness(float visibleness) { - setScaleX(visibleness); - setScaleY(visibleness); - setAlpha(visibleness); - } -} 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 deleted file mode 100644 index d914f7d0b..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class CheckBoxSettingViewHolder extends SettingViewHolder { - private CheckBoxSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - private CheckBox mCheckbox; - - public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - mCheckbox = root.findViewById(R.id.checkbox); - } - - @Override - public void bind(SettingsItem item) { - mItem = (CheckBoxSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setText(""); - mTextSettingDescription.setVisibility(View.GONE); - } - - mCheckbox.setChecked(mItem.isChecked()); - } - - @Override - public void onClick(View clicked) { - mCheckbox.toggle(); - - getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); - } -} 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 deleted file mode 100644 index 09ea93010..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; -import org.citra.citra_emu.utils.Log; - -public final class DateTimeViewHolder extends SettingViewHolder { - private DateTimeSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public DateTimeViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - Log.error("test " + mTextSettingName); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - Log.error("test " + mTextSettingDescription); - } - - @Override - public void bind(SettingsItem item) { - mItem = (DateTimeSetting) item; - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onDateTimeClick(mItem, getAdapterPosition()); - } -} 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 deleted file mode 100644 index baf80ed76..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class HeaderViewHolder extends SettingViewHolder { - private TextView mHeaderName; - - public HeaderViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - itemView.setOnClickListener(null); - } - - @Override - protected void findViews(View root) { - mHeaderName = root.findViewById(R.id.text_header_name); - } - - @Override - public void bind(SettingsItem item) { - mHeaderName.setText(item.getNameId()); - } - - @Override - public void onClick(View clicked) { - // no-op - } -} \ 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 deleted file mode 100644 index 7d95c250a..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class InputBindingSettingViewHolder extends SettingViewHolder { - private InputBindingSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - private Context mContext; - - public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) { - super(itemView, adapter); - - mContext = context; - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - - mItem = (InputBindingSetting) item; - - mTextSettingName.setText(item.getNameId()); - - String key = sharedPreferences.getString(mItem.getKey(), ""); - if (key != null && !key.isEmpty()) { - mTextSettingDescription.setText(key); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onInputBindingClick(mItem, getAdapterPosition()); - } -} 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 deleted file mode 100644 index be0853ff0..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; -import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; -import org.citra.citra_emu.ui.main.MainActivity; - -public final class PremiumViewHolder extends SettingViewHolder { - private TextView mHeaderName; - private TextView mTextDescription; - private SettingsFragmentView mView; - - public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) { - super(itemView, adapter); - mView = view; - itemView.setOnClickListener(this); - } - - @Override - protected void findViews(View root) { - mHeaderName = root.findViewById(R.id.text_setting_name); - mTextDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - updateText(); - } - - @Override - public void onClick(View clicked) { - if (MainActivity.isPremiumActive()) { - return; - } - - // Invoke billing flow if Premium is not already active, then refresh the UI to indicate - // the purchase has completed. - MainActivity.invokePremiumBilling(() -> updateText()); - } - - /** - * Update the text shown to the user, based on whether Premium is active - */ - private void updateText() { - if (MainActivity.isPremiumActive()) { - mHeaderName.setText(R.string.premium_settings_welcome); - mTextDescription.setText(R.string.premium_settings_welcome_description); - } else { - mHeaderName.setText(R.string.premium_settings_upsell); - mTextDescription.setText(R.string.premium_settings_upsell_description); - } - } -} 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 deleted file mode 100644 index 2643ea121..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; - -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private SettingsAdapter mAdapter; - - public SettingViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView); - - mAdapter = adapter; - - itemView.setOnClickListener(this); - - findViews(itemView); - } - - protected SettingsAdapter getAdapter() { - return mAdapter; - } - - /** - * Gets handles to all this ViewHolder's child views using their XML-defined identifiers. - * - * @param root The newly inflated top-level view. - */ - protected abstract void findViews(View root); - - /** - * Called by the adapter to set this ViewHolder's child views to display the list item - * it must now represent. - * - * @param item The list item that should be represented by this ViewHolder. - */ - public abstract void bind(SettingsItem item); - - /** - * Called when this ViewHolder's view is clicked on. Implementations should usually pass - * this event up to the adapter. - * - * @param clicked The view that was clicked on. - */ - public abstract void onClick(View clicked); -} 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 deleted file mode 100644 index a175af9f8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.content.res.Resources; -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SingleChoiceViewHolder extends SettingViewHolder { - private SettingsItem mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = item; - - mTextSettingName.setText(item.getNameId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - } else if (item instanceof SingleChoiceSetting) { - SingleChoiceSetting setting = (SingleChoiceSetting) item; - int selected = setting.getSelectedValue(); - Resources resMgr = mTextSettingDescription.getContext().getResources(); - String[] choices = resMgr.getStringArray(setting.getChoicesId()); - int[] values = resMgr.getIntArray(setting.getValuesId()); - for (int i = 0; i < values.length; ++i) { - if (values[i] == selected) { - mTextSettingDescription.setText(choices[i]); - } - } - } else if (item instanceof PremiumSingleChoiceSetting) { - PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item; - int selected = setting.getSelectedValue(); - Resources resMgr = mTextSettingDescription.getContext().getResources(); - String[] choices = resMgr.getStringArray(setting.getChoicesId()); - int[] values = resMgr.getIntArray(setting.getValuesId()); - for (int i = 0; i < values.length; ++i) { - if (values[i] == selected) { - mTextSettingDescription.setText(choices[i]); - } - } - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - int position = getAdapterPosition(); - if (mItem instanceof SingleChoiceSetting) { - getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position); - } else if (mItem instanceof PremiumSingleChoiceSetting) { - getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position); - } else if (mItem instanceof StringSingleChoiceSetting) { - getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position); - } - } -} 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 deleted file mode 100644 index 3dd048a29..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SliderViewHolder extends SettingViewHolder { - private SliderSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SliderViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = (SliderSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onSliderClick(mItem, getAdapterPosition()); - } -} 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 deleted file mode 100644 index cb8c3e92a..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SubmenuViewHolder extends SettingViewHolder { - private SubmenuSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SubmenuViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = (SubmenuSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onSubmenuClick(mItem); - } -} \ 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 deleted file mode 100644 index 8ae6b70d7..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java +++ /dev/null @@ -1,341 +0,0 @@ -package org.citra.citra_emu.features.settings.utils; - -import androidx.annotation.NonNull; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.SettingSection; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.ui.SettingsActivityView; -import org.citra.citra_emu.utils.BiMap; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.Log; -import org.ini4j.Wini; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * Contains static methods for interacting with .ini files in which settings are stored. - */ -public final class SettingsFile { - public static final String FILE_NAME_CONFIG = "config"; - - public static final String KEY_CPU_JIT = "use_cpu_jit"; - - public static final String KEY_DESIGN = "design"; - - public static final String KEY_PREMIUM = "premium"; - - public static final String KEY_HW_RENDERER = "use_hw_renderer"; - public static final String KEY_HW_SHADER = "use_hw_shader"; - public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; - public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; - public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"; - public static final String KEY_USE_VSYNC = "use_vsync_new"; - public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; - public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit"; - public static final String KEY_FRAME_LIMIT = "frame_limit"; - public static final String KEY_BACKGROUND_RED = "bg_red"; - public static final String KEY_BACKGROUND_BLUE = "bg_blue"; - public static final String KEY_BACKGROUND_GREEN = "bg_green"; - public static final String KEY_RENDER_3D = "render_3d"; - public static final String KEY_FACTOR_3D = "factor_3d"; - public static final String KEY_PP_SHADER_NAME = "pp_shader_name"; - public static final String KEY_FILTER_MODE = "filter_mode"; - public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name"; - public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation"; - - public static final String KEY_LAYOUT_OPTION = "layout_option"; - public static final String KEY_SWAP_SCREEN = "swap_screen"; - public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size"; - public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift"; - public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift"; - - public static final String KEY_DUMP_TEXTURES = "dump_textures"; - public static final String KEY_CUSTOM_TEXTURES = "custom_textures"; - public static final String KEY_PRELOAD_TEXTURES = "preload_textures"; - - public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine"; - public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; - public static final String KEY_VOLUME = "volume"; - public static final String KEY_MIC_INPUT_TYPE = "mic_input_type"; - - public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; - - public static final String KEY_IS_NEW_3DS = "is_new_3ds"; - public static final String KEY_REGION_VALUE = "region_value"; - public static final String KEY_LANGUAGE = "language"; - - public static final String KEY_INIT_CLOCK = "init_clock"; - public static final String KEY_INIT_TIME = "init_time"; - - public static final String KEY_BUTTON_A = "button_a"; - public static final String KEY_BUTTON_B = "button_b"; - public static final String KEY_BUTTON_X = "button_x"; - public static final String KEY_BUTTON_Y = "button_y"; - public static final String KEY_BUTTON_SELECT = "button_select"; - public static final String KEY_BUTTON_START = "button_start"; - public static final String KEY_BUTTON_UP = "button_up"; - public static final String KEY_BUTTON_DOWN = "button_down"; - public static final String KEY_BUTTON_LEFT = "button_left"; - public static final String KEY_BUTTON_RIGHT = "button_right"; - public static final String KEY_BUTTON_L = "button_l"; - public static final String KEY_BUTTON_R = "button_r"; - public static final String KEY_BUTTON_ZL = "button_zl"; - public static final String KEY_BUTTON_ZR = "button_zr"; - public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"; - public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"; - public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"; - public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"; - public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"; - public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"; - public static final String KEY_CIRCLEPAD_UP = "circlepad_up"; - public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down"; - public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left"; - public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right"; - public static final String KEY_CSTICK_UP = "cstick_up"; - public static final String KEY_CSTICK_DOWN = "cstick_down"; - public static final String KEY_CSTICK_LEFT = "cstick_left"; - public static final String KEY_CSTICK_RIGHT = "cstick_right"; - - public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name"; - public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config"; - public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip"; - public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name"; - public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config"; - public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip"; - public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name"; - public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config"; - public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip"; - - public static final String KEY_LOG_FILTER = "log_filter"; - - private static BiMap sectionsMap = new BiMap<>(); - - static { - //TODO: Add members to sectionsMap when game-specific settings are added - } - - - private SettingsFile() { - } - - /** - * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. - * - * @param ini The ini file to load the settings from - * @param isCustomGame - * @param view The current view. - * @return An Observable that emits a HashMap of the file's contents, then completes. - */ - static HashMap readFile(final File ini, boolean isCustomGame, SettingsActivityView view) { - HashMap sections = new Settings.SettingsSectionMap(); - - BufferedReader reader = null; - - try { - reader = new BufferedReader(new FileReader(ini)); - - SettingSection current = null; - for (String line; (line = reader.readLine()) != null; ) { - if (line.startsWith("[") && line.endsWith("]")) { - current = sectionFromLine(line, isCustomGame); - sections.put(current.getName(), current); - } else if ((current != null)) { - Setting setting = settingFromLine(current, line); - if (setting != null) { - current.putSetting(setting); - } - } - } - } catch (FileNotFoundException e) { - Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage()); - if (view != null) - view.onSettingsFileNotFound(); - } catch (IOException e) { - Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage()); - if (view != null) - view.onSettingsFileNotFound(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage()); - } - } - } - - return sections; - } - - public static HashMap readFile(final String fileName, SettingsActivityView view) { - return readFile(getSettingsFile(fileName), false, view); - } - - /** - * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. - * - * @param gameId the id of the game to load it's settings. - * @param view The current view. - */ - public static HashMap readCustomGameSettings(final String gameId, SettingsActivityView view) { - return readFile(getCustomGameSettingsFile(gameId), true, view); - } - - /** - * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error - * telling why it failed. - * - * @param fileName The target filename without a path or extension. - * @param sections The HashMap containing the Settings we want to serialize. - * @param view The current view. - */ - public static void saveFile(final String fileName, TreeMap sections, - SettingsActivityView view) { - File ini = getSettingsFile(fileName); - - try { - Wini writer = new Wini(ini); - - Set keySet = sections.keySet(); - for (String key : keySet) { - SettingSection section = sections.get(key); - writeSection(writer, section); - } - writer.store(); - } catch (IOException e) { - Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); - view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false); - } - } - - - public static void saveCustomGameSettings(final String gameId, final HashMap sections) { - Set sortedSections = new TreeSet<>(sections.keySet()); - - for (String sectionKey : sortedSections) { - SettingSection section = sections.get(sectionKey); - - HashMap settings = section.getSettings(); - Set sortedKeySet = new TreeSet<>(settings.keySet()); - - for (String settingKey : sortedKeySet) { - Setting setting = settings.get(settingKey); - NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString()); - } - } - } - - private static String mapSectionNameFromIni(String generalSectionName) { - if (sectionsMap.getForward(generalSectionName) != null) { - return sectionsMap.getForward(generalSectionName); - } - - return generalSectionName; - } - - private static String mapSectionNameToIni(String generalSectionName) { - if (sectionsMap.getBackward(generalSectionName) != null) { - return sectionsMap.getBackward(generalSectionName); - } - - return generalSectionName; - } - - @NonNull - private static File getSettingsFile(String fileName) { - return new File( - DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini"); - } - - private static File getCustomGameSettingsFile(String gameId) { - return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini"); - } - - private static SettingSection sectionFromLine(String line, boolean isCustomGame) { - String sectionName = line.substring(1, line.length() - 1); - if (isCustomGame) { - sectionName = mapSectionNameToIni(sectionName); - } - return new SettingSection(sectionName); - } - - /** - * For a line of text, determines what type of data is being represented, and returns - * a Setting object containing this data. - * - * @param current The section currently being parsed by the consuming method. - * @param line The line of text being parsed. - * @return A typed Setting containing the key/value contained in the line. - */ - private static Setting settingFromLine(SettingSection current, String line) { - String[] splitLine = line.split("="); - - if (splitLine.length != 2) { - Log.warning("Skipping invalid config line \"" + line + "\""); - return null; - } - - String key = splitLine[0].trim(); - String value = splitLine[1].trim(); - - if (value.isEmpty()) { - Log.warning("Skipping null value in config line \"" + line + "\""); - return null; - } - - try { - int valueAsInt = Integer.parseInt(value); - - return new IntSetting(key, current.getName(), valueAsInt); - } catch (NumberFormatException ex) { - } - - try { - float valueAsFloat = Float.parseFloat(value); - - return new FloatSetting(key, current.getName(), valueAsFloat); - } catch (NumberFormatException ex) { - } - - return new StringSetting(key, current.getName(), value); - } - - /** - * Writes the contents of a Section HashMap to disk. - * - * @param parser A Wini pointed at a file on disk. - * @param section A section containing settings to be written to the file. - */ - private static void writeSection(Wini parser, SettingSection section) { - // Write the section header. - String header = section.getName(); - - // Write this section's values. - HashMap settings = section.getSettings(); - Set keySet = settings.keySet(); - - for (String key : keySet) { - Setting setting = settings.get(key); - parser.put(header, setting.getKey(), setting.getValueAsString()); - } - } -} 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 deleted file mode 100644 index c18ecd4c3..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.citra.citra_emu.fragments; - -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.FileProvider; - -import com.nononsenseapps.filepicker.FilePickerFragment; - -import org.citra.citra_emu.R; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class CustomFilePickerFragment extends FilePickerFragment { - private static String ALL_FILES = "*"; - private int mTitle; - private static List extensions = Collections.singletonList(ALL_FILES); - - @NonNull - @Override - public Uri toUri(@NonNull final File file) { - return FileProvider - .getUriForFile(getContext(), - getContext().getApplicationContext().getPackageName() + ".filesprovider", - file); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (mode == MODE_DIR) { - TextView ok = getActivity().findViewById(R.id.nnf_button_ok); - ok.setText(R.string.select_dir); - - TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); - cancel.setVisibility(View.GONE); - } - } - - @Override - protected View inflateRootView(LayoutInflater inflater, ViewGroup container) { - View view = super.inflateRootView(inflater, container); - if (mTitle != 0) { - Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar); - ViewGroup parent = (ViewGroup) toolbar.getParent(); - int index = parent.indexOfChild(toolbar); - View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false); - TextView title = newToolbar.findViewById(R.id.filepicker_title); - title.setText(mTitle); - parent.removeView(toolbar); - parent.addView(newToolbar, index); - } - return view; - } - - public void setTitle(int title) { - mTitle = title; - } - - public void setAllowedExtensions(String allowedExtensions) { - if (allowedExtensions == null) - return; - - extensions = Arrays.asList(allowedExtensions.split(",")); - } - - @Override - protected boolean isItemVisible(@NonNull final File file) { - // Some users jump to the conclusion that Dolphin isn't able to detect their - // files if the files don't show up in the file picker when mode == MODE_DIR. - // To avoid this, show files even when the user needs to select a directory. - return (showHiddenItems || !file.isHidden()) && - (file.isDirectory() || extensions.contains(ALL_FILES) || - extensions.contains(fileExtension(file.getName()).toLowerCase())); - } - - @Override - public boolean isCheckable(@NonNull final File file) { - // We need to make a small correction to the isCheckable logic due to - // overriding isItemVisible to show files when mode == MODE_DIR. - // AbstractFilePickerFragment always treats files as checkable when - // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. - return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); - } - - @Override - public void goUp() { - if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) { - goToDir(new File("/storage/")); - return; - } - if (mCurrentPath.equals(new File("/storage/"))){ - return; - } - super.goUp(); - } - - @Override - public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { - if(viewHolder.file.equals(new File("/storage/emulated/"))) - viewHolder.file = new File("/storage/emulated/0/"); - super.onClickDir(view, viewHolder); - } - - private static String fileExtension(@NonNull String filename) { - int i = filename.lastIndexOf('.'); - return i < 0 ? "" : filename.substring(i + 1); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java deleted file mode 100644 index cdb40d6f8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java +++ /dev/null @@ -1,380 +0,0 @@ -package org.citra.citra_emu.fragments; - -import android.content.Context; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Color; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.view.Choreographer; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.overlay.InputOverlay; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; -import org.citra.citra_emu.utils.DirectoryStateReceiver; -import org.citra.citra_emu.utils.EmulationMenuSettings; -import org.citra.citra_emu.utils.Log; - -public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { - private static final String KEY_GAMEPATH = "gamepath"; - - private static final Handler perfStatsUpdateHandler = new Handler(); - - private SharedPreferences mPreferences; - - private InputOverlay mInputOverlay; - - private EmulationState mEmulationState; - - private DirectoryStateReceiver directoryStateReceiver; - - private EmulationActivity activity; - - private TextView mPerfStats; - - private Runnable perfStatsUpdater; - - public static EmulationFragment newInstance(String gamePath) { - Bundle args = new Bundle(); - args.putString(KEY_GAMEPATH, gamePath); - - EmulationFragment fragment = new EmulationFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - if (context instanceof EmulationActivity) { - activity = (EmulationActivity) context; - NativeLibrary.setEmulationActivity((EmulationActivity) context); - } else { - throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); - } - } - - /** - * Initialize anything that doesn't depend on the layout / views in here. - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // So this fragment doesn't restart on configuration changes; i.e. rotation. - setRetainInstance(true); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - - String gamePath = getArguments().getString(KEY_GAMEPATH); - mEmulationState = new EmulationState(gamePath); - } - - /** - * Initialize the UI and start emulation in here. - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View contents = inflater.inflate(R.layout.fragment_emulation, container, false); - - SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); - surfaceView.getHolder().addCallback(this); - - mInputOverlay = contents.findViewById(R.id.surface_input_overlay); - mPerfStats = contents.findViewById(R.id.show_fps_text); - mPerfStats.setTextColor(Color.YELLOW); - - Button doneButton = contents.findViewById(R.id.done_control_config); - if (doneButton != null) { - doneButton.setOnClickListener(v -> stopConfiguringControls()); - } - - // Show/hide the "Show FPS" overlay - updateShowFpsOverlay(); - - // The new Surface created here will get passed to the native code via onSurfaceChanged. - return contents; - } - - @Override - public void onResume() { - super.onResume(); - Choreographer.getInstance().postFrameCallback(this); - if (DirectoryInitialization.areCitraDirectoriesReady()) { - mEmulationState.run(activity.isActivityRecreated()); - } else { - setupCitraDirectoriesThenStartEmulation(); - } - } - - @Override - public void onPause() { - if (directoryStateReceiver != null) { - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); - directoryStateReceiver = null; - } - - if (mEmulationState.isRunning()) { - mEmulationState.pause(); - } - - Choreographer.getInstance().removeFrameCallback(this); - super.onPause(); - } - - @Override - public void onDetach() { - NativeLibrary.clearEmulationActivity(); - super.onDetach(); - } - - private void setupCitraDirectoriesThenStartEmulation() { - IntentFilter statusIntentFilter = new IntentFilter( - DirectoryInitialization.BROADCAST_ACTION); - - directoryStateReceiver = - new DirectoryStateReceiver(directoryInitializationState -> - { - if (directoryInitializationState == - DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { - mEmulationState.run(activity.isActivityRecreated()); - } else if (directoryInitializationState == - DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { - Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT) - .show(); - } else if (directoryInitializationState == - DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { - Toast.makeText(getContext(), R.string.external_storage_not_mounted, - Toast.LENGTH_SHORT) - .show(); - } - }); - - // Registers the DirectoryStateReceiver and its intent filters - LocalBroadcastManager.getInstance(getActivity()).registerReceiver( - directoryStateReceiver, - statusIntentFilter); - DirectoryInitialization.start(getActivity()); - } - - public void refreshInputOverlay() { - mInputOverlay.refreshControls(); - } - - public void resetInputOverlay() { - // Reset button scale - SharedPreferences.Editor editor = mPreferences.edit(); - editor.putInt("controlScale", 50); - editor.apply(); - - mInputOverlay.resetButtonPlacement(); - } - - public void updateShowFpsOverlay() { - if (true) { - final int SYSTEM_FPS = 0; - final int FPS = 1; - final int FRAMETIME = 2; - final int SPEED = 3; - - perfStatsUpdater = () -> - { - final double[] perfStats = NativeLibrary.GetPerfStats(); - if (perfStats[FPS] > 0) { - mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS]), - (int) (perfStats[SPEED] * 100.0))); - } - - perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000); - }; - perfStatsUpdateHandler.post(perfStatsUpdater); - - mPerfStats.setVisibility(View.VISIBLE); - } else { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); - } - - mPerfStats.setVisibility(View.GONE); - } - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - // We purposely don't do anything here. - // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); - mEmulationState.newSurface(holder.getSurface()); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - mEmulationState.clearSurface(); - } - - @Override - public void doFrame(long frameTimeNanos) { - Choreographer.getInstance().postFrameCallback(this); - NativeLibrary.DoFrame(); - } - - public void stopEmulation() { - mEmulationState.stop(); - } - - public void startConfiguringControls() { - getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); - mInputOverlay.setIsInEditMode(true); - } - - public void stopConfiguringControls() { - getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); - mInputOverlay.setIsInEditMode(false); - } - - public boolean isConfiguringControls() { - return mInputOverlay.isInEditMode(); - } - - private static class EmulationState { - private final String mGamePath; - private State state; - private Surface mSurface; - private boolean mRunWhenSurfaceIsValid; - - EmulationState(String gamePath) { - mGamePath = gamePath; - // Starting state is stopped. - state = State.STOPPED; - } - - public synchronized boolean isStopped() { - return state == State.STOPPED; - } - - // Getters for the current state - - public synchronized boolean isPaused() { - return state == State.PAUSED; - } - - public synchronized boolean isRunning() { - return state == State.RUNNING; - } - - public synchronized void stop() { - if (state != State.STOPPED) { - Log.debug("[EmulationFragment] Stopping emulation."); - state = State.STOPPED; - NativeLibrary.StopEmulation(); - } else { - Log.warning("[EmulationFragment] Stop called while already stopped."); - } - } - - // State changing methods - - public synchronized void pause() { - if (state != State.PAUSED) { - state = State.PAUSED; - Log.debug("[EmulationFragment] Pausing emulation."); - - // Release the surface before pausing, since emulation has to be running for that. - NativeLibrary.SurfaceDestroyed(); - NativeLibrary.PauseEmulation(); - } else { - Log.warning("[EmulationFragment] Pause called while already paused."); - } - } - - public synchronized void run(boolean isActivityRecreated) { - if (isActivityRecreated) { - if (NativeLibrary.IsRunning()) { - state = State.PAUSED; - } - } else { - Log.debug("[EmulationFragment] activity resumed or fresh start"); - } - - // If the surface is set, run now. Otherwise, wait for it to get set. - if (mSurface != null) { - runWithValidSurface(); - } else { - mRunWhenSurfaceIsValid = true; - } - } - - // Surface callbacks - public synchronized void newSurface(Surface surface) { - mSurface = surface; - if (mRunWhenSurfaceIsValid) { - runWithValidSurface(); - } - } - - public synchronized void clearSurface() { - if (mSurface == null) { - Log.warning("[EmulationFragment] clearSurface called, but surface already null."); - } else { - mSurface = null; - Log.debug("[EmulationFragment] Surface destroyed."); - - if (state == State.RUNNING) { - NativeLibrary.SurfaceDestroyed(); - state = State.PAUSED; - } else if (state == State.PAUSED) { - Log.warning("[EmulationFragment] Surface cleared while emulation paused."); - } else { - Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); - } - } - } - - private void runWithValidSurface() { - mRunWhenSurfaceIsValid = false; - if (state == State.STOPPED) { - NativeLibrary.SurfaceChanged(mSurface); - Thread mEmulationThread = new Thread(() -> - { - Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePath); - }, "NativeEmulation"); - mEmulationThread.start(); - - } else if (state == State.PAUSED) { - Log.debug("[EmulationFragment] Resuming emulation."); - NativeLibrary.SurfaceChanged(mSurface); - NativeLibrary.UnPauseEmulation(); - } else { - Log.debug("[EmulationFragment] Bug, run called while already running."); - } - state = State.RUNNING; - } - - private enum State { - STOPPED, RUNNING, PAUSED - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java deleted file mode 100644 index a4ffc59c7..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.citra.citra_emu.model; - -import android.content.ContentValues; -import android.database.Cursor; - -import java.nio.file.Paths; - -public final class Game { - private String mTitle; - private String mDescription; - private String mPath; - private String mGameId; - private String mCompany; - private String mRegions; - - public Game(String title, String description, String regions, String path, - String gameId, String company) { - mTitle = title; - mDescription = description; - mRegions = regions; - mPath = path; - mGameId = gameId; - mCompany = company; - } - - public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) { - ContentValues values = new ContentValues(); - - if (gameId.isEmpty()) { - // Homebrew, etc. may not have a game ID, use filename as a unique identifier - gameId = Paths.get(path).getFileName().toString(); - } - - values.put(GameDatabase.KEY_GAME_TITLE, title); - values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); - values.put(GameDatabase.KEY_GAME_REGIONS, regions); - values.put(GameDatabase.KEY_GAME_PATH, path); - values.put(GameDatabase.KEY_GAME_ID, gameId); - values.put(GameDatabase.KEY_GAME_COMPANY, company); - - return values; - } - - public static Game fromCursor(Cursor cursor) { - return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE), - cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), - cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), - cursor.getString(GameDatabase.GAME_COLUMN_PATH), - cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), - cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); - } - - public String getTitle() { - return mTitle; - } - - public String getDescription() { - return mDescription; - } - - public String getCompany() { - return mCompany; - } - - public String getRegions() { - return mRegions; - } - - public String getPath() { - return mPath; - } - - public String getGameId() { - return mGameId; - } -} 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 deleted file mode 100644 index 8232d0489..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.citra.citra_emu.model; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.utils.Log; - -import java.io.File; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import rx.Observable; - -/** - * A helper class that provides several utilities simplifying interaction with - * the SQLite database. - */ -public final class GameDatabase extends SQLiteOpenHelper { - public static final int COLUMN_DB_ID = 0; - public static final int GAME_COLUMN_PATH = 1; - public static final int GAME_COLUMN_TITLE = 2; - public static final int GAME_COLUMN_DESCRIPTION = 3; - public static final int GAME_COLUMN_REGIONS = 4; - public static final int GAME_COLUMN_GAME_ID = 5; - public static final int GAME_COLUMN_COMPANY = 6; - public static final int FOLDER_COLUMN_PATH = 1; - public static final String KEY_DB_ID = "_id"; - public static final String KEY_GAME_PATH = "path"; - public static final String KEY_GAME_TITLE = "title"; - public static final String KEY_GAME_DESCRIPTION = "description"; - public static final String KEY_GAME_REGIONS = "regions"; - public static final String KEY_GAME_ID = "game_id"; - public static final String KEY_GAME_COMPANY = "company"; - public static final String KEY_FOLDER_PATH = "path"; - public static final String TABLE_NAME_FOLDERS = "folders"; - public static final String TABLE_NAME_GAMES = "games"; - private static final int DB_VERSION = 2; - private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; - private static final String TYPE_INTEGER = " INTEGER"; - private static final String TYPE_STRING = " TEXT"; - - private static final String CONSTRAINT_UNIQUE = " UNIQUE"; - - private static final String SEPARATOR = ", "; - - private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" - + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR - + KEY_GAME_PATH + TYPE_STRING + SEPARATOR - + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR - + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR - + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR - + KEY_GAME_ID + TYPE_STRING + SEPARATOR - + KEY_GAME_COMPANY + TYPE_STRING + ")"; - - private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" - + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR - + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")"; - - private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; - private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; - - public GameDatabase(Context context) { - // Superclass constructor builds a database or uses an existing one. - super(context, "games.db", null, DB_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase database) { - Log.debug("[GameDatabase] GameDatabase - Creating database..."); - - execSqlAndLog(database, SQL_CREATE_GAMES); - execSqlAndLog(database, SQL_CREATE_FOLDERS); - } - - @Override - public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) { - Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases.."); - execSqlAndLog(database, SQL_DELETE_FOLDERS); - execSqlAndLog(database, SQL_CREATE_FOLDERS); - - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - @Override - public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { - Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " + - newVersion); - - // Delete all the games - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - public void resetDatabase(SQLiteDatabase database) { - execSqlAndLog(database, SQL_DELETE_FOLDERS); - execSqlAndLog(database, SQL_CREATE_FOLDERS); - - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - public void scanLibrary(SQLiteDatabase database) { - // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. - Cursor fileCursor = database.query(TABLE_NAME_GAMES, - null, // Get all columns. - null, // Get all rows. - null, - null, // No grouping. - null, - null); // Order of games is irrelevant. - - // Possibly overly defensive, but ensures that moveToNext() does not skip a row. - fileCursor.moveToPosition(-1); - - while (fileCursor.moveToNext()) { - String gamePath = fileCursor.getString(GAME_COLUMN_PATH); - File game = new File(gamePath); - - if (!game.exists()) { - Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " + - gamePath); - database.delete(TABLE_NAME_GAMES, - KEY_DB_ID + " = ?", - new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); - } - } - - // Get a cursor listing all the folders the user has added to the library. - Cursor folderCursor = database.query(TABLE_NAME_FOLDERS, - null, // Get all columns. - null, // Get all rows. - null, - null, // No grouping. - null, - null); // Order of folders is irrelevant. - - Set allowedExtensions = new HashSet(Arrays.asList( - ".xci", ".nsp", ".nca", ".nro")); - - // Possibly overly defensive, but ensures that moveToNext() does not skip a row. - folderCursor.moveToPosition(-1); - - // Iterate through all results of the DB query (i.e. all folders in the library.) - while (folderCursor.moveToNext()) { - String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); - - File folder = new File(folderPath); - // If the folder is empty because it no longer exists, remove it from the library. - if (!folder.exists()) { - Log.error( - "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); - database.delete(TABLE_NAME_FOLDERS, - KEY_DB_ID + " = ?", - new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); - } - - addGamesRecursive(database, folder, allowedExtensions, 3); - } - - fileCursor.close(); - folderCursor.close(); - - database.close(); - } - - private static void addGamesRecursive(SQLiteDatabase database, File parent, Set allowedExtensions, int depth) { - if (depth <= 0) { - return; - } - - File[] children = parent.listFiles(); - if (children != null) { - for (File file : children) { - if (file.isHidden()) { - continue; - } - - if (file.isDirectory()) { - Set newExtensions = new HashSet<>(Arrays.asList( - ".xci", ".nsp", ".nca", ".nro")); - addGamesRecursive(database, file, newExtensions, depth - 1); - } else { - String filePath = file.getPath(); - - int extensionStart = filePath.lastIndexOf('.'); - if (extensionStart > 0) { - String fileExtension = filePath.substring(extensionStart); - - // Check that the file has an extension we care about before trying to read out of it. - if (allowedExtensions.contains(fileExtension.toLowerCase())) { - attemptToAddGame(database, filePath); - } - } - } - } - } - } - - private static void attemptToAddGame(SQLiteDatabase database, String filePath) { - String name = NativeLibrary.GetTitle(filePath); - - // If the game's title field is empty, use the filename. - if (name.isEmpty()) { - name = filePath.substring(filePath.lastIndexOf("/") + 1); - } - - String gameId = NativeLibrary.GetGameId(filePath); - - // If the game's ID field is empty, use the filename without extension. - if (gameId.isEmpty()) { - gameId = filePath.substring(filePath.lastIndexOf("/") + 1, - filePath.lastIndexOf(".")); - } - - ContentValues game = Game.asContentValues(name, - NativeLibrary.GetDescription(filePath).replace("\n", " "), - NativeLibrary.GetRegions(filePath), - filePath, - gameId, - NativeLibrary.GetCompany(filePath)); - - // Try to update an existing game first. - int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. - game, - // The values to fill the row with. - KEY_GAME_ID + " = ?", - // The WHERE clause used to find the right row. - new String[]{game.getAsString( - KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this, - // which is provided as an array because there - // could potentially be more than one argument. - - // If update fails, insert a new game instead. - if (rowsMatched == 0) { - Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)); - database.insert(TABLE_NAME_GAMES, null, game); - } else { - Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)); - } - } - - public Observable getGames() { - return Observable.create(subscriber -> - { - Log.info("[GameDatabase] Reading games list..."); - - SQLiteDatabase database = getReadableDatabase(); - Cursor resultCursor = database.query( - TABLE_NAME_GAMES, - null, - null, - null, - null, - null, - KEY_GAME_TITLE + " ASC" - ); - - // Pass the result cursor to the consumer. - subscriber.onNext(resultCursor); - - // Tell the consumer we're done; it will unsubscribe implicitly. - subscriber.onCompleted(); - }); - } - - private void execSqlAndLog(SQLiteDatabase database, String sql) { - Log.verbose("[GameDatabase] Executing SQL: " + sql); - database.execSQL(sql); - } -} 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 deleted file mode 100644 index 33b289fc4..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.citra.citra_emu.model; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; - -import androidx.annotation.NonNull; - -import org.citra.citra_emu.BuildConfig; -import org.citra.citra_emu.utils.Log; - -/** - * Provides an interface allowing Activities to interact with the SQLite database. - * CRUD methods in this class can be called by Activities using getContentResolver(). - */ -public final class GameProvider extends ContentProvider { - public static final String REFRESH_LIBRARY = "refresh"; - public static final String RESET_LIBRARY = "reset"; - - public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider"; - public static final Uri URI_FOLDER = - Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/"); - public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/"); - public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/"); - - public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder"; - public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game"; - - - private GameDatabase mDbHelper; - - @Override - public boolean onCreate() { - Log.info("[GameProvider] Creating Content Provider..."); - - mDbHelper = new GameDatabase(getContext()); - - return true; - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - Log.info("[GameProvider] Querying URI: " + uri); - - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - - String table = uri.getLastPathSegment(); - - if (table == null) { - Log.error("[GameProvider] Badly formatted URI: " + uri); - return null; - } - - Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; - } - - @Override - public String getType(@NonNull Uri uri) { - Log.verbose("[GameProvider] Getting MIME type for URI: " + uri); - String lastSegment = uri.getLastPathSegment(); - - if (lastSegment == null) { - Log.error("[GameProvider] Badly formatted URI: " + uri); - return null; - } - - if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) { - return MIME_TYPE_FOLDER; - } else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) { - return MIME_TYPE_GAME; - } - - Log.error("[GameProvider] Unknown MIME type for URI: " + uri); - return null; - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - Log.info("[GameProvider] Inserting row at URI: " + uri); - - SQLiteDatabase database = mDbHelper.getWritableDatabase(); - String table = uri.getLastPathSegment(); - - if (table != null) { - if (table.equals(RESET_LIBRARY)) { - mDbHelper.resetDatabase(database); - return uri; - } - if (table.equals(REFRESH_LIBRARY)) { - Log.info( - "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."); - mDbHelper.scanLibrary(database); - return uri; - } - - long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE); - - // If insertion was successful... - if (id > 0) { - // If we just added a folder, add its contents to the game list. - if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) { - mDbHelper.scanLibrary(database); - } - - // Notify the UI that its contents should be refreshed. - getContext().getContentResolver().notifyChange(uri, null); - uri = Uri.withAppendedPath(uri, Long.toString(id)); - } else { - Log.error("[GameProvider] Row already exists: " + uri + " id: " + id); - } - } else { - Log.error("[GameProvider] Badly formatted URI: " + uri); - } - - database.close(); - - return uri; - } - - @Override - public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { - Log.error("[GameProvider] Delete operations unsupported. URI: " + uri); - return 0; - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - Log.error("[GameProvider] Update operations unsupported. URI: " + uri); - return 0; - } -} 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 deleted file mode 100644 index cdb2f7666..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java +++ /dev/null @@ -1,878 +0,0 @@ -/** - * Copyright 2013 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.overlay; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Display; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.View; -import android.view.View.OnTouchListener; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.NativeLibrary.ButtonState; -import org.citra.citra_emu.NativeLibrary.ButtonType; -import org.citra.citra_emu.R; -import org.citra.citra_emu.utils.EmulationMenuSettings; - -import java.util.HashSet; -import java.util.Set; - -/** - * Draws the interactive input overlay on top of the - * {@link SurfaceView} that is rendering emulation. - */ -public final class InputOverlay extends SurfaceView implements OnTouchListener { - private final Set overlayButtons = new HashSet<>(); - private final Set overlayDpads = new HashSet<>(); - private final Set overlayJoysticks = new HashSet<>(); - - private boolean mIsInEditMode = false; - private InputOverlayDrawableButton mButtonBeingConfigured; - private InputOverlayDrawableDpad mDpadBeingConfigured; - private InputOverlayDrawableJoystick mJoystickBeingConfigured; - - private SharedPreferences mPreferences; - - // Stores the ID of the pointer that interacted with the 3DS touchscreen. - private int mTouchscreenPointerId = -1; - - /** - * Constructor - * - * @param context The current {@link Context}. - * @param attrs {@link AttributeSet} for parsing XML attributes. - */ - public InputOverlay(Context context, AttributeSet attrs) { - super(context, attrs); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!mPreferences.getBoolean("OverlayInit", false)) { - defaultOverlay(); - } - - // Reset 3ds touchscreen pointer ID - mTouchscreenPointerId = -1; - - // Load the controls. - refreshControls(); - - // Set the on touch listener. - setOnTouchListener(this); - - // Force draw - setWillNotDraw(false); - - // Request focus for the overlay so it has priority on presses. - requestFocus(); - } - - /** - * Resizes a {@link Bitmap} by a given scale factor - * - * @param context The current {@link Context} - * @param bitmap The {@link Bitmap} to scale. - * @param scale The scale factor for the bitmap. - * @return The scaled {@link Bitmap} - */ - public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) { - // Determine the button size based on the smaller screen dimension. - // This makes sure the buttons are the same size in both portrait and landscape. - DisplayMetrics dm = context.getResources().getDisplayMetrics(); - int minDimension = Math.min(dm.widthPixels, dm.heightPixels); - - return Bitmap.createScaledBitmap(bitmap, - (int) (minDimension * scale), - (int) (minDimension * scale), - true); - } - - /** - * Initializes an InputOverlayDrawableButton, given by resId, with all of the - * parameters set for it to be properly shown on the InputOverlay. - *

- * This works due to the way the X and Y coordinates are stored within - * the {@link SharedPreferences}. - *

- * In the input overlay configuration menu, - * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). - * the X and Y coordinates of the button at the END of its touch event - * (when you remove your finger/stylus from the touchscreen) are then stored - * within a SharedPreferences instance so that those values can be retrieved here. - *

- * This has a few benefits over the conventional way of storing the values - * (ie. within the Citra ini file). - *

    - *
  • No native calls
  • - *
  • Keeps Android-only values inside the Android environment
  • - *
- *

- * Technically no modifications should need to be performed on the returned - * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait - * for Android to call the onDraw method. - * - * @param context The current {@link Context}. - * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). - * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). - * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. - * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. - */ - private static InputOverlayDrawableButton initializeOverlayButton(Context context, - int defaultResId, int pressedResId, int buttonId, String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale; - - switch (buttonId) { - case ButtonType.BUTTON_HOME: - case ButtonType.BUTTON_START: - case ButtonType.BUTTON_SELECT: - scale = 0.08f; - break; - case ButtonType.TRIGGER_L: - case ButtonType.TRIGGER_R: - case ButtonType.BUTTON_ZL: - case ButtonType.BUTTON_ZR: - scale = 0.18f; - break; - default: - scale = 0.11f; - break; - } - - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableButton. - final Bitmap defaultStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); - final Bitmap pressedStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); - final InputOverlayDrawableButton overlayDrawable = - new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - String xKey; - String yKey; - - xKey = buttonId + orientation + "-X"; - yKey = buttonId + orientation + "-Y"; - - int drawableX = (int) sPrefs.getFloat(xKey, 0f); - int drawableY = (int) sPrefs.getFloat(yKey, 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableButton. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. - overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableDpad} - * - * @param context The current {@link Context}. - * @param defaultResId The {@link Bitmap} resource ID of the default sate. - * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction. - * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. - * @param buttonUp Identifier for the up button. - * @param buttonDown Identifier for the down button. - * @param buttonLeft Identifier for the left button. - * @param buttonRight Identifier for the right button. - * @return the initialized {@link InputOverlayDrawableDpad} - */ - private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, - int defaultResId, - int pressedOneDirectionResId, - int pressedTwoDirectionsResId, - int buttonUp, - int buttonDown, - int buttonLeft, - int buttonRight, - String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale = 0.22f; - - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableDpad. - final Bitmap defaultStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); - final Bitmap pressedOneDirectionStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), - scale); - final Bitmap pressedTwoDirectionsStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), - scale); - final InputOverlayDrawableDpad overlayDrawable = - new InputOverlayDrawableDpad(res, defaultStateBitmap, - pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, - buttonUp, buttonDown, buttonLeft, buttonRight); - - // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); - int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableDpad. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. - overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableJoystick} - * - * @param context The current {@link Context} - * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). - * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). - * @param pressedResInner Resource ID for the pressed inner image of the joystick. - * @param joystick Identifier for which joystick this is. - * @return the initialized {@link InputOverlayDrawableJoystick}. - */ - private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, - int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on user preference - float scale = 0.275f; - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableJoystick. - final Bitmap bitmapOuter = - resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); - final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); - final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f); - int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f); - - // Decide inner scale based on joystick ID - float outerScale = 1.f; - if (joystick == ButtonType.STICK_C) { - outerScale = 2.f; - } - - // Now set the bounds for the InputOverlayDrawableJoystick. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. - int outerSize = bitmapOuter.getWidth(); - Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale)); - Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); - - // Send the drawableId to the joystick so it can be referenced when saving control position. - final InputOverlayDrawableJoystick overlayDrawable - = new InputOverlayDrawableJoystick(res, bitmapOuter, - bitmapInnerDefault, bitmapInnerPressed, - outerRect, innerRect, joystick); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - - return overlayDrawable; - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - for (InputOverlayDrawableButton button : overlayButtons) { - button.draw(canvas); - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) { - dpad.draw(canvas); - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - joystick.draw(canvas); - } - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (isInEditMode()) { - return onTouchWhileEditing(event); - } - - int pointerIndex = event.getActionIndex(); - - if (mPreferences.getBoolean("isTouchEnabled", true)) { - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - if (NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true)) { - mTouchscreenPointerId = event.getPointerId(pointerIndex); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mTouchscreenPointerId == event.getPointerId(pointerIndex)) { - // We don't really care where the touch has been released. We only care whether it has been - // released or not. - NativeLibrary.onTouchEvent(0, 0, false); - mTouchscreenPointerId = -1; - } - break; - } - - for (int i = 0; i < event.getPointerCount(); i++) { - if (mTouchscreenPointerId == event.getPointerId(i)) { - NativeLibrary.onTouchMoved(event.getX(i), event.getY(i)); - } - } - } - - for (InputOverlayDrawableButton button : overlayButtons) { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If a pointer enters the bounds of a button, press that button. - if (button.getBounds() - .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { - button.setPressedState(true); - button.setTrackId(event.getPointerId(pointerIndex)); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), - ButtonState.PRESSED); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - // If a pointer ends, release the button it was pressing. - if (button.getTrackId() == event.getPointerId(pointerIndex)) { - button.setPressedState(false); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), - ButtonState.RELEASED); - } - break; - } - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If a pointer enters the bounds of a button, press that button. - if (dpad.getBounds() - .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { - dpad.setTrackId(event.getPointerId(pointerIndex)); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - // If a pointer ends, release the buttons. - if (dpad.getTrackId() == event.getPointerId(pointerIndex)) { - for (int i = 0; i < 4; i++) { - dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i), - NativeLibrary.ButtonState.RELEASED); - } - dpad.setTrackId(-1); - } - break; - } - - if (dpad.getTrackId() != -1) { - for (int i = 0; i < event.getPointerCount(); i++) { - if (dpad.getTrackId() == event.getPointerId(i)) { - float touchX = event.getX(i); - float touchY = event.getY(i); - float maxY = dpad.getBounds().bottom; - float maxX = dpad.getBounds().right; - touchX -= dpad.getBounds().centerX(); - maxX -= dpad.getBounds().centerX(); - touchY -= dpad.getBounds().centerY(); - maxY -= dpad.getBounds().centerY(); - final float AxisX = touchX / maxX; - final float AxisY = touchY / maxY; - - boolean up = false; - boolean down = false; - boolean left = false; - boolean right = false; - if (EmulationMenuSettings.getDpadSlideEnable() || - (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN || - (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { - if (AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), - NativeLibrary.ButtonState.PRESSED); - up = true; - } else { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), - NativeLibrary.ButtonState.RELEASED); - } - if (AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), - NativeLibrary.ButtonState.PRESSED); - down = true; - } else { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), - NativeLibrary.ButtonState.RELEASED); - } - if (AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), - NativeLibrary.ButtonState.PRESSED); - left = true; - } else { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), - NativeLibrary.ButtonState.RELEASED); - } - if (AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), - NativeLibrary.ButtonState.PRESSED); - right = true; - } else { - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), - NativeLibrary.ButtonState.RELEASED); - } - - // Set state - if (up) { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); - } else if (down) { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); - } else if (left) { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); - } else if (right) { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); - } else { - dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); - } - } - } - } - } - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - joystick.TrackEvent(event); - int axisID = joystick.getId(); - float[] axises = joystick.getAxisValues(); - - NativeLibrary - .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]); - } - - invalidate(); - - return true; - } - - public boolean onTouchWhileEditing(MotionEvent event) { - int pointerIndex = event.getActionIndex(); - int fingerPositionX = (int) event.getX(pointerIndex); - int fingerPositionY = (int) event.getY(pointerIndex); - - String orientation = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? - "-Portrait" : ""; - - // Maybe combine Button and Joystick as subclasses of the same parent? - // Or maybe create an interface like IMoveableHUDControl? - - for (InputOverlayDrawableButton button : overlayButtons) { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If no button is being moved now, remember the currently touched button to move. - if (mButtonBeingConfigured == null && - button.getBounds().contains(fingerPositionX, fingerPositionY)) { - mButtonBeingConfigured = button; - mButtonBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mButtonBeingConfigured != null) { - mButtonBeingConfigured.onConfigureTouch(event); - invalidate(); - return true; - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mButtonBeingConfigured == button) { - // Persist button position by saving new place. - saveControlPosition(mButtonBeingConfigured.getId(), - mButtonBeingConfigured.getBounds().left, - mButtonBeingConfigured.getBounds().top, orientation); - mButtonBeingConfigured = null; - } - break; - } - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If no button is being moved now, remember the currently touched button to move. - if (mButtonBeingConfigured == null && - dpad.getBounds().contains(fingerPositionX, fingerPositionY)) { - mDpadBeingConfigured = dpad; - mDpadBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mDpadBeingConfigured != null) { - mDpadBeingConfigured.onConfigureTouch(event); - invalidate(); - return true; - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mDpadBeingConfigured == dpad) { - // Persist button position by saving new place. - saveControlPosition(mDpadBeingConfigured.getId(0), - mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, - orientation); - mDpadBeingConfigured = null; - } - break; - } - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - if (mJoystickBeingConfigured == null && - joystick.getBounds().contains(fingerPositionX, fingerPositionY)) { - mJoystickBeingConfigured = joystick; - mJoystickBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mJoystickBeingConfigured != null) { - mJoystickBeingConfigured.onConfigureTouch(event); - invalidate(); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mJoystickBeingConfigured != null) { - saveControlPosition(mJoystickBeingConfigured.getId(), - mJoystickBeingConfigured.getBounds().left, - mJoystickBeingConfigured.getBounds().top, orientation); - mJoystickBeingConfigured = null; - } - break; - } - } - - return true; - } - - private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, - boolean right) { - if (up) { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); - } else if (down) { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); - } else if (left) { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); - } else if (right) { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); - } - } - - private void addOverlayControls(String orientation) { - if (mPreferences.getBoolean("buttonToggle0", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a, - R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation)); - } - if (mPreferences.getBoolean("buttonToggle1", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b, - R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation)); - } - if (mPreferences.getBoolean("buttonToggle2", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x, - R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation)); - } - if (mPreferences.getBoolean("buttonToggle3", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y, - R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation)); - } - if (mPreferences.getBoolean("buttonToggle4", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, - R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation)); - } - if (mPreferences.getBoolean("buttonToggle5", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, - R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation)); - } - if (mPreferences.getBoolean("buttonToggle6", false)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl, - R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation)); - } - if (mPreferences.getBoolean("buttonToggle7", false)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr, - R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation)); - } - if (mPreferences.getBoolean("buttonToggle8", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, - R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation)); - } - if (mPreferences.getBoolean("buttonToggle9", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, - R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation)); - } - if (mPreferences.getBoolean("buttonToggle10", true)) { - overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad, - R.drawable.dpad_pressed_one_direction, - R.drawable.dpad_pressed_two_directions, - ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, - ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); - } - if (mPreferences.getBoolean("buttonToggle11", true)) { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range, - R.drawable.stick_main, R.drawable.stick_main_pressed, - ButtonType.STICK_LEFT, orientation)); - } - if (mPreferences.getBoolean("buttonToggle12", false)) { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range, - R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation)); - } - } - - public void refreshControls() { - // Remove all the overlay buttons from the HashSet. - overlayButtons.clear(); - overlayDpads.clear(); - overlayJoysticks.clear(); - - String orientation = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? - "-Portrait" : ""; - - // Add all the enabled overlay items back to the HashSet. - if (EmulationMenuSettings.getShowOverlay()) { - addOverlayControls(orientation); - } - - invalidate(); - } - - private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); - sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); - sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); - sPrefsEditor.apply(); - } - - public void setIsInEditMode(boolean isInEditMode) { - mIsInEditMode = isInEditMode; - } - - private void defaultOverlay() { - if (!mPreferences.getBoolean("OverlayInit", false)) { - // It's possible that a user has created their overlay before this was added - // Only change the overlay if the 'A' button is not in the upper corner. - if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) { - defaultOverlayLandscape(); - } - if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) { - defaultOverlayPortrait(); - } - } - - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - sPrefsEditor.putBoolean("OverlayInit", true); - sPrefsEditor.apply(); - } - - public void resetButtonPlacement() { - boolean isLandscape = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - - if (isLandscape) { - defaultOverlayLandscape(); - } else { - defaultOverlayPortrait(); - } - - refreshControls(); - } - - private void defaultOverlayLandscape() { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for height. - if (maxY > maxX) { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void defaultOverlayPortrait() { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for height. - if (maxY < maxX) { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - String portrait = "-Portrait"; - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - public boolean isInEditMode() { - return mIsInEditMode; - } -} 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 deleted file mode 100644 index 81352296c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright 2013 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableButton { - // The ID identifying what type of button this Drawable represents. - private int mButtonType; - private int mTrackId; - private int mPreviousTouchX, mPreviousTouchY; - private int mControlPositionX, mControlPositionY; - private int mWidth; - private int mHeight; - private BitmapDrawable mDefaultStateBitmap; - private BitmapDrawable mPressedStateBitmap; - private boolean mPressedState = false; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. - * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. - * @param buttonType Identifier for this type of button. - */ - public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, - Bitmap pressedStateBitmap, int buttonType) { - mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); - mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); - mButtonType = buttonType; - - mWidth = mDefaultStateBitmap.getIntrinsicWidth(); - mHeight = mDefaultStateBitmap.getIntrinsicHeight(); - } - - /** - * Gets this InputOverlayDrawableButton's button ID. - * - * @return this InputOverlayDrawableButton's button ID. - */ - public int getId() { - return mButtonType; - } - - public int getTrackId() { - return mTrackId; - } - - public void setTrackId(int trackId) { - mTrackId = trackId; - } - - public boolean onConfigureTouch(MotionEvent event) { - int pointerIndex = event.getActionIndex(); - int fingerPositionX = (int) event.getX(pointerIndex); - int fingerPositionY = (int) event.getY(pointerIndex); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - case MotionEvent.ACTION_MOVE: - mControlPositionX += fingerPositionX - mPreviousTouchX; - mControlPositionY += fingerPositionY - mPreviousTouchY; - setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, - getHeight() + mControlPositionY); - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - - } - return true; - } - - public void setPosition(int x, int y) { - mControlPositionX = x; - mControlPositionY = y; - } - - public void draw(Canvas canvas) { - getCurrentStateBitmapDrawable().draw(canvas); - } - - private BitmapDrawable getCurrentStateBitmapDrawable() { - return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; - } - - public void setBounds(int left, int top, int right, int bottom) { - mDefaultStateBitmap.setBounds(left, top, right, bottom); - mPressedStateBitmap.setBounds(left, top, right, bottom); - } - - public Rect getBounds() { - return mDefaultStateBitmap.getBounds(); - } - - public int getWidth() { - return mWidth; - } - - public int getHeight() { - return mHeight; - } - - public void setPressedState(boolean isPressed) { - mPressedState = isPressed; - } -} 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 deleted file mode 100644 index 87f3b7cd9..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Copyright 2016 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableDpad { - public static final int STATE_DEFAULT = 0; - public static final int STATE_PRESSED_UP = 1; - public static final int STATE_PRESSED_DOWN = 2; - public static final int STATE_PRESSED_LEFT = 3; - public static final int STATE_PRESSED_RIGHT = 4; - public static final int STATE_PRESSED_UP_LEFT = 5; - public static final int STATE_PRESSED_UP_RIGHT = 6; - public static final int STATE_PRESSED_DOWN_LEFT = 7; - public static final int STATE_PRESSED_DOWN_RIGHT = 8; - public static final float VIRT_AXIS_DEADZONE = 0.5f; - // The ID identifying what type of button this Drawable represents. - private int[] mButtonType = new int[4]; - private int mTrackId; - private int mPreviousTouchX, mPreviousTouchY; - private int mControlPositionX, mControlPositionY; - private int mWidth; - private int mHeight; - private BitmapDrawable mDefaultStateBitmap; - private BitmapDrawable mPressedOneDirectionStateBitmap; - private BitmapDrawable mPressedTwoDirectionsStateBitmap; - private int mPressState = STATE_DEFAULT; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param defaultStateBitmap {@link Bitmap} of the default state. - * @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction. - * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. - * @param buttonUp Identifier for the up button. - * @param buttonDown Identifier for the down button. - * @param buttonLeft Identifier for the left button. - * @param buttonRight Identifier for the right button. - */ - public InputOverlayDrawableDpad(Resources res, - Bitmap defaultStateBitmap, - Bitmap pressedOneDirectionStateBitmap, - Bitmap pressedTwoDirectionsStateBitmap, - int buttonUp, int buttonDown, - int buttonLeft, int buttonRight) { - mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); - mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); - mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); - - mWidth = mDefaultStateBitmap.getIntrinsicWidth(); - mHeight = mDefaultStateBitmap.getIntrinsicHeight(); - - mButtonType[0] = buttonUp; - mButtonType[1] = buttonDown; - mButtonType[2] = buttonLeft; - mButtonType[3] = buttonRight; - - mTrackId = -1; - } - - public void draw(Canvas canvas) { - int px = mControlPositionX + (getWidth() / 2); - int py = mControlPositionY + (getHeight() / 2); - switch (mPressState) { - case STATE_DEFAULT: - mDefaultStateBitmap.draw(canvas); - break; - case STATE_PRESSED_UP: - mPressedOneDirectionStateBitmap.draw(canvas); - break; - case STATE_PRESSED_RIGHT: - canvas.save(); - canvas.rotate(90, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN: - canvas.save(); - canvas.rotate(180, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_LEFT: - canvas.save(); - canvas.rotate(270, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_UP_LEFT: - mPressedTwoDirectionsStateBitmap.draw(canvas); - break; - case STATE_PRESSED_UP_RIGHT: - canvas.save(); - canvas.rotate(90, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN_RIGHT: - canvas.save(); - canvas.rotate(180, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN_LEFT: - canvas.save(); - canvas.rotate(270, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - } - } - - /** - * Gets one of the InputOverlayDrawableDpad's button IDs. - * - * @return the requested InputOverlayDrawableDpad's button ID. - */ - public int getId(int direction) { - return mButtonType[direction]; - } - - public int getTrackId() { - return mTrackId; - } - - public void setTrackId(int trackId) { - mTrackId = trackId; - } - - public boolean onConfigureTouch(MotionEvent event) { - int pointerIndex = event.getActionIndex(); - int fingerPositionX = (int) event.getX(pointerIndex); - int fingerPositionY = (int) event.getY(pointerIndex); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - case MotionEvent.ACTION_MOVE: - mControlPositionX += fingerPositionX - mPreviousTouchX; - mControlPositionY += fingerPositionY - mPreviousTouchY; - setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, - getHeight() + mControlPositionY); - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - - } - return true; - } - - public void setPosition(int x, int y) { - mControlPositionX = x; - mControlPositionY = y; - } - - public void setBounds(int left, int top, int right, int bottom) { - mDefaultStateBitmap.setBounds(left, top, right, bottom); - mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); - mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); - } - - public Rect getBounds() { - return mDefaultStateBitmap.getBounds(); - } - - public int getWidth() { - return mWidth; - } - - public int getHeight() { - return mHeight; - } - - public void setState(int pressState) { - mPressState = pressState; - } -} 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 deleted file mode 100644 index 956a8b1e9..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright 2013 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -import org.citra.citra_emu.NativeLibrary.ButtonType; -import org.citra.citra_emu.utils.EmulationMenuSettings; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableJoystick { - private final int[] axisIDs = {0, 0, 0, 0}; - private final float[] axises = {0f, 0f}; - private int trackId = -1; - private int mJoystickType; - private int mControlPositionX, mControlPositionY; - private int mPreviousTouchX, mPreviousTouchY; - private int mWidth; - private int mHeight; - private Rect mVirtBounds; - private Rect mOrigBounds; - private BitmapDrawable mOuterBitmap; - private BitmapDrawable mDefaultStateInnerBitmap; - private BitmapDrawable mPressedStateInnerBitmap; - private BitmapDrawable mBoundsBoxBitmap; - private boolean mPressedState = false; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick. - * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. - * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. - * @param rectOuter {@link Rect} which represents the outer joystick bounds. - * @param rectInner {@link Rect} which represents the inner joystick bounds. - * @param joystick Identifier for which joystick this is. - */ - public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, - Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed, - Rect rectOuter, Rect rectInner, int joystick) { - axisIDs[0] = joystick + 1; // Up - axisIDs[1] = joystick + 2; // Down - axisIDs[2] = joystick + 3; // Left - axisIDs[3] = joystick + 4; // Right - mJoystickType = joystick; - - mOuterBitmap = new BitmapDrawable(res, bitmapOuter); - mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); - mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); - mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); - mWidth = bitmapOuter.getWidth(); - mHeight = bitmapOuter.getHeight(); - - setBounds(rectOuter); - mDefaultStateInnerBitmap.setBounds(rectInner); - mPressedStateInnerBitmap.setBounds(rectInner); - mVirtBounds = getBounds(); - mOrigBounds = mOuterBitmap.copyBounds(); - mBoundsBoxBitmap.setAlpha(0); - mBoundsBoxBitmap.setBounds(getVirtBounds()); - SetInnerBounds(); - } - - /** - * Gets this InputOverlayDrawableJoystick's button ID. - * - * @return this InputOverlayDrawableJoystick's button ID. - */ - public int getId() { - return mJoystickType; - } - - public void draw(Canvas canvas) { - mOuterBitmap.draw(canvas); - getCurrentStateBitmapDrawable().draw(canvas); - mBoundsBoxBitmap.draw(canvas); - } - - public void TrackEvent(MotionEvent event) { - int pointerIndex = event.getActionIndex(); - - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { - mPressedState = true; - mOuterBitmap.setAlpha(0); - mBoundsBoxBitmap.setAlpha(255); - if (EmulationMenuSettings.getJoystickRelCenter()) { - getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(), - (int) event.getY(pointerIndex) - getVirtBounds().centerY()); - } - mBoundsBoxBitmap.setBounds(getVirtBounds()); - trackId = event.getPointerId(pointerIndex); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (trackId == event.getPointerId(pointerIndex)) { - mPressedState = false; - axises[0] = axises[1] = 0.0f; - mOuterBitmap.setAlpha(255); - mBoundsBoxBitmap.setAlpha(0); - setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, - mOrigBounds.bottom)); - setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, - mOrigBounds.bottom)); - SetInnerBounds(); - trackId = -1; - } - break; - } - - if (trackId == -1) - return; - - for (int i = 0; i < event.getPointerCount(); i++) { - if (trackId == event.getPointerId(i)) { - float touchX = event.getX(i); - float touchY = event.getY(i); - float maxY = getVirtBounds().bottom; - float maxX = getVirtBounds().right; - touchX -= getVirtBounds().centerX(); - maxX -= getVirtBounds().centerX(); - touchY -= getVirtBounds().centerY(); - maxY -= getVirtBounds().centerY(); - final float AxisX = touchX / maxX; - final float AxisY = touchY / maxY; - - // Clamp the circle pad input to a circle - final float angle = (float) Math.atan2(AxisY, AxisX); - float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY); - if(radius > 1.0f) - { - radius = 1.0f; - } - axises[0] = ((float)Math.cos(angle) * radius); - axises[1] = ((float)Math.sin(angle) * radius); - SetInnerBounds(); - } - } - } - - public boolean onConfigureTouch(MotionEvent event) { - int pointerIndex = event.getActionIndex(); - int fingerPositionX = (int) event.getX(pointerIndex); - int fingerPositionY = (int) event.getY(pointerIndex); - - int scale = 1; - if (mJoystickType == ButtonType.STICK_C) { - // C-stick is scaled down to be half the size of the circle pad - scale = 2; - } - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - case MotionEvent.ACTION_MOVE: - int deltaX = fingerPositionX - mPreviousTouchX; - int deltaY = fingerPositionY - mPreviousTouchY; - mControlPositionX += deltaX; - mControlPositionY += deltaY; - setBounds(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); - setVirtBounds(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); - SetInnerBounds(); - setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY))); - mPreviousTouchX = fingerPositionX; - mPreviousTouchY = fingerPositionY; - break; - } - return true; - } - - - public float[] getAxisValues() { - return axises; - } - - public int[] getAxisIDs() { - return axisIDs; - } - - private void SetInnerBounds() { - int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2)); - int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2)); - - if (mJoystickType == ButtonType.STICK_LEFT) { - X += 1; - Y += 1; - } - - if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2)) - X = getVirtBounds().centerX() + (getVirtBounds().width() / 2); - if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2)) - X = getVirtBounds().centerX() - (getVirtBounds().width() / 2); - if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2)) - Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2); - if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2)) - Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2); - - int width = mPressedStateInnerBitmap.getBounds().width() / 2; - int height = mPressedStateInnerBitmap.getBounds().height() / 2; - mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); - mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); - } - - public void setPosition(int x, int y) { - mControlPositionX = x; - mControlPositionY = y; - } - - private BitmapDrawable getCurrentStateBitmapDrawable() { - return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; - } - - public Rect getBounds() { - return mOuterBitmap.getBounds(); - } - - public void setBounds(Rect bounds) { - mOuterBitmap.setBounds(bounds); - } - - private void setOrigBounds(Rect bounds) { - mOrigBounds = bounds; - } - - private Rect getVirtBounds() { - return mVirtBounds; - } - - private void setVirtBounds(Rect bounds) { - mVirtBounds = bounds; - } - - public int getWidth() { - return mWidth; - } - - public int getHeight() { - return mHeight; - } -} 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 deleted file mode 100644 index 96ccc08bb..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.citra.citra_emu.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Implementation from: - * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36 - */ -public class DividerItemDecoration extends RecyclerView.ItemDecoration { - - private Drawable mDivider; - private boolean mShowFirstDivider = false; - private boolean mShowLastDivider = false; - - public DividerItemDecoration(Context context, AttributeSet attrs) { - final TypedArray a = context - .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider}); - mDivider = a.getDrawable(0); - a.recycle(); - } - - public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider, - boolean showLastDivider) { - this(context, attrs); - mShowFirstDivider = showFirstDivider; - mShowLastDivider = showLastDivider; - } - - public DividerItemDecoration(Drawable divider) { - mDivider = divider; - } - - public DividerItemDecoration(Drawable divider, boolean showFirstDivider, - boolean showLastDivider) { - this(divider); - mShowFirstDivider = showFirstDivider; - mShowLastDivider = showLastDivider; - } - - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, - @NonNull RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - if (mDivider == null) { - return; - } - if (parent.getChildAdapterPosition(view) < 1) { - return; - } - - if (getOrientation(parent) == LinearLayoutManager.VERTICAL) { - outRect.top = mDivider.getIntrinsicHeight(); - } else { - outRect.left = mDivider.getIntrinsicWidth(); - } - } - - @Override - public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - if (mDivider == null) { - super.onDrawOver(c, parent, state); - return; - } - - // Initialization needed to avoid compiler warning - int left = 0, right = 0, top = 0, bottom = 0, size; - int orientation = getOrientation(parent); - int childCount = parent.getChildCount(); - - if (orientation == LinearLayoutManager.VERTICAL) { - size = mDivider.getIntrinsicHeight(); - left = parent.getPaddingLeft(); - right = parent.getWidth() - parent.getPaddingRight(); - } else { //horizontal - size = mDivider.getIntrinsicWidth(); - top = parent.getPaddingTop(); - bottom = parent.getHeight() - parent.getPaddingBottom(); - } - - for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) { - View child = parent.getChildAt(i); - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); - - if (orientation == LinearLayoutManager.VERTICAL) { - top = child.getTop() - params.topMargin; - bottom = top + size; - } else { //horizontal - left = child.getLeft() - params.leftMargin; - right = left + size; - } - mDivider.setBounds(left, top, right, bottom); - mDivider.draw(c); - } - - // show last divider - if (mShowLastDivider && childCount > 0) { - View child = parent.getChildAt(childCount - 1); - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); - if (orientation == LinearLayoutManager.VERTICAL) { - top = child.getBottom() + params.bottomMargin; - bottom = top + size; - } else { // horizontal - left = child.getRight() + params.rightMargin; - right = left + size; - } - mDivider.setBounds(left, top, right, bottom); - mDivider.draw(c); - } - } - - private int getOrientation(RecyclerView parent) { - if (parent.getLayoutManager() instanceof LinearLayoutManager) { - LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); - return layoutManager.getOrientation(); - } else { - throw new IllegalStateException( - "DividerItemDecoration can only be used with a LinearLayoutManager."); - } - } -} 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 deleted file mode 100644 index d07fe30d8..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.citra.citra_emu.ui; - -import android.view.View; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.NonNull; -import androidx.slidingpanelayout.widget.SlidingPaneLayout; - -public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback - implements SlidingPaneLayout.PanelSlideListener { - private final SlidingPaneLayout mSlidingPaneLayout; - - public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) { - super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen()); - mSlidingPaneLayout = slidingPaneLayout; - slidingPaneLayout.addPanelSlideListener(this); - } - - @Override - public void handleOnBackPressed() { - mSlidingPaneLayout.close(); - } - - @Override - public void onPanelSlide(@NonNull View panel, float slideOffset) { - } - - @Override - public void onPanelOpened(@NonNull View panel) { - setEnabled(true); - } - - @Override - public void onPanelClosed(@NonNull View panel) { - setEnabled(false); - } -} 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 deleted file mode 100644 index 4ba419a48..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java +++ /dev/null @@ -1,267 +0,0 @@ -package org.citra.citra_emu.ui.main; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; -import org.citra.citra_emu.features.settings.ui.SettingsActivity; -import org.citra.citra_emu.model.GameProvider; -import org.citra.citra_emu.ui.platform.PlatformGamesFragment; -import org.citra.citra_emu.utils.AddDirectoryHelper; -import org.citra.citra_emu.utils.BillingManager; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.FileBrowserHelper; -import org.citra.citra_emu.utils.PermissionsHandler; -import org.citra.citra_emu.utils.PicassoUtils; -import org.citra.citra_emu.utils.StartupHandler; -import org.citra.citra_emu.utils.ThemeUtil; - -import java.util.Arrays; -import java.util.Collections; - -/** - * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which - * individually display a grid of available games for each Fragment, in a tabbed layout. - */ -public final class MainActivity extends AppCompatActivity implements MainView { - private Toolbar mToolbar; - private int mFrameLayoutId; - private PlatformGamesFragment mPlatformGamesFragment; - - private MainPresenter mPresenter = new MainPresenter(this); - - // Singleton to manage user billing state - private static BillingManager mBillingManager; - - private static MenuItem mPremiumButton; - - @Override - protected void onCreate(Bundle savedInstanceState) { - ThemeUtil.applyTheme(); - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - findViews(); - - setSupportActionBar(mToolbar); - - mFrameLayoutId = R.id.games_platform_frame; - mPresenter.onCreate(); - - if (savedInstanceState == null) { - StartupHandler.HandleInit(this); - if (PermissionsHandler.hasWriteAccess(this)) { - mPlatformGamesFragment = new PlatformGamesFragment(); - getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) - .commit(); - } - } else { - mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment"); - } - PicassoUtils.init(); - - // Setup billing manager, so we can globally query for Premium status - mBillingManager = new BillingManager(this); - - // Dismiss previous notifications (should not happen unless a crash occurred) - EmulationActivity.tryDismissRunningNotification(this); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (PermissionsHandler.hasWriteAccess(this)) { - if (getSupportFragmentManager() == null) { - return; - } - if (outState == null) { - return; - } - getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment); - } - } - - @Override - protected void onResume() { - super.onResume(); - mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); - } - - // TODO: Replace with a ButterKnife injection. - private void findViews() { - mToolbar = findViewById(R.id.toolbar_main); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_game_grid, menu); - mPremiumButton = menu.findItem(R.id.button_premium); - - if (mBillingManager.isPremiumCached()) { - // User had premium in a previous session, hide upsell option - setPremiumButtonVisible(false); - } - - return true; - } - - static public void setPremiumButtonVisible(boolean isVisible) { - if (mPremiumButton != null) { - mPremiumButton.setVisible(isVisible); - } - } - - /** - * MainView - */ - - @Override - public void setVersionString(String version) { - mToolbar.setSubtitle(version); - } - - @Override - public void refresh() { - getContentResolver().insert(GameProvider.URI_REFRESH, null); - refreshFragment(); - } - - @Override - public void launchSettingsActivity(String menuTag) { - if (PermissionsHandler.hasWriteAccess(this)) { - SettingsActivity.launch(this, menuTag, ""); - } else { - PermissionsHandler.checkWritePermission(this); - } - } - - @Override - public void launchFileListActivity(int request) { - if (PermissionsHandler.hasWriteAccess(this)) { - switch (request) { - case MainPresenter.REQUEST_ADD_DIRECTORY: - FileBrowserHelper.openDirectoryPicker(this, - MainPresenter.REQUEST_ADD_DIRECTORY, - R.string.select_game_folder, - Arrays.asList("xci", "nsp", "cci", "3ds", - "cxi", "app", "3dsx", "cia", - "rar", "zip", "7z", "torrent", - "tar", "gz", "nro")); - break; - case MainPresenter.REQUEST_INSTALL_CIA: - FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA, - R.string.install_cia_title, - Collections.singletonList("cia"), true); - break; - } - } else { - PermissionsHandler.checkWritePermission(this); - } - } - - /** - * @param requestCode An int describing whether the Activity that is returning did so successfully. - * @param resultCode An int describing what Activity is giving us this callback. - * @param result The information the returning Activity is providing us. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) { - super.onActivityResult(requestCode, resultCode, result); - switch (requestCode) { - case MainPresenter.REQUEST_ADD_DIRECTORY: - // If the user picked a file, as opposed to just backing out. - if (resultCode == MainActivity.RESULT_OK) { - // When a new directory is picked, we currently will reset the existing games - // database. This effectively means that only one game directory is supported. - // TODO(bunnei): Consider fixing this in the future, or removing code for this. - getContentResolver().insert(GameProvider.URI_RESET, null); - // Add the new directory - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result)); - } - break; - case MainPresenter.REQUEST_INSTALL_CIA: - // If the user picked a file, as opposed to just backing out. - if (resultCode == MainActivity.RESULT_OK) { - mPresenter.refeshGameList(); - } - break; - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - DirectoryInitialization.start(this); - - mPlatformGamesFragment = new PlatformGamesFragment(); - getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) - .commit(); - - // Immediately prompt user to select a game directory on first boot - if (mPresenter != null) { - mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); - } - } else { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) - .show(); - } - break; - default: - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - break; - } - } - - /** - * Called by the framework whenever any actionbar/toolbar icon is clicked. - * - * @param item The icon that was clicked on. - * @return True if the event was handled, false to bubble it up to the OS. - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return mPresenter.handleOptionSelection(item.getItemId()); - } - - private void refreshFragment() { - if (mPlatformGamesFragment != null) { - mPlatformGamesFragment.refresh(); - } - } - - @Override - protected void onDestroy() { - EmulationActivity.tryDismissRunningNotification(this); - super.onDestroy(); - } - - /** - * @return true if Premium subscription is currently active - */ - public static boolean isPremiumActive() { - return mBillingManager.isPremiumActive(); - } - - /** - * Invokes the billing flow for Premium - * - * @param callback Optional callback, called once, on completion of billing - */ - public static void invokePremiumBilling(Runnable callback) { - mBillingManager.invokePremiumBilling(callback); - } -} 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 deleted file mode 100644 index 4e9994c2a..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.citra.citra_emu.ui.main; - -import android.os.SystemClock; - -import org.citra.citra_emu.BuildConfig; -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.model.GameDatabase; -import org.citra.citra_emu.utils.AddDirectoryHelper; - -public final class MainPresenter { - public static final int REQUEST_ADD_DIRECTORY = 1; - public static final int REQUEST_INSTALL_CIA = 2; - - private final MainView mView; - private String mDirToAdd; - private long mLastClickTime = 0; - - public MainPresenter(MainView view) { - mView = view; - } - - public void onCreate() { - String versionName = BuildConfig.VERSION_NAME; - mView.setVersionString(versionName); - refeshGameList(); - } - - public void launchFileListActivity(int request) { - if (mView != null) { - mView.launchFileListActivity(request); - } - } - - public boolean handleOptionSelection(int itemId) { - // Double-click prevention, using threshold of 500 ms - if (SystemClock.elapsedRealtime() - mLastClickTime < 500) { - return false; - } - mLastClickTime = SystemClock.elapsedRealtime(); - - switch (itemId) { - case R.id.menu_settings_core: - mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG); - return true; - - case R.id.button_add_directory: - launchFileListActivity(REQUEST_ADD_DIRECTORY); - return true; - - case R.id.button_install_cia: - launchFileListActivity(REQUEST_INSTALL_CIA); - return true; - - case R.id.button_premium: - mView.launchSettingsActivity(Settings.SECTION_PREMIUM); - return true; - } - - return false; - } - - public void addDirIfNeeded(AddDirectoryHelper helper) { - if (mDirToAdd != null) { - helper.addDirectory(mDirToAdd, mView::refresh); - - mDirToAdd = null; - } - } - - public void onDirectorySelected(String dir) { - mDirToAdd = dir; - } - - public void refeshGameList() { - GameDatabase databaseHelper = CitraApplication.databaseHelper; - databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); - mView.refresh(); - } -} 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 deleted file mode 100644 index de7c04875..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.citra.citra_emu.ui.main; - -/** - * Abstraction for the screen that shows on application launch. - * Implementations will differ primarily to target touch-screen - * or non-touch screen devices. - */ -public interface MainView { - /** - * Pass the view the native library's version string. Displaying - * it is optional. - * - * @param version A string pulled from native code. - */ - void setVersionString(String version); - - /** - * Tell the view to refresh its contents. - */ - void refresh(); - - void launchSettingsActivity(String menuTag); - - void launchFileListActivity(int request); -} 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 deleted file mode 100644 index 9fc30796f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.citra.citra_emu.ui.platform; - -import android.database.Cursor; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.adapters.GameAdapter; -import org.citra.citra_emu.model.GameDatabase; - -public final class PlatformGamesFragment extends Fragment implements PlatformGamesView { - private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this); - - private GameAdapter mAdapter; - private RecyclerView mRecyclerView; - private TextView mTextView; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_grid, container, false); - - findViews(rootView); - - mPresenter.onCreateView(); - - return rootView; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - int columns = getResources().getInteger(R.integer.game_grid_columns); - RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns); - mAdapter = new GameAdapter(); - - mRecyclerView.setLayoutManager(layoutManager); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); - - // Add swipe down to refresh gesture - final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); - pullToRefresh.setOnRefreshListener(() -> { - GameDatabase databaseHelper = CitraApplication.databaseHelper; - databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); - refresh(); - pullToRefresh.setRefreshing(false); - }); - } - - @Override - public void refresh() { - mPresenter.refresh(); - updateTextView(); - } - - @Override - public void showGames(Cursor games) { - if (mAdapter != null) { - mAdapter.swapCursor(games); - } - updateTextView(); - } - - private void updateTextView() { - mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - - private void findViews(View root) { - mRecyclerView = root.findViewById(R.id.grid_games); - mTextView = root.findViewById(R.id.gamelist_empty_text); - } -} 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 deleted file mode 100644 index 9d8040e1b..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.citra.citra_emu.ui.platform; - - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.model.GameDatabase; -import org.citra.citra_emu.utils.Log; - -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; - -public final class PlatformGamesPresenter { - private final PlatformGamesView mView; - - public PlatformGamesPresenter(PlatformGamesView view) { - mView = view; - } - - public void onCreateView() { - loadGames(); - } - - public void refresh() { - Log.debug("[PlatformGamesPresenter] : Refreshing..."); - loadGames(); - } - - private void loadGames() { - Log.debug("[PlatformGamesPresenter] : Loading games..."); - - GameDatabase databaseHelper = CitraApplication.databaseHelper; - - databaseHelper.getGames() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(games -> - { - Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor..."); - - mView.showGames(games); - }); - } -} 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 deleted file mode 100644 index 4332121eb..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.citra.citra_emu.ui.platform; - -import android.database.Cursor; - -/** - * Abstraction for a screen representing a single platform's games. - */ -public interface PlatformGamesView { - /** - * Tell the view to refresh its contents. - */ - void refresh(); - - /** - * To be called when an asynchronous database read completes. Passes the - * result, in this case a {@link Cursor}, to the view. - * - * @param games A Cursor containing the games read from the database. - */ - void showGames(Cursor games); -} 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 deleted file mode 100644 index 886846ec5..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.citra.citra_emu.utils; - -public interface Action1 { - void call(T t); -} 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 deleted file mode 100644 index 7578c353f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.AsyncQueryHandler; -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import org.citra.citra_emu.model.GameDatabase; -import org.citra.citra_emu.model.GameProvider; - -public class AddDirectoryHelper { - private Context mContext; - - public AddDirectoryHelper(Context context) { - this.mContext = context; - } - - public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) { - AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) { - @Override - protected void onInsertComplete(int token, Object cookie, Uri uri) { - addDirectoryListener.onDirectoryAdded(); - } - }; - - ContentValues file = new ContentValues(); - file.put(GameDatabase.KEY_FOLDER_PATH, dir); - - handler.startInsert(0, // We don't need to identify this call to the handler - null, // We don't need to pass additional data to the handler - GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder - file); - } - - public interface AddDirectoryListener { - void onDirectoryAdded(); - } -} 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 deleted file mode 100644 index dfbab1780..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.citra.citra_emu.utils; - -import java.util.HashMap; -import java.util.Map; - -public class BiMap { - private Map forward = new HashMap(); - private Map backward = new HashMap(); - - public synchronized void add(K key, V value) { - forward.put(key, value); - backward.put(value, key); - } - - public synchronized V getForward(K key) { - return forward.get(key); - } - - public synchronized K getBackward(V key) { - return backward.get(key); - } -} 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 deleted file mode 100644 index 5dc54c235..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java +++ /dev/null @@ -1,215 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.app.Activity; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.widget.Toast; - -import com.android.billingclient.api.AcknowledgePurchaseParams; -import com.android.billingclient.api.AcknowledgePurchaseResponseListener; -import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.BillingClientStateListener; -import com.android.billingclient.api.BillingFlowParams; -import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.Purchase.PurchasesResult; -import com.android.billingclient.api.PurchasesUpdatedListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsParams; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.ui.main.MainActivity; - -import java.util.ArrayList; -import java.util.List; - -public class BillingManager implements PurchasesUpdatedListener { - private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium"; - - private final Activity mActivity; - private BillingClient mBillingClient; - private SkuDetails mSkuPremium; - private boolean mIsPremiumActive = false; - private boolean mIsServiceConnected = false; - private Runnable mUpdateBillingCallback; - - private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - - public BillingManager(Activity activity) { - mActivity = activity; - mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build(); - querySkuDetails(); - } - - static public boolean isPremiumCached() { - return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false); - } - - /** - * @return true if Premium subscription is currently active - */ - public boolean isPremiumActive() { - return mIsPremiumActive; - } - - /** - * Invokes the billing flow for Premium - * - * @param callback Optional callback, called once, on completion of billing - */ - public void invokePremiumBilling(Runnable callback) { - if (mSkuPremium == null) { - return; - } - - // Optional callback to refresh the UI for the caller when billing completes - mUpdateBillingCallback = callback; - - // Invoke the billing flow - BillingFlowParams flowParams = BillingFlowParams.newBuilder() - .setSkuDetails(mSkuPremium) - .build(); - mBillingClient.launchBillingFlow(mActivity, flowParams); - } - - private void updatePremiumState(boolean isPremiumActive) { - mIsPremiumActive = isPremiumActive; - - // Cache state for synchronous UI - SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive); - editor.apply(); - - // No need to show button in action bar if Premium is active - MainActivity.setPremiumButtonVisible(!isPremiumActive); - } - - @Override - public void onPurchasesUpdated(BillingResult billingResult, List purchaseList) { - if (purchaseList == null || purchaseList.isEmpty()) { - // Premium is not active, or billing is unavailable - updatePremiumState(false); - return; - } - - Purchase premiumPurchase = null; - for (Purchase purchase : purchaseList) { - if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) { - premiumPurchase = purchase; - } - } - - if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { - // Premium has been purchased - updatePremiumState(true); - - // Acknowledge the purchase if it hasn't already been acknowledged. - if (!premiumPurchase.isAcknowledged()) { - AcknowledgePurchaseParams acknowledgePurchaseParams = - AcknowledgePurchaseParams.newBuilder() - .setPurchaseToken(premiumPurchase.getPurchaseToken()) - .build(); - - AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> { - Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show(); - }; - mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); - } - - if (mUpdateBillingCallback != null) { - try { - mUpdateBillingCallback.run(); - } catch (Exception e) { - e.printStackTrace(); - } - mUpdateBillingCallback = null; - } - } - } - - private void onQuerySkuDetailsFinished(List skuDetailsList) { - if (skuDetailsList == null) { - // This can happen when no user is signed in - return; - } - - if (skuDetailsList.isEmpty()) { - return; - } - - mSkuPremium = skuDetailsList.get(0); - - queryPurchases(); - } - - private void querySkuDetails() { - Runnable queryToExecute = new Runnable() { - @Override - public void run() { - SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); - List skuList = new ArrayList<>(); - - skuList.add(BILLING_SKU_PREMIUM); - params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); - - mBillingClient.querySkuDetailsAsync(params.build(), - (billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList)); - } - }; - - executeServiceRequest(queryToExecute); - } - - private void onQueryPurchasesFinished(PurchasesResult result) { - // Have we been disposed of in the meantime? If so, or bad result code, then quit - if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) { - updatePremiumState(false); - return; - } - // Update the UI and purchases inventory with new list of purchases - onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList()); - } - - private void queryPurchases() { - Runnable queryToExecute = new Runnable() { - @Override - public void run() { - final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); - onQueryPurchasesFinished(purchasesResult); - } - }; - - executeServiceRequest(queryToExecute); - } - - private void startServiceConnection(final Runnable executeOnFinish) { - mBillingClient.startConnection(new BillingClientStateListener() { - @Override - public void onBillingSetupFinished(BillingResult billingResult) { - if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { - mIsServiceConnected = true; - } - - if (executeOnFinish != null) { - executeOnFinish.run(); - } - } - - @Override - public void onBillingServiceDisconnected() { - mIsServiceConnected = false; - } - }); - } - - private void executeServiceRequest(Runnable runnable) { - if (mIsServiceConnected) { - runnable.run(); - } else { - // If billing service was disconnected, we try to reconnect 1 time. - startServiceConnection(runnable); - } - } -} 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 deleted file mode 100644 index f801a05f0..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; - -/** - * Some controllers have incorrect mappings. This class has special-case fixes for them. - */ -public class ControllerMappingHelper { - /** - * Some controllers report extra button presses that can be ignored. - */ - public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) { - if (isDualShock4(inputDevice)) { - // The two analog triggers generate analog motion events as well as a keycode. - // We always prefer to use the analog values, so throw away the button press - return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2; - } - return false; - } - - /** - * Scale an axis to be zero-centered with a proper range. - */ - public float scaleAxis(InputDevice inputDevice, int axis, float value) { - if (isDualShock4(inputDevice)) { - // Android doesn't have correct mappings for this controller's triggers. It reports them - // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] - // Scale them to properly zero-centered with a range of [0.0, 1.0]. - if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { - return (value + 1) / 2.0f; - } - } else if (isXboxOneWireless(inputDevice)) { - // Same as the DualShock 4, the mappings are missing. - if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { - return (value + 1) / 2.0f; - } - if (axis == MotionEvent.AXIS_GENERIC_1) { - // This axis is stuck at ~.5. Ignore it. - return 0.0f; - } - } else if (isMogaPro2Hid(inputDevice)) { - // This controller has a broken axis that reports a constant value. Ignore it. - if (axis == MotionEvent.AXIS_GENERIC_1) { - return 0.0f; - } - } - return value; - } - - private boolean isDualShock4(InputDevice inputDevice) { - // Sony DualShock 4 controller - return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; - } - - private boolean isXboxOneWireless(InputDevice inputDevice) { - // Microsoft Xbox One controller - return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; - } - - private boolean isMogaPro2Hid(InputDevice inputDevice) { - // Moga Pro 2 HID - return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271; - } -} 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 deleted file mode 100644 index 58e552f5e..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright 2014 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.utils; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Environment; -import android.preference.PreferenceManager; - -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.citra.citra_emu.NativeLibrary; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A service that spawns its own thread in order to copy several binary and shader files - * from the Citra APK to the external file system. - */ -public final class DirectoryInitialization { - public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST"; - - public static final String EXTRA_STATE = "directoryState"; - private static volatile DirectoryInitializationState directoryState = null; - private static String userPath; - private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false); - - public static void start(Context context) { - // Can take a few seconds to run, so don't block UI thread. - //noinspection TrivialFunctionalExpressionUsage - ((Runnable) () -> init(context)).run(); - } - - private static void init(Context context) { - if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true)) - return; - - if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { - if (PermissionsHandler.hasWriteAccess(context)) { - if (setCitraUserDirectory()) { - initializeInternalStorage(context); - NativeLibrary.CreateConfigFile(); - directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; - } else { - directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; - } - } else { - directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; - } - } - - isCitraDirectoryInitializationRunning.set(false); - sendBroadcastState(directoryState, context); - } - - private static void deleteDirectoryRecursively(File file) { - if (file.isDirectory()) { - for (File child : file.listFiles()) - deleteDirectoryRecursively(child); - } - file.delete(); - } - - public static boolean areCitraDirectoriesReady() { - return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; - } - - public static String getUserDirectory() { - if (directoryState == null) { - throw new IllegalStateException("DirectoryInitialization has to run at least once!"); - } else if (isCitraDirectoryInitializationRunning.get()) { - throw new IllegalStateException( - "DirectoryInitialization has to finish running first!"); - } - return userPath; - } - - private static native void SetSysDirectory(String path); - - private static boolean setCitraUserDirectory() { - if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { - File externalPath = Environment.getExternalStorageDirectory(); - if (externalPath != null) { - userPath = externalPath.getAbsolutePath() + "/citra-emu"; - Log.debug("[DirectoryInitialization] User Dir: " + userPath); - // NativeLibrary.SetUserDirectory(userPath); - return true; - } - - } - - return false; - } - - private static void initializeInternalStorage(Context context) { - File sysDirectory = new File(context.getFilesDir(), "Sys"); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - String revision = NativeLibrary.GetGitRevision(); - if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) { - // There is no extracted Sys directory, or there is a Sys directory from another - // version of Citra that might contain outdated files. Let's (re-)extract Sys. - deleteDirectoryRecursively(sysDirectory); - copyAssetFolder("Sys", sysDirectory, true, context); - - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("sysDirectoryVersion", revision); - editor.apply(); - } - - // Let the native code know where the Sys directory is. - SetSysDirectory(sysDirectory.getPath()); - } - - private static void sendBroadcastState(DirectoryInitializationState state, Context context) { - Intent localIntent = - new Intent(BROADCAST_ACTION) - .putExtra(EXTRA_STATE, state); - LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent); - } - - private static void copyAsset(String asset, File output, Boolean overwrite, Context context) { - Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output); - - try { - if (!output.exists() || overwrite) { - InputStream in = context.getAssets().open(asset); - OutputStream out = new FileOutputStream(output); - copyFile(in, out); - in.close(); - out.close(); - } - } catch (IOException e) { - Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + - e.getMessage()); - } - } - - private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite, - Context context) { - Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + - outputFolder); - - try { - boolean createdFolder = false; - for (String file : context.getAssets().list(assetFolder)) { - if (!createdFolder) { - outputFolder.mkdir(); - createdFolder = true; - } - copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), - overwrite, context); - copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite, - context); - } - } catch (IOException e) { - Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder + - e.getMessage()); - } - } - - private static void copyFile(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int read; - - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } - - public enum DirectoryInitializationState { - CITRA_DIRECTORIES_INITIALIZED, - EXTERNAL_STORAGE_PERMISSION_NEEDED, - CANT_FIND_EXTERNAL_STORAGE - } -} 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 deleted file mode 100644 index 5d1e951ca..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; - -public class DirectoryStateReceiver extends BroadcastReceiver { - Action1 callback; - - public DirectoryStateReceiver(Action1 callback) { - this.callback = callback; - } - - @Override - public void onReceive(Context context, Intent intent) { - DirectoryInitializationState state = (DirectoryInitializationState) intent - .getSerializableExtra(DirectoryInitialization.EXTRA_STATE); - callback.call(state); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java deleted file mode 100644 index 9664f8464..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import org.citra.citra_emu.CitraApplication; - -public class EmulationMenuSettings { - private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - - // These must match what is defined in src/core/settings.h - public static final int LayoutOption_Default = 0; - public static final int LayoutOption_SingleScreen = 1; - public static final int LayoutOption_LargeScreen = 2; - public static final int LayoutOption_SideScreen = 3; - public static final int LayoutOption_MobilePortrait = 4; - public static final int LayoutOption_MobileLandscape = 5; - - public static boolean getJoystickRelCenter() { - return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true); - } - - public static void setJoystickRelCenter(boolean value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value); - editor.apply(); - } - - public static boolean getDpadSlideEnable() { - return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true); - } - - public static void setDpadSlideEnable(boolean value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value); - editor.apply(); - } - - public static int getLandscapeScreenLayout() { - return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape); - } - - public static void setLandscapeScreenLayout(int value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value); - editor.apply(); - } - - public static boolean getShowFps() { - return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false); - } - - public static void setShowFps(boolean value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean("EmulationMenuSettings_ShowFps", value); - editor.apply(); - } - - public static boolean getSwapScreens() { - return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false); - } - - public static void setSwapScreens(boolean value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean("EmulationMenuSettings_SwapScreens", value); - editor.apply(); - } - - public static boolean getShowOverlay() { - return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true); - } - - public static void setShowOverlay(boolean value) { - final SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean("EmulationMenuSettings_ShowOverylay", value); - editor.apply(); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java deleted file mode 100644 index baf691f5c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.Intent; -import android.net.Uri; -import android.os.Environment; - -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; - -import com.nononsenseapps.filepicker.FilePickerActivity; -import com.nononsenseapps.filepicker.Utils; - -import org.citra.citra_emu.activities.CustomFilePickerActivity; - -import java.io.File; -import java.util.List; - -public final class FileBrowserHelper { - public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List extensions) { - Intent i = new Intent(activity, CustomFilePickerActivity.class); - - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); - i.putExtra(FilePickerActivity.EXTRA_START_PATH, - Environment.getExternalStorageDirectory().getPath()); - i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); - i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); - - activity.startActivityForResult(i, requestCode); - } - - public static void openFilePicker(FragmentActivity activity, int requestCode, int title, - List extensions, boolean allowMultiple) { - Intent i = new Intent(activity, CustomFilePickerActivity.class); - - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple); - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); - i.putExtra(FilePickerActivity.EXTRA_START_PATH, - Environment.getExternalStorageDirectory().getPath()); - i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); - i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); - - activity.startActivityForResult(i, requestCode); - } - - @Nullable - public static String getSelectedDirectory(Intent result) { - // Use the provided utility method to parse the result - List files = Utils.getSelectedFilesFromResult(result); - if (!files.isEmpty()) { - File file = Utils.getFileForUri(files.get(0)); - return file.getAbsolutePath(); - } - - return null; - } - - @Nullable - public static String[] getSelectedFiles(Intent result) { - // Use the provided utility method to parse the result - List files = Utils.getSelectedFilesFromResult(result); - if (!files.isEmpty()) { - String[] paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) - paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); - return paths; - } - - return null; - } -} 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 deleted file mode 100644 index f9025171b..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.citra.citra_emu.utils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class FileUtil { - public static byte[] getBytesFromFile(File file) throws IOException { - final long length = file.length(); - - // You cannot create an array using a long type. - if (length > Integer.MAX_VALUE) { - // File is too large - throw new IOException("File is too large!"); - } - - byte[] bytes = new byte[(int) length]; - - int offset = 0; - int numRead; - - try (InputStream is = new FileInputStream(file)) { - while (offset < bytes.length - && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - - return bytes; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java deleted file mode 100644 index 31c415779..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 2014 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.citra.citra_emu.utils; - -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; - -/** - * A service that shows a permanent notification in the background to avoid the app getting - * cleared from memory by the system. - */ -public class ForegroundService extends Service { - private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; - - private void showRunningNotification() { - // Intent is used to resume emulation if the notification is clicked - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, - new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id)) - .setSmallIcon(R.drawable.ic_stat_notification_logo) - .setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.app_notification_running)) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setOngoing(true) - .setVibrate(null) - .setSound(null) - .setContentIntent(contentIntent); - startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build()); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - showRunningNotification(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; - } - - @Override - public void onDestroy() { - NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java deleted file mode 100644 index b790c2480..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.graphics.Bitmap; - -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Request; -import com.squareup.picasso.RequestHandler; - -import org.citra.citra_emu.NativeLibrary; - -import java.nio.IntBuffer; - -public class GameIconRequestHandler extends RequestHandler { - @Override - public boolean canHandleRequest(Request data) { - return "iso".equals(data.uri.getScheme()); - } - - @Override - public Result load(Request request, int networkPolicy) { - String url = request.uri.getHost() + request.uri.getPath(); - int[] vector = NativeLibrary.GetIcon(url); - Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); - bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); - return new Result(bitmap, Picasso.LoadedFrom.DISK); - } -} 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 deleted file mode 100644 index 070d01eb1..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.citra.citra_emu.utils; - -import org.citra.citra_emu.BuildConfig; - -/** - * Contains methods that call through to {@link android.util.Log}, but - * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log - * levels in release builds. - */ -public final class Log { - private static final String TAG = "Citra Frontend"; - - private Log() { - } - - public static void verbose(String message) { - if (BuildConfig.DEBUG) { - android.util.Log.v(TAG, message); - } - } - - public static void debug(String message) { - if (BuildConfig.DEBUG) { - android.util.Log.d(TAG, message); - } - } - - public static void info(String message) { - android.util.Log.i(TAG, message); - } - - public static void warning(String message) { - android.util.Log.w(TAG, message); - } - - public static void error(String message) { - android.util.Log.e(TAG, message); - } -} 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 deleted file mode 100644 index a29e23e8d..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; - -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; - -import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; - -public class PermissionsHandler { - public static final int REQUEST_CODE_WRITE_PERMISSION = 500; - - // We use permissions acceptance as an indicator if this is a first boot for the user. - public static boolean isFirstBoot(final FragmentActivity activity) { - return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; - } - - @TargetApi(Build.VERSION_CODES.M) - public static boolean checkWritePermission(final FragmentActivity activity) { - if (isFirstBoot(activity)) { - activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, - REQUEST_CODE_WRITE_PERMISSION); - return false; - } - - return true; - } - - public static boolean hasWriteAccess(Context context) { - return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - } -} 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 deleted file mode 100644 index 892b46387..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; - -import com.squareup.picasso.Transformation; - -public class PicassoRoundedCornersTransformation implements Transformation { - @Override - public Bitmap transform(Bitmap icon) { - final int width = icon.getWidth(); - final int height = icon.getHeight(); - final Rect rect = new Rect(0, 0, width, height); - final int size = Math.min(width, height); - final int x = (width - size) / 2; - final int y = (height - size) / 2; - - Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size); - if (squaredBitmap != icon) { - icon.recycle(); - } - - Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setShader(shader); - - canvas.drawRoundRect(new RectF(rect), 10, 10, paint); - - squaredBitmap.recycle(); - - return output; - } - - @Override - public String key() { - return "circle"; - } -} \ 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 deleted file mode 100644 index c99726685..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.graphics.Bitmap; -import android.net.Uri; -import android.widget.ImageView; - -import com.squareup.picasso.Picasso; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; - -import java.io.IOException; - -import androidx.annotation.Nullable; - -public class PicassoUtils { - private static boolean mPicassoInitialized = false; - - public static void init() { - if (mPicassoInitialized) { - return; - } - Picasso picassoInstance = new Picasso.Builder(CitraApplication.getAppContext()) - .addRequestHandler(new GameIconRequestHandler()) - .build(); - - Picasso.setSingletonInstance(picassoInstance); - mPicassoInitialized = true; - } - - public static void loadGameIcon(ImageView imageView, String gamePath) { - Picasso - .get() - .load(Uri.parse("iso:/" + gamePath)) - .fit() - .centerInside() - .config(Bitmap.Config.RGB_565) - .error(R.drawable.no_icon) - .transform(new PicassoRoundedCornersTransformation()) - .into(imageView); - } - - // Blocking call. Load image from file and crop/resize it to fit in width x height. - @Nullable - public static Bitmap LoadBitmapFromFile(String uri, int width, int height) { - try { - return Picasso.get() - .load(Uri.parse(uri)) - .config(Bitmap.Config.ARGB_8888) - .centerCrop() - .resize(width, height) - .get(); - } catch (IOException e) { - return null; - } - } -} 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 deleted file mode 100644 index 9112bf90c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.FragmentActivity; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.activities.EmulationActivity; - -public final class StartupHandler { - private static void handlePermissionsCheck(FragmentActivity parent) { - // Ask the user to grant write permission if it's not already granted - PermissionsHandler.checkWritePermission(parent); - - String start_file = ""; - Bundle extras = parent.getIntent().getExtras(); - if (extras != null) { - start_file = extras.getString("AutoStartFile"); - } - - if (!TextUtils.isEmpty(start_file)) { - // Start the emulation activity, send the ISO passed in and finish the main activity - Intent emulation_intent = new Intent(parent, EmulationActivity.class); - emulation_intent.putExtra("SelectedGame", start_file); - parent.startActivity(emulation_intent); - parent.finish(); - } - } - - public static void HandleInit(FragmentActivity parent) { - if (PermissionsHandler.isFirstBoot(parent)) { - // Prompt user with standard first boot disclaimer - new AlertDialog.Builder(parent) - .setTitle(R.string.app_name) - .setIcon(R.mipmap.ic_launcher) - .setMessage(parent.getResources().getString(R.string.app_disclaimer)) - .setPositiveButton(android.R.string.ok, null) - .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) - .show(); - } - } -} 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 deleted file mode 100644 index 74ef3867f..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.citra.citra_emu.utils; - -import android.content.SharedPreferences; -import android.os.Build; -import android.preference.PreferenceManager; - -import androidx.appcompat.app.AppCompatDelegate; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.features.settings.utils.SettingsFile; - -public class ThemeUtil { - private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); - - private static void applyTheme(int designValue) { - switch (designValue) { - case 0: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - break; - case 1: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - break; - case 2: - AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : - AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); - break; - } - } - - public static void applyTheme() { - applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0)); - } -} 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 deleted file mode 100644 index 50dbcbe18..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.citra.citra_emu.viewholders; - -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; - -/** - * A simple class that stores references to views so that the GameAdapter doesn't need to - * keep calling findViewById(), which is expensive. - */ -public class GameViewHolder extends RecyclerView.ViewHolder { - private View itemView; - public ImageView imageIcon; - public TextView textGameTitle; - public TextView textCompany; - public TextView textFileName; - - public String gameId; - - // TODO Not need any of this stuff. Currently only the properties dialog needs it. - public String path; - public String title; - public String description; - public String regions; - public String company; - - public GameViewHolder(View itemView) { - super(itemView); - - this.itemView = itemView; - itemView.setTag(this); - - imageIcon = itemView.findViewById(R.id.image_game_screen); - textGameTitle = itemView.findViewById(R.id.text_game_title); - textCompany = itemView.findViewById(R.id.text_company); - textFileName = itemView.findViewById(R.id.text_filename); - } - - public View getItemView() { - return itemView; - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java new file mode 100644 index 000000000..e15612a36 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java @@ -0,0 +1,631 @@ +/* + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu; + +import android.app.Activity; +import android.app.Dialog; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.Surface; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; + +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; +import org.yuzu.yuzu_emu.utils.Log; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; + +/** + * Class which contains methods that interact + * with the native side of the Citra code. + */ +public final class NativeLibrary { + /** + * Default touchscreen device + */ + public static final String TouchScreenDevice = "Touchscreen"; + public static WeakReference sEmulationActivity = new WeakReference<>(null); + + private static boolean alertResult = false; + private static String alertPromptResult = ""; + private static int alertPromptButton = 0; + private static final Object alertPromptLock = new Object(); + private static boolean alertPromptInProgress = false; + private static String alertPromptCaption = ""; + private static int alertPromptButtonConfig = 0; + private static EditText alertPromptEditText = null; + + static { + try { + System.loadLibrary("yuzu-android"); + } catch (UnsatisfiedLinkError ex) { + Log.error("[NativeLibrary] " + ex.toString()); + } + } + + private NativeLibrary() { + // Disallows instantiation. + } + + /** + * Handles button press events for a gamepad. + * + * @param Device The input descriptor of the gamepad. + * @param Button Key code identifying which button was pressed. + * @param Action Mask identifying which action is happening (button pressed down, or button released). + * @return If we handled the button press. + */ + public static native boolean onGamePadEvent(String Device, int Button, int Action); + + /** + * Handles gamepad movement events. + * + * @param Device The device ID of the gamepad. + * @param Axis The axis ID + * @param x_axis The value of the x-axis represented by the given ID. + * @param y_axis The value of the y-axis represented by the given ID + */ + public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis); + + /** + * Handles gamepad movement events. + * + * @param Device The device ID of the gamepad. + * @param Axis_id The axis ID + * @param axis_val The value of the axis represented by the given ID. + */ + public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val); + + /** + * Handles touch events. + * + * @param x_axis The value of the x-axis. + * @param y_axis The value of the y-axis + * @param pressed To identify if the touch held down or released. + * @return true if the pointer is within the touchscreen + */ + public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed); + + /** + * Handles touch movement. + * + * @param x_axis The value of the instantaneous x-axis. + * @param y_axis The value of the instantaneous y-axis. + */ + public static native void onTouchMoved(float x_axis, float y_axis); + + public static native void ReloadSettings(); + + public static native String GetUserSetting(String gameID, String Section, String Key); + + public static native void SetUserSetting(String gameID, String Section, String Key, String Value); + + public static native void InitGameIni(String gameID); + + /** + * Gets the embedded icon within the given ROM. + * + * @param filename the file path to the ROM. + * @return an integer array containing the color data for the icon. + */ + public static native int[] GetIcon(String filename); + + /** + * Gets the embedded title of the given ISO/ROM. + * + * @param filename The file path to the ISO/ROM. + * @return the embedded title of the ISO/ROM. + */ + public static native String GetTitle(String filename); + + public static native String GetDescription(String filename); + + public static native String GetGameId(String filename); + + public static native String GetRegions(String filename); + + public static native String GetCompany(String filename); + + public static native String GetGitRevision(); + + /** + * Sets the current working user directory + * If not set, it auto-detects a location + */ + public static native void SetUserDirectory(String directory); + + // Create the config.ini file. + public static native void CreateConfigFile(); + + public static native int DefaultCPUCore(); + + /** + * Begins emulation. + */ + public static native void Run(String path); + + /** + * Begins emulation from the specified savestate. + */ + public static native void Run(String path, String savestatePath, boolean deleteSavestate); + + // Surface Handling + public static native void SurfaceChanged(Surface surf); + + public static native void SurfaceDestroyed(); + + public static native void DoFrame(); + + /** + * Unpauses emulation from a paused state. + */ + public static native void UnPauseEmulation(); + + /** + * Pauses emulation. + */ + public static native void PauseEmulation(); + + /** + * Stops emulation. + */ + public static native void StopEmulation(); + + /** + * Returns true if emulation is running (or is paused). + */ + public static native boolean IsRunning(); + + /** + * Returns the performance stats for the current game + **/ + public static native double[] GetPerfStats(); + + /** + * Notifies the core emulation that the orientation has changed. + */ + public static native void NotifyOrientationChange(int layout_option, int rotation); + + public enum CoreError { + ErrorSystemFiles, + ErrorSavestate, + ErrorUnknown, + } + + private static boolean coreErrorAlertResult = false; + private static final Object coreErrorAlertLock = new Object(); + + public static class CoreErrorDialogFragment extends DialogFragment { + static CoreErrorDialogFragment newInstance(String title, String message) { + CoreErrorDialogFragment frag = new CoreErrorDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); + final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); + + return new AlertDialog.Builder(emulationActivity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_button, (dialog, which) -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }) + .setNegativeButton(R.string.abort_button, (dialog, which) -> { + coreErrorAlertResult = false; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).setOnDismissListener(dialog -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).create(); + } + } + + private static void OnCoreErrorImpl(String title, String message) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return; + } + + CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message); + fragment.show(emulationActivity.getSupportFragmentManager(), "coreError"); + } + + /** + * Handles a core error. + * @return true: continue; false: abort + */ + public static boolean OnCoreError(CoreError error, String details) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + + String title, message; + switch (error) { + case ErrorSystemFiles: { + title = emulationActivity.getString(R.string.system_archive_not_found); + message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details); + break; + } + case ErrorSavestate: { + title = emulationActivity.getString(R.string.save_load_error); + message = details; + break; + } + case ErrorUnknown: { + title = emulationActivity.getString(R.string.fatal_error); + message = emulationActivity.getString(R.string.fatal_error_message); + break; + } + default: { + return true; + } + } + + // Show the AlertDialog on the main thread. + emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message)); + + // Wait for the lock to notify that it is complete. + synchronized (coreErrorAlertLock) { + try { + coreErrorAlertLock.wait(); + } catch (Exception ignored) { + } + } + + return coreErrorAlertResult; + } + + public static boolean isPortraitMode() { + return YuzuApplication.getAppContext().getResources().getConfiguration().orientation == + Configuration.ORIENTATION_PORTRAIT; + } + + public static int landscapeScreenLayout() { + return EmulationMenuSettings.getLandscapeScreenLayout(); + } + + public static boolean displayAlertMsg(final String caption, final String text, + final boolean yesNo) { + Log.error("[NativeLibrary] Alert: " + text); + final EmulationActivity emulationActivity = sEmulationActivity.get(); + boolean result = false; + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert."); + } else { + // Create object used for waiting. + final Object lock = new Object(); + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(caption) + .setMessage(text); + + // If not yes/no dialog just have one button that dismisses modal, + // otherwise have a yes and no button that sets alertResult accordingly. + if (!yesNo) { + builder + .setCancelable(false) + .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> + { + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }); + } else { + alertResult = false; + + builder + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> + { + alertResult = true; + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }) + .setNegativeButton(android.R.string.no, (dialog, whichButton) -> + { + alertResult = false; + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }); + } + + // Show the AlertDialog on the main thread. + emulationActivity.runOnUiThread(builder::show); + + // Wait for the lock to notify that it is complete. + synchronized (lock) { + try { + lock.wait(); + } catch (Exception e) { + } + } + + if (yesNo) + result = alertResult; + } + return result; + } + + public static void retryDisplayAlertPrompt() { + if (!alertPromptInProgress) { + return; + } + displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show(); + } + + public static String displayAlertPrompt(String caption, String text, int buttonConfig) { + alertPromptCaption = caption; + alertPromptButtonConfig = buttonConfig; + alertPromptInProgress = true; + + // Show the AlertDialog on the main thread + sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show()); + + // Wait for the lock to notify that it is complete + synchronized (alertPromptLock) { + try { + alertPromptLock.wait(); + } catch (Exception e) { + } + } + alertPromptInProgress = false; + + return alertPromptResult; + } + + public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + alertPromptResult = ""; + alertPromptButton = 0; + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = params.rightMargin = YuzuApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin); + + // Set up the input + alertPromptEditText = new EditText(YuzuApplication.getAppContext()); + alertPromptEditText.setText(text); + alertPromptEditText.setSingleLine(); + alertPromptEditText.setLayoutParams(params); + + FrameLayout container = new FrameLayout(emulationActivity); + container.addView(alertPromptEditText); + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(caption) + .setView(container) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + alertPromptButton = buttonConfig; + alertPromptResult = alertPromptEditText.getText().toString(); + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }) + .setOnDismissListener(dialogInterface -> + { + alertPromptResult = ""; + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }); + + if (buttonConfig > 0) { + builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> + { + alertPromptResult = ""; + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }); + } + + return builder; + } + + public static int alertPromptButton() { + return alertPromptButton; + } + + public static void exitEmulationActivity(int resultCode) { + final int Success = 0; + final int ErrorNotInitialized = 1; + final int ErrorGetLoader = 2; + final int ErrorSystemMode = 3; + final int ErrorLoader = 4; + final int ErrorLoader_ErrorEncrypted = 5; + final int ErrorLoader_ErrorInvalidFormat = 6; + final int ErrorSystemFiles = 7; + final int ErrorVideoCore = 8; + final int ErrorVideoCore_ErrorGenericDrivers = 9; + final int ErrorVideoCore_ErrorBelowGL33 = 10; + final int ShutdownRequested = 11; + final int ErrorUnknown = 12; + + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null, can't exit."); + return; + } + + int captionId = R.string.loader_error_invalid_format; + if (resultCode == ErrorLoader_ErrorEncrypted) { + captionId = R.string.loader_error_encrypted; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(captionId) + .setMessage(Html.fromHtml("Please follow the guides to redump your game cartidges or installed titles.", Html.FROM_HTML_MODE_LEGACY)) + .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish()) + .setOnDismissListener(dialogInterface -> emulationActivity.finish()); + emulationActivity.runOnUiThread(() -> { + AlertDialog alert = builder.create(); + alert.show(); + ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); + }); + } + + public static void setEmulationActivity(EmulationActivity emulationActivity) { + Log.verbose("[NativeLibrary] Registering EmulationActivity."); + sEmulationActivity = new WeakReference<>(emulationActivity); + } + + public static void clearEmulationActivity() { + Log.verbose("[NativeLibrary] Unregistering EmulationActivity."); + + sEmulationActivity.clear(); + } + + private static final Object cameraPermissionLock = new Object(); + private static boolean cameraPermissionGranted = false; + public static final int REQUEST_CODE_NATIVE_CAMERA = 800; + + public static boolean RequestCameraPermission() { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) { + // Permission already granted + return true; + } + emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA); + + // Wait until result is returned + synchronized (cameraPermissionLock) { + try { + cameraPermissionLock.wait(); + } catch (InterruptedException ignored) { + } + } + return cameraPermissionGranted; + } + + public static void CameraPermissionResult(boolean granted) { + cameraPermissionGranted = granted; + synchronized (cameraPermissionLock) { + cameraPermissionLock.notify(); + } + } + + private static final Object micPermissionLock = new Object(); + private static boolean micPermissionGranted = false; + public static final int REQUEST_CODE_NATIVE_MIC = 900; + + public static boolean RequestMicPermission() { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + // Permission already granted + return true; + } + emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC); + + // Wait until result is returned + synchronized (micPermissionLock) { + try { + micPermissionLock.wait(); + } catch (InterruptedException ignored) { + } + } + return micPermissionGranted; + } + + public static void MicPermissionResult(boolean granted) { + micPermissionGranted = granted; + synchronized (micPermissionLock) { + micPermissionLock.notify(); + } + } + + /** + * Logs the Citra version, Android version and, CPU. + */ + public static native void LogDeviceInfo(); + + /** + * Button type for use in onTouchEvent + */ + public static final class ButtonType { + public static final int BUTTON_A = 0; + public static final int BUTTON_B = 1; + public static final int BUTTON_X = 2; + public static final int BUTTON_Y = 3; + public static final int BUTTON_START = 11; + public static final int BUTTON_SELECT = 12; + public static final int BUTTON_HOME = 19; + public static final int BUTTON_ZL = 9; + public static final int BUTTON_ZR = 10; + public static final int DPAD_UP = 14; + public static final int DPAD_DOWN = 16; + public static final int DPAD_LEFT = 13; + public static final int DPAD_RIGHT = 15; + public static final int STICK_LEFT = 5; + public static final int STICK_LEFT_UP = 714; + public static final int STICK_LEFT_DOWN = 715; + public static final int STICK_LEFT_LEFT = 716; + public static final int STICK_LEFT_RIGHT = 717; + public static final int STICK_C = 6; + public static final int STICK_C_UP = 719; + public static final int STICK_C_DOWN = 720; + public static final int STICK_C_LEFT = 771; + public static final int STICK_C_RIGHT = 772; + public static final int TRIGGER_L = 7; + public static final int TRIGGER_R = 8; + public static final int DPAD = 780; + public static final int BUTTON_DEBUG = 781; + public static final int BUTTON_GPIO14 = 782; + } + + /** + * Button states + */ + public static final class ButtonState { + public static final int RELEASED = 0; + public static final int PRESSED = 1; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java new file mode 100644 index 000000000..700916f87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java @@ -0,0 +1,56 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.yuzu.yuzu_emu; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; + +import org.yuzu.yuzu_emu.model.GameDatabase; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.PermissionsHandler; + +public class YuzuApplication extends Application { + public static GameDatabase databaseHelper; + private static YuzuApplication application; + + private void createNotificationChannel() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.app_notification_channel_name); + String description = getString(R.string.app_notification_channel_description); + NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW); + channel.setDescription(description); + channel.setSound(null, null); + channel.setVibrationPattern(null); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + @Override + public void onCreate() { + super.onCreate(); + application = this; + + if (PermissionsHandler.hasWriteAccess(getApplicationContext())) { + DirectoryInitialization.start(getApplicationContext()); + } + + NativeLibrary.LogDeviceInfo(); + createNotificationChannel(); + + databaseHelper = new GameDatabase(this); + } + + public static Context getAppContext() { + return application.getApplicationContext(); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java new file mode 100644 index 000000000..a79780814 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java @@ -0,0 +1,38 @@ +package org.yuzu.yuzu_emu.activities; + +import android.content.Intent; +import android.os.Environment; + +import androidx.annotation.Nullable; + +import com.nononsenseapps.filepicker.AbstractFilePickerFragment; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.yuzu.yuzu_emu.fragments.CustomFilePickerFragment; + +import java.io.File; + +public class CustomFilePickerActivity extends FilePickerActivity { + public static final String EXTRA_TITLE = "filepicker.intent.TITLE"; + public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS"; + + @Override + protected AbstractFilePickerFragment getFragment( + @Nullable final String startPath, final int mode, final boolean allowMultiple, + final boolean allowCreateDir, final boolean allowExistingFile, + final boolean singleClick) { + CustomFilePickerFragment fragment = new CustomFilePickerFragment(); + // startPath is allowed to be null. In that case, default folder should be SD-card and not "/" + fragment.setArgs( + startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), + mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); + + Intent intent = getIntent(); + int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0); + fragment.setTitle(title); + String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS); + fragment.setAllowedExtensions(allowedExtensions); + + return fragment; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java new file mode 100644 index 000000000..cd64a3298 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java @@ -0,0 +1,537 @@ +package org.yuzu.yuzu_emu.activities; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.SparseIntArray; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NotificationManagerCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.InputBindingSetting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; +import org.yuzu.yuzu_emu.fragments.EmulationFragment; +import org.yuzu.yuzu_emu.fragments.MenuFragment; +import org.yuzu.yuzu_emu.utils.ControllerMappingHelper; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; +import org.yuzu.yuzu_emu.utils.ForegroundService; + +import java.lang.annotation.Retention; +import java.util.List; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public final class EmulationActivity extends AppCompatActivity { + private static final String BACKSTACK_NAME_MENU = "menu"; + + private static final String BACKSTACK_NAME_SUBMENU = "submenu"; + + public static final String EXTRA_SELECTED_GAME = "SelectedGame"; + public static final String EXTRA_SELECTED_TITLE = "SelectedTitle"; + public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0; + public static final int MENU_ACTION_TOGGLE_CONTROLS = 1; + public static final int MENU_ACTION_ADJUST_SCALE = 2; + public static final int MENU_ACTION_EXIT = 3; + public static final int MENU_ACTION_SHOW_FPS = 4; + public static final int MENU_ACTION_RESET_OVERLAY = 6; + public static final int MENU_ACTION_SHOW_OVERLAY = 7; + public static final int MENU_ACTION_OPEN_SETTINGS = 8; + private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; + private View mDecorView; + private EmulationFragment mEmulationFragment; + private SharedPreferences mPreferences; + private ControllerMappingHelper mControllerMappingHelper; + private Intent foregroundService; + private boolean activityRecreated; + private String mSelectedTitle; + private String mPath; + + private boolean mMenuVisible; + + public static void launch(FragmentActivity activity, String path, String title) { + Intent launcher = new Intent(activity, EmulationActivity.class); + + launcher.putExtra(EXTRA_SELECTED_GAME, path); + launcher.putExtra(EXTRA_SELECTED_TITLE, title); + activity.startActivity(launcher); + } + + public static void tryDismissRunningNotification(Activity activity) { + NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION); + } + + @Override + protected void onDestroy() { + stopService(foregroundService); + super.onDestroy(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + // Get params we were passed + Intent gameToEmulate = getIntent(); + mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME); + mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE); + activityRecreated = false; + } else { + activityRecreated = true; + restoreState(savedInstanceState); + } + + mControllerMappingHelper = new ControllerMappingHelper(); + + // Get a handle to the Window containing the UI. + mDecorView = getWindow().getDecorView(); + mDecorView.setOnSystemUiVisibilityChangeListener(visibility -> + { + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + // Go back to immersive fullscreen mode in 3s + Handler handler = new Handler(getMainLooper()); + handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */); + } + }); + // Set these options now so that the SurfaceView the game renders into is the right size. + enableFullscreenImmersive(); + + setTheme(R.style.YuzuEmulationBase); + + setContentView(R.layout.activity_emulation); + + // Find or create the EmulationFragment + mEmulationFragment = (EmulationFragment) getSupportFragmentManager() + .findFragmentById(R.id.frame_emulation_fragment); + if (mEmulationFragment == null) { + mEmulationFragment = EmulationFragment.newInstance(mPath); + getSupportFragmentManager().beginTransaction() + .add(R.id.frame_emulation_fragment, mEmulationFragment) + .commit(); + } + + setTitle(mSelectedTitle); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Start a foreground service to prevent the app from getting killed in the background + foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); + startForegroundService(foregroundService); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putString(EXTRA_SELECTED_GAME, mPath); + outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle); + super.onSaveInstanceState(outState); + } + + protected void restoreState(Bundle savedInstanceState) { + mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME); + mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE); + + // If an alert prompt was in progress when state was restored, retry displaying it + NativeLibrary.retryDisplayAlertPrompt(); + } + + @Override + public void onRestart() { + super.onRestart(); + } + + @Override + public void onBackPressed() { + toggleMenu(); + } + + private void enableFullscreenImmersive() { + // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. + mDecorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE); + } + + public void handleMenuAction(int action) { + switch (action) { + case MENU_ACTION_EXIT: + mEmulationFragment.stopEmulation(); + finish(); + break; + } + } + + private void editControlsPlacement() { + if (mEmulationFragment.isConfiguringControls()) { + mEmulationFragment.stopConfiguringControls(); + } else { + mEmulationFragment.startConfiguringControls(); + } + } + + // Gets button presses + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mMenuVisible || event.getKeyCode() == KeyEvent.KEYCODE_BACK) + { + return super.dispatchKeyEvent(event); + } + + int action; + int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); + + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + // Handling the case where the back button is pressed. + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + onBackPressed(); + return true; + } + + // Normal key events. + action = NativeLibrary.ButtonState.PRESSED; + break; + case KeyEvent.ACTION_UP: + action = NativeLibrary.ButtonState.RELEASED; + break; + default: + return false; + } + InputDevice input = event.getDevice(); + + if (input == null) { + // Controller was disconnected + return false; + } + + return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action); + } + + private void toggleControls() { + final SharedPreferences.Editor editor = mPreferences.edit(); + boolean[] enabledButtons = new boolean[14]; + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.emulation_toggle_controls); + + for (int i = 0; i < enabledButtons.length; i++) { + // Buttons that are disabled by default + boolean defaultValue = true; + switch (i) { + case 6: // ZL + case 7: // ZR + case 12: // C-stick + defaultValue = false; + break; + } + + enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue); + } + builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons, + (dialog, indexSelected, isChecked) -> editor + .putBoolean("buttonToggle" + indexSelected, isChecked)); + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + editor.apply(); + + mEmulationFragment.refreshInputOverlay(); + }); + + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + } + + private void adjustScale() { + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.dialog_seekbar, null); + + final SeekBar seekbar = view.findViewById(R.id.seekbar); + final TextView value = view.findViewById(R.id.text_value); + final TextView units = view.findViewById(R.id.text_units); + + seekbar.setMax(150); + seekbar.setProgress(mPreferences.getInt("controlScale", 50)); + seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onStartTrackingTouch(SeekBar seekBar) { + } + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + value.setText(String.valueOf(progress + 50)); + } + + public void onStopTrackingTouch(SeekBar seekBar) { + setControlScale(seekbar.getProgress()); + } + }); + + value.setText(String.valueOf(seekbar.getProgress() + 50)); + units.setText("%"); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.emulation_control_scale); + builder.setView(view); + final int previousProgress = seekbar.getProgress(); + builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { + setControlScale(previousProgress); + }); + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + setControlScale(seekbar.getProgress()); + }); + builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> { + setControlScale(50); + }); + + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + } + + private void setControlScale(int scale) { + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("controlScale", scale); + editor.apply(); + mEmulationFragment.refreshInputOverlay(); + } + + private void resetOverlay() { + new AlertDialog.Builder(this) + .setTitle(getString(R.string.emulation_touch_overlay_reset)) + .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay()) + .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { + }) + .create() + .show(); + } + + private static boolean areCoordinatesOutside(@Nullable View view, float x, float y) + { + if (view == null) + { + return true; + } + + Rect viewBounds = new Rect(); + view.getGlobalVisibleRect(viewBounds); + return !viewBounds.contains(Math.round(x), Math.round(y)); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) + { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) + { + boolean anyMenuClosed = false; + + Fragment submenu = getSupportFragmentManager().findFragmentById(R.id.frame_submenu); + if (submenu != null && areCoordinatesOutside(submenu.getView(), event.getX(), event.getY())) + { + closeSubmenu(); + submenu = null; + anyMenuClosed = true; + } + + if (submenu == null) + { + Fragment menu = getSupportFragmentManager().findFragmentById(R.id.frame_menu); + if (menu != null && areCoordinatesOutside(menu.getView(), event.getX(), event.getY())) + { + closeMenu(); + anyMenuClosed = true; + } + } + + if (anyMenuClosed) + { + return true; + } + } + + return super.dispatchTouchEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mMenuVisible) + { + return false; + } + + if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) { + return super.dispatchGenericMotionEvent(event); + } + + // Don't attempt to do anything if we are disconnecting a device. + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + return true; + } + + InputDevice input = event.getDevice(); + List motions = input.getMotionRanges(); + + float[] axisValuesCirclePad = {0.0f, 0.0f}; + float[] axisValuesCStick = {0.0f, 0.0f}; + float[] axisValuesDPad = {0.0f, 0.0f}; + boolean isTriggerPressedLMapped = false; + boolean isTriggerPressedRMapped = false; + boolean isTriggerPressedZLMapped = false; + boolean isTriggerPressedZRMapped = false; + boolean isTriggerPressedL = false; + boolean isTriggerPressedR = false; + boolean isTriggerPressedZL = false; + boolean isTriggerPressedZR = false; + + for (InputDevice.MotionRange range : motions) { + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); + int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); + int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); + + if (nextMapping == -1 || guestOrientation == -1) { + // Axis is unmapped + continue; + } + + if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) { + // Skip joystick wobble + value = 0.f; + } + + if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) { + axisValuesCirclePad[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) { + axisValuesCStick[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.DPAD) { + axisValuesDPad[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) { + isTriggerPressedLMapped = true; + isTriggerPressedL = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) { + isTriggerPressedRMapped = true; + isTriggerPressedR = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) { + isTriggerPressedZLMapped = true; + isTriggerPressedZL = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) { + isTriggerPressedZRMapped = true; + isTriggerPressedZR = value != 0.f; + } + } + + // Circle-Pad and C-Stick status + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]); + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]); + + // Triggers L/R and ZL/ZR + if (isTriggerPressedLMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedRMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedZLMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedZRMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + + // Work-around to allow D-pad axis to be bound to emulated buttons + if (axisValuesDPad[0] == 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[0] < 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[0] > 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED); + } + if (axisValuesDPad[1] == 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[1] < 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[1] > 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED); + } + + return true; + } + + public boolean isActivityRecreated() { + return activityRecreated; + } + + @Retention(SOURCE) + @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE, + MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS}) + public @interface MenuAction { + } + + private boolean closeSubmenu() + { + return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_SUBMENU, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + + private boolean closeMenu() + { + mMenuVisible = false; + return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_MENU, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + + private void toggleMenu() + { + if (!closeMenu()) { + // Removing the menu failed, so that means it wasn't visible. Add it. + Fragment fragment = MenuFragment.newInstance(); + getSupportFragmentManager().beginTransaction() + .setCustomAnimations( + R.animator.menu_slide_in_from_start, + R.animator.menu_slide_out_to_start, + R.animator.menu_slide_in_from_start, + R.animator.menu_slide_out_to_start) + .add(R.id.frame_menu, fragment) + .addToBackStack(BACKSTACK_NAME_MENU) + .commit(); + mMenuVisible = true; + } + } + +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java new file mode 100644 index 000000000..fa785741b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java @@ -0,0 +1,247 @@ +package org.yuzu.yuzu_emu.adapters; + +import android.database.Cursor; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.model.GameDatabase; +import org.yuzu.yuzu_emu.ui.DividerItemDecoration; +import org.yuzu.yuzu_emu.utils.Log; +import org.yuzu.yuzu_emu.utils.PicassoUtils; +import org.yuzu.yuzu_emu.viewholders.GameViewHolder; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +/** + * This adapter gets its information from a database Cursor. This fact, paired with the usage of + * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) + * large dataset. + */ +public final class GameAdapter extends RecyclerView.Adapter implements + View.OnClickListener { + private Cursor mCursor; + private GameDataSetObserver mObserver; + + private boolean mDatasetValid; + private long mLastClickTime = 0; + + /** + * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will + * display no data until a Cursor is supplied by a CursorLoader. + */ + public GameAdapter() { + mDatasetValid = false; + mObserver = new GameDataSetObserver(); + } + + /** + * Called by the LayoutManager when it is necessary to create a new view. + * + * @param parent The RecyclerView (I think?) the created view will be thrown into. + * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. + * @return The created ViewHolder with references to all the child view's members. + */ + @Override + public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + // Create a new view. + View gameCard = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.card_game, parent, false); + + gameCard.setOnClickListener(this); + + // Use that view to create a ViewHolder. + return new GameViewHolder(gameCard); + } + + /** + * Called by the LayoutManager when a new view is not necessary because we can recycle + * an existing one (for example, if a view just scrolled onto the screen from the bottom, we + * can use the view that just scrolled off the top instead of inflating a new one.) + * + * @param holder A ViewHolder representing the view we're recycling. + * @param position The position of the 'new' view in the dataset. + */ + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public void onBindViewHolder(@NonNull GameViewHolder holder, int position) { + if (mDatasetValid) { + if (mCursor.moveToPosition(position)) { + PicassoUtils.loadGameIcon(holder.imageIcon, + mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); + + holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); + holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); + + final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); + holder.textFileName.setText(gamePath.getFileName().toString()); + + // TODO These shouldn't be necessary once the move to a DB-based model is complete. + holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); + holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH); + holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); + holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); + holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); + holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); + + final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled; + View itemView = holder.getItemView(); + itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId)); + } else { + Log.error("[GameAdapter] Can't bind view; Cursor is not valid."); + } + } else { + Log.error("[GameAdapter] Can't bind view; dataset is not valid."); + } + } + + /** + * Called by the LayoutManager to find out how much data we have. + * + * @return Size of the dataset. + */ + @Override + public int getItemCount() { + if (mDatasetValid && mCursor != null) { + return mCursor.getCount(); + } + Log.error("[GameAdapter] Dataset is not valid."); + return 0; + } + + /** + * Return the contents of the _id column for a given row. + * + * @param position The row for which Android wants an ID. + * @return A valid ID from the database, or 0 if not available. + */ + @Override + public long getItemId(int position) { + if (mDatasetValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(GameDatabase.COLUMN_DB_ID); + } + } + + Log.error("[GameAdapter] Dataset is not valid."); + return 0; + } + + /** + * Tell Android whether or not each item in the dataset has a stable identifier. + * Which it does, because it's a database, so always tell Android 'true'. + * + * @param hasStableIds ignored. + */ + @Override + public void setHasStableIds(boolean hasStableIds) { + super.setHasStableIds(true); + } + + /** + * When a load is finished, call this to replace the existing data with the newly-loaded + * data. + * + * @param cursor The newly-loaded Cursor. + */ + public void swapCursor(Cursor cursor) { + // Sanity check. + if (cursor == mCursor) { + return; + } + + // Before getting rid of the old cursor, disassociate it from the Observer. + final Cursor oldCursor = mCursor; + if (oldCursor != null && mObserver != null) { + oldCursor.unregisterDataSetObserver(mObserver); + } + + mCursor = cursor; + if (mCursor != null) { + // Attempt to associate the new Cursor with the Observer. + if (mObserver != null) { + mCursor.registerDataSetObserver(mObserver); + } + + mDatasetValid = true; + } else { + mDatasetValid = false; + } + + notifyDataSetChanged(); + } + + /** + * Launches the game that was clicked on. + * + * @param view The card representing the game the user wants to play. + */ + @Override + public void onClick(View view) { + // Double-click prevention, using threshold of 1000 ms + if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) { + return; + } + mLastClickTime = SystemClock.elapsedRealtime(); + + GameViewHolder holder = (GameViewHolder) view.getTag(); + + EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title); + } + + public static class SpacesItemDecoration extends DividerItemDecoration { + private int space; + + public SpacesItemDecoration(Drawable divider, int space) { + super(divider); + this.space = space; + } + + @Override + public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent, + @NonNull RecyclerView.State state) { + outRect.left = 0; + outRect.right = 0; + outRect.bottom = space; + outRect.top = 0; + } + } + + private boolean isValidGame(String path) { + return Stream.of( + ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix)); + } + + private final class GameDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + super.onChanged(); + + mDatasetValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + + mDatasetValid = false; + notifyDataSetChanged(); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java new file mode 100644 index 000000000..4aeb41472 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java @@ -0,0 +1,264 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.yuzu.yuzu_emu.applets; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.InputFilter; +import android.text.Spanned; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.utils.Log; + +import java.util.Objects; + +public final class SoftwareKeyboard { + /// Corresponds to Frontend::ButtonConfig + private interface ButtonConfig { + int Single = 0; /// Ok button + int Dual = 1; /// Cancel | Ok buttons + int Triple = 2; /// Cancel | I Forgot | Ok buttons + int None = 3; /// No button (returned by swkbdInputText in special cases) + } + + /// Corresponds to Frontend::ValidationError + public enum ValidationError { + None, + // Button Selection + ButtonOutOfRange, + // Configured Filters + MaxDigitsExceeded, + AtSignNotAllowed, + PercentNotAllowed, + BackslashNotAllowed, + ProfanityNotAllowed, + CallbackFailed, + // Allowed Input Type + FixedLengthRequired, + MaxLengthExceeded, + BlankInputNotAllowed, + EmptyInputNotAllowed, + } + + public static class KeyboardConfig implements java.io.Serializable { + public int button_config; + public int max_text_length; + public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input + public String hint_text; /// Displayed in the field as a hint before + @Nullable + public String[] button_text; /// Contains the button text that the caller provides + } + + /// Corresponds to Frontend::KeyboardData + public static class KeyboardData { + public int button; + public String text; + + private KeyboardData(int button, String text) { + this.button = button; + this.text = text; + } + } + + private static class Filter implements InputFilter { + @Override + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, + int dstart, int dend) { + String text = new StringBuilder(dest) + .replace(dstart, dend, source.subSequence(start, end).toString()) + .toString(); + if (ValidateFilters(text) == ValidationError.None) { + return null; // Accept replacement + } + return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged + } + } + + public static class KeyboardDialogFragment extends DialogFragment { + static KeyboardDialogFragment newInstance(KeyboardConfig config) { + KeyboardDialogFragment frag = new KeyboardDialogFragment(); + Bundle args = new Bundle(); + args.putSerializable("config", config); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = getActivity(); + assert emulationActivity != null; + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = params.rightMargin = + YuzuApplication.getAppContext().getResources().getDimensionPixelSize( + R.dimen.dialog_margin); + + KeyboardConfig config = Objects.requireNonNull( + (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); + + // Set up the input + EditText editText = new EditText(YuzuApplication.getAppContext()); + editText.setHint(config.hint_text); + editText.setSingleLine(!config.multiline_mode); + editText.setLayoutParams(params); + editText.setFilters(new InputFilter[]{ + new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); + + FrameLayout container = new FrameLayout(emulationActivity); + container.addView(editText); + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(R.string.software_keyboard) + .setView(container); + setCancelable(false); + + switch (config.button_config) { + case ButtonConfig.Triple: { + final String text = config.button_text[1].isEmpty() + ? emulationActivity.getString(R.string.i_forgot) + : config.button_text[1]; + builder.setNeutralButton(text, null); + } + // fallthrough + case ButtonConfig.Dual: { + final String text = config.button_text[0].isEmpty() + ? emulationActivity.getString(android.R.string.cancel) + : config.button_text[0]; + builder.setNegativeButton(text, null); + } + // fallthrough + case ButtonConfig.Single: { + final String text = config.button_text[2].isEmpty() + ? emulationActivity.getString(android.R.string.ok) + : config.button_text[2]; + builder.setPositiveButton(text, null); + break; + } + } + + final AlertDialog dialog = builder.create(); + dialog.create(); + if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { + data.button = config.button_config; + data.text = editText.getText().toString(); + final ValidationError error = ValidateInput(data.text); + if (error != ValidationError.None) { + HandleValidationError(config, error); + return; + } + + dialog.dismiss(); + + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + } + if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { + data.button = 1; + dialog.dismiss(); + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + } + if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { + data.button = 0; + dialog.dismiss(); + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + } + + return dialog; + } + } + + private static KeyboardData data; + private static final Object finishLock = new Object(); + + private static void ExecuteImpl(KeyboardConfig config) { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + + data = new KeyboardData(0, ""); + + KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); + fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); + } + + private static void HandleValidationError(KeyboardConfig config, ValidationError error) { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + String message = ""; + switch (error) { + case FixedLengthRequired: + message = + emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); + break; + case MaxLengthExceeded: + message = + emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); + break; + case BlankInputNotAllowed: + message = emulationActivity.getString(R.string.blank_input_not_allowed); + break; + case EmptyInputNotAllowed: + message = emulationActivity.getString(R.string.empty_input_not_allowed); + break; + } + + new AlertDialog.Builder(emulationActivity) + .setTitle(R.string.software_keyboard) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + public static KeyboardData Execute(KeyboardConfig config) { + if (config.button_config == ButtonConfig.None) { + Log.error("Unexpected button config None"); + return new KeyboardData(0, ""); + } + + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); + + synchronized (finishLock) { + try { + finishLock.wait(); + } catch (Exception ignored) { + } + } + + return data; + } + + public static void ShowError(String error) { + NativeLibrary.displayAlertMsg( + YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), + error, false); + } + + private static native ValidationError ValidateFilters(String text); + + private static native ValidationError ValidateInput(String text); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/MotionAlertDialog.java new file mode 100644 index 000000000..874c1acbc --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/MotionAlertDialog.java @@ -0,0 +1,140 @@ +package org.yuzu.yuzu_emu.dialogs; + +import android.content.Context; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import org.yuzu.yuzu_emu.features.settings.model.view.InputBindingSetting; +import org.yuzu.yuzu_emu.utils.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link AlertDialog} derivative that listens for + * motion events from controllers and joysticks. + */ +public final class MotionAlertDialog extends AlertDialog { + // The selected input preference + private final InputBindingSetting setting; + private final ArrayList mPreviousValues = new ArrayList<>(); + private int mPrevDeviceId = 0; + private boolean mWaitingForEvent = true; + + /** + * Constructor + * + * @param context The current {@link Context}. + * @param setting The Preference to show this dialog for. + */ + public MotionAlertDialog(Context context, InputBindingSetting setting) { + super(context); + + this.setting = setting; + } + + public boolean onKeyEvent(int keyCode, KeyEvent event) { + Log.debug("[MotionAlertDialog] Received key event: " + event.getAction()); + switch (event.getAction()) { + case KeyEvent.ACTION_UP: + setting.onKeyInput(event); + dismiss(); + // Even if we ignore the key, we still consume it. Thus return true regardless. + return true; + + default: + return false; + } + } + + @Override + public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { + return super.onKeyLongPress(keyCode, event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Handle this key if we care about it, otherwise pass it down the framework + return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) { + // Handle this event if we care about it, otherwise pass it down the framework + return onMotionEvent(event) || super.dispatchGenericMotionEvent(event); + } + + private boolean onMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) + return false; + if (event.getAction() != MotionEvent.ACTION_MOVE) + return false; + + InputDevice input = event.getDevice(); + + List motionRanges = input.getMotionRanges(); + + if (input.getId() != mPrevDeviceId) { + mPreviousValues.clear(); + } + mPrevDeviceId = input.getId(); + boolean firstEvent = mPreviousValues.isEmpty(); + + int numMovedAxis = 0; + float axisMoveValue = 0.0f; + InputDevice.MotionRange lastMovedRange = null; + char lastMovedDir = '?'; + if (mWaitingForEvent) { + for (int i = 0; i < motionRanges.size(); i++) { + InputDevice.MotionRange range = motionRanges.get(i); + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue); + if (firstEvent) { + mPreviousValues.add(value); + } else { + float previousValue = mPreviousValues.get(i); + + // Only handle the axes that are not neutral (more than 0.5) + // but ignore any axis that has a constant value (e.g. always 1) + if (Math.abs(value) > 0.5f && value != previousValue) { + // It is common to have multiple axes with the same physical input. For example, + // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. + // To handle this, we ignore an axis motion that's the exact same as a motion + // we already saw. This way, we ignore axes with two names, but catch the case + // where a joystick is moved in two directions. + // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html + if (value != axisMoveValue) { + axisMoveValue = value; + numMovedAxis++; + lastMovedRange = range; + lastMovedDir = value < 0.0f ? '-' : '+'; + } + } + // Special case for d-pads (axis value jumps between 0 and 1 without any values + // in between). Without this, the user would need to press the d-pad twice + // due to the first press being caught by the "if (firstEvent)" case further up. + else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) { + numMovedAxis++; + lastMovedRange = range; + lastMovedDir = previousValue < 0.0f ? '-' : '+'; + } + } + + mPreviousValues.set(i, value); + } + + // If only one axis moved, that's the winner. + if (numMovedAxis == 1) { + mWaitingForEvent = false; + setting.onMotionInput(input, lastMovedRange, lastMovedDir); + dismiss(); + } + } + return true; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.java new file mode 100644 index 000000000..965f1b2a2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.java @@ -0,0 +1,23 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +public final class BooleanSetting extends Setting { + private boolean mValue; + + public BooleanSetting(String key, String section, boolean value) { + super(key, section); + mValue = value; + } + + public boolean getValue() { + return mValue; + } + + public void setValue(boolean value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return mValue ? "True" : "False"; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.java new file mode 100644 index 000000000..9b9b60092 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.java @@ -0,0 +1,23 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +public final class FloatSetting extends Setting { + private float mValue; + + public FloatSetting(String key, String section, float value) { + super(key, section); + mValue = value; + } + + public float getValue() { + return mValue; + } + + public void setValue(float value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return Float.toString(mValue); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.java new file mode 100644 index 000000000..001f27579 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.java @@ -0,0 +1,23 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +public final class IntSetting extends Setting { + private int mValue; + + public IntSetting(String key, String section, int value) { + super(key, section); + mValue = value; + } + + public int getValue() { + return mValue; + } + + public void setValue(int value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return Integer.toString(mValue); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Setting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Setting.java new file mode 100644 index 000000000..28003078a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Setting.java @@ -0,0 +1,42 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +/** + * Abstraction for a setting item as read from / written to yuzu's configuration ini files. + * These files generally consist of a key/value pair, though the type of value is ambiguous and + * must be inferred at read-time. The type of value determines which child of this class is used + * to represent the Setting. + */ +public abstract class Setting { + private String mKey; + private String mSection; + + /** + * Base constructor. + * + * @param key Everything to the left of the = in a line from the ini file. + * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets. + */ + public Setting(String key, String section) { + mKey = key; + mSection = section; + } + + /** + * @return The identifier used to write this setting to the ini file. + */ + public String getKey() { + return mKey; + } + + /** + * @return The name of the header under which this Setting should be written in the ini file. + */ + public String getSection() { + return mSection; + } + + /** + * @return A representation of this Setting's backing value converted to a String (e.g. for serialization). + */ + public abstract String getValueAsString(); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.java new file mode 100644 index 000000000..da9d40c78 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.java @@ -0,0 +1,55 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +import java.util.HashMap; + +/** + * A semantically-related group of Settings objects. These Settings are + * internally stored as a HashMap. + */ +public final class SettingSection { + private String mName; + + private HashMap mSettings = new HashMap<>(); + + /** + * Create a new SettingSection with no Settings in it. + * + * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets. + */ + public SettingSection(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + /** + * Convenience method; inserts a value directly into the backing HashMap. + * + * @param setting The Setting to be inserted. + */ + public void putSetting(Setting setting) { + mSettings.put(setting.getKey(), setting); + } + + /** + * Convenience method; gets a value directly from the backing HashMap. + * + * @param key Used to retrieve the Setting. + * @return A Setting object (you should probably cast this before using) + */ + public Setting getSetting(String key) { + return mSettings.get(key); + } + + public HashMap getSettings() { + return mSettings; + } + + public void mergeSection(SettingSection settingSection) { + for (Setting setting : settingSection.mSettings.values()) { + putSetting(setting); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.java new file mode 100644 index 000000000..efde45ee9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.java @@ -0,0 +1,131 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +import android.text.TextUtils; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class Settings { + public static final String SECTION_PREMIUM = "Premium"; + public static final String SECTION_CORE = "Core"; + public static final String SECTION_SYSTEM = "System"; + public static final String SECTION_CONTROLS = "Controls"; + public static final String SECTION_RENDERER = "Renderer"; + public static final String SECTION_LAYOUT = "Layout"; + public static final String SECTION_UTILITY = "Utility"; + public static final String SECTION_AUDIO = "Audio"; + public static final String SECTION_DEBUG = "Debug"; + + private String gameId; + + private static final Map> configFileSectionsMap = new HashMap<>(); + + static { + configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG)); + } + + /** + * A HashMap that constructs a new SettingSection instead of returning null + * when getting a key not already in the map + */ + public static final class SettingsSectionMap extends HashMap { + @Override + public SettingSection get(Object key) { + if (!(key instanceof String)) { + return null; + } + + String stringKey = (String) key; + + if (!super.containsKey(stringKey)) { + SettingSection section = new SettingSection(stringKey); + super.put(stringKey, section); + return section; + } + return super.get(key); + } + } + + private HashMap sections = new Settings.SettingsSectionMap(); + + public SettingSection getSection(String sectionName) { + return sections.get(sectionName); + } + + public boolean isEmpty() { + return sections.isEmpty(); + } + + public HashMap getSections() { + return sections; + } + + public void loadSettings(SettingsActivityView view) { + sections = new Settings.SettingsSectionMap(); + loadCitraSettings(view); + + if (!TextUtils.isEmpty(gameId)) { + loadCustomGameSettings(gameId, view); + } + } + + private void loadCitraSettings(SettingsActivityView view) { + for (Map.Entry> entry : configFileSectionsMap.entrySet()) { + String fileName = entry.getKey(); + sections.putAll(SettingsFile.readFile(fileName, view)); + } + } + + private void loadCustomGameSettings(String gameId, SettingsActivityView view) { + // custom game settings + mergeSections(SettingsFile.readCustomGameSettings(gameId, view)); + } + + private void mergeSections(HashMap updatedSections) { + for (Map.Entry entry : updatedSections.entrySet()) { + if (sections.containsKey(entry.getKey())) { + SettingSection originalSection = sections.get(entry.getKey()); + SettingSection updatedSection = entry.getValue(); + originalSection.mergeSection(updatedSection); + } else { + sections.put(entry.getKey(), entry.getValue()); + } + } + } + + public void loadSettings(String gameId, SettingsActivityView view) { + this.gameId = gameId; + loadSettings(view); + } + + public void saveSettings(SettingsActivityView view) { + if (TextUtils.isEmpty(gameId)) { + view.showToastMessage(YuzuApplication.getAppContext().getString(R.string.ini_saved), false); + + for (Map.Entry> entry : configFileSectionsMap.entrySet()) { + String fileName = entry.getKey(); + List sectionNames = entry.getValue(); + TreeMap iniSections = new TreeMap<>(); + for (String section : sectionNames) { + iniSections.put(section, sections.get(section)); + } + + SettingsFile.saveFile(fileName, iniSections, view); + } + } else { + // custom game settings + view.showToastMessage(YuzuApplication.getAppContext().getString(R.string.gameid_saved, gameId), false); + + SettingsFile.saveCustomGameSettings(gameId, sections); + } + + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.java new file mode 100644 index 000000000..0b5128382 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.java @@ -0,0 +1,23 @@ +package org.yuzu.yuzu_emu.features.settings.model; + +public final class StringSetting extends Setting { + private String mValue; + + public StringSetting(String key, String section, String value) { + super(key, section); + mValue = value; + } + + public String getValue() { + return mValue; + } + + public void setValue(String value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return mValue; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/CheckBoxSetting.java new file mode 100644 index 000000000..c5c4e14f3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/CheckBoxSetting.java @@ -0,0 +1,80 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting; +import org.yuzu.yuzu_emu.features.settings.model.IntSetting; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsFragmentView; + +public final class CheckBoxSetting extends SettingsItem { + private boolean mDefaultValue; + private boolean mShowPerformanceWarning; + private SettingsFragmentView mView; + + public CheckBoxSetting(String key, String section, int titleId, int descriptionId, + boolean defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + mShowPerformanceWarning = false; + } + + public CheckBoxSetting(String key, String section, int titleId, int descriptionId, + boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + mView = view; + mShowPerformanceWarning = show_performance_warning; + } + + public boolean isChecked() { + if (getSetting() == null) { + return mDefaultValue; + } + + // Try integer setting + try { + IntSetting setting = (IntSetting) getSetting(); + return setting.getValue() == 1; + } catch (ClassCastException exception) { + } + + // Try boolean setting + try { + BooleanSetting setting = (BooleanSetting) getSetting(); + return setting.getValue() == true; + } catch (ClassCastException exception) { + } + + return mDefaultValue; + } + + /** + * Write a value to the backing boolean. If that boolean was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param checked Pretty self explanatory. + * @return null if overwritten successfully; otherwise, a newly created BooleanSetting. + */ + public IntSetting setChecked(boolean checked) { + // Show a performance warning if the setting has been disabled + if (mShowPerformanceWarning && !checked) { + mView.showToastMessage(YuzuApplication.getAppContext().getString(R.string.performance_warning), true); + } + + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(checked ? 1 : 0); + return null; + } + } + + @Override + public int getType() { + return TYPE_CHECKBOX; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.java new file mode 100644 index 000000000..17b2f1188 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.java @@ -0,0 +1,40 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; + +public final class DateTimeSetting extends SettingsItem { + private String mDefaultValue; + + public DateTimeSetting(String key, String section, int titleId, int descriptionId, + String defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + } + + public String getValue() { + if (getSetting() != null) { + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + public StringSetting setSelectedValue(String datetime) { + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), datetime); + setSetting(setting); + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + setting.setValue(datetime); + return null; + } + } + + @Override + public int getType() { + return TYPE_DATETIME_SETTING; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.java new file mode 100644 index 000000000..c41ac3d54 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.java @@ -0,0 +1,14 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; + +public final class HeaderSetting extends SettingsItem { + public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) { + super(key, null, setting, titleId, descriptionId); + } + + @Override + public int getType() { + return SettingsItem.TYPE_HEADER; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputBindingSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputBindingSetting.java new file mode 100644 index 000000000..4ad54421e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputBindingSetting.java @@ -0,0 +1,382 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.widget.Toast; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; + +public final class InputBindingSetting extends SettingsItem { + private static final String INPUT_MAPPING_PREFIX = "InputMapping"; + + public InputBindingSetting(String key, String section, int titleId, Setting setting) { + super(key, section, setting, titleId, 0); + } + + public String getValue() { + if (getSetting() == null) { + return ""; + } + + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } + + /** + * Returns true if this key is for the 3DS Circle Pad + */ + private boolean IsCirclePad() { + switch (getKey()) { + case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad + */ + public boolean IsHorizontalOrientation() { + switch (getKey()) { + case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: + case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS C-Stick + */ + private boolean IsCStick() { + switch (getKey()) { + case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: + case SettingsFile.KEY_CSTICK_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS D-Pad + */ + private boolean IsDPad() { + switch (getKey()) { + case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_DPAD_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real + * triggers on the 3DS, but we support them as such on a physical gamepad. + */ + public boolean IsTrigger() { + switch (getKey()) { + case SettingsFile.KEY_BUTTON_L: + case SettingsFile.KEY_BUTTON_R: + case SettingsFile.KEY_BUTTON_ZL: + case SettingsFile.KEY_BUTTON_ZR: + return true; + } + return false; + } + + /** + * Returns true if a gamepad axis can be used to map this key. + */ + public boolean IsAxisMappingSupported() { + return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger(); + } + + /** + * Returns true if a gamepad button can be used to map this key. + */ + private boolean IsButtonMappingSupported() { + return !IsAxisMappingSupported() || IsTrigger(); + } + + /** + * Returns the yuzu button code for the settings key. + */ + private int getButtonCode() { + switch (getKey()) { + case SettingsFile.KEY_BUTTON_A: + return NativeLibrary.ButtonType.BUTTON_A; + case SettingsFile.KEY_BUTTON_B: + return NativeLibrary.ButtonType.BUTTON_B; + case SettingsFile.KEY_BUTTON_X: + return NativeLibrary.ButtonType.BUTTON_X; + case SettingsFile.KEY_BUTTON_Y: + return NativeLibrary.ButtonType.BUTTON_Y; + case SettingsFile.KEY_BUTTON_L: + return NativeLibrary.ButtonType.TRIGGER_L; + case SettingsFile.KEY_BUTTON_R: + return NativeLibrary.ButtonType.TRIGGER_R; + case SettingsFile.KEY_BUTTON_ZL: + return NativeLibrary.ButtonType.BUTTON_ZL; + case SettingsFile.KEY_BUTTON_ZR: + return NativeLibrary.ButtonType.BUTTON_ZR; + case SettingsFile.KEY_BUTTON_SELECT: + return NativeLibrary.ButtonType.BUTTON_SELECT; + case SettingsFile.KEY_BUTTON_START: + return NativeLibrary.ButtonType.BUTTON_START; + case SettingsFile.KEY_BUTTON_UP: + return NativeLibrary.ButtonType.DPAD_UP; + case SettingsFile.KEY_BUTTON_DOWN: + return NativeLibrary.ButtonType.DPAD_DOWN; + case SettingsFile.KEY_BUTTON_LEFT: + return NativeLibrary.ButtonType.DPAD_LEFT; + case SettingsFile.KEY_BUTTON_RIGHT: + return NativeLibrary.ButtonType.DPAD_RIGHT; + } + return -1; + } + + /** + * Returns the settings key for the specified yuzu button code. + */ + private static String getButtonKey(int buttonCode) { + switch (buttonCode) { + case NativeLibrary.ButtonType.BUTTON_A: + return SettingsFile.KEY_BUTTON_A; + case NativeLibrary.ButtonType.BUTTON_B: + return SettingsFile.KEY_BUTTON_B; + case NativeLibrary.ButtonType.BUTTON_X: + return SettingsFile.KEY_BUTTON_X; + case NativeLibrary.ButtonType.BUTTON_Y: + return SettingsFile.KEY_BUTTON_Y; + case NativeLibrary.ButtonType.TRIGGER_L: + return SettingsFile.KEY_BUTTON_L; + case NativeLibrary.ButtonType.TRIGGER_R: + return SettingsFile.KEY_BUTTON_R; + case NativeLibrary.ButtonType.BUTTON_ZL: + return SettingsFile.KEY_BUTTON_ZL; + case NativeLibrary.ButtonType.BUTTON_ZR: + return SettingsFile.KEY_BUTTON_ZR; + case NativeLibrary.ButtonType.BUTTON_SELECT: + return SettingsFile.KEY_BUTTON_SELECT; + case NativeLibrary.ButtonType.BUTTON_START: + return SettingsFile.KEY_BUTTON_START; + case NativeLibrary.ButtonType.DPAD_UP: + return SettingsFile.KEY_BUTTON_UP; + case NativeLibrary.ButtonType.DPAD_DOWN: + return SettingsFile.KEY_BUTTON_DOWN; + case NativeLibrary.ButtonType.DPAD_LEFT: + return SettingsFile.KEY_BUTTON_LEFT; + case NativeLibrary.ButtonType.DPAD_RIGHT: + return SettingsFile.KEY_BUTTON_RIGHT; + } + return ""; + } + + /** + * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old + * settings on re-mapping or clearing of a setting. + */ + private String getReverseKey() { + String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey(); + + if (IsAxisMappingSupported() && !IsTrigger()) { + // Triggers are the only axis-supported mappings without orientation + reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1); + } + + return reverseKey; + } + + /** + * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. + */ + public void removeOldMapping() { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Try remove all possible keys we wrote for this setting + String oldKey = preferences.getString(getReverseKey(), ""); + if (!oldKey.equals("")) { + editor.remove(getKey()); // Used for ui text + editor.remove(oldKey); // Used for button mapping + editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation + editor.remove(oldKey + "_GuestButton"); // Used for axis button + } + + // Apply changes + editor.apply(); + } + + /** + * Helper function to get the settings key for an gamepad button. + */ + public static String getInputButtonKey(int keyCode) { + return INPUT_MAPPING_PREFIX + "_Button_" + keyCode; + } + + /** + * Helper function to get the settings key for an gamepad axis. + */ + public static String getInputAxisKey(int axis) { + return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis; + } + + /** + * Helper function to get the settings key for an gamepad axis button (stick or trigger). + */ + public static String getInputAxisButtonKey(int axis) { + return getInputAxisKey(axis) + "_GuestButton"; + } + + /** + * Helper function to get the settings key for an gamepad axis orientation. + */ + public static String getInputAxisOrientationKey(int axis) { + return getInputAxisKey(axis) + "_GuestOrientation"; + } + + /** + * Helper function to write a gamepad button mapping for the setting. + */ + private void WriteButtonMapping(String key) { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Remove mapping for another setting using this input + int oldButtonCode = preferences.getInt(key, -1); + if (oldButtonCode != -1) { + String oldKey = getButtonKey(oldButtonCode); + editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten + } + + // Cleanup old mapping for this setting + removeOldMapping(); + + // Write new mapping + editor.putInt(key, getButtonCode()); + + // Write next reverse mapping for future cleanup + editor.putString(getReverseKey(), key); + + // Apply changes + editor.apply(); + } + + /** + * Helper function to write a gamepad axis mapping for the setting. + */ + private void WriteAxisMapping(int axis, int value) { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Cleanup old mapping + removeOldMapping(); + + // Write new mapping + editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1); + editor.putInt(getInputAxisButtonKey(axis), value); + + // Write next reverse mapping for future cleanup + editor.putString(getReverseKey(), getInputAxisKey(axis)); + + // Apply changes + editor.apply(); + } + + /** + * Saves the provided key input setting as an Android preference. + * + * @param keyEvent KeyEvent of this key press. + */ + public void onKeyInput(KeyEvent keyEvent) { + if (!IsButtonMappingSupported()) { + Toast.makeText(YuzuApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show(); + return; + } + + InputDevice device = keyEvent.getDevice(); + + WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode())); + + String uiString = device.getName() + ": Button " + keyEvent.getKeyCode(); + setUiString(uiString); + } + + /** + * Saves the provided motion input setting as an Android preference. + * + * @param device InputDevice from which the input event originated. + * @param motionRange MotionRange of the movement + * @param axisDir Either '-' or '+' (currently unused) + */ + public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, + char axisDir) { + if (!IsAxisMappingSupported()) { + Toast.makeText(YuzuApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show(); + return; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + int button; + if (IsCirclePad()) { + button = NativeLibrary.ButtonType.STICK_LEFT; + } else if (IsCStick()) { + button = NativeLibrary.ButtonType.STICK_C; + } else if (IsDPad()) { + button = NativeLibrary.ButtonType.DPAD; + } else { + button = getButtonCode(); + } + + WriteAxisMapping(motionRange.getAxis(), button); + + String uiString = device.getName() + ": Axis " + motionRange.getAxis(); + setUiString(uiString); + + editor.apply(); + } + + /** + * Sets the string to use in the configuration UI for the gamepad input. + */ + private StringSetting setUiString(String ui) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), ""); + setSetting(setting); + + editor.putString(setting.getKey(), ui); + editor.apply(); + + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + + editor.putString(setting.getKey(), ui); + editor.apply(); + + return null; + } + } + + @Override + public int getType() { + return TYPE_INPUT_BINDING; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumHeader.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumHeader.java new file mode 100644 index 000000000..9bf95ce51 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumHeader.java @@ -0,0 +1,12 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +public final class PremiumHeader extends SettingsItem { + public PremiumHeader() { + super(null, null, null, 0, 0); + } + + @Override + public int getType() { + return SettingsItem.TYPE_PREMIUM; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumSingleChoiceSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumSingleChoiceSetting.java new file mode 100644 index 000000000..0c4570c8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PremiumSingleChoiceSetting.java @@ -0,0 +1,59 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsFragmentView; + +public final class PremiumSingleChoiceSetting extends SettingsItem { + private int mDefaultValue; + + private int mChoicesId; + private int mValuesId; + private SettingsFragmentView mView; + + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + + public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId, + int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + mView = view; + } + + public int getChoicesId() { + return mChoicesId; + } + + public int getValuesId() { + return mValuesId; + } + + public int getSelectedValue() { + return mPreferences.getInt(getKey(), mDefaultValue); + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public void setSelectedValue(int selection) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt(getKey(), selection); + editor.apply(); + mView.showToastMessage(YuzuApplication.getAppContext().getString(R.string.design_updated), false); + } + + @Override + public int getType() { + return TYPE_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.java new file mode 100644 index 000000000..db7fb791a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.java @@ -0,0 +1,107 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +/** + * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. + * Each one corresponds to a {@link Setting} object, so this class's subclasses + * should vaguely correspond to those subclasses. There are a few with multiple analogues + * and a few with none (Headers, for example, do not correspond to anything in the ini + * file.) + */ +public abstract class SettingsItem { + public static final int TYPE_HEADER = 0; + public static final int TYPE_CHECKBOX = 1; + public static final int TYPE_SINGLE_CHOICE = 2; + public static final int TYPE_SLIDER = 3; + public static final int TYPE_SUBMENU = 4; + public static final int TYPE_INPUT_BINDING = 5; + public static final int TYPE_STRING_SINGLE_CHOICE = 6; + public static final int TYPE_DATETIME_SETTING = 7; + public static final int TYPE_PREMIUM = 8; + + private String mKey; + private String mSection; + + private Setting mSetting; + + private int mNameId; + private int mDescriptionId; + private boolean mIsPremium; + + /** + * Base constructor. Takes a key / section name in case the third parameter, the Setting, + * is null; in which case, one can be constructed and saved using the key / section. + * + * @param key Identifier for the Setting represented by this Item. + * @param section Section to which the Setting belongs. + * @param setting A possibly-null backing Setting, to be modified on UI events. + * @param nameId Resource ID for a text string to be displayed as this setting's name. + * @param descriptionId Resource ID for a text string to be displayed as this setting's description. + */ + public SettingsItem(String key, String section, Setting setting, int nameId, + int descriptionId) { + mKey = key; + mSection = section; + mSetting = setting; + mNameId = nameId; + mDescriptionId = descriptionId; + mIsPremium = (section == Settings.SECTION_PREMIUM); + } + + /** + * @return The identifier for the backing Setting. + */ + public String getKey() { + return mKey; + } + + /** + * @return The header under which the backing Setting belongs. + */ + public String getSection() { + return mSection; + } + + /** + * @return The backing Setting, possibly null. + */ + public Setting getSetting() { + return mSetting; + } + + /** + * Replace the backing setting with a new one. Generally used in cases where + * the backing setting is null. + * + * @param setting A non-null Setting. + */ + public void setSetting(Setting setting) { + mSetting = setting; + } + + /** + * @return A resource ID for a text string representing this Setting's name. + */ + public int getNameId() { + return mNameId; + } + + public int getDescriptionId() { + return mDescriptionId; + } + + public boolean isPremium() { + return mIsPremium; + } + + /** + * Used by {@link SettingsAdapter}'s onCreateViewHolder() + * method to determine which type of ViewHolder should be created. + * + * @return An integer (ideally, one of the constants defined in this file) + */ + public abstract int getType(); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.java new file mode 100644 index 000000000..619df6d52 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.java @@ -0,0 +1,60 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.IntSetting; +import org.yuzu.yuzu_emu.features.settings.model.Setting; + +public final class SingleChoiceSetting extends SettingsItem { + private int mDefaultValue; + + private int mChoicesId; + private int mValuesId; + + public SingleChoiceSetting(String key, String section, int titleId, int descriptionId, + int choicesId, int valuesId, int defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + } + + public int getChoicesId() { + return mChoicesId; + } + + public int getValuesId() { + return mValuesId; + } + + public int getSelectedValue() { + if (getSetting() != null) { + IntSetting setting = (IntSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public IntSetting setSelectedValue(int selection) { + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + @Override + public int getType() { + return TYPE_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.java new file mode 100644 index 000000000..8ac25b66e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.java @@ -0,0 +1,101 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.FloatSetting; +import org.yuzu.yuzu_emu.features.settings.model.IntSetting; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.utils.Log; + +public final class SliderSetting extends SettingsItem { + private int mMin; + private int mMax; + private int mDefaultValue; + + private String mUnits; + + public SliderSetting(String key, String section, int titleId, int descriptionId, + int min, int max, String units, int defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mMin = min; + mMax = max; + mUnits = units; + mDefaultValue = defaultValue; + } + + public int getMin() { + return mMin; + } + + public int getMax() { + return mMax; + } + + public int getDefaultValue() { + return mDefaultValue; + } + + public int getSelectedValue() { + Setting setting = getSetting(); + + if (setting == null) { + return mDefaultValue; + } + + if (setting instanceof IntSetting) { + IntSetting intSetting = (IntSetting) setting; + return intSetting.getValue(); + } else if (setting instanceof FloatSetting) { + FloatSetting floatSetting = (FloatSetting) setting; + return Math.round(floatSetting.getValue()); + } else { + Log.error("[SliderSetting] Error casting setting type."); + return -1; + } + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public IntSetting setSelectedValue(int selection) { + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + /** + * Write a value to the backing float. If that float was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the float. + * @return null if overwritten successfully otherwise; a newly created FloatSetting. + */ + public FloatSetting setSelectedValue(float selection) { + if (getSetting() == null) { + FloatSetting setting = new FloatSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + FloatSetting setting = (FloatSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + public String getUnits() { + return mUnits; + } + + @Override + public int getType() { + return TYPE_SLIDER; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.java new file mode 100644 index 000000000..419abfd49 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.java @@ -0,0 +1,82 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; + +public class StringSingleChoiceSetting extends SettingsItem { + private String mDefaultValue; + + private String[] mChoicesId; + private String[] mValuesId; + + public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId, + String[] choicesId, String[] valuesId, String defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + } + + public String[] getChoicesId() { + return mChoicesId; + } + + public String[] getValuesId() { + return mValuesId; + } + + public String getValueAt(int index) { + if (mValuesId == null) + return null; + + if (index >= 0 && index < mValuesId.length) { + return mValuesId[index]; + } + + return ""; + } + + public String getSelectedValue() { + if (getSetting() != null) { + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + public int getSelectValueIndex() { + String selectedValue = getSelectedValue(); + for (int i = 0; i < mValuesId.length; i++) { + if (mValuesId[i].equals(selectedValue)) { + return i; + } + } + + return -1; + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public StringSetting setSelectedValue(String selection) { + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + @Override + public int getType() { + return TYPE_STRING_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.java new file mode 100644 index 000000000..3f5b3c8d0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.java @@ -0,0 +1,21 @@ +package org.yuzu.yuzu_emu.features.settings.model.view; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; + +public final class SubmenuSetting extends SettingsItem { + private String mMenuKey; + + public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) { + super(key, null, setting, titleId, descriptionId); + mMenuKey = menuKey; + } + + public String getMenuKey() { + return mMenuKey; + } + + @Override + public int getType() { + return TYPE_SUBMENU; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java new file mode 100644 index 000000000..916ced382 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java @@ -0,0 +1,215 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.provider.Settings; +import android.view.Menu; +import android.view.MenuInflater; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; + +public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { + private static final String ARG_MENU_TAG = "menu_tag"; + private static final String ARG_GAME_ID = "game_id"; + private static final String FRAGMENT_TAG = "settings"; + private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this); + + private ProgressDialog dialog; + + public static void launch(Context context, String menuTag, String gameId) { + Intent settings = new Intent(context, SettingsActivity.class); + settings.putExtra(ARG_MENU_TAG, menuTag); + settings.putExtra(ARG_GAME_ID, gameId); + context.startActivity(settings); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_settings); + + Intent launcher = getIntent(); + String gameID = launcher.getStringExtra(ARG_GAME_ID); + String menuTag = launcher.getStringExtra(ARG_MENU_TAG); + + mPresenter.onCreate(savedInstanceState, menuTag, gameID); + + // Show "Back" button in the action bar for navigation + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_settings, menu); + + return true; + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + // Critical: If super method is not called, rotations will be busted. + super.onSaveInstanceState(outState); + mPresenter.saveState(outState); + } + + @Override + protected void onStart() { + super.onStart(); + mPresenter.onStart(); + } + + /** + * If this is called, the user has left the settings screen (potentially through the + * home button) and will expect their changes to be persisted. So we kick off an + * IntentService which will do so on a background thread. + */ + @Override + protected void onStop() { + super.onStop(); + + mPresenter.onStop(isFinishing()); + + // Update framebuffer layout when closing the settings + NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(), + getWindowManager().getDefaultDisplay().getRotation()); + } + + @Override + public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) { + if (!addToStack && getFragment() != null) { + return; + } + + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + if (addToStack) { + if (areSystemAnimationsEnabled()) { + transaction.setCustomAnimations( + R.animator.settings_enter, + R.animator.settings_exit, + R.animator.settings_pop_enter, + R.animator.setttings_pop_exit); + } + + transaction.addToBackStack(null); + } + transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG); + + transaction.commit(); + } + + private boolean areSystemAnimationsEnabled() { + float duration = Settings.Global.getFloat( + getContentResolver(), + Settings.Global.ANIMATOR_DURATION_SCALE, 1); + float transition = Settings.Global.getFloat( + getContentResolver(), + Settings.Global.TRANSITION_ANIMATION_SCALE, 1); + return duration != 0 && transition != 0; + } + + @Override + public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) { + LocalBroadcastManager.getInstance(this).registerReceiver( + receiver, + filter); + DirectoryInitialization.start(this); + } + + @Override + public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) { + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); + } + + @Override + public void showLoading() { + if (dialog == null) { + dialog = new ProgressDialog(this); + dialog.setMessage(getString(R.string.load_settings)); + dialog.setIndeterminate(true); + } + + dialog.show(); + } + + @Override + public void hideLoading() { + dialog.dismiss(); + } + + @Override + public void showPermissionNeededHint() { + Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public void showExternalStorageNotMountedHint() { + Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public org.yuzu.yuzu_emu.features.settings.model.Settings getSettings() { + return mPresenter.getSettings(); + } + + @Override + public void setSettings(org.yuzu.yuzu_emu.features.settings.model.Settings settings) { + mPresenter.setSettings(settings); + } + + @Override + public void onSettingsFileLoaded(org.yuzu.yuzu_emu.features.settings.model.Settings settings) { + SettingsFragmentView fragment = getFragment(); + + if (fragment != null) { + fragment.onSettingsFileLoaded(settings); + } + } + + @Override + public void onSettingsFileNotFound() { + SettingsFragmentView fragment = getFragment(); + + if (fragment != null) { + fragment.loadDefaultSettings(); + } + } + + @Override + public void showToastMessage(String message, boolean is_long) { + Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); + } + + @Override + public void onSettingChanged() { + mPresenter.onSettingChanged(); + } + + private SettingsFragment getFragment() { + return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java new file mode 100644 index 000000000..ba6b6762b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java @@ -0,0 +1,125 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.TextUtils; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState; +import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; +import org.yuzu.yuzu_emu.utils.Log; +import org.yuzu.yuzu_emu.utils.ThemeUtil; + +import java.io.File; + +public final class SettingsActivityPresenter { + private static final String KEY_SHOULD_SAVE = "should_save"; + + private SettingsActivityView mView; + + private Settings mSettings = new Settings(); + + private boolean mShouldSave; + + private DirectoryStateReceiver directoryStateReceiver; + + private String menuTag; + private String gameId; + + public SettingsActivityPresenter(SettingsActivityView view) { + mView = view; + } + + public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) { + if (savedInstanceState == null) { + this.menuTag = menuTag; + this.gameId = gameId; + } else { + mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); + } + } + + public void onStart() { + prepareDirectoriesIfNeeded(); + } + + void loadSettingsUI() { + if (mSettings.isEmpty()) { + if (!TextUtils.isEmpty(gameId)) { + mSettings.loadSettings(gameId, mView); + } else { + mSettings.loadSettings(mView); + } + } + + mView.showSettingsFragment(menuTag, false, gameId); + mView.onSettingsFileLoaded(mSettings); + } + + private void prepareDirectoriesIfNeeded() { + File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini"); + if (!configFile.exists()) { + Log.error(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini"); + Log.error("yuzu config file could not be found!"); + } + if (DirectoryInitialization.areDirectoriesReady()) { + loadSettingsUI(); + } else { + mView.showLoading(); + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitialization.BROADCAST_ACTION); + + directoryStateReceiver = + new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { + mView.hideLoading(); + loadSettingsUI(); + } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { + mView.showPermissionNeededHint(); + mView.hideLoading(); + } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { + mView.showExternalStorageNotMountedHint(); + mView.hideLoading(); + } + }); + + mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter); + } + } + + public void setSettings(Settings settings) { + mSettings = settings; + } + + public Settings getSettings() { + return mSettings; + } + + public void onStop(boolean finishing) { + if (directoryStateReceiver != null) { + mView.stopListeningToDirectoryInitializationService(directoryStateReceiver); + directoryStateReceiver = null; + } + + if (mSettings != null && finishing && mShouldSave) { + Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI..."); + mSettings.saveSettings(mView); + } + + ThemeUtil.applyTheme(); + + NativeLibrary.ReloadSettings(); + } + + public void onSettingChanged() { + mShouldSave = true; + } + + public void saveState(Bundle outState) { + outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java new file mode 100644 index 000000000..5aff3bcf7 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java @@ -0,0 +1,103 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.content.IntentFilter; + +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; + +/** + * Abstraction for the Activity that manages SettingsFragments. + */ +public interface SettingsActivityView { + /** + * Show a new SettingsFragment. + * + * @param menuTag Identifier for the settings group that should be displayed. + * @param addToStack Whether or not this fragment should replace a previous one. + */ + void showSettingsFragment(String menuTag, boolean addToStack, String gameId); + + /** + * Called by a contained Fragment to get access to the Setting HashMap + * loaded from disk, so that each Fragment doesn't need to perform its own + * read operation. + * + * @return A possibly null HashMap of Settings. + */ + Settings getSettings(); + + /** + * Used to provide the Activity with Settings HashMaps if a Fragment already + * has one; for example, if a rotation occurs, the Fragment will not be killed, + * but the Activity will, so the Activity needs to have its HashMaps resupplied. + * + * @param settings The ArrayList of all the Settings HashMaps. + */ + void setSettings(Settings settings); + + /** + * Called when an asynchronous load operation completes. + * + * @param settings The (possibly null) result of the ini load operation. + */ + void onSettingsFileLoaded(Settings settings); + + /** + * Called when an asynchronous load operation fails. + */ + void onSettingsFileNotFound(); + + /** + * Display a popup text message on screen. + * + * @param message The contents of the onscreen message. + * @param is_long Whether this should be a long Toast or short one. + */ + void showToastMessage(String message, boolean is_long); + + /** + * End the activity. + */ + void finish(); + + /** + * Called by a containing Fragment to tell the Activity that a setting was changed; + * unless this has been called, the Activity will not save to disk. + */ + void onSettingChanged(); + + /** + * Show loading dialog while loading the settings + */ + void showLoading(); + + /** + * Hide the loading the dialog + */ + void hideLoading(); + + /** + * Show a hint to the user that the app needs write to external storage access + */ + void showPermissionNeededHint(); + + /** + * Show a hint to the user that the app needs the external storage to be mounted + */ + void showExternalStorageNotMountedHint(); + + /** + * Start the DirectoryInitialization and listen for the result. + * + * @param receiver the broadcast receiver for the DirectoryInitialization + * @param filter the Intent broadcasts to be received. + */ + void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter); + + /** + * Stop listening to the DirectoryInitialization. + * + * @param receiver The broadcast receiver to unregister. + */ + void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.java new file mode 100644 index 000000000..1102d6af1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.java @@ -0,0 +1,487 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.DatePicker; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.TimePicker; + +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.dialogs.MotionAlertDialog; +import org.yuzu.yuzu_emu.features.settings.model.FloatSetting; +import org.yuzu.yuzu_emu.features.settings.model.IntSetting; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.CheckBoxSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.InputBindingSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.DateTimeViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.HeaderViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.PremiumViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.SettingViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.SingleChoiceViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.SliderViewHolder; +import org.yuzu.yuzu_emu.features.settings.ui.viewholder.SubmenuViewHolder; +import org.yuzu.yuzu_emu.ui.main.MainActivity; +import org.yuzu.yuzu_emu.utils.Log; + +import java.util.ArrayList; + +public final class SettingsAdapter extends RecyclerView.Adapter + implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener { + private SettingsFragmentView mView; + private Context mContext; + private ArrayList mSettings; + + private SettingsItem mClickedItem; + private int mClickedPosition; + private int mSeekbarProgress; + + private AlertDialog mDialog; + private TextView mTextSliderValue; + + public SettingsAdapter(SettingsFragmentView view, Context context) { + mView = view; + mContext = context; + mClickedPosition = -1; + } + + @Override + public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + switch (viewType) { + case SettingsItem.TYPE_HEADER: + view = inflater.inflate(R.layout.list_item_settings_header, parent, false); + return new HeaderViewHolder(view, this); + + case SettingsItem.TYPE_CHECKBOX: + view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false); + return new CheckBoxSettingViewHolder(view, this); + + case SettingsItem.TYPE_SINGLE_CHOICE: + case SettingsItem.TYPE_STRING_SINGLE_CHOICE: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SingleChoiceViewHolder(view, this); + + case SettingsItem.TYPE_SLIDER: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SliderViewHolder(view, this); + + case SettingsItem.TYPE_SUBMENU: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SubmenuViewHolder(view, this); + + case SettingsItem.TYPE_INPUT_BINDING: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new InputBindingSettingViewHolder(view, this, mContext); + + case SettingsItem.TYPE_DATETIME_SETTING: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new DateTimeViewHolder(view, this); + + case SettingsItem.TYPE_PREMIUM: + view = inflater.inflate(R.layout.premium_item_setting, parent, false); + return new PremiumViewHolder(view, this, mView); + + default: + Log.error("[SettingsAdapter] Invalid view type: " + viewType); + return null; + } + } + + @Override + public void onBindViewHolder(SettingViewHolder holder, int position) { + holder.bind(getItem(position)); + } + + private SettingsItem getItem(int position) { + return mSettings.get(position); + } + + @Override + public int getItemCount() { + if (mSettings != null) { + return mSettings.size(); + } else { + return 0; + } + } + + @Override + public int getItemViewType(int position) { + return getItem(position).getType(); + } + + public void setSettings(ArrayList settings) { + mSettings = settings; + notifyDataSetChanged(); + } + + public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { + IntSetting setting = item.setChecked(checked); + notifyItemChanged(position); + + if (setting != null) { + mView.putSetting(setting); + } + + mView.onSettingChanged(); + } + + public void onSingleChoiceClick(PremiumSingleChoiceSetting item) { + mClickedItem = item; + + int value = getSelectionForSingleChoiceValue(item); + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), value, this); + + mDialog = builder.show(); + } + + public void onSingleChoiceClick(SingleChoiceSetting item) { + mClickedItem = item; + + int value = getSelectionForSingleChoiceValue(item); + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), value, this); + + mDialog = builder.show(); + } + + public void onSingleChoiceClick(SingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); + } + + public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); + } + + public void onStringSingleChoiceClick(StringSingleChoiceSetting item) { + mClickedItem = item; + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); + + mDialog = builder.show(); + } + + public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onStringSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item)); + } + + DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog(); + + public void onDateTimeClick(DateTimeSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); + View view = inflater.inflate(R.layout.sysclock_datetime_picker, null); + + DatePicker dp = view.findViewById(R.id.date_picker); + TimePicker tp = view.findViewById(R.id.time_picker); + + //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69) + String settingValue = item.getValue(); + dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10))); + + tp.setIs24HourView(true); + tp.setHour(Integer.parseInt(settingValue.substring(11, 13))); + tp.setMinute(Integer.parseInt(settingValue.substring(14, 16))); + + DialogInterface.OnClickListener ok = (dialog, which) -> { + //set it + int year = dp.getYear(); + if (year < 2000) { + year = 2000; + } + String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length()); + String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length()); + String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length()); + String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length()); + String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01"; + + StringSetting setting = item.setSelectedValue(datetime); + if (setting != null) { + mView.putSetting(setting); + } + + mView.onSettingChanged(); + + mClickedItem = null; + closeDialog(); + }; + + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, ok); + builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); + mDialog = builder.show(); + } + + public void onSliderClick(SliderSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + mSeekbarProgress = item.getSelectedValue(); + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); + View view = inflater.inflate(R.layout.dialog_seekbar, null); + + SeekBar seekbar = view.findViewById(R.id.seekbar); + + builder.setTitle(item.getNameId()); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, this); + builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); + builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> { + seekbar.setProgress(item.getDefaultValue()); + onClick(dialog, which); + }); + mDialog = builder.show(); + + mTextSliderValue = view.findViewById(R.id.text_value); + mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); + + TextView units = view.findViewById(R.id.text_units); + units.setText(item.getUnits()); + + seekbar.setMin(item.getMin()); + seekbar.setMax(item.getMax()); + seekbar.setProgress(mSeekbarProgress); + + seekbar.setOnSeekBarChangeListener(this); + } + + public void onSubmenuClick(SubmenuSetting item) { + mView.loadSubMenu(item.getMenuKey()); + } + + public void onInputBindingClick(final InputBindingSetting item, final int position) { + final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item); + dialog.setTitle(R.string.input_binding); + + int messageResId = R.string.input_binding_description; + if (item.IsAxisMappingSupported() && !item.IsTrigger()) { + // Use specialized message for axis left/right or up/down + if (item.IsHorizontalOrientation()) { + messageResId = R.string.input_binding_description_horizontal_axis; + } else { + messageResId = R.string.input_binding_description_vertical_axis; + } + } + + dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId()))); + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this); + dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) -> + item.removeOldMapping()); + dialog.setOnDismissListener(dialog1 -> + { + StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue()); + notifyItemChanged(position); + + mView.putSetting(setting); + + mView.onSettingChanged(); + }); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (mClickedItem instanceof SingleChoiceSetting) { + SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; + + int value = getValueForSingleChoiceSelection(scSetting, which); + if (scSetting.getSelectedValue() != value) { + mView.onSettingChanged(); + } + + // Get the backing Setting, which may be null (if for example it was missing from the file) + IntSetting setting = scSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + + closeDialog(); + } else if (mClickedItem instanceof PremiumSingleChoiceSetting) { + PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem; + scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which)); + closeDialog(); + } else if (mClickedItem instanceof StringSingleChoiceSetting) { + StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; + String value = scSetting.getValueAt(which); + if (!scSetting.getSelectedValue().equals(value)) + mView.onSettingChanged(); + + StringSetting setting = scSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + + closeDialog(); + } else if (mClickedItem instanceof SliderSetting) { + SliderSetting sliderSetting = (SliderSetting) mClickedItem; + if (sliderSetting.getSelectedValue() != mSeekbarProgress) { + mView.onSettingChanged(); + } + + if (sliderSetting.getSetting() instanceof FloatSetting) { + float value = (float) mSeekbarProgress; + + FloatSetting setting = sliderSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + } else { + IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress); + if (setting != null) { + mView.putSetting(setting); + } + } + + closeDialog(); + } + + mClickedItem = null; + mSeekbarProgress = -1; + } + + public void closeDialog() { + if (mDialog != null) { + if (mClickedPosition != -1) { + notifyItemChanged(mClickedPosition); + mClickedPosition = -1; + } + mDialog.dismiss(); + mDialog = null; + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + mSeekbarProgress = progress; + mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) { + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + return valuesArray[which]; + } else { + return which; + } + } + + private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) { + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + return valuesArray[which]; + } else { + return which; + } + } + + private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) { + int value = item.getSelectedValue(); + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + for (int index = 0; index < valuesArray.length; index++) { + int current = valuesArray[index]; + if (current == value) { + return index; + } + } + } else { + return value; + } + + return -1; + } + + private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) { + int value = item.getSelectedValue(); + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + for (int index = 0; index < valuesArray.length; index++) { + int current = valuesArray[index]; + if (current == value) { + return index; + } + } + } else { + return value; + } + + return -1; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.java new file mode 100644 index 000000000..845f6b7b4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.java @@ -0,0 +1,136 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.ui.DividerItemDecoration; + +import java.util.ArrayList; + +public final class SettingsFragment extends Fragment implements SettingsFragmentView { + private static final String ARGUMENT_MENU_TAG = "menu_tag"; + private static final String ARGUMENT_GAME_ID = "game_id"; + + private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this); + private SettingsActivityView mActivity; + + private SettingsAdapter mAdapter; + + public static Fragment newInstance(String menuTag, String gameId) { + SettingsFragment fragment = new SettingsFragment(); + + Bundle arguments = new Bundle(); + arguments.putString(ARGUMENT_MENU_TAG, menuTag); + arguments.putString(ARGUMENT_GAME_ID, gameId); + + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + mActivity = (SettingsActivityView) context; + mPresenter.onAttach(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRetainInstance(true); + String menuTag = getArguments().getString(ARGUMENT_MENU_TAG); + String gameId = getArguments().getString(ARGUMENT_GAME_ID); + + mAdapter = new SettingsAdapter(this, getActivity()); + + mPresenter.onCreate(menuTag, gameId); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_settings, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + LinearLayoutManager manager = new LinearLayoutManager(getActivity()); + + RecyclerView recyclerView = view.findViewById(R.id.list_settings); + + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(manager); + recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); + + SettingsActivityView activity = (SettingsActivityView) getActivity(); + + mPresenter.onViewCreated(activity.getSettings()); + } + + @Override + public void onDetach() { + super.onDetach(); + mActivity = null; + + if (mAdapter != null) { + mAdapter.closeDialog(); + } + } + + @Override + public void onSettingsFileLoaded(Settings settings) { + mPresenter.setSettings(settings); + } + + @Override + public void passSettingsToActivity(Settings settings) { + if (mActivity != null) { + mActivity.setSettings(settings); + } + } + + @Override + public void showSettingsList(ArrayList settingsList) { + mAdapter.setSettings(settingsList); + } + + @Override + public void loadDefaultSettings() { + mPresenter.loadDefaultSettings(); + } + + @Override + public void loadSubMenu(String menuKey) { + mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID)); + } + + @Override + public void showToastMessage(String message, boolean is_long) { + mActivity.showToastMessage(message, is_long); + } + + @Override + public void putSetting(Setting setting) { + mPresenter.putSetting(setting); + } + + @Override + public void onSettingChanged() { + mActivity.onSettingChanged(); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.java new file mode 100644 index 000000000..27f0adf29 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.java @@ -0,0 +1,322 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.text.TextUtils; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.SettingSection; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.CheckBoxSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.HeaderSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.InputBindingSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.PremiumHeader; +import org.yuzu.yuzu_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; +import org.yuzu.yuzu_emu.utils.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +public final class SettingsFragmentPresenter { + private SettingsFragmentView mView; + + private String mMenuTag; + private String mGameID; + + private Settings mSettings; + private ArrayList mSettingsList; + + public SettingsFragmentPresenter(SettingsFragmentView view) { + mView = view; + } + + public void onCreate(String menuTag, String gameId) { + mGameID = gameId; + mMenuTag = menuTag; + } + + public void onViewCreated(Settings settings) { + setSettings(settings); + } + + /** + * If the screen is rotated, the Activity will forget the settings map. This fragment + * won't, though; so rather than have the Activity reload from disk, have the fragment pass + * the settings map back to the Activity. + */ + public void onAttach() { + if (mSettings != null) { + mView.passSettingsToActivity(mSettings); + } + } + + public void putSetting(Setting setting) { + mSettings.getSection(setting.getSection()).putSetting(setting); + } + + private StringSetting asStringSetting(Setting setting) { + if (setting == null) { + return null; + } + + StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString()); + putSetting(stringSetting); + return stringSetting; + } + + public void loadDefaultSettings() { + loadSettingsList(); + } + + public void setSettings(Settings settings) { + if (mSettingsList == null && settings != null) { + mSettings = settings; + + loadSettingsList(); + } else { + mView.getActivity().setTitle(R.string.preferences_settings); + mView.showSettingsList(mSettingsList); + } + } + + private void loadSettingsList() { + if (!TextUtils.isEmpty(mGameID)) { + mView.getActivity().setTitle("Game Settings: " + mGameID); + } + ArrayList sl = new ArrayList<>(); + + if (mMenuTag == null) { + return; + } + + switch (mMenuTag) { + case SettingsFile.FILE_NAME_CONFIG: + addConfigSettings(sl); + break; + case Settings.SECTION_PREMIUM: + addPremiumSettings(sl); + break; + case Settings.SECTION_CORE: + addGeneralSettings(sl); + break; + case Settings.SECTION_SYSTEM: + addSystemSettings(sl); + break; + case Settings.SECTION_CONTROLS: + addInputSettings(sl); + break; + case Settings.SECTION_RENDERER: + addGraphicsSettings(sl); + break; + case Settings.SECTION_AUDIO: + addAudioSettings(sl); + break; + case Settings.SECTION_DEBUG: + addDebugSettings(sl); + break; + default: + mView.showToastMessage("Unimplemented menu", false); + return; + } + + mSettingsList = sl; + mView.showSettingsList(mSettingsList); + } + + private void addConfigSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_settings); + + sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG)); + } + + private void addPremiumSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_premium); + + SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM); + Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN); + + sl.add(new PremiumHeader()); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView)); + } else { + // Pre-Android 10 does not support System Default + sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView)); + } + + //Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME); + //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)); + } + + private void addGeneralSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_general); + + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED); + Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT); + + 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)); + 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)); + } + + private void addSystemSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_system); + + SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM); + Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE); + Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE); + Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK); + Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME); + + sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language)); + 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)); + 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)); + } + + private void addInputSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_controls); + + SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS); + Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A); + Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B); + Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X); + Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y); + Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); + Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START); + Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL); + Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL); + Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL); + Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL); + Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL); + Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL); + // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP); + // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); + // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); + // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); + Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L); + Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R); + Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL); + Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR); + + sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart)); + + sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz)); + + sl.add(new HeaderSetting(null, null, R.string.controller_c, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz)); + + sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz)); + + // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing. + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight)); + + sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR)); + } + + private void addGraphicsSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_graphics); + + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); + Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE); + Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); + Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D); + Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D); + Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE); + SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT); + Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE); + Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT); + Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT); + SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY); + Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES); + Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES); + //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES); + + sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); + 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)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode)); + 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)); + 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)); + + sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode)); + sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d)); + + sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0)); + 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)); + 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)); + 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)); + + sl.add(new HeaderSetting(null, null, R.string.utility, 0)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures)); + //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes yuzu. + //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures)); + } + + private void addAudioSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_audio); + + SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO); + Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING); + Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE); + + sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch)); + 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)); + } + + private void addDebugSettings(ArrayList sl) { + mView.getActivity().setTitle(R.string.preferences_debug); + + SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE); + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT); + Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER); + Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER); + Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC); + + sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable)); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.java new file mode 100644 index 000000000..3c1743fab --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.java @@ -0,0 +1,78 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import androidx.fragment.app.FragmentActivity; + +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; + +import java.util.ArrayList; + +/** + * Abstraction for a screen showing a list of settings. Instances of + * this type of view will each display a layer of the setting hierarchy. + */ +public interface SettingsFragmentView { + /** + * Called by the containing Activity to notify the Fragment that an + * asynchronous load operation completed. + * + * @param settings The (possibly null) result of the ini load operation. + */ + void onSettingsFileLoaded(Settings settings); + + /** + * Pass a settings HashMap to the containing activity, so that it can + * share the HashMap with other SettingsFragments; useful so that rotations + * do not require an additional load operation. + * + * @param settings An ArrayList containing all the settings HashMaps. + */ + void passSettingsToActivity(Settings settings); + + /** + * Pass an ArrayList to the View so that it can be displayed on screen. + * + * @param settingsList The result of converting the HashMap to an ArrayList + */ + void showSettingsList(ArrayList settingsList); + + /** + * Called by the containing Activity when an asynchronous load operation fails. + * Instructs the Fragment to load the settings screen with defaults selected. + */ + void loadDefaultSettings(); + + /** + * @return The Fragment's containing activity. + */ + FragmentActivity getActivity(); + + /** + * Tell the Fragment to tell the containing Activity to show a new + * Fragment containing a submenu of settings. + * + * @param menuKey Identifier for the settings group that should be shown. + */ + void loadSubMenu(String menuKey); + + /** + * Tell the Fragment to tell the containing activity to display a toast message. + * + * @param message Text to be shown in the Toast + * @param is_long Whether this should be a long Toast or short one. + */ + void showToastMessage(String message, boolean is_long); + + /** + * Have the fragment add a setting to the HashMap. + * + * @param setting The (possibly previously missing) new setting. + */ + void putSetting(Setting setting); + + /** + * Have the fragment tell the containing Activity that a setting was modified. + */ + void onSettingChanged(); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFrameLayout.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFrameLayout.java new file mode 100644 index 000000000..f753368a8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFrameLayout.java @@ -0,0 +1,48 @@ +package org.yuzu.yuzu_emu.features.settings.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * FrameLayout subclass with few Properties added to simplify animations. + * Don't remove the methods appearing as unused, in order not to break the menu animations + */ +public final class SettingsFrameLayout extends FrameLayout { + private float mVisibleness = 1.0f; + + public SettingsFrameLayout(Context context) { + super(context); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public float getYFraction() { + return getY() / getHeight(); + } + + public void setYFraction(float yFraction) { + final int height = getHeight(); + setY((height > 0) ? (yFraction * height) : -9999); + } + + public float getVisibleness() { + return mVisibleness; + } + + public void setVisibleness(float visibleness) { + setScaleX(visibleness); + setScaleY(visibleness); + setAlpha(visibleness); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java new file mode 100644 index 000000000..2b05739e7 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java @@ -0,0 +1,54 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.CheckBoxSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class CheckBoxSettingViewHolder extends SettingViewHolder { + private CheckBoxSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + private CheckBox mCheckbox; + + public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + mCheckbox = root.findViewById(R.id.checkbox); + } + + @Override + public void bind(SettingsItem item) { + mItem = (CheckBoxSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setText(""); + mTextSettingDescription.setVisibility(View.GONE); + } + + mCheckbox.setChecked(mItem.isChecked()); + } + + @Override + public void onClick(View clicked) { + mCheckbox.toggle(); + + getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.java new file mode 100644 index 000000000..60a65b71a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.java @@ -0,0 +1,47 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; +import org.yuzu.yuzu_emu.utils.Log; + +public final class DateTimeViewHolder extends SettingViewHolder { + private DateTimeSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public DateTimeViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + Log.error("test " + mTextSettingName); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + Log.error("test " + mTextSettingDescription); + } + + @Override + public void bind(SettingsItem item) { + mItem = (DateTimeSetting) item; + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onDateTimeClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.java new file mode 100644 index 000000000..afe021149 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.java @@ -0,0 +1,32 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class HeaderViewHolder extends SettingViewHolder { + private TextView mHeaderName; + + public HeaderViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + itemView.setOnClickListener(null); + } + + @Override + protected void findViews(View root) { + mHeaderName = root.findViewById(R.id.text_header_name); + } + + @Override + public void bind(SettingsItem item) { + mHeaderName.setText(item.getNameId()); + } + + @Override + public void onClick(View clicked) { + // no-op + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java new file mode 100644 index 000000000..6f8bef7d7 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java @@ -0,0 +1,55 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.InputBindingSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class InputBindingSettingViewHolder extends SettingViewHolder { + private InputBindingSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + private Context mContext; + + public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) { + super(itemView, adapter); + + mContext = context; + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + + mItem = (InputBindingSetting) item; + + mTextSettingName.setText(item.getNameId()); + + String key = sharedPreferences.getString(mItem.getKey(), ""); + if (key != null && !key.isEmpty()) { + mTextSettingDescription.setText(key); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onInputBindingClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/PremiumViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/PremiumViewHolder.java new file mode 100644 index 000000000..1f862b281 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/PremiumViewHolder.java @@ -0,0 +1,57 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsFragmentView; +import org.yuzu.yuzu_emu.ui.main.MainActivity; + +public final class PremiumViewHolder extends SettingViewHolder { + private TextView mHeaderName; + private TextView mTextDescription; + private SettingsFragmentView mView; + + public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) { + super(itemView, adapter); + mView = view; + itemView.setOnClickListener(this); + } + + @Override + protected void findViews(View root) { + mHeaderName = root.findViewById(R.id.text_setting_name); + mTextDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + updateText(); + } + + @Override + public void onClick(View clicked) { + if (MainActivity.isPremiumActive()) { + return; + } + + // Invoke billing flow if Premium is not already active, then refresh the UI to indicate + // the purchase has completed. + MainActivity.invokePremiumBilling(() -> updateText()); + } + + /** + * Update the text shown to the user, based on whether Premium is active + */ + private void updateText() { + if (MainActivity.isPremiumActive()) { + mHeaderName.setText(R.string.premium_settings_welcome); + mTextDescription.setText(R.string.premium_settings_welcome_description); + } else { + mHeaderName.setText(R.string.premium_settings_upsell); + mTextDescription.setText(R.string.premium_settings_upsell_description); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.java new file mode 100644 index 000000000..268b7c386 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.java @@ -0,0 +1,49 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private SettingsAdapter mAdapter; + + public SettingViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView); + + mAdapter = adapter; + + itemView.setOnClickListener(this); + + findViews(itemView); + } + + protected SettingsAdapter getAdapter() { + return mAdapter; + } + + /** + * Gets handles to all this ViewHolder's child views using their XML-defined identifiers. + * + * @param root The newly inflated top-level view. + */ + protected abstract void findViews(View root); + + /** + * Called by the adapter to set this ViewHolder's child views to display the list item + * it must now represent. + * + * @param item The list item that should be represented by this ViewHolder. + */ + public abstract void bind(SettingsItem item); + + /** + * Called when this ViewHolder's view is clicked on. Implementations should usually pass + * this event up to the adapter. + * + * @param clicked The view that was clicked on. + */ + public abstract void onClick(View clicked); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java new file mode 100644 index 000000000..e3766f55e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java @@ -0,0 +1,76 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.content.res.Resources; +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class SingleChoiceViewHolder extends SettingViewHolder { + private SettingsItem mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = item; + + mTextSettingName.setText(item.getNameId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + } else if (item instanceof SingleChoiceSetting) { + SingleChoiceSetting setting = (SingleChoiceSetting) item; + int selected = setting.getSelectedValue(); + Resources resMgr = mTextSettingDescription.getContext().getResources(); + String[] choices = resMgr.getStringArray(setting.getChoicesId()); + int[] values = resMgr.getIntArray(setting.getValuesId()); + for (int i = 0; i < values.length; ++i) { + if (values[i] == selected) { + mTextSettingDescription.setText(choices[i]); + } + } + } else if (item instanceof PremiumSingleChoiceSetting) { + PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item; + int selected = setting.getSelectedValue(); + Resources resMgr = mTextSettingDescription.getContext().getResources(); + String[] choices = resMgr.getStringArray(setting.getChoicesId()); + int[] values = resMgr.getIntArray(setting.getValuesId()); + for (int i = 0; i < values.length; ++i) { + if (values[i] == selected) { + mTextSettingDescription.setText(choices[i]); + } + } + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + int position = getAdapterPosition(); + if (mItem instanceof SingleChoiceSetting) { + getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position); + } else if (mItem instanceof PremiumSingleChoiceSetting) { + getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position); + } else if (mItem instanceof StringSingleChoiceSetting) { + getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.java new file mode 100644 index 000000000..1f60a37ce --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.java @@ -0,0 +1,45 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class SliderViewHolder extends SettingViewHolder { + private SliderSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SliderViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = (SliderSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSliderClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.java new file mode 100644 index 000000000..1aca4723d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.java @@ -0,0 +1,45 @@ +package org.yuzu.yuzu_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem; +import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter; + +public final class SubmenuViewHolder extends SettingViewHolder { + private SubmenuSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SubmenuViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = (SubmenuSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSubmenuClick(mItem); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.java new file mode 100644 index 000000000..9e58dedc2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.java @@ -0,0 +1,341 @@ +package org.yuzu.yuzu_emu.features.settings.utils; + +import androidx.annotation.NonNull; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.FloatSetting; +import org.yuzu.yuzu_emu.features.settings.model.IntSetting; +import org.yuzu.yuzu_emu.features.settings.model.Setting; +import org.yuzu.yuzu_emu.features.settings.model.SettingSection; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.model.StringSetting; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView; +import org.yuzu.yuzu_emu.utils.BiMap; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.Log; +import org.ini4j.Wini; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Contains static methods for interacting with .ini files in which settings are stored. + */ +public final class SettingsFile { + public static final String FILE_NAME_CONFIG = "config"; + + public static final String KEY_CPU_JIT = "use_cpu_jit"; + + public static final String KEY_DESIGN = "design"; + + public static final String KEY_PREMIUM = "premium"; + + public static final String KEY_HW_RENDERER = "use_hw_renderer"; + public static final String KEY_HW_SHADER = "use_hw_shader"; + public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; + public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; + public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"; + public static final String KEY_USE_VSYNC = "use_vsync_new"; + public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; + public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit"; + public static final String KEY_FRAME_LIMIT = "frame_limit"; + public static final String KEY_BACKGROUND_RED = "bg_red"; + public static final String KEY_BACKGROUND_BLUE = "bg_blue"; + public static final String KEY_BACKGROUND_GREEN = "bg_green"; + public static final String KEY_RENDER_3D = "render_3d"; + public static final String KEY_FACTOR_3D = "factor_3d"; + public static final String KEY_PP_SHADER_NAME = "pp_shader_name"; + public static final String KEY_FILTER_MODE = "filter_mode"; + public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name"; + public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation"; + + public static final String KEY_LAYOUT_OPTION = "layout_option"; + public static final String KEY_SWAP_SCREEN = "swap_screen"; + public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size"; + public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift"; + public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift"; + + public static final String KEY_DUMP_TEXTURES = "dump_textures"; + public static final String KEY_CUSTOM_TEXTURES = "custom_textures"; + public static final String KEY_PRELOAD_TEXTURES = "preload_textures"; + + public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine"; + public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; + public static final String KEY_VOLUME = "volume"; + public static final String KEY_MIC_INPUT_TYPE = "mic_input_type"; + + public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; + + public static final String KEY_IS_NEW_3DS = "is_new_3ds"; + public static final String KEY_REGION_VALUE = "region_value"; + public static final String KEY_LANGUAGE = "language"; + + public static final String KEY_INIT_CLOCK = "init_clock"; + public static final String KEY_INIT_TIME = "init_time"; + + public static final String KEY_BUTTON_A = "button_a"; + public static final String KEY_BUTTON_B = "button_b"; + public static final String KEY_BUTTON_X = "button_x"; + public static final String KEY_BUTTON_Y = "button_y"; + public static final String KEY_BUTTON_SELECT = "button_select"; + public static final String KEY_BUTTON_START = "button_start"; + public static final String KEY_BUTTON_UP = "button_up"; + public static final String KEY_BUTTON_DOWN = "button_down"; + public static final String KEY_BUTTON_LEFT = "button_left"; + public static final String KEY_BUTTON_RIGHT = "button_right"; + public static final String KEY_BUTTON_L = "button_l"; + public static final String KEY_BUTTON_R = "button_r"; + public static final String KEY_BUTTON_ZL = "button_zl"; + public static final String KEY_BUTTON_ZR = "button_zr"; + public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"; + public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"; + public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"; + public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"; + public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"; + public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"; + public static final String KEY_CIRCLEPAD_UP = "circlepad_up"; + public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down"; + public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left"; + public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right"; + public static final String KEY_CSTICK_UP = "cstick_up"; + public static final String KEY_CSTICK_DOWN = "cstick_down"; + public static final String KEY_CSTICK_LEFT = "cstick_left"; + public static final String KEY_CSTICK_RIGHT = "cstick_right"; + + public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name"; + public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config"; + public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip"; + public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name"; + public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config"; + public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip"; + public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name"; + public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config"; + public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip"; + + public static final String KEY_LOG_FILTER = "log_filter"; + + private static BiMap sectionsMap = new BiMap<>(); + + static { + //TODO: Add members to sectionsMap when game-specific settings are added + } + + + private SettingsFile() { + } + + /** + * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param ini The ini file to load the settings from + * @param isCustomGame + * @param view The current view. + * @return An Observable that emits a HashMap of the file's contents, then completes. + */ + static HashMap readFile(final File ini, boolean isCustomGame, SettingsActivityView view) { + HashMap sections = new Settings.SettingsSectionMap(); + + BufferedReader reader = null; + + try { + reader = new BufferedReader(new FileReader(ini)); + + SettingSection current = null; + for (String line; (line = reader.readLine()) != null; ) { + if (line.startsWith("[") && line.endsWith("]")) { + current = sectionFromLine(line, isCustomGame); + sections.put(current.getName(), current); + } else if ((current != null)) { + Setting setting = settingFromLine(current, line); + if (setting != null) { + current.putSetting(setting); + } + } + } + } catch (FileNotFoundException e) { + Log.error("[SettingsFile] File not found: " + e.getMessage()); + if (view != null) + view.onSettingsFileNotFound(); + } catch (IOException e) { + Log.error("[SettingsFile] Error reading from: " + e.getMessage()); + if (view != null) + view.onSettingsFileNotFound(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.error("[SettingsFile] Error closing: " + e.getMessage()); + } + } + } + + return sections; + } + + public static HashMap readFile(final String fileName, SettingsActivityView view) { + return readFile(getSettingsFile(fileName), false, view); + } + + /** + * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param gameId the id of the game to load it's settings. + * @param view The current view. + */ + public static HashMap readCustomGameSettings(final String gameId, SettingsActivityView view) { + return readFile(getCustomGameSettingsFile(gameId), true, view); + } + + /** + * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error + * telling why it failed. + * + * @param fileName The target filename without a path or extension. + * @param sections The HashMap containing the Settings we want to serialize. + * @param view The current view. + */ + public static void saveFile(final String fileName, TreeMap sections, + SettingsActivityView view) { + File ini = getSettingsFile(fileName); + + try { + Wini writer = new Wini(ini); + + Set keySet = sections.keySet(); + for (String key : keySet) { + SettingSection section = sections.get(key); + writeSection(writer, section); + } + writer.store(); + } catch (IOException e) { + Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); + view.showToastMessage(YuzuApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false); + } + } + + + public static void saveCustomGameSettings(final String gameId, final HashMap sections) { + Set sortedSections = new TreeSet<>(sections.keySet()); + + for (String sectionKey : sortedSections) { + SettingSection section = sections.get(sectionKey); + + HashMap settings = section.getSettings(); + Set sortedKeySet = new TreeSet<>(settings.keySet()); + + for (String settingKey : sortedKeySet) { + Setting setting = settings.get(settingKey); + NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString()); + } + } + } + + private static String mapSectionNameFromIni(String generalSectionName) { + if (sectionsMap.getForward(generalSectionName) != null) { + return sectionsMap.getForward(generalSectionName); + } + + return generalSectionName; + } + + private static String mapSectionNameToIni(String generalSectionName) { + if (sectionsMap.getBackward(generalSectionName) != null) { + return sectionsMap.getBackward(generalSectionName); + } + + return generalSectionName; + } + + @NonNull + private static File getSettingsFile(String fileName) { + return new File( + DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini"); + } + + private static File getCustomGameSettingsFile(String gameId) { + return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini"); + } + + private static SettingSection sectionFromLine(String line, boolean isCustomGame) { + String sectionName = line.substring(1, line.length() - 1); + if (isCustomGame) { + sectionName = mapSectionNameToIni(sectionName); + } + return new SettingSection(sectionName); + } + + /** + * For a line of text, determines what type of data is being represented, and returns + * a Setting object containing this data. + * + * @param current The section currently being parsed by the consuming method. + * @param line The line of text being parsed. + * @return A typed Setting containing the key/value contained in the line. + */ + private static Setting settingFromLine(SettingSection current, String line) { + String[] splitLine = line.split("="); + + if (splitLine.length != 2) { + Log.warning("Skipping invalid config line \"" + line + "\""); + return null; + } + + String key = splitLine[0].trim(); + String value = splitLine[1].trim(); + + if (value.isEmpty()) { + Log.warning("Skipping null value in config line \"" + line + "\""); + return null; + } + + try { + int valueAsInt = Integer.parseInt(value); + + return new IntSetting(key, current.getName(), valueAsInt); + } catch (NumberFormatException ex) { + } + + try { + float valueAsFloat = Float.parseFloat(value); + + return new FloatSetting(key, current.getName(), valueAsFloat); + } catch (NumberFormatException ex) { + } + + return new StringSetting(key, current.getName(), value); + } + + /** + * Writes the contents of a Section HashMap to disk. + * + * @param parser A Wini pointed at a file on disk. + * @param section A section containing settings to be written to the file. + */ + private static void writeSection(Wini parser, SettingSection section) { + // Write the section header. + String header = section.getName(); + + // Write this section's values. + HashMap settings = section.getSettings(); + Set keySet = settings.keySet(); + + for (String key : keySet) { + Setting setting = settings.get(key); + parser.put(header, setting.getKey(), setting.getValueAsString()); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java new file mode 100644 index 000000000..2658b1445 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java @@ -0,0 +1,120 @@ +package org.yuzu.yuzu_emu.fragments; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; + +import com.nononsenseapps.filepicker.FilePickerFragment; + +import org.yuzu.yuzu_emu.R; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CustomFilePickerFragment extends FilePickerFragment { + private static String ALL_FILES = "*"; + private int mTitle; + private static List extensions = Collections.singletonList(ALL_FILES); + + @NonNull + @Override + public Uri toUri(@NonNull final File file) { + return FileProvider + .getUriForFile(getContext(), + getContext().getApplicationContext().getPackageName() + ".filesprovider", + file); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mode == MODE_DIR) { + TextView ok = getActivity().findViewById(R.id.nnf_button_ok); + ok.setText(R.string.select_dir); + + TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); + cancel.setVisibility(View.GONE); + } + } + + @Override + protected View inflateRootView(LayoutInflater inflater, ViewGroup container) { + View view = super.inflateRootView(inflater, container); + if (mTitle != 0) { + Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar); + ViewGroup parent = (ViewGroup) toolbar.getParent(); + int index = parent.indexOfChild(toolbar); + View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false); + TextView title = newToolbar.findViewById(R.id.filepicker_title); + title.setText(mTitle); + parent.removeView(toolbar); + parent.addView(newToolbar, index); + } + return view; + } + + public void setTitle(int title) { + mTitle = title; + } + + public void setAllowedExtensions(String allowedExtensions) { + if (allowedExtensions == null) + return; + + extensions = Arrays.asList(allowedExtensions.split(",")); + } + + @Override + protected boolean isItemVisible(@NonNull final File file) { + // Some users jump to the conclusion that Dolphin isn't able to detect their + // files if the files don't show up in the file picker when mode == MODE_DIR. + // To avoid this, show files even when the user needs to select a directory. + return (showHiddenItems || !file.isHidden()) && + (file.isDirectory() || extensions.contains(ALL_FILES) || + extensions.contains(fileExtension(file.getName()).toLowerCase())); + } + + @Override + public boolean isCheckable(@NonNull final File file) { + // We need to make a small correction to the isCheckable logic due to + // overriding isItemVisible to show files when mode == MODE_DIR. + // AbstractFilePickerFragment always treats files as checkable when + // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. + return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); + } + + @Override + public void goUp() { + if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) { + goToDir(new File("/storage/")); + return; + } + if (mCurrentPath.equals(new File("/storage/"))){ + return; + } + super.goUp(); + } + + @Override + public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { + if(viewHolder.file.equals(new File("/storage/emulated/"))) + viewHolder.file = new File("/storage/emulated/0/"); + super.onClickDir(view, viewHolder); + } + + private static String fileExtension(@NonNull String filename) { + int i = filename.lastIndexOf('.'); + return i < 0 ? "" : filename.substring(i + 1); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java new file mode 100644 index 000000000..b2083f858 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java @@ -0,0 +1,380 @@ +package org.yuzu.yuzu_emu.fragments; + +import android.content.Context; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.overlay.InputOverlay; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState; +import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; +import org.yuzu.yuzu_emu.utils.Log; + +public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { + private static final String KEY_GAMEPATH = "gamepath"; + + private static final Handler perfStatsUpdateHandler = new Handler(); + + private SharedPreferences mPreferences; + + private InputOverlay mInputOverlay; + + private EmulationState mEmulationState; + + private DirectoryStateReceiver directoryStateReceiver; + + private EmulationActivity activity; + + private TextView mPerfStats; + + private Runnable perfStatsUpdater; + + public static EmulationFragment newInstance(String gamePath) { + Bundle args = new Bundle(); + args.putString(KEY_GAMEPATH, gamePath); + + EmulationFragment fragment = new EmulationFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof EmulationActivity) { + activity = (EmulationActivity) context; + NativeLibrary.setEmulationActivity((EmulationActivity) context); + } else { + throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // So this fragment doesn't restart on configuration changes; i.e. rotation. + setRetainInstance(true); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + + String gamePath = getArguments().getString(KEY_GAMEPATH); + mEmulationState = new EmulationState(gamePath); + } + + /** + * Initialize the UI and start emulation in here. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View contents = inflater.inflate(R.layout.fragment_emulation, container, false); + + SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); + surfaceView.getHolder().addCallback(this); + + mInputOverlay = contents.findViewById(R.id.surface_input_overlay); + mPerfStats = contents.findViewById(R.id.show_fps_text); + mPerfStats.setTextColor(Color.YELLOW); + + Button doneButton = contents.findViewById(R.id.done_control_config); + if (doneButton != null) { + doneButton.setOnClickListener(v -> stopConfiguringControls()); + } + + // Show/hide the "Show FPS" overlay + updateShowFpsOverlay(); + + // The new Surface created here will get passed to the native code via onSurfaceChanged. + return contents; + } + + @Override + public void onResume() { + super.onResume(); + Choreographer.getInstance().postFrameCallback(this); + if (DirectoryInitialization.areDirectoriesReady()) { + mEmulationState.run(activity.isActivityRecreated()); + } else { + setupDirectoriesThenStartEmulation(); + } + } + + @Override + public void onPause() { + if (directoryStateReceiver != null) { + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); + directoryStateReceiver = null; + } + + if (mEmulationState.isRunning()) { + mEmulationState.pause(); + } + + Choreographer.getInstance().removeFrameCallback(this); + super.onPause(); + } + + @Override + public void onDetach() { + NativeLibrary.clearEmulationActivity(); + super.onDetach(); + } + + private void setupDirectoriesThenStartEmulation() { + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitialization.BROADCAST_ACTION); + + directoryStateReceiver = + new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == + DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { + mEmulationState.run(activity.isActivityRecreated()); + } else if (directoryInitializationState == + DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { + Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } else if (directoryInitializationState == + DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { + Toast.makeText(getContext(), R.string.external_storage_not_mounted, + Toast.LENGTH_SHORT) + .show(); + } + }); + + // Registers the DirectoryStateReceiver and its intent filters + LocalBroadcastManager.getInstance(getActivity()).registerReceiver( + directoryStateReceiver, + statusIntentFilter); + DirectoryInitialization.start(getActivity()); + } + + public void refreshInputOverlay() { + mInputOverlay.refreshControls(); + } + + public void resetInputOverlay() { + // Reset button scale + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("controlScale", 50); + editor.apply(); + + mInputOverlay.resetButtonPlacement(); + } + + public void updateShowFpsOverlay() { + if (true) { + final int SYSTEM_FPS = 0; + final int FPS = 1; + final int FRAMETIME = 2; + final int SPEED = 3; + + perfStatsUpdater = () -> + { + final double[] perfStats = NativeLibrary.GetPerfStats(); + if (perfStats[FPS] > 0) { + mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS]), + (int) (perfStats[SPEED] * 100.0))); + } + + perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000); + }; + perfStatsUpdateHandler.post(perfStatsUpdater); + + mPerfStats.setVisibility(View.VISIBLE); + } else { + if (perfStatsUpdater != null) { + perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); + } + + mPerfStats.setVisibility(View.GONE); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); + mEmulationState.newSurface(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mEmulationState.clearSurface(); + } + + @Override + public void doFrame(long frameTimeNanos) { + Choreographer.getInstance().postFrameCallback(this); + NativeLibrary.DoFrame(); + } + + public void stopEmulation() { + mEmulationState.stop(); + } + + public void startConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); + mInputOverlay.setIsInEditMode(true); + } + + public void stopConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); + mInputOverlay.setIsInEditMode(false); + } + + public boolean isConfiguringControls() { + return mInputOverlay.isInEditMode(); + } + + private static class EmulationState { + private final String mGamePath; + private State state; + private Surface mSurface; + private boolean mRunWhenSurfaceIsValid; + + EmulationState(String gamePath) { + mGamePath = gamePath; + // Starting state is stopped. + state = State.STOPPED; + } + + public synchronized boolean isStopped() { + return state == State.STOPPED; + } + + // Getters for the current state + + public synchronized boolean isPaused() { + return state == State.PAUSED; + } + + public synchronized boolean isRunning() { + return state == State.RUNNING; + } + + public synchronized void stop() { + if (state != State.STOPPED) { + Log.debug("[EmulationFragment] Stopping emulation."); + state = State.STOPPED; + NativeLibrary.StopEmulation(); + } else { + Log.warning("[EmulationFragment] Stop called while already stopped."); + } + } + + // State changing methods + + public synchronized void pause() { + if (state != State.PAUSED) { + state = State.PAUSED; + Log.debug("[EmulationFragment] Pausing emulation."); + + // Release the surface before pausing, since emulation has to be running for that. + NativeLibrary.SurfaceDestroyed(); + NativeLibrary.PauseEmulation(); + } else { + Log.warning("[EmulationFragment] Pause called while already paused."); + } + } + + public synchronized void run(boolean isActivityRecreated) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + state = State.PAUSED; + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start"); + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (mSurface != null) { + runWithValidSurface(); + } else { + mRunWhenSurfaceIsValid = true; + } + } + + // Surface callbacks + public synchronized void newSurface(Surface surface) { + mSurface = surface; + if (mRunWhenSurfaceIsValid) { + runWithValidSurface(); + } + } + + public synchronized void clearSurface() { + if (mSurface == null) { + Log.warning("[EmulationFragment] clearSurface called, but surface already null."); + } else { + mSurface = null; + Log.debug("[EmulationFragment] Surface destroyed."); + + if (state == State.RUNNING) { + NativeLibrary.SurfaceDestroyed(); + state = State.PAUSED; + } else if (state == State.PAUSED) { + Log.warning("[EmulationFragment] Surface cleared while emulation paused."); + } else { + Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); + } + } + } + + private void runWithValidSurface() { + mRunWhenSurfaceIsValid = false; + if (state == State.STOPPED) { + NativeLibrary.SurfaceChanged(mSurface); + Thread mEmulationThread = new Thread(() -> + { + Log.debug("[EmulationFragment] Starting emulation thread."); + NativeLibrary.Run(mGamePath); + }, "NativeEmulation"); + mEmulationThread.start(); + + } else if (state == State.PAUSED) { + Log.debug("[EmulationFragment] Resuming emulation."); + NativeLibrary.SurfaceChanged(mSurface); + NativeLibrary.UnPauseEmulation(); + } else { + Log.debug("[EmulationFragment] Bug, run called while already running."); + } + state = State.RUNNING; + } + + private enum State { + STOPPED, RUNNING, PAUSED + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MenuFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MenuFragment.java new file mode 100644 index 000000000..5dc3f5545 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MenuFragment.java @@ -0,0 +1,129 @@ +package org.yuzu.yuzu_emu.fragments; + +import android.content.pm.PackageManager; +import android.graphics.Rect; +import android.os.Bundle; +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; + +import com.google.android.material.color.MaterialColors; +import com.google.android.material.elevation.ElevationOverlayProvider; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; + + +public final class MenuFragment extends Fragment implements View.OnClickListener +{ + private static final String KEY_TITLE = "title"; + private static final String KEY_WII = "wii"; + private static SparseIntArray buttonsActionsMap = new SparseIntArray(); + + private int mCutInset = 0; + + static + { + buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT); + } + + public static MenuFragment newInstance() + { + MenuFragment fragment = new MenuFragment(); + + Bundle arguments = new Bundle(); + fragment.setArguments(arguments); + + return fragment; + } + + // This is primarily intended to account for any navigation bar at the bottom of the screen + private int getBottomPaddingRequired() + { + Rect visibleFrame = new Rect(); + requireActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleFrame); + return visibleFrame.bottom - visibleFrame.top - getResources().getDisplayMetrics().heightPixels; + } + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) + { + View rootView = inflater.inflate(R.layout.fragment_ingame_menu, container, false); + + LinearLayout options = rootView.findViewById(R.id.layout_options); + +// mPauseEmulation = options.findViewById(R.id.menu_pause_emulation); +// mUnpauseEmulation = options.findViewById(R.id.menu_unpause_emulation); +// +// updatePauseUnpauseVisibility(); +// +// if (!requireActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) +// { +// options.findViewById(R.id.menu_overlay_controls).setVisibility(View.GONE); +// } +// +// if (!getArguments().getBoolean(KEY_WII, true)) +// { +// options.findViewById(R.id.menu_refresh_wiimotes).setVisibility(View.GONE); +// } + + int bottomPaddingRequired = getBottomPaddingRequired(); + + // Provide a safe zone between the navigation bar and Exit Emulation to avoid accidental touches + float density = getResources().getDisplayMetrics().density; + if (bottomPaddingRequired >= 32 * density) + { + bottomPaddingRequired += 32 * density; + } + + if (bottomPaddingRequired > rootView.getPaddingBottom()) + { + rootView.setPadding(rootView.getPaddingLeft(), rootView.getPaddingTop(), + rootView.getPaddingRight(), bottomPaddingRequired); + } + + for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++) + { + Button button = (Button) options.getChildAt(childIndex); + + button.setOnClickListener(this); + } + + rootView.findViewById(R.id.menu_exit).setOnClickListener(this); + +// mTitleText = rootView.findViewById(R.id.text_game_title); +// String title = getArguments().getString(KEY_TITLE, null); +// if (title != null) +// { +// mTitleText.setText(title); +// } + + if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) + { +// rootView.post(() -> NativeLibrary.SetObscuredPixelsLeft(rootView.getWidth())); + } + + return rootView; + } + + @Override + public void onClick(View button) + { + int action = buttonsActionsMap.get(button.getId()); + EmulationActivity activity = (EmulationActivity) requireActivity(); + activity.handleMenuAction(action); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java new file mode 100644 index 000000000..bc1b19bd1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java @@ -0,0 +1,76 @@ +package org.yuzu.yuzu_emu.model; + +import android.content.ContentValues; +import android.database.Cursor; + +import java.nio.file.Paths; + +public final class Game { + private String mTitle; + private String mDescription; + private String mPath; + private String mGameId; + private String mCompany; + private String mRegions; + + public Game(String title, String description, String regions, String path, + String gameId, String company) { + mTitle = title; + mDescription = description; + mRegions = regions; + mPath = path; + mGameId = gameId; + mCompany = company; + } + + public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) { + ContentValues values = new ContentValues(); + + if (gameId.isEmpty()) { + // Homebrew, etc. may not have a game ID, use filename as a unique identifier + gameId = Paths.get(path).getFileName().toString(); + } + + values.put(GameDatabase.KEY_GAME_TITLE, title); + values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); + values.put(GameDatabase.KEY_GAME_REGIONS, regions); + values.put(GameDatabase.KEY_GAME_PATH, path); + values.put(GameDatabase.KEY_GAME_ID, gameId); + values.put(GameDatabase.KEY_GAME_COMPANY, company); + + return values; + } + + public static Game fromCursor(Cursor cursor) { + return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE), + cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), + cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), + cursor.getString(GameDatabase.GAME_COLUMN_PATH), + cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), + cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); + } + + public String getTitle() { + return mTitle; + } + + public String getDescription() { + return mDescription; + } + + public String getCompany() { + return mCompany; + } + + public String getRegions() { + return mRegions; + } + + public String getPath() { + return mPath; + } + + public String getGameId() { + return mGameId; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java new file mode 100644 index 000000000..ac5db1c36 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java @@ -0,0 +1,276 @@ +package org.yuzu.yuzu_emu.model; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.utils.Log; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import rx.Observable; + +/** + * A helper class that provides several utilities simplifying interaction with + * the SQLite database. + */ +public final class GameDatabase extends SQLiteOpenHelper { + public static final int COLUMN_DB_ID = 0; + public static final int GAME_COLUMN_PATH = 1; + public static final int GAME_COLUMN_TITLE = 2; + public static final int GAME_COLUMN_DESCRIPTION = 3; + public static final int GAME_COLUMN_REGIONS = 4; + public static final int GAME_COLUMN_GAME_ID = 5; + public static final int GAME_COLUMN_COMPANY = 6; + public static final int FOLDER_COLUMN_PATH = 1; + public static final String KEY_DB_ID = "_id"; + public static final String KEY_GAME_PATH = "path"; + public static final String KEY_GAME_TITLE = "title"; + public static final String KEY_GAME_DESCRIPTION = "description"; + public static final String KEY_GAME_REGIONS = "regions"; + public static final String KEY_GAME_ID = "game_id"; + public static final String KEY_GAME_COMPANY = "company"; + public static final String KEY_FOLDER_PATH = "path"; + public static final String TABLE_NAME_FOLDERS = "folders"; + public static final String TABLE_NAME_GAMES = "games"; + private static final int DB_VERSION = 2; + private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; + private static final String TYPE_INTEGER = " INTEGER"; + private static final String TYPE_STRING = " TEXT"; + + private static final String CONSTRAINT_UNIQUE = " UNIQUE"; + + private static final String SEPARATOR = ", "; + + private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" + + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR + + KEY_GAME_PATH + TYPE_STRING + SEPARATOR + + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR + + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR + + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR + + KEY_GAME_ID + TYPE_STRING + SEPARATOR + + KEY_GAME_COMPANY + TYPE_STRING + ")"; + + private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" + + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR + + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")"; + + private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; + private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; + + public GameDatabase(Context context) { + // Superclass constructor builds a database or uses an existing one. + super(context, "games.db", null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase database) { + Log.debug("[GameDatabase] GameDatabase - Creating database..."); + + execSqlAndLog(database, SQL_CREATE_GAMES); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + } + + @Override + public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) { + Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases.."); + execSqlAndLog(database, SQL_DELETE_FOLDERS); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " + + newVersion); + + // Delete all the games + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + public void resetDatabase(SQLiteDatabase database) { + execSqlAndLog(database, SQL_DELETE_FOLDERS); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + public void scanLibrary(SQLiteDatabase database) { + // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. + Cursor fileCursor = database.query(TABLE_NAME_GAMES, + null, // Get all columns. + null, // Get all rows. + null, + null, // No grouping. + null, + null); // Order of games is irrelevant. + + // Possibly overly defensive, but ensures that moveToNext() does not skip a row. + fileCursor.moveToPosition(-1); + + while (fileCursor.moveToNext()) { + String gamePath = fileCursor.getString(GAME_COLUMN_PATH); + File game = new File(gamePath); + + if (!game.exists()) { + Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " + + gamePath); + database.delete(TABLE_NAME_GAMES, + KEY_DB_ID + " = ?", + new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); + } + } + + // Get a cursor listing all the folders the user has added to the library. + Cursor folderCursor = database.query(TABLE_NAME_FOLDERS, + null, // Get all columns. + null, // Get all rows. + null, + null, // No grouping. + null, + null); // Order of folders is irrelevant. + + Set allowedExtensions = new HashSet(Arrays.asList( + ".xci", ".nsp", ".nca", ".nro")); + + // Possibly overly defensive, but ensures that moveToNext() does not skip a row. + folderCursor.moveToPosition(-1); + + // Iterate through all results of the DB query (i.e. all folders in the library.) + while (folderCursor.moveToNext()) { + String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); + + File folder = new File(folderPath); + // If the folder is empty because it no longer exists, remove it from the library. + if (!folder.exists()) { + Log.error( + "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); + database.delete(TABLE_NAME_FOLDERS, + KEY_DB_ID + " = ?", + new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); + } + + addGamesRecursive(database, folder, allowedExtensions, 3); + } + + fileCursor.close(); + folderCursor.close(); + + database.close(); + } + + private static void addGamesRecursive(SQLiteDatabase database, File parent, Set allowedExtensions, int depth) { + if (depth <= 0) { + return; + } + + File[] children = parent.listFiles(); + if (children != null) { + for (File file : children) { + if (file.isHidden()) { + continue; + } + + if (file.isDirectory()) { + Set newExtensions = new HashSet<>(Arrays.asList( + ".xci", ".nsp", ".nca", ".nro")); + addGamesRecursive(database, file, newExtensions, depth - 1); + } else { + String filePath = file.getPath(); + + int extensionStart = filePath.lastIndexOf('.'); + if (extensionStart > 0) { + String fileExtension = filePath.substring(extensionStart); + + // Check that the file has an extension we care about before trying to read out of it. + if (allowedExtensions.contains(fileExtension.toLowerCase())) { + attemptToAddGame(database, filePath); + } + } + } + } + } + } + + private static void attemptToAddGame(SQLiteDatabase database, String filePath) { + String name = NativeLibrary.GetTitle(filePath); + + // If the game's title field is empty, use the filename. + if (name.isEmpty()) { + name = filePath.substring(filePath.lastIndexOf("/") + 1); + } + + String gameId = NativeLibrary.GetGameId(filePath); + + // If the game's ID field is empty, use the filename without extension. + if (gameId.isEmpty()) { + gameId = filePath.substring(filePath.lastIndexOf("/") + 1, + filePath.lastIndexOf(".")); + } + + ContentValues game = Game.asContentValues(name, + NativeLibrary.GetDescription(filePath).replace("\n", " "), + NativeLibrary.GetRegions(filePath), + filePath, + gameId, + NativeLibrary.GetCompany(filePath)); + + // Try to update an existing game first. + int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. + game, + // The values to fill the row with. + KEY_GAME_ID + " = ?", + // The WHERE clause used to find the right row. + new String[]{game.getAsString( + KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this, + // which is provided as an array because there + // could potentially be more than one argument. + + // If update fails, insert a new game instead. + if (rowsMatched == 0) { + Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)); + database.insert(TABLE_NAME_GAMES, null, game); + } else { + Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)); + } + } + + public Observable getGames() { + return Observable.create(subscriber -> + { + Log.info("[GameDatabase] Reading games list..."); + + SQLiteDatabase database = getReadableDatabase(); + Cursor resultCursor = database.query( + TABLE_NAME_GAMES, + null, + null, + null, + null, + null, + KEY_GAME_TITLE + " ASC" + ); + + // Pass the result cursor to the consumer. + subscriber.onNext(resultCursor); + + // Tell the consumer we're done; it will unsubscribe implicitly. + subscriber.onCompleted(); + }); + } + + private void execSqlAndLog(SQLiteDatabase database, String sql) { + Log.verbose("[GameDatabase] Executing SQL: " + sql); + database.execSQL(sql); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.java new file mode 100644 index 000000000..eff4b1e2d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.java @@ -0,0 +1,138 @@ +package org.yuzu.yuzu_emu.model; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.yuzu.yuzu_emu.BuildConfig; +import org.yuzu.yuzu_emu.utils.Log; + +/** + * Provides an interface allowing Activities to interact with the SQLite database. + * CRUD methods in this class can be called by Activities using getContentResolver(). + */ +public final class GameProvider extends ContentProvider { + public static final String REFRESH_LIBRARY = "refresh"; + public static final String RESET_LIBRARY = "reset"; + + public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider"; + public static final Uri URI_FOLDER = + Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/"); + public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/"); + public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/"); + + public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder"; + public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game"; + + + private GameDatabase mDbHelper; + + @Override + public boolean onCreate() { + Log.info("[GameProvider] Creating Content Provider..."); + + mDbHelper = new GameDatabase(getContext()); + + return true; + } + + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + Log.info("[GameProvider] Querying URI: " + uri); + + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + + String table = uri.getLastPathSegment(); + + if (table == null) { + Log.error("[GameProvider] Badly formatted URI: " + uri); + return null; + } + + Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + + return cursor; + } + + @Override + public String getType(@NonNull Uri uri) { + Log.verbose("[GameProvider] Getting MIME type for URI: " + uri); + String lastSegment = uri.getLastPathSegment(); + + if (lastSegment == null) { + Log.error("[GameProvider] Badly formatted URI: " + uri); + return null; + } + + if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) { + return MIME_TYPE_FOLDER; + } else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) { + return MIME_TYPE_GAME; + } + + Log.error("[GameProvider] Unknown MIME type for URI: " + uri); + return null; + } + + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + Log.info("[GameProvider] Inserting row at URI: " + uri); + + SQLiteDatabase database = mDbHelper.getWritableDatabase(); + String table = uri.getLastPathSegment(); + + if (table != null) { + if (table.equals(RESET_LIBRARY)) { + mDbHelper.resetDatabase(database); + return uri; + } + if (table.equals(REFRESH_LIBRARY)) { + Log.info( + "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."); + mDbHelper.scanLibrary(database); + return uri; + } + + long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE); + + // If insertion was successful... + if (id > 0) { + // If we just added a folder, add its contents to the game list. + if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) { + mDbHelper.scanLibrary(database); + } + + // Notify the UI that its contents should be refreshed. + getContext().getContentResolver().notifyChange(uri, null); + uri = Uri.withAppendedPath(uri, Long.toString(id)); + } else { + Log.error("[GameProvider] Row already exists: " + uri + " id: " + id); + } + } else { + Log.error("[GameProvider] Badly formatted URI: " + uri); + } + + database.close(); + + return uri; + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + Log.error("[GameProvider] Delete operations unsupported. URI: " + uri); + return 0; + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + Log.error("[GameProvider] Update operations unsupported. URI: " + uri); + return 0; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java new file mode 100644 index 000000000..043a164ce --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java @@ -0,0 +1,878 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.overlay; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnTouchListener; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.NativeLibrary.ButtonState; +import org.yuzu.yuzu_emu.NativeLibrary.ButtonType; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; + +import java.util.HashSet; +import java.util.Set; + +/** + * Draws the interactive input overlay on top of the + * {@link SurfaceView} that is rendering emulation. + */ +public final class InputOverlay extends SurfaceView implements OnTouchListener { + private final Set overlayButtons = new HashSet<>(); + private final Set overlayDpads = new HashSet<>(); + private final Set overlayJoysticks = new HashSet<>(); + + private boolean mIsInEditMode = false; + private InputOverlayDrawableButton mButtonBeingConfigured; + private InputOverlayDrawableDpad mDpadBeingConfigured; + private InputOverlayDrawableJoystick mJoystickBeingConfigured; + + private SharedPreferences mPreferences; + + // Stores the ID of the pointer that interacted with the 3DS touchscreen. + private int mTouchscreenPointerId = -1; + + /** + * Constructor + * + * @param context The current {@link Context}. + * @param attrs {@link AttributeSet} for parsing XML attributes. + */ + public InputOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (!mPreferences.getBoolean("OverlayInit", false)) { + defaultOverlay(); + } + + // Reset 3ds touchscreen pointer ID + mTouchscreenPointerId = -1; + + // Load the controls. + refreshControls(); + + // Set the on touch listener. + setOnTouchListener(this); + + // Force draw + setWillNotDraw(false); + + // Request focus for the overlay so it has priority on presses. + requestFocus(); + } + + /** + * Resizes a {@link Bitmap} by a given scale factor + * + * @param context The current {@link Context} + * @param bitmap The {@link Bitmap} to scale. + * @param scale The scale factor for the bitmap. + * @return The scaled {@link Bitmap} + */ + public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) { + // Determine the button size based on the smaller screen dimension. + // This makes sure the buttons are the same size in both portrait and landscape. + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + int minDimension = Math.min(dm.widthPixels, dm.heightPixels); + + return Bitmap.createScaledBitmap(bitmap, + (int) (minDimension * scale), + (int) (minDimension * scale), + true); + } + + /** + * Initializes an InputOverlayDrawableButton, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + *

+ * This works due to the way the X and Y coordinates are stored within + * the {@link SharedPreferences}. + *

+ * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + *

+ * This has a few benefits over the conventional way of storing the values + * (ie. within the yuzu ini file). + *

    + *
  • No native calls
  • + *
  • Keeps Android-only values inside the Android environment
  • + *
+ *

+ * Technically no modifications should need to be performed on the returned + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current {@link Context}. + * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). + * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. + * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. + */ + private static InputOverlayDrawableButton initializeOverlayButton(Context context, + int defaultResId, int pressedResId, int buttonId, String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on button ID and user preference + float scale; + + switch (buttonId) { + case ButtonType.BUTTON_HOME: + case ButtonType.BUTTON_START: + case ButtonType.BUTTON_SELECT: + scale = 0.08f; + break; + case ButtonType.TRIGGER_L: + case ButtonType.TRIGGER_R: + case ButtonType.BUTTON_ZL: + case ButtonType.BUTTON_ZR: + scale = 0.18f; + break; + default: + scale = 0.11f; + break; + } + + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableButton. + final Bitmap defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); + final Bitmap pressedStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); + final InputOverlayDrawableButton overlayDrawable = + new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + String xKey; + String yKey; + + xKey = buttonId + orientation + "-X"; + yKey = buttonId + orientation + "-Y"; + + int drawableX = (int) sPrefs.getFloat(xKey, 0f); + int drawableY = (int) sPrefs.getFloat(yKey, 0f); + + int width = overlayDrawable.getWidth(); + int height = overlayDrawable.getHeight(); + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + /** + * Initializes an {@link InputOverlayDrawableDpad} + * + * @param context The current {@link Context}. + * @param defaultResId The {@link Bitmap} resource ID of the default sate. + * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction. + * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. + * @param buttonUp Identifier for the up button. + * @param buttonDown Identifier for the down button. + * @param buttonLeft Identifier for the left button. + * @param buttonRight Identifier for the right button. + * @return the initialized {@link InputOverlayDrawableDpad} + */ + private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, + int defaultResId, + int pressedOneDirectionResId, + int pressedTwoDirectionsResId, + int buttonUp, + int buttonDown, + int buttonLeft, + int buttonRight, + String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on button ID and user preference + float scale = 0.22f; + + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableDpad. + final Bitmap defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); + final Bitmap pressedOneDirectionStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), + scale); + final Bitmap pressedTwoDirectionsStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), + scale); + final InputOverlayDrawableDpad overlayDrawable = + new InputOverlayDrawableDpad(res, defaultStateBitmap, + pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, + buttonUp, buttonDown, buttonLeft, buttonRight); + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); + int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); + + int width = overlayDrawable.getWidth(); + int height = overlayDrawable.getHeight(); + + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + /** + * Initializes an {@link InputOverlayDrawableJoystick} + * + * @param context The current {@link Context} + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). + * @param pressedResInner Resource ID for the pressed inner image of the joystick. + * @param joystick Identifier for which joystick this is. + * @return the initialized {@link InputOverlayDrawableJoystick}. + */ + private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, + int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on user preference + float scale = 0.275f; + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableJoystick. + final Bitmap bitmapOuter = + resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); + final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); + final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f); + int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f); + + // Decide inner scale based on joystick ID + float outerScale = 1.f; + if (joystick == ButtonType.STICK_C) { + outerScale = 2.f; + } + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. + int outerSize = bitmapOuter.getWidth(); + Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale)); + Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); + + // Send the drawableId to the joystick so it can be referenced when saving control position. + final InputOverlayDrawableJoystick overlayDrawable + = new InputOverlayDrawableJoystick(res, bitmapOuter, + bitmapInnerDefault, bitmapInnerPressed, + outerRect, innerRect, joystick); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + for (InputOverlayDrawableButton button : overlayButtons) { + button.draw(canvas); + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + dpad.draw(canvas); + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + joystick.draw(canvas); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isInEditMode()) { + return onTouchWhileEditing(event); + } + + int pointerIndex = event.getActionIndex(); + + if (mPreferences.getBoolean("isTouchEnabled", true)) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true)) { + mTouchscreenPointerId = event.getPointerId(pointerIndex); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mTouchscreenPointerId == event.getPointerId(pointerIndex)) { + // We don't really care where the touch has been released. We only care whether it has been + // released or not. + NativeLibrary.onTouchEvent(0, 0, false); + mTouchscreenPointerId = -1; + } + break; + } + + for (int i = 0; i < event.getPointerCount(); i++) { + if (mTouchscreenPointerId == event.getPointerId(i)) { + NativeLibrary.onTouchMoved(event.getX(i), event.getY(i)); + } + } + } + + for (InputOverlayDrawableButton button : overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If a pointer enters the bounds of a button, press that button. + if (button.getBounds() + .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + button.setPressedState(true); + button.setTrackId(event.getPointerId(pointerIndex)); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), + ButtonState.PRESSED); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + // If a pointer ends, release the button it was pressing. + if (button.getTrackId() == event.getPointerId(pointerIndex)) { + button.setPressedState(false); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), + ButtonState.RELEASED); + } + break; + } + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If a pointer enters the bounds of a button, press that button. + if (dpad.getBounds() + .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + dpad.setTrackId(event.getPointerId(pointerIndex)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + // If a pointer ends, release the buttons. + if (dpad.getTrackId() == event.getPointerId(pointerIndex)) { + for (int i = 0; i < 4; i++) { + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i), + NativeLibrary.ButtonState.RELEASED); + } + dpad.setTrackId(-1); + } + break; + } + + if (dpad.getTrackId() != -1) { + for (int i = 0; i < event.getPointerCount(); i++) { + if (dpad.getTrackId() == event.getPointerId(i)) { + float touchX = event.getX(i); + float touchY = event.getY(i); + float maxY = dpad.getBounds().bottom; + float maxX = dpad.getBounds().right; + touchX -= dpad.getBounds().centerX(); + maxX -= dpad.getBounds().centerX(); + touchY -= dpad.getBounds().centerY(); + maxY -= dpad.getBounds().centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY / maxY; + + boolean up = false; + boolean down = false; + boolean left = false; + boolean right = false; + if (EmulationMenuSettings.getDpadSlideEnable() || + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN || + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { + if (AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), + NativeLibrary.ButtonState.PRESSED); + up = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), + NativeLibrary.ButtonState.PRESSED); + down = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), + NativeLibrary.ButtonState.PRESSED); + left = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), + NativeLibrary.ButtonState.PRESSED); + right = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), + NativeLibrary.ButtonState.RELEASED); + } + + // Set state + if (up) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); + } else if (down) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); + } else if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); + } else if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); + } else { + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); + } + } + } + } + } + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + joystick.TrackEvent(event); + int axisID = joystick.getId(); + float[] axises = joystick.getAxisValues(); + + NativeLibrary + .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]); + } + + invalidate(); + + return true; + } + + public boolean onTouchWhileEditing(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + + String orientation = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? + "-Portrait" : ""; + + // Maybe combine Button and Joystick as subclasses of the same parent? + // Or maybe create an interface like IMoveableHUDControl? + + for (InputOverlayDrawableButton button : overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If no button is being moved now, remember the currently touched button to move. + if (mButtonBeingConfigured == null && + button.getBounds().contains(fingerPositionX, fingerPositionY)) { + mButtonBeingConfigured = button; + mButtonBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mButtonBeingConfigured != null) { + mButtonBeingConfigured.onConfigureTouch(event); + invalidate(); + return true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mButtonBeingConfigured == button) { + // Persist button position by saving new place. + saveControlPosition(mButtonBeingConfigured.getId(), + mButtonBeingConfigured.getBounds().left, + mButtonBeingConfigured.getBounds().top, orientation); + mButtonBeingConfigured = null; + } + break; + } + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If no button is being moved now, remember the currently touched button to move. + if (mButtonBeingConfigured == null && + dpad.getBounds().contains(fingerPositionX, fingerPositionY)) { + mDpadBeingConfigured = dpad; + mDpadBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mDpadBeingConfigured != null) { + mDpadBeingConfigured.onConfigureTouch(event); + invalidate(); + return true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mDpadBeingConfigured == dpad) { + // Persist button position by saving new place. + saveControlPosition(mDpadBeingConfigured.getId(0), + mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, + orientation); + mDpadBeingConfigured = null; + } + break; + } + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (mJoystickBeingConfigured == null && + joystick.getBounds().contains(fingerPositionX, fingerPositionY)) { + mJoystickBeingConfigured = joystick; + mJoystickBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mJoystickBeingConfigured != null) { + mJoystickBeingConfigured.onConfigureTouch(event); + invalidate(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mJoystickBeingConfigured != null) { + saveControlPosition(mJoystickBeingConfigured.getId(), + mJoystickBeingConfigured.getBounds().left, + mJoystickBeingConfigured.getBounds().top, orientation); + mJoystickBeingConfigured = null; + } + break; + } + } + + return true; + } + + private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, + boolean right) { + if (up) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); + } else if (down) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); + } else if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); + } else if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); + } + } + + private void addOverlayControls(String orientation) { + if (mPreferences.getBoolean("buttonToggle0", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a, + R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation)); + } + if (mPreferences.getBoolean("buttonToggle1", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b, + R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation)); + } + if (mPreferences.getBoolean("buttonToggle2", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x, + R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation)); + } + if (mPreferences.getBoolean("buttonToggle3", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y, + R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation)); + } + if (mPreferences.getBoolean("buttonToggle4", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, + R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation)); + } + if (mPreferences.getBoolean("buttonToggle5", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, + R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation)); + } + if (mPreferences.getBoolean("buttonToggle6", false)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl, + R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation)); + } + if (mPreferences.getBoolean("buttonToggle7", false)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr, + R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation)); + } + if (mPreferences.getBoolean("buttonToggle8", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, + R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation)); + } + if (mPreferences.getBoolean("buttonToggle9", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, + R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle10", true)) { + overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad, + R.drawable.dpad_pressed_one_direction, + R.drawable.dpad_pressed_two_directions, + ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, + ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle11", true)) { + overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range, + R.drawable.stick_main, R.drawable.stick_main_pressed, + ButtonType.STICK_LEFT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle12", false)) { + overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range, + R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation)); + } + } + + public void refreshControls() { + // Remove all the overlay buttons from the HashSet. + overlayButtons.clear(); + overlayDpads.clear(); + overlayJoysticks.clear(); + + String orientation = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? + "-Portrait" : ""; + + // Add all the enabled overlay items back to the HashSet. + if (EmulationMenuSettings.getShowOverlay()) { + addOverlayControls(orientation); + } + + invalidate(); + } + + private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); + sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); + sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); + sPrefsEditor.apply(); + } + + public void setIsInEditMode(boolean isInEditMode) { + mIsInEditMode = isInEditMode; + } + + private void defaultOverlay() { + if (!mPreferences.getBoolean("OverlayInit", false)) { + // It's possible that a user has created their overlay before this was added + // Only change the overlay if the 'A' button is not in the upper corner. + if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) { + defaultOverlayLandscape(); + } + if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) { + defaultOverlayPortrait(); + } + } + + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + sPrefsEditor.putBoolean("OverlayInit", true); + sPrefsEditor.apply(); + } + + public void resetButtonPlacement() { + boolean isLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + + if (isLandscape) { + defaultOverlayLandscape(); + } else { + defaultOverlayPortrait(); + } + + refreshControls(); + } + + private void defaultOverlayLandscape() { + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + // Get screen size + Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); + DisplayMetrics outMetrics = new DisplayMetrics(); + display.getMetrics(outMetrics); + float maxX = outMetrics.heightPixels; + float maxY = outMetrics.widthPixels; + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY > maxX) { + float tmp = maxX; + maxX = maxY; + maxY = tmp; + } + Resources res = getResources(); + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY)); + + // We want to commit right away, otherwise the overlay could load before this is saved. + sPrefsEditor.commit(); + } + + private void defaultOverlayPortrait() { + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + // Get screen size + Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); + DisplayMetrics outMetrics = new DisplayMetrics(); + display.getMetrics(outMetrics); + float maxX = outMetrics.heightPixels; + float maxY = outMetrics.widthPixels; + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY < maxX) { + float tmp = maxX; + maxX = maxY; + maxY = tmp; + } + Resources res = getResources(); + String portrait = "-Portrait"; + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); + + // We want to commit right away, otherwise the overlay could load before this is saved. + sPrefsEditor.commit(); + } + + public boolean isInEditMode() { + return mIsInEditMode; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.java new file mode 100644 index 000000000..fe523c6c4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.java @@ -0,0 +1,122 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableButton { + // The ID identifying what type of button this Drawable represents. + private int mButtonType; + private int mTrackId; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mWidth; + private int mHeight; + private BitmapDrawable mDefaultStateBitmap; + private BitmapDrawable mPressedStateBitmap; + private boolean mPressedState = false; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. + * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. + * @param buttonType Identifier for this type of button. + */ + public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, + Bitmap pressedStateBitmap, int buttonType) { + mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); + mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); + mButtonType = buttonType; + + mWidth = mDefaultStateBitmap.getIntrinsicWidth(); + mHeight = mDefaultStateBitmap.getIntrinsicHeight(); + } + + /** + * Gets this InputOverlayDrawableButton's button ID. + * + * @return this InputOverlayDrawableButton's button ID. + */ + public int getId() { + return mButtonType; + } + + public int getTrackId() { + return mTrackId; + } + + public void setTrackId(int trackId) { + mTrackId = trackId; + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + mControlPositionX += fingerPositionX - mPreviousTouchX; + mControlPositionY += fingerPositionY - mPreviousTouchY; + setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, + getHeight() + mControlPositionY); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + + } + return true; + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void draw(Canvas canvas) { + getCurrentStateBitmapDrawable().draw(canvas); + } + + private BitmapDrawable getCurrentStateBitmapDrawable() { + return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; + } + + public void setBounds(int left, int top, int right, int bottom) { + mDefaultStateBitmap.setBounds(left, top, right, bottom); + mPressedStateBitmap.setBounds(left, top, right, bottom); + } + + public Rect getBounds() { + return mDefaultStateBitmap.getBounds(); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void setPressedState(boolean isPressed) { + mPressedState = isPressed; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.java new file mode 100644 index 000000000..a14840d98 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.java @@ -0,0 +1,193 @@ +/** + * Copyright 2016 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableDpad { + public static final int STATE_DEFAULT = 0; + public static final int STATE_PRESSED_UP = 1; + public static final int STATE_PRESSED_DOWN = 2; + public static final int STATE_PRESSED_LEFT = 3; + public static final int STATE_PRESSED_RIGHT = 4; + public static final int STATE_PRESSED_UP_LEFT = 5; + public static final int STATE_PRESSED_UP_RIGHT = 6; + public static final int STATE_PRESSED_DOWN_LEFT = 7; + public static final int STATE_PRESSED_DOWN_RIGHT = 8; + public static final float VIRT_AXIS_DEADZONE = 0.5f; + // The ID identifying what type of button this Drawable represents. + private int[] mButtonType = new int[4]; + private int mTrackId; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mWidth; + private int mHeight; + private BitmapDrawable mDefaultStateBitmap; + private BitmapDrawable mPressedOneDirectionStateBitmap; + private BitmapDrawable mPressedTwoDirectionsStateBitmap; + private int mPressState = STATE_DEFAULT; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param defaultStateBitmap {@link Bitmap} of the default state. + * @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction. + * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. + * @param buttonUp Identifier for the up button. + * @param buttonDown Identifier for the down button. + * @param buttonLeft Identifier for the left button. + * @param buttonRight Identifier for the right button. + */ + public InputOverlayDrawableDpad(Resources res, + Bitmap defaultStateBitmap, + Bitmap pressedOneDirectionStateBitmap, + Bitmap pressedTwoDirectionsStateBitmap, + int buttonUp, int buttonDown, + int buttonLeft, int buttonRight) { + mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); + mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); + mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); + + mWidth = mDefaultStateBitmap.getIntrinsicWidth(); + mHeight = mDefaultStateBitmap.getIntrinsicHeight(); + + mButtonType[0] = buttonUp; + mButtonType[1] = buttonDown; + mButtonType[2] = buttonLeft; + mButtonType[3] = buttonRight; + + mTrackId = -1; + } + + public void draw(Canvas canvas) { + int px = mControlPositionX + (getWidth() / 2); + int py = mControlPositionY + (getHeight() / 2); + switch (mPressState) { + case STATE_DEFAULT: + mDefaultStateBitmap.draw(canvas); + break; + case STATE_PRESSED_UP: + mPressedOneDirectionStateBitmap.draw(canvas); + break; + case STATE_PRESSED_RIGHT: + canvas.save(); + canvas.rotate(90, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN: + canvas.save(); + canvas.rotate(180, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_LEFT: + canvas.save(); + canvas.rotate(270, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_UP_LEFT: + mPressedTwoDirectionsStateBitmap.draw(canvas); + break; + case STATE_PRESSED_UP_RIGHT: + canvas.save(); + canvas.rotate(90, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN_RIGHT: + canvas.save(); + canvas.rotate(180, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN_LEFT: + canvas.save(); + canvas.rotate(270, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + } + } + + /** + * Gets one of the InputOverlayDrawableDpad's button IDs. + * + * @return the requested InputOverlayDrawableDpad's button ID. + */ + public int getId(int direction) { + return mButtonType[direction]; + } + + public int getTrackId() { + return mTrackId; + } + + public void setTrackId(int trackId) { + mTrackId = trackId; + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + mControlPositionX += fingerPositionX - mPreviousTouchX; + mControlPositionY += fingerPositionY - mPreviousTouchY; + setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, + getHeight() + mControlPositionY); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + + } + return true; + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void setBounds(int left, int top, int right, int bottom) { + mDefaultStateBitmap.setBounds(left, top, right, bottom); + mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); + mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); + } + + public Rect getBounds() { + return mDefaultStateBitmap.getBounds(); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void setState(int pressState) { + mPressState = pressState; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.java new file mode 100644 index 000000000..875719311 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.java @@ -0,0 +1,264 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +import org.yuzu.yuzu_emu.NativeLibrary.ButtonType; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableJoystick { + private final int[] axisIDs = {0, 0, 0, 0}; + private final float[] axises = {0f, 0f}; + private int trackId = -1; + private int mJoystickType; + private int mControlPositionX, mControlPositionY; + private int mPreviousTouchX, mPreviousTouchY; + private int mWidth; + private int mHeight; + private Rect mVirtBounds; + private Rect mOrigBounds; + private BitmapDrawable mOuterBitmap; + private BitmapDrawable mDefaultStateInnerBitmap; + private BitmapDrawable mPressedStateInnerBitmap; + private BitmapDrawable mBoundsBoxBitmap; + private boolean mPressedState = false; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick. + * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. + * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. + * @param rectOuter {@link Rect} which represents the outer joystick bounds. + * @param rectInner {@link Rect} which represents the inner joystick bounds. + * @param joystick Identifier for which joystick this is. + */ + public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, + Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed, + Rect rectOuter, Rect rectInner, int joystick) { + axisIDs[0] = joystick + 1; // Up + axisIDs[1] = joystick + 2; // Down + axisIDs[2] = joystick + 3; // Left + axisIDs[3] = joystick + 4; // Right + mJoystickType = joystick; + + mOuterBitmap = new BitmapDrawable(res, bitmapOuter); + mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); + mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); + mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); + mWidth = bitmapOuter.getWidth(); + mHeight = bitmapOuter.getHeight(); + + setBounds(rectOuter); + mDefaultStateInnerBitmap.setBounds(rectInner); + mPressedStateInnerBitmap.setBounds(rectInner); + mVirtBounds = getBounds(); + mOrigBounds = mOuterBitmap.copyBounds(); + mBoundsBoxBitmap.setAlpha(0); + mBoundsBoxBitmap.setBounds(getVirtBounds()); + SetInnerBounds(); + } + + /** + * Gets this InputOverlayDrawableJoystick's button ID. + * + * @return this InputOverlayDrawableJoystick's button ID. + */ + public int getId() { + return mJoystickType; + } + + public void draw(Canvas canvas) { + mOuterBitmap.draw(canvas); + getCurrentStateBitmapDrawable().draw(canvas); + mBoundsBoxBitmap.draw(canvas); + } + + public void TrackEvent(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + mPressedState = true; + mOuterBitmap.setAlpha(0); + mBoundsBoxBitmap.setAlpha(255); + if (EmulationMenuSettings.getJoystickRelCenter()) { + getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(), + (int) event.getY(pointerIndex) - getVirtBounds().centerY()); + } + mBoundsBoxBitmap.setBounds(getVirtBounds()); + trackId = event.getPointerId(pointerIndex); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (trackId == event.getPointerId(pointerIndex)) { + mPressedState = false; + axises[0] = axises[1] = 0.0f; + mOuterBitmap.setAlpha(255); + mBoundsBoxBitmap.setAlpha(0); + setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + SetInnerBounds(); + trackId = -1; + } + break; + } + + if (trackId == -1) + return; + + for (int i = 0; i < event.getPointerCount(); i++) { + if (trackId == event.getPointerId(i)) { + float touchX = event.getX(i); + float touchY = event.getY(i); + float maxY = getVirtBounds().bottom; + float maxX = getVirtBounds().right; + touchX -= getVirtBounds().centerX(); + maxX -= getVirtBounds().centerX(); + touchY -= getVirtBounds().centerY(); + maxY -= getVirtBounds().centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY / maxY; + + // Clamp the circle pad input to a circle + final float angle = (float) Math.atan2(AxisY, AxisX); + float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY); + if(radius > 1.0f) + { + radius = 1.0f; + } + axises[0] = ((float)Math.cos(angle) * radius); + axises[1] = ((float)Math.sin(angle) * radius); + SetInnerBounds(); + } + } + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + + int scale = 1; + if (mJoystickType == ButtonType.STICK_C) { + // C-stick is scaled down to be half the size of the circle pad + scale = 2; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + int deltaX = fingerPositionX - mPreviousTouchX; + int deltaY = fingerPositionY - mPreviousTouchY; + mControlPositionX += deltaX; + mControlPositionY += deltaY; + setBounds(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); + setVirtBounds(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); + SetInnerBounds(); + setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY))); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + } + return true; + } + + + public float[] getAxisValues() { + return axises; + } + + public int[] getAxisIDs() { + return axisIDs; + } + + private void SetInnerBounds() { + int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2)); + int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2)); + + if (mJoystickType == ButtonType.STICK_LEFT) { + X += 1; + Y += 1; + } + + if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2)) + X = getVirtBounds().centerX() + (getVirtBounds().width() / 2); + if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2)) + X = getVirtBounds().centerX() - (getVirtBounds().width() / 2); + if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2)) + Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2); + if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2)) + Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2); + + int width = mPressedStateInnerBitmap.getBounds().width() / 2; + int height = mPressedStateInnerBitmap.getBounds().height() / 2; + mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); + mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + private BitmapDrawable getCurrentStateBitmapDrawable() { + return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; + } + + public Rect getBounds() { + return mOuterBitmap.getBounds(); + } + + public void setBounds(Rect bounds) { + mOuterBitmap.setBounds(bounds); + } + + private void setOrigBounds(Rect bounds) { + mOrigBounds = bounds; + } + + private Rect getVirtBounds() { + return mVirtBounds; + } + + private void setVirtBounds(Rect bounds) { + mVirtBounds = bounds; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/DividerItemDecoration.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/DividerItemDecoration.java new file mode 100644 index 000000000..f725382e6 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/DividerItemDecoration.java @@ -0,0 +1,130 @@ +package org.yuzu.yuzu_emu.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Implementation from: + * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36 + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private Drawable mDivider; + private boolean mShowFirstDivider = false; + private boolean mShowLastDivider = false; + + public DividerItemDecoration(Context context, AttributeSet attrs) { + final TypedArray a = context + .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider}); + mDivider = a.getDrawable(0); + a.recycle(); + } + + public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider, + boolean showLastDivider) { + this(context, attrs); + mShowFirstDivider = showFirstDivider; + mShowLastDivider = showLastDivider; + } + + public DividerItemDecoration(Drawable divider) { + mDivider = divider; + } + + public DividerItemDecoration(Drawable divider, boolean showFirstDivider, + boolean showLastDivider) { + this(divider); + mShowFirstDivider = showFirstDivider; + mShowLastDivider = showLastDivider; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, + @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + if (mDivider == null) { + return; + } + if (parent.getChildAdapterPosition(view) < 1) { + return; + } + + if (getOrientation(parent) == LinearLayoutManager.VERTICAL) { + outRect.top = mDivider.getIntrinsicHeight(); + } else { + outRect.left = mDivider.getIntrinsicWidth(); + } + } + + @Override + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + if (mDivider == null) { + super.onDrawOver(c, parent, state); + return; + } + + // Initialization needed to avoid compiler warning + int left = 0, right = 0, top = 0, bottom = 0, size; + int orientation = getOrientation(parent); + int childCount = parent.getChildCount(); + + if (orientation == LinearLayoutManager.VERTICAL) { + size = mDivider.getIntrinsicHeight(); + left = parent.getPaddingLeft(); + right = parent.getWidth() - parent.getPaddingRight(); + } else { //horizontal + size = mDivider.getIntrinsicWidth(); + top = parent.getPaddingTop(); + bottom = parent.getHeight() - parent.getPaddingBottom(); + } + + for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) { + View child = parent.getChildAt(i); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + + if (orientation == LinearLayoutManager.VERTICAL) { + top = child.getTop() - params.topMargin; + bottom = top + size; + } else { //horizontal + left = child.getLeft() - params.leftMargin; + right = left + size; + } + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + + // show last divider + if (mShowLastDivider && childCount > 0) { + View child = parent.getChildAt(childCount - 1); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + if (orientation == LinearLayoutManager.VERTICAL) { + top = child.getBottom() + params.bottomMargin; + bottom = top + size; + } else { // horizontal + left = child.getRight() + params.rightMargin; + right = left + size; + } + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + private int getOrientation(RecyclerView parent) { + if (parent.getLayoutManager() instanceof LinearLayoutManager) { + LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); + return layoutManager.getOrientation(); + } else { + throw new IllegalStateException( + "DividerItemDecoration can only be used with a LinearLayoutManager."); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/TwoPaneOnBackPressedCallback.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/TwoPaneOnBackPressedCallback.java new file mode 100644 index 000000000..78e5f0965 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/TwoPaneOnBackPressedCallback.java @@ -0,0 +1,37 @@ +package org.yuzu.yuzu_emu.ui; + +import android.view.View; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.slidingpanelayout.widget.SlidingPaneLayout; + +public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback + implements SlidingPaneLayout.PanelSlideListener { + private final SlidingPaneLayout mSlidingPaneLayout; + + public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) { + super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen()); + mSlidingPaneLayout = slidingPaneLayout; + slidingPaneLayout.addPanelSlideListener(this); + } + + @Override + public void handleOnBackPressed() { + mSlidingPaneLayout.close(); + } + + @Override + public void onPanelSlide(@NonNull View panel, float slideOffset) { + } + + @Override + public void onPanelOpened(@NonNull View panel) { + setEnabled(true); + } + + @Override + public void onPanelClosed(@NonNull View panel) { + setEnabled(false); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java new file mode 100644 index 000000000..6558a05c9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java @@ -0,0 +1,267 @@ +package org.yuzu.yuzu_emu.ui.main; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity; +import org.yuzu.yuzu_emu.model.GameProvider; +import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment; +import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; +import org.yuzu.yuzu_emu.utils.BillingManager; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.FileBrowserHelper; +import org.yuzu.yuzu_emu.utils.PermissionsHandler; +import org.yuzu.yuzu_emu.utils.PicassoUtils; +import org.yuzu.yuzu_emu.utils.StartupHandler; +import org.yuzu.yuzu_emu.utils.ThemeUtil; + +import java.util.Arrays; +import java.util.Collections; + +/** + * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which + * individually display a grid of available games for each Fragment, in a tabbed layout. + */ +public final class MainActivity extends AppCompatActivity implements MainView { + private Toolbar mToolbar; + private int mFrameLayoutId; + private PlatformGamesFragment mPlatformGamesFragment; + + private MainPresenter mPresenter = new MainPresenter(this); + + // Singleton to manage user billing state + private static BillingManager mBillingManager; + + private static MenuItem mPremiumButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + ThemeUtil.applyTheme(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + findViews(); + + setSupportActionBar(mToolbar); + + mFrameLayoutId = R.id.games_platform_frame; + mPresenter.onCreate(); + + if (savedInstanceState == null) { + StartupHandler.HandleInit(this); + if (PermissionsHandler.hasWriteAccess(this)) { + mPlatformGamesFragment = new PlatformGamesFragment(); + getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) + .commit(); + } + } else { + mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment"); + } + PicassoUtils.init(); + + // Setup billing manager, so we can globally query for Premium status + mBillingManager = new BillingManager(this); + + // Dismiss previous notifications (should not happen unless a crash occurred) + EmulationActivity.tryDismissRunningNotification(this); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (PermissionsHandler.hasWriteAccess(this)) { + if (getSupportFragmentManager() == null) { + return; + } + if (outState == null) { + return; + } + getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment); + } + } + + @Override + protected void onResume() { + super.onResume(); + mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); + } + + // TODO: Replace with a ButterKnife injection. + private void findViews() { + mToolbar = findViewById(R.id.toolbar_main); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_game_grid, menu); + mPremiumButton = menu.findItem(R.id.button_premium); + + if (mBillingManager.isPremiumCached()) { + // User had premium in a previous session, hide upsell option + setPremiumButtonVisible(false); + } + + return true; + } + + static public void setPremiumButtonVisible(boolean isVisible) { + if (mPremiumButton != null) { + mPremiumButton.setVisible(isVisible); + } + } + + /** + * MainView + */ + + @Override + public void setVersionString(String version) { + mToolbar.setSubtitle(version); + } + + @Override + public void refresh() { + getContentResolver().insert(GameProvider.URI_REFRESH, null); + refreshFragment(); + } + + @Override + public void launchSettingsActivity(String menuTag) { + if (PermissionsHandler.hasWriteAccess(this)) { + SettingsActivity.launch(this, menuTag, ""); + } else { + PermissionsHandler.checkWritePermission(this); + } + } + + @Override + public void launchFileListActivity(int request) { + if (PermissionsHandler.hasWriteAccess(this)) { + switch (request) { + case MainPresenter.REQUEST_ADD_DIRECTORY: + FileBrowserHelper.openDirectoryPicker(this, + MainPresenter.REQUEST_ADD_DIRECTORY, + R.string.select_game_folder, + Arrays.asList("xci", "nsp", "cci", "3ds", + "cxi", "app", "3dsx", "cia", + "rar", "zip", "7z", "torrent", + "tar", "gz", "nro")); + break; + case MainPresenter.REQUEST_INSTALL_CIA: + FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA, + R.string.install_cia_title, + Collections.singletonList("cia"), true); + break; + } + } else { + PermissionsHandler.checkWritePermission(this); + } + } + + /** + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + super.onActivityResult(requestCode, resultCode, result); + switch (requestCode) { + case MainPresenter.REQUEST_ADD_DIRECTORY: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + // When a new directory is picked, we currently will reset the existing games + // database. This effectively means that only one game directory is supported. + // TODO(bunnei): Consider fixing this in the future, or removing code for this. + getContentResolver().insert(GameProvider.URI_RESET, null); + // Add the new directory + mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result)); + } + break; + case MainPresenter.REQUEST_INSTALL_CIA: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + mPresenter.refeshGameList(); + } + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + DirectoryInitialization.start(this); + + mPlatformGamesFragment = new PlatformGamesFragment(); + getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) + .commit(); + + // Immediately prompt user to select a game directory on first boot + if (mPresenter != null) { + mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); + } + } else { + Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + break; + } + } + + /** + * Called by the framework whenever any actionbar/toolbar icon is clicked. + * + * @param item The icon that was clicked on. + * @return True if the event was handled, false to bubble it up to the OS. + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return mPresenter.handleOptionSelection(item.getItemId()); + } + + private void refreshFragment() { + if (mPlatformGamesFragment != null) { + mPlatformGamesFragment.refresh(); + } + } + + @Override + protected void onDestroy() { + EmulationActivity.tryDismissRunningNotification(this); + super.onDestroy(); + } + + /** + * @return true if Premium subscription is currently active + */ + public static boolean isPremiumActive() { + return mBillingManager.isPremiumActive(); + } + + /** + * Invokes the billing flow for Premium + * + * @param callback Optional callback, called once, on completion of billing + */ + public static void invokePremiumBilling(Runnable callback) { + mBillingManager.invokePremiumBilling(callback); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java new file mode 100644 index 000000000..2608df2c2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java @@ -0,0 +1,82 @@ +package org.yuzu.yuzu_emu.ui.main; + +import android.os.SystemClock; + +import org.yuzu.yuzu_emu.BuildConfig; +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.model.Settings; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; +import org.yuzu.yuzu_emu.model.GameDatabase; +import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; + +public final class MainPresenter { + public static final int REQUEST_ADD_DIRECTORY = 1; + public static final int REQUEST_INSTALL_CIA = 2; + + private final MainView mView; + private String mDirToAdd; + private long mLastClickTime = 0; + + public MainPresenter(MainView view) { + mView = view; + } + + public void onCreate() { + String versionName = BuildConfig.VERSION_NAME; + mView.setVersionString(versionName); + refeshGameList(); + } + + public void launchFileListActivity(int request) { + if (mView != null) { + mView.launchFileListActivity(request); + } + } + + public boolean handleOptionSelection(int itemId) { + // Double-click prevention, using threshold of 500 ms + if (SystemClock.elapsedRealtime() - mLastClickTime < 500) { + return false; + } + mLastClickTime = SystemClock.elapsedRealtime(); + + switch (itemId) { + case R.id.menu_settings_core: + mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG); + return true; + + case R.id.button_add_directory: + launchFileListActivity(REQUEST_ADD_DIRECTORY); + return true; + + case R.id.button_install_cia: + launchFileListActivity(REQUEST_INSTALL_CIA); + return true; + + case R.id.button_premium: + mView.launchSettingsActivity(Settings.SECTION_PREMIUM); + return true; + } + + return false; + } + + public void addDirIfNeeded(AddDirectoryHelper helper) { + if (mDirToAdd != null) { + helper.addDirectory(mDirToAdd, mView::refresh); + + mDirToAdd = null; + } + } + + public void onDirectorySelected(String dir) { + mDirToAdd = dir; + } + + public void refeshGameList() { + GameDatabase databaseHelper = YuzuApplication.databaseHelper; + databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); + mView.refresh(); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.java new file mode 100644 index 000000000..c2ff086ee --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.java @@ -0,0 +1,25 @@ +package org.yuzu.yuzu_emu.ui.main; + +/** + * Abstraction for the screen that shows on application launch. + * Implementations will differ primarily to target touch-screen + * or non-touch screen devices. + */ +public interface MainView { + /** + * Pass the view the native library's version string. Displaying + * it is optional. + * + * @param version A string pulled from native code. + */ + void setVersionString(String version); + + /** + * Tell the view to refresh its contents. + */ + void refresh(); + + void launchSettingsActivity(String menuTag); + + void launchFileListActivity(int request); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java new file mode 100644 index 000000000..6c327b1b8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java @@ -0,0 +1,86 @@ +package org.yuzu.yuzu_emu.ui.platform; + +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.adapters.GameAdapter; +import org.yuzu.yuzu_emu.model.GameDatabase; + +public final class PlatformGamesFragment extends Fragment implements PlatformGamesView { + private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this); + + private GameAdapter mAdapter; + private RecyclerView mRecyclerView; + private TextView mTextView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_grid, container, false); + + findViews(rootView); + + mPresenter.onCreateView(); + + return rootView; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + int columns = getResources().getInteger(R.integer.game_grid_columns); + RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns); + mAdapter = new GameAdapter(); + + mRecyclerView.setLayoutManager(layoutManager); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); + + // Add swipe down to refresh gesture + final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); + pullToRefresh.setOnRefreshListener(() -> { + GameDatabase databaseHelper = YuzuApplication.databaseHelper; + databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); + refresh(); + pullToRefresh.setRefreshing(false); + }); + } + + @Override + public void refresh() { + mPresenter.refresh(); + updateTextView(); + } + + @Override + public void showGames(Cursor games) { + if (mAdapter != null) { + mAdapter.swapCursor(games); + } + updateTextView(); + } + + private void updateTextView() { + mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + + private void findViews(View root) { + mRecyclerView = root.findViewById(R.id.grid_games); + mTextView = root.findViewById(R.id.gamelist_empty_text); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.java new file mode 100644 index 000000000..effd4a7d1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.java @@ -0,0 +1,42 @@ +package org.yuzu.yuzu_emu.ui.platform; + + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.model.GameDatabase; +import org.yuzu.yuzu_emu.utils.Log; + +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +public final class PlatformGamesPresenter { + private final PlatformGamesView mView; + + public PlatformGamesPresenter(PlatformGamesView view) { + mView = view; + } + + public void onCreateView() { + loadGames(); + } + + public void refresh() { + Log.debug("[PlatformGamesPresenter] : Refreshing..."); + loadGames(); + } + + private void loadGames() { + Log.debug("[PlatformGamesPresenter] : Loading games..."); + + GameDatabase databaseHelper = YuzuApplication.databaseHelper; + + databaseHelper.getGames() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(games -> + { + Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor..."); + + mView.showGames(games); + }); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.java new file mode 100644 index 000000000..a4e3f1eea --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.java @@ -0,0 +1,21 @@ +package org.yuzu.yuzu_emu.ui.platform; + +import android.database.Cursor; + +/** + * Abstraction for a screen representing a single platform's games. + */ +public interface PlatformGamesView { + /** + * Tell the view to refresh its contents. + */ + void refresh(); + + /** + * To be called when an asynchronous database read completes. Passes the + * result, in this case a {@link Cursor}, to the view. + * + * @param games A Cursor containing the games read from the database. + */ + void showGames(Cursor games); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.java new file mode 100644 index 000000000..2d99138f7 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.java @@ -0,0 +1,5 @@ +package org.yuzu.yuzu_emu.utils; + +public interface Action1 { + void call(T t); +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.java new file mode 100644 index 000000000..2a90e75d8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.java @@ -0,0 +1,38 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.AsyncQueryHandler; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; + +import org.yuzu.yuzu_emu.model.GameDatabase; +import org.yuzu.yuzu_emu.model.GameProvider; + +public class AddDirectoryHelper { + private Context mContext; + + public AddDirectoryHelper(Context context) { + this.mContext = context; + } + + public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) { + AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) { + @Override + protected void onInsertComplete(int token, Object cookie, Uri uri) { + addDirectoryListener.onDirectoryAdded(); + } + }; + + ContentValues file = new ContentValues(); + file.put(GameDatabase.KEY_FOLDER_PATH, dir); + + handler.startInsert(0, // We don't need to identify this call to the handler + null, // We don't need to pass additional data to the handler + GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder + file); + } + + public interface AddDirectoryListener { + void onDirectoryAdded(); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.java new file mode 100644 index 000000000..eaf25fa43 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.java @@ -0,0 +1,22 @@ +package org.yuzu.yuzu_emu.utils; + +import java.util.HashMap; +import java.util.Map; + +public class BiMap { + private Map forward = new HashMap(); + private Map backward = new HashMap(); + + public synchronized void add(K key, V value) { + forward.put(key, value); + backward.put(value, key); + } + + public synchronized V getForward(K key) { + return forward.get(key); + } + + public synchronized K getBackward(V key) { + return backward.get(key); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BillingManager.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BillingManager.java new file mode 100644 index 000000000..3d6dd1481 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BillingManager.java @@ -0,0 +1,215 @@ +package org.yuzu.yuzu_emu.utils; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchasesUpdatedListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; +import org.yuzu.yuzu_emu.ui.main.MainActivity; + +import java.util.ArrayList; +import java.util.List; + +public class BillingManager implements PurchasesUpdatedListener { + private final String BILLING_SKU_PREMIUM = "yuzu.yuzu_emu.product_id.premium"; + + private final Activity mActivity; + private BillingClient mBillingClient; + private SkuDetails mSkuPremium; + private boolean mIsPremiumActive = false; + private boolean mIsServiceConnected = false; + private Runnable mUpdateBillingCallback; + + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + + public BillingManager(Activity activity) { + mActivity = activity; + mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build(); + querySkuDetails(); + } + + static public boolean isPremiumCached() { + return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false); + } + + /** + * @return true if Premium subscription is currently active + */ + public boolean isPremiumActive() { + return mIsPremiumActive; + } + + /** + * Invokes the billing flow for Premium + * + * @param callback Optional callback, called once, on completion of billing + */ + public void invokePremiumBilling(Runnable callback) { + if (mSkuPremium == null) { + return; + } + + // Optional callback to refresh the UI for the caller when billing completes + mUpdateBillingCallback = callback; + + // Invoke the billing flow + BillingFlowParams flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(mSkuPremium) + .build(); + mBillingClient.launchBillingFlow(mActivity, flowParams); + } + + private void updatePremiumState(boolean isPremiumActive) { + mIsPremiumActive = isPremiumActive; + + // Cache state for synchronous UI + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive); + editor.apply(); + + // No need to show button in action bar if Premium is active + MainActivity.setPremiumButtonVisible(!isPremiumActive); + } + + @Override + public void onPurchasesUpdated(BillingResult billingResult, List purchaseList) { + if (purchaseList == null || purchaseList.isEmpty()) { + // Premium is not active, or billing is unavailable + updatePremiumState(false); + return; + } + + Purchase premiumPurchase = null; + for (Purchase purchase : purchaseList) { + if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) { + premiumPurchase = purchase; + } + } + + if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { + // Premium has been purchased + updatePremiumState(true); + + // Acknowledge the purchase if it hasn't already been acknowledged. + if (!premiumPurchase.isAcknowledged()) { + AcknowledgePurchaseParams acknowledgePurchaseParams = + AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(premiumPurchase.getPurchaseToken()) + .build(); + + AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> { + Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show(); + }; + mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); + } + + if (mUpdateBillingCallback != null) { + try { + mUpdateBillingCallback.run(); + } catch (Exception e) { + e.printStackTrace(); + } + mUpdateBillingCallback = null; + } + } + } + + private void onQuerySkuDetailsFinished(List skuDetailsList) { + if (skuDetailsList == null) { + // This can happen when no user is signed in + return; + } + + if (skuDetailsList.isEmpty()) { + return; + } + + mSkuPremium = skuDetailsList.get(0); + + queryPurchases(); + } + + private void querySkuDetails() { + Runnable queryToExecute = new Runnable() { + @Override + public void run() { + SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); + List skuList = new ArrayList<>(); + + skuList.add(BILLING_SKU_PREMIUM); + params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); + + mBillingClient.querySkuDetailsAsync(params.build(), + (billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList)); + } + }; + + executeServiceRequest(queryToExecute); + } + + private void onQueryPurchasesFinished(PurchasesResult result) { + // Have we been disposed of in the meantime? If so, or bad result code, then quit + if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) { + updatePremiumState(false); + return; + } + // Update the UI and purchases inventory with new list of purchases + onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList()); + } + + private void queryPurchases() { + Runnable queryToExecute = new Runnable() { + @Override + public void run() { + final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); + onQueryPurchasesFinished(purchasesResult); + } + }; + + executeServiceRequest(queryToExecute); + } + + private void startServiceConnection(final Runnable executeOnFinish) { + mBillingClient.startConnection(new BillingClientStateListener() { + @Override + public void onBillingSetupFinished(BillingResult billingResult) { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + mIsServiceConnected = true; + } + + if (executeOnFinish != null) { + executeOnFinish.run(); + } + } + + @Override + public void onBillingServiceDisconnected() { + mIsServiceConnected = false; + } + }); + } + + private void executeServiceRequest(Runnable runnable) { + if (mIsServiceConnected) { + runnable.run(); + } else { + // If billing service was disconnected, we try to reconnect 1 time. + startServiceConnection(runnable); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.java new file mode 100644 index 000000000..92fa50edf --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.java @@ -0,0 +1,66 @@ +package org.yuzu.yuzu_emu.utils; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * Some controllers have incorrect mappings. This class has special-case fixes for them. + */ +public class ControllerMappingHelper { + /** + * Some controllers report extra button presses that can be ignored. + */ + public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) { + if (isDualShock4(inputDevice)) { + // The two analog triggers generate analog motion events as well as a keycode. + // We always prefer to use the analog values, so throw away the button press + return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2; + } + return false; + } + + /** + * Scale an axis to be zero-centered with a proper range. + */ + public float scaleAxis(InputDevice inputDevice, int axis, float value) { + if (isDualShock4(inputDevice)) { + // Android doesn't have correct mappings for this controller's triggers. It reports them + // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] + // Scale them to properly zero-centered with a range of [0.0, 1.0]. + if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { + return (value + 1) / 2.0f; + } + } else if (isXboxOneWireless(inputDevice)) { + // Same as the DualShock 4, the mappings are missing. + if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { + return (value + 1) / 2.0f; + } + if (axis == MotionEvent.AXIS_GENERIC_1) { + // This axis is stuck at ~.5. Ignore it. + return 0.0f; + } + } else if (isMogaPro2Hid(inputDevice)) { + // This controller has a broken axis that reports a constant value. Ignore it. + if (axis == MotionEvent.AXIS_GENERIC_1) { + return 0.0f; + } + } + return value; + } + + private boolean isDualShock4(InputDevice inputDevice) { + // Sony DualShock 4 controller + return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; + } + + private boolean isXboxOneWireless(InputDevice inputDevice) { + // Microsoft Xbox One controller + return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; + } + + private boolean isMogaPro2Hid(InputDevice inputDevice) { + // Moga Pro 2 HID + return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java new file mode 100644 index 000000000..bac52bb2a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java @@ -0,0 +1,186 @@ +/** + * Copyright 2014 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.utils; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Environment; +import android.preference.PreferenceManager; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.yuzu.yuzu_emu.NativeLibrary; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A service that spawns its own thread in order to copy several binary and shader files + * from the yuzu APK to the external file system. + */ +public final class DirectoryInitialization { + public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST"; + + public static final String EXTRA_STATE = "directoryState"; + private static volatile DirectoryInitializationState directoryState = null; + private static String userPath; + private static AtomicBoolean isDirectoryInitializationRunning = new AtomicBoolean(false); + + public static void start(Context context) { + // Can take a few seconds to run, so don't block UI thread. + //noinspection TrivialFunctionalExpressionUsage + ((Runnable) () -> init(context)).run(); + } + + private static void init(Context context) { + if (!isDirectoryInitializationRunning.compareAndSet(false, true)) + return; + + if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { + if (PermissionsHandler.hasWriteAccess(context)) { + if (setUserDirectory()) { + initializeInternalStorage(context); + NativeLibrary.CreateConfigFile(); + directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED; + } else { + directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; + } + } else { + directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; + } + } + + isDirectoryInitializationRunning.set(false); + sendBroadcastState(directoryState, context); + } + + private static void deleteDirectoryRecursively(File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + deleteDirectoryRecursively(child); + } + file.delete(); + } + + public static boolean areDirectoriesReady() { + return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED; + } + + public static String getUserDirectory() { + if (directoryState == null) { + throw new IllegalStateException("DirectoryInitialization has to run at least once!"); + } else if (isDirectoryInitializationRunning.get()) { + throw new IllegalStateException( + "DirectoryInitialization has to finish running first!"); + } + return userPath; + } + + private static native void SetSysDirectory(String path); + + private static boolean setUserDirectory() { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + File externalPath = Environment.getExternalStorageDirectory(); + if (externalPath != null) { + userPath = externalPath.getAbsolutePath() + "/yuzu-emu"; + Log.debug("[DirectoryInitialization] User Dir: " + userPath); + // NativeLibrary.SetUserDirectory(userPath); + return true; + } + + } + + return false; + } + + private static void initializeInternalStorage(Context context) { + File sysDirectory = new File(context.getFilesDir(), "Sys"); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String revision = NativeLibrary.GetGitRevision(); + if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) { + // There is no extracted Sys directory, or there is a Sys directory from another + // version of yuzu that might contain outdated files. Let's (re-)extract Sys. + deleteDirectoryRecursively(sysDirectory); + copyAssetFolder("Sys", sysDirectory, true, context); + + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("sysDirectoryVersion", revision); + editor.apply(); + } + + // Let the native code know where the Sys directory is. + SetSysDirectory(sysDirectory.getPath()); + } + + private static void sendBroadcastState(DirectoryInitializationState state, Context context) { + Intent localIntent = + new Intent(BROADCAST_ACTION) + .putExtra(EXTRA_STATE, state); + LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent); + } + + private static void copyAsset(String asset, File output, Boolean overwrite, Context context) { + Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output); + + try { + if (!output.exists() || overwrite) { + InputStream in = context.getAssets().open(asset); + OutputStream out = new FileOutputStream(output); + copyFile(in, out); + in.close(); + out.close(); + } + } catch (IOException e) { + Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + + e.getMessage()); + } + } + + private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite, + Context context) { + Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + + outputFolder); + + try { + boolean createdFolder = false; + for (String file : context.getAssets().list(assetFolder)) { + if (!createdFolder) { + outputFolder.mkdir(); + createdFolder = true; + } + copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), + overwrite, context); + copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite, + context); + } + } catch (IOException e) { + Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder + + e.getMessage()); + } + } + + private static void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + + public enum DirectoryInitializationState { + YUZU_DIRECTORIES_INITIALIZED, + EXTERNAL_STORAGE_PERMISSION_NEEDED, + CANT_FIND_EXTERNAL_STORAGE + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryStateReceiver.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryStateReceiver.java new file mode 100644 index 000000000..62c9b018a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryStateReceiver.java @@ -0,0 +1,22 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState; + +public class DirectoryStateReceiver extends BroadcastReceiver { + Action1 callback; + + public DirectoryStateReceiver(Action1 callback) { + this.callback = callback; + } + + @Override + public void onReceive(Context context, Intent intent) { + DirectoryInitializationState state = (DirectoryInitializationState) intent + .getSerializableExtra(DirectoryInitialization.EXTRA_STATE); + callback.call(state); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.java new file mode 100644 index 000000000..0b11b9cc0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.java @@ -0,0 +1,78 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.yuzu.yuzu_emu.YuzuApplication; + +public class EmulationMenuSettings { + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + + // These must match what is defined in src/core/settings.h + public static final int LayoutOption_Default = 0; + public static final int LayoutOption_SingleScreen = 1; + public static final int LayoutOption_LargeScreen = 2; + public static final int LayoutOption_SideScreen = 3; + public static final int LayoutOption_MobilePortrait = 4; + public static final int LayoutOption_MobileLandscape = 5; + + public static boolean getJoystickRelCenter() { + return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true); + } + + public static void setJoystickRelCenter(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value); + editor.apply(); + } + + public static boolean getDpadSlideEnable() { + return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true); + } + + public static void setDpadSlideEnable(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value); + editor.apply(); + } + + public static int getLandscapeScreenLayout() { + return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape); + } + + public static void setLandscapeScreenLayout(int value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value); + editor.apply(); + } + + public static boolean getShowFps() { + return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false); + } + + public static void setShowFps(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_ShowFps", value); + editor.apply(); + } + + public static boolean getSwapScreens() { + return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false); + } + + public static void setSwapScreens(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_SwapScreens", value); + editor.apply(); + } + + public static boolean getShowOverlay() { + return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true); + } + + public static void setShowOverlay(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_ShowOverylay", value); + editor.apply(); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java new file mode 100644 index 000000000..ad3ec3dc1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java @@ -0,0 +1,73 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.nononsenseapps.filepicker.Utils; + +import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity; + +import java.io.File; +import java.util.List; + +public final class FileBrowserHelper { + public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List extensions) { + Intent i = new Intent(activity, CustomFilePickerActivity.class); + + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); + + activity.startActivityForResult(i, requestCode); + } + + public static void openFilePicker(FragmentActivity activity, int requestCode, int title, + List extensions, boolean allowMultiple) { + Intent i = new Intent(activity, CustomFilePickerActivity.class); + + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); + + activity.startActivityForResult(i, requestCode); + } + + @Nullable + public static String getSelectedDirectory(Intent result) { + // Use the provided utility method to parse the result + List files = Utils.getSelectedFilesFromResult(result); + if (!files.isEmpty()) { + File file = Utils.getFileForUri(files.get(0)); + return file.getAbsolutePath(); + } + + return null; + } + + @Nullable + public static String[] getSelectedFiles(Intent result) { + // Use the provided utility method to parse the result + List files = Utils.getSelectedFilesFromResult(result); + if (!files.isEmpty()) { + String[] paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) + paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); + return paths; + } + + return null; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java new file mode 100644 index 000000000..11d06c7ee --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java @@ -0,0 +1,37 @@ +package org.yuzu.yuzu_emu.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class FileUtil { + public static byte[] getBytesFromFile(File file) throws IOException { + final long length = file.length(); + + // You cannot create an array using a long type. + if (length > Integer.MAX_VALUE) { + // File is too large + throw new IOException("File is too large!"); + } + + byte[] bytes = new byte[(int) length]; + + int offset = 0; + int numRead; + + try (InputStream is = new FileInputStream(file)) { + while (offset < bytes.length + && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += numRead; + } + } + + // Ensure all the bytes have been read in + if (offset < bytes.length) { + throw new IOException("Could not completely read file " + file.getName()); + } + + return bytes; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.java new file mode 100644 index 000000000..8834c7bc5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.java @@ -0,0 +1,63 @@ +/** + * Copyright 2014 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.yuzu.yuzu_emu.utils; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; + +/** + * A service that shows a permanent notification in the background to avoid the app getting + * cleared from memory by the system. + */ +public class ForegroundService extends Service { + private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; + + private void showRunningNotification() { + // Intent is used to resume emulation if the notification is clicked + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id)) + .setSmallIcon(R.drawable.ic_stat_notification_logo) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.app_notification_running)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .setVibrate(null) + .setSound(null) + .setContentIntent(contentIntent); + startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build()); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + showRunningNotification(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + @Override + public void onDestroy() { + NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java new file mode 100644 index 000000000..b75dc9a62 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java @@ -0,0 +1,27 @@ +package org.yuzu.yuzu_emu.utils; + +import android.graphics.Bitmap; + +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Request; +import com.squareup.picasso.RequestHandler; + +import org.yuzu.yuzu_emu.NativeLibrary; + +import java.nio.IntBuffer; + +public class GameIconRequestHandler extends RequestHandler { + @Override + public boolean canHandleRequest(Request data) { + return "iso".equals(data.uri.getScheme()); + } + + @Override + public Result load(Request request, int networkPolicy) { + String url = request.uri.getHost() + request.uri.getPath(); + int[] vector = NativeLibrary.GetIcon(url); + Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); + bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); + return new Result(bitmap, Picasso.LoadedFrom.DISK); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.java new file mode 100644 index 000000000..ccf54138d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.java @@ -0,0 +1,39 @@ +package org.yuzu.yuzu_emu.utils; + +import org.yuzu.yuzu_emu.BuildConfig; + +/** + * Contains methods that call through to {@link android.util.Log}, but + * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log + * levels in release builds. + */ +public final class Log { + private static final String TAG = "Yuzu Frontend"; + + private Log() { + } + + public static void verbose(String message) { + if (BuildConfig.DEBUG) { + android.util.Log.v(TAG, message); + } + } + + public static void debug(String message) { + if (BuildConfig.DEBUG) { + android.util.Log.d(TAG, message); + } + } + + public static void info(String message) { + android.util.Log.i(TAG, message); + } + + public static void warning(String message) { + android.util.Log.w(TAG, message); + } + + public static void error(String message) { + android.util.Log.e(TAG, message); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java new file mode 100644 index 000000000..2eb200da4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java @@ -0,0 +1,35 @@ +package org.yuzu.yuzu_emu.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + +public class PermissionsHandler { + public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + + // We use permissions acceptance as an indicator if this is a first boot for the user. + public static boolean isFirstBoot(final FragmentActivity activity) { + return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; + } + + @TargetApi(Build.VERSION_CODES.M) + public static boolean checkWritePermission(final FragmentActivity activity) { + if (isFirstBoot(activity)) { + activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION); + return false; + } + + return true; + } + + public static boolean hasWriteAccess(Context context) { + return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoRoundedCornersTransformation.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoRoundedCornersTransformation.java new file mode 100644 index 000000000..03057b0d5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoRoundedCornersTransformation.java @@ -0,0 +1,45 @@ +package org.yuzu.yuzu_emu.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.squareup.picasso.Transformation; + +public class PicassoRoundedCornersTransformation implements Transformation { + @Override + public Bitmap transform(Bitmap icon) { + final int width = icon.getWidth(); + final int height = icon.getHeight(); + final Rect rect = new Rect(0, 0, width, height); + final int size = Math.min(width, height); + final int x = (width - size) / 2; + final int y = (height - size) / 2; + + Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size); + if (squaredBitmap != icon) { + icon.recycle(); + } + + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(shader); + + canvas.drawRoundRect(new RectF(rect), 10, 10, paint); + + squaredBitmap.recycle(); + + return output; + } + + @Override + public String key() { + return "circle"; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java new file mode 100644 index 000000000..5033691b3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java @@ -0,0 +1,57 @@ +package org.yuzu.yuzu_emu.utils; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.ImageView; + +import com.squareup.picasso.Picasso; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.R; + +import java.io.IOException; + +import androidx.annotation.Nullable; + +public class PicassoUtils { + private static boolean mPicassoInitialized = false; + + public static void init() { + if (mPicassoInitialized) { + return; + } + Picasso picassoInstance = new Picasso.Builder(YuzuApplication.getAppContext()) + .addRequestHandler(new GameIconRequestHandler()) + .build(); + + Picasso.setSingletonInstance(picassoInstance); + mPicassoInitialized = true; + } + + public static void loadGameIcon(ImageView imageView, String gamePath) { + Picasso + .get() + .load(Uri.parse("iso:/" + gamePath)) + .fit() + .centerInside() + .config(Bitmap.Config.RGB_565) + .error(R.drawable.no_icon) + .transform(new PicassoRoundedCornersTransformation()) + .into(imageView); + } + + // Blocking call. Load image from file and crop/resize it to fit in width x height. + @Nullable + public static Bitmap LoadBitmapFromFile(String uri, int width, int height) { + try { + return Picasso.get() + .load(Uri.parse(uri)) + .config(Bitmap.Config.ARGB_8888) + .centerCrop() + .resize(width, height) + .get(); + } catch (IOException e) { + return null; + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java new file mode 100644 index 000000000..5d22e8e08 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java @@ -0,0 +1,45 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; + +public final class StartupHandler { + private static void handlePermissionsCheck(FragmentActivity parent) { + // Ask the user to grant write permission if it's not already granted + PermissionsHandler.checkWritePermission(parent); + + String start_file = ""; + Bundle extras = parent.getIntent().getExtras(); + if (extras != null) { + start_file = extras.getString("AutoStartFile"); + } + + if (!TextUtils.isEmpty(start_file)) { + // Start the emulation activity, send the ISO passed in and finish the main activity + Intent emulation_intent = new Intent(parent, EmulationActivity.class); + emulation_intent.putExtra("SelectedGame", start_file); + parent.startActivity(emulation_intent); + parent.finish(); + } + } + + public static void HandleInit(FragmentActivity parent) { + if (PermissionsHandler.isFirstBoot(parent)) { + // Prompt user with standard first boot disclaimer + new AlertDialog.Builder(parent) + .setTitle(R.string.app_name) + .setIcon(R.mipmap.ic_launcher) + .setMessage(parent.getResources().getString(R.string.app_disclaimer)) + .setPositiveButton(android.R.string.ok, null) + .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) + .show(); + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeUtil.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeUtil.java new file mode 100644 index 000000000..4e4d48039 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeUtil.java @@ -0,0 +1,34 @@ +package org.yuzu.yuzu_emu.utils; + +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; + +import androidx.appcompat.app.AppCompatDelegate; + +import org.yuzu.yuzu_emu.YuzuApplication; +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile; + +public class ThemeUtil { + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); + + private static void applyTheme(int designValue) { + switch (designValue) { + case 0: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + break; + case 1: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + break; + case 2: + AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : + AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); + break; + } + } + + public static void applyTheme() { + applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0)); + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java new file mode 100644 index 000000000..2dc0f34f3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java @@ -0,0 +1,46 @@ +package org.yuzu.yuzu_emu.viewholders; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import org.yuzu.yuzu_emu.R; + +/** + * A simple class that stores references to views so that the GameAdapter doesn't need to + * keep calling findViewById(), which is expensive. + */ +public class GameViewHolder extends RecyclerView.ViewHolder { + private View itemView; + public ImageView imageIcon; + public TextView textGameTitle; + public TextView textCompany; + public TextView textFileName; + + public String gameId; + + // TODO Not need any of this stuff. Currently only the properties dialog needs it. + public String path; + public String title; + public String description; + public String regions; + public String company; + + public GameViewHolder(View itemView) { + super(itemView); + + this.itemView = itemView; + itemView.setTag(this); + + imageIcon = itemView.findViewById(R.id.image_game_screen); + textGameTitle = itemView.findViewById(R.id.text_game_title); + textCompany = itemView.findViewById(R.id.text_company); + textFileName = itemView.findViewById(R.id.text_filename); + } + + public View getItemView() { + return itemView; + } +} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b343a1453..f548931f1 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -110,7 +110,7 @@ static Core::SystemResultStatus RunEmulation(const std::string& filepath) { extern "C" { -void Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject surf) { s_surf = ANativeWindow_fromSurface(env, surf); @@ -122,7 +122,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, LOG_INFO(Frontend, "surface changed"); } -void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, [[maybe_unused]] jclass clazz) { ANativeWindow_release(s_surf); s_surf = nullptr; @@ -131,32 +131,32 @@ void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, } } -void Java_org_citra_citra_1emu_NativeLibrary_DoFrame(JNIEnv* env, [[maybe_unused]] jclass clazz) {} +void Java_org_yuzu_yuzu_1emu_NativeLibrary_DoFrame(JNIEnv* env, [[maybe_unused]] jclass clazz) {} -void Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env, [[maybe_unused]] jclass clazz, jint layout_option, jint rotation) {} -void Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory( +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_directory) {} -void Java_org_citra_citra_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) {} -void Java_org_citra_citra_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) {} -void Java_org_citra_citra_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) {} -jboolean Java_org_citra_citra_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env, +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return static_cast(!stop_run); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_device, jint j_button, jint action) { @@ -164,7 +164,7 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] return static_cast(true); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y) { @@ -183,83 +183,83 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent([[maybe_unus return static_cast(false); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_device, jint axis_id, jfloat axis_val) { return {}; } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jfloat x, jfloat y, jboolean pressed) { return static_cast(emu_window->OnTouchEvent(x, y, pressed)); } -void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jfloat x, jfloat y) { emu_window->OnTouchMoved(x, y); } -jintArray Java_org_citra_citra_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env, +jintArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_file) { return {}; } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { return env->NewStringUTF(""); } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_filename) { return j_filename; } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetGameId([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_filename) { return j_filename; } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetRegions([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { return env->NewStringUTF(""); } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetCompany([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { return env->NewStringUTF(""); } -jstring Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return {}; } -void Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile +void Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) {} -jint Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore([[maybe_unused]] JNIEnv* env, +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return {}; } -void Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( +void Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_file, [[maybe_unused]] jstring j_savestate, [[maybe_unused]] jboolean j_delete_savestate) {} -void Java_org_citra_citra_1emu_NativeLibrary_ReloadSettings([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) {} -jstring Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting([[maybe_unused]] JNIEnv* env, +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id, jstring j_section, jstring j_key) { @@ -274,7 +274,7 @@ jstring Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting([[maybe_unused]] return env->NewStringUTF(""); } -void Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id, jstring j_section, jstring j_key, jstring j_value) { @@ -289,7 +289,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting([[maybe_unused]] JNI env->ReleaseStringUTFChars(j_value, value.data()); } -void Java_org_citra_citra_1emu_NativeLibrary_InitGameIni([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id) { std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); @@ -297,7 +297,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_InitGameIni([[maybe_unused]] JNIEnv env->ReleaseStringUTFChars(j_game_id, game_id.data()); } -jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats([[maybe_unused]] JNIEnv* env, +jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { jdoubleArray j_stats = env->NewDoubleArray(4); @@ -314,10 +314,10 @@ jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats([[maybe_unused return j_stats; } -void Java_org_citra_citra_1emu_utils_DirectoryInitialization_SetSysDirectory( +void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) {} -void Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) { const std::string path = GetJString(env, j_path); @@ -333,7 +333,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2([[maybe_un } } -void Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv* env, +void Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { LOG_INFO(Frontend, "yuzu Version: {}-{}", Common::g_scm_branch, Common::g_scm_desc); LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 16c90e215..3b23f380b 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -7,119 +7,119 @@ extern "C" { #endif -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_StopEmulation(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, jclass clazz); -JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_IsRunning(JNIEnv* env, +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, jclass clazz); -JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent( +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent( JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action); -JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent( +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEvent( JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y); -JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent( +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent( JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); -JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, jclass clazz, jfloat x, jfloat y, jboolean pressed); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jfloat x, jfloat y); -JNIEXPORT jintArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env, +JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz, jstring j_file); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env, +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz, jstring j_filename); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetDescription( +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription( JNIEnv* env, jclass clazz, jstring j_filename); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env, +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz, jstring j_filename); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env, +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env, jclass clazz, jstring j_filename); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env, +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env, jclass clazz, jstring j_filename); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory( +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory( JNIEnv* env, jclass clazz, jstring j_directory); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_utils_DirectoryInitialization_SetSysDirectory( +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory( JNIEnv* env, jclass clazz, jstring path_); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env, jclass clazz, jstring path); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, jclass clazz); -JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, +JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz, jboolean enable); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange( +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange( JNIEnv* env, jclass clazz, jint layout_option, jint rotation); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2( +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2( JNIEnv* env, jclass clazz, jstring j_path); JNIEXPORT void JNICALL -Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( +Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, jclass clazz, jobject surf); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InitGameIni(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz, jstring j_game_id); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting( +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting( JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key, jstring j_value); -JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting( +JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting( JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key); -JNIEXPORT jdoubleArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, +JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, jclass clazz); #ifdef __cplusplus diff --git a/src/android/app/src/main/res/animator/menu_slide_in_from_end.xml b/src/android/app/src/main/res/animator/menu_slide_in_from_end.xml new file mode 100644 index 000000000..3f1495c75 --- /dev/null +++ b/src/android/app/src/main/res/animator/menu_slide_in_from_end.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml b/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml new file mode 100644 index 000000000..4612aee13 --- /dev/null +++ b/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/menu_slide_out_to_end.xml b/src/android/app/src/main/res/animator/menu_slide_out_to_end.xml new file mode 100644 index 000000000..d650237d6 --- /dev/null +++ b/src/android/app/src/main/res/animator/menu_slide_out_to_end.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml b/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml new file mode 100644 index 000000000..c00478946 --- /dev/null +++ b/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file 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 deleted file mode 100644 index 9bcf883e1..000000000 --- a/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/layout/activity_cheats.xml b/src/android/app/src/main/res/layout/activity_cheats.xml deleted file mode 100644 index b9414ab6d..000000000 --- a/src/android/app/src/main/res/layout/activity_cheats.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml index 7d7f36925..debc26e6c 100644 --- a/src/android/app/src/main/res/layout/activity_emulation.xml +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -1,17 +1,32 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:keepScreenOn="true" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/frame_content"> + android:layout_height="match_parent"/> - + android:orientation="horizontal" + android:baselineAligned="false"> - \ No newline at end of file + + + + + + + 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 deleted file mode 100644 index 25b1a268a..000000000 --- a/src/android/app/src/main/res/layout/fragment_cheat_details.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - -