summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar bunnei2023-06-05 21:43:43 -0700
committerGravatar GitHub2023-06-05 21:43:43 -0700
commitcb95d7fe1b6d81899fe6b279400da2c991e3132c (patch)
treea856ac45b1053009c4c11ee141c49d7faa4c8a19 /src
parentMerge pull request #10611 from liamwhite/audio-deadlock (diff)
parentMerge pull request #10633 from t895/variable-surface-ratio (diff)
downloadyuzu-cb95d7fe1b6d81899fe6b279400da2c991e3132c.tar.gz
yuzu-cb95d7fe1b6d81899fe6b279400da2c991e3132c.tar.xz
yuzu-cb95d7fe1b6d81899fe6b279400da2c991e3132c.zip
Merge pull request #10508 from yuzu-emu/lime
Project Lime - yuzu Android Port
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt5
-rw-r--r--src/android/.gitignore65
-rw-r--r--src/android/app/build.gradle.kts248
-rw-r--r--src/android/app/proguard-rules.pro24
-rw-r--r--src/android/app/src/ea/res/drawable/ic_yuzu.xml22
-rw-r--r--src/android/app/src/ea/res/drawable/ic_yuzu_full.xml12
-rw-r--r--src/android/app/src/ea/res/drawable/ic_yuzu_title.xml24
-rw-r--r--src/android/app/src/main/AndroidManifest.xml91
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt508
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt333
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt134
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt69
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt54
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt70
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt121
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt100
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt48
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt101
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt302
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt131
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt158
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt40
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt64
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt58
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt62
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt243
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt84
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt57
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt340
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt122
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt465
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt58
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt48
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt60
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt35
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt48
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt241
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt125
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt613
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt330
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt210
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt70
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicenseBottomSheetDialogFragment.kt59
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt137
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt62
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/PermissionDeniedDialogFragment.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ResetSettingsDialogFragment.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt236
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt329
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt86
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt41
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt109
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/License.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt47
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt1064
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt148
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt274
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt282
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt165
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt470
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/ThemeProvider.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt112
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt330
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt70
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt98
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt152
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt47
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt360
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt40
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt168
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt40
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt97
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt46
-rw-r--r--src/android/app/src/main/jni/CMakeLists.txt28
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.cpp35
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.h12
-rw-r--r--src/android/app/src/main/jni/applets/software_keyboard.cpp277
-rw-r--r--src/android/app/src/main/jni/applets/software_keyboard.h78
-rw-r--r--src/android/app/src/main/jni/config.cpp297
-rw-r--r--src/android/app/src/main/jni/config.h37
-rw-r--r--src/android/app/src/main/jni/default_ini.h507
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.cpp79
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.h64
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp116
-rw-r--r--src/android/app/src/main/jni/id_cache.h19
-rw-r--r--src/android/app/src/main/jni/native.cpp774
-rw-r--r--src/android/app/src/main/jni/native.h165
-rw-r--r--src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml16
-rw-r--r--src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_settings_fragment_in.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_settings_fragment_out.xml10
-rw-r--r--src/android/app/src/main/res/animator/menu_slide_in_from_start.xml20
-rw-r--r--src/android/app/src/main/res/animator/menu_slide_out_to_start.xml21
-rw-r--r--src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.pngbin0 -> 46179 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.pngbin0 -> 48264 bytes
-rw-r--r--src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.pngbin0 -> 56651 bytes
-rw-r--r--src/android/app/src/main/res/drawable/default_icon.jpgbin0 -> 6285 bytes
-rw-r--r--src/android/app/src/main/res/drawable/dpad_standard.xml24
-rw-r--r--src/android/app/src/main/res/drawable/dpad_standard_cardinal_depressed.xml24
-rw-r--r--src/android/app/src/main/res/drawable/dpad_standard_diagonal_depressed.xml24
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_a.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_a_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_b.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_b_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_home.xml21
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_home_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_minus.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_minus_depressed.xml9
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_plus.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_plus_depressed.xml9
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_screenshot.xml21
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_screenshot_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_x.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_x_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_y.xml22
-rw-r--r--src/android/app/src/main/res/drawable/facebutton_y_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/ic_add.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_arrow_forward.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_back.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_cartridge.xml12
-rw-r--r--src/android/app/src/main/res/drawable/ic_cartridge_outline.xml12
-rw-r--r--src/android/app/src/main/res/drawable/ic_check.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_check_circle.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_clear.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_controller.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_diamond.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_discord.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_exit.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_firmware.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_folder_open.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_github.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_icon_bg.xml751
-rw-r--r--src/android/app/src/main/res/drawable/ic_info_outline.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_install.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_key.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_launcher.xml6
-rw-r--r--src/android/app/src/main/res/drawable/ic_log.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_nfc.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_notification.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_options.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_palette.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_pause.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_play.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_save.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_search.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_settings.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_settings_outline.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_unlock.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_website.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_yuzu.xml22
-rw-r--r--src/android/app/src/main/res/drawable/ic_yuzu_full.xml12
-rw-r--r--src/android/app/src/main/res/drawable/ic_yuzu_title.xml24
-rw-r--r--src/android/app/src/main/res/drawable/joystick.xml45
-rw-r--r--src/android/app/src/main/res/drawable/joystick_depressed.xml10
-rw-r--r--src/android/app/src/main/res/drawable/joystick_range.xml38
-rw-r--r--src/android/app/src/main/res/drawable/l_shoulder.xml23
-rw-r--r--src/android/app/src/main/res/drawable/l_shoulder_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/premium_background.xml9
-rw-r--r--src/android/app/src/main/res/drawable/r_shoulder.xml23
-rw-r--r--src/android/app/src/main/res/drawable/r_shoulder_depressed.xml8
-rw-r--r--src/android/app/src/main/res/drawable/selector_cartridge.xml5
-rw-r--r--src/android/app/src/main/res/drawable/selector_settings.xml5
-rw-r--r--src/android/app/src/main/res/drawable/zl_trigger.xml25
-rw-r--r--src/android/app/src/main/res/drawable/zl_trigger_depressed.xml10
-rw-r--r--src/android/app/src/main/res/drawable/zr_trigger.xml25
-rw-r--r--src/android/app/src/main/res/drawable/zr_trigger_depressed.xml10
-rw-r--r--src/android/app/src/main/res/layout-w600dp/activity_main.xml58
-rw-r--r--src/android/app/src/main/res/layout-w600dp/fragment_setup.xml40
-rw-r--r--src/android/app/src/main/res/layout-w600dp/page_setup.xml65
-rw-r--r--src/android/app/src/main/res/layout/activity_emulation.xml13
-rw-r--r--src/android/app/src/main/res/layout/activity_main.xml58
-rw-r--r--src/android/app/src/main/res/layout/activity_settings.xml50
-rw-r--r--src/android/app/src/main/res/layout/card_game.xml67
-rw-r--r--src/android/app/src/main/res/layout/card_home_option.xml60
-rw-r--r--src/android/app/src/main/res/layout/dialog_edit_text.xml23
-rw-r--r--src/android/app/src/main/res/layout/dialog_license.xml64
-rw-r--r--src/android/app/src/main/res/layout/dialog_overlay_adjust.xml67
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml24
-rw-r--r--src/android/app/src/main/res/layout/dialog_slider.xml37
-rw-r--r--src/android/app/src/main/res/layout/fragment_about.xml232
-rw-r--r--src/android/app/src/main/res/layout/fragment_early_access.xml242
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml70
-rw-r--r--src/android/app/src/main/res/layout/fragment_games.xml34
-rw-r--r--src/android/app/src/main/res/layout/fragment_home_settings.xml34
-rw-r--r--src/android/app/src/main/res/layout/fragment_licenses.xml30
-rw-r--r--src/android/app/src/main/res/layout/fragment_search.xml183
-rw-r--r--src/android/app/src/main/res/layout/fragment_settings.xml14
-rw-r--r--src/android/app/src/main/res/layout/fragment_setup.xml42
-rw-r--r--src/android/app/src/main/res/layout/header_in_game.xml14
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting.xml41
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting_switch.xml50
-rw-r--r--src/android/app/src/main/res/layout/list_item_settings_header.xml20
-rw-r--r--src/android/app/src/main/res/layout/page_setup.xml72
-rw-r--r--src/android/app/src/main/res/menu-w600dp/menu_navigation.xml19
-rw-r--r--src/android/app/src/main/res/menu/menu_in_game.xml24
-rw-r--r--src/android/app/src/main/res/menu/menu_navigation.xml19
-rw-r--r--src/android/app/src/main/res/menu/menu_overlay_options.xml45
-rw-r--r--src/android/app/src/main/res/menu/menu_settings.xml2
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml59
-rw-r--r--src/android/app/src/main/res/values-night-v31/themes.xml31
-rw-r--r--src/android/app/src/main/res/values-night/themes.xml9
-rw-r--r--src/android/app/src/main/res/values-night/yuzu_colors.xml37
-rw-r--r--src/android/app/src/main/res/values-v31/themes.xml31
-rw-r--r--src/android/app/src/main/res/values-w600dp/bools.xml4
-rw-r--r--src/android/app/src/main/res/values-w600dp/dimens.xml5
-rw-r--r--src/android/app/src/main/res/values/arrays.xml227
-rw-r--r--src/android/app/src/main/res/values/bools.xml4
-rw-r--r--src/android/app/src/main/res/values/dimens.xml18
-rw-r--r--src/android/app/src/main/res/values/integers.xml37
-rw-r--r--src/android/app/src/main/res/values/strings.xml866
-rw-r--r--src/android/app/src/main/res/values/styles.xml36
-rw-r--r--src/android/app/src/main/res/values/themes.xml51
-rw-r--r--src/android/app/src/main/res/values/yuzu_colors.xml37
-rw-r--r--src/android/app/src/main/res/xml/data_extraction_rules.xml20
-rw-r--r--src/android/app/src/main/res/xml/data_extraction_rules_api_31.xml43
-rw-r--r--src/android/app/src/main/res/xml/nfc_tech_filter.xml6
-rw-r--r--src/android/build.gradle.kts13
-rw-r--r--src/android/gradle.properties16
-rw-r--r--src/android/gradle/wrapper/gradle-wrapper.jarbin0 -> 54708 bytes
-rw-r--r--src/android/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xsrc/android/gradlew175
-rw-r--r--src/android/gradlew.bat87
-rw-r--r--src/android/settings.gradle.kts21
-rw-r--r--src/audio_core/sink/sink_stream.cpp3
-rw-r--r--src/common/CMakeLists.txt13
-rw-r--r--src/common/dynamic_library.cpp2
-rw-r--r--src/common/dynamic_library.h3
-rw-r--r--src/common/error.cpp3
-rw-r--r--src/common/fs/file.cpp38
-rw-r--r--src/common/fs/fs_android.cpp98
-rw-r--r--src/common/fs/fs_android.h62
-rw-r--r--src/common/fs/path_util.cpp26
-rw-r--r--src/common/fs/path_util.h8
-rw-r--r--src/common/host_memory.cpp12
-rw-r--r--src/common/logging/backend.cpp26
-rw-r--r--src/common/logging/text_formatter.cpp35
-rw-r--r--src/common/logging/text_formatter.h2
-rw-r--r--src/core/CMakeLists.txt1
-rw-r--r--src/core/crypto/key_manager.cpp8
-rw-r--r--src/core/crypto/key_manager.h3
-rw-r--r--src/core/device_memory.cpp8
-rw-r--r--src/core/frontend/emu_window.cpp2
-rw-r--r--src/core/frontend/emu_window.h48
-rw-r--r--src/core/frontend/graphics_context.h62
-rw-r--r--src/core/hid/emulated_console.cpp32
-rw-r--r--src/core/hid/emulated_console.h4
-rw-r--r--src/core/hid/emulated_controller.cpp26
-rw-r--r--src/core/hid/emulated_controller.h2
-rw-r--r--src/core/hle/kernel/k_address_space_info.cpp5
-rw-r--r--src/core/hle/service/acc/profile_manager.cpp1
-rw-r--r--src/input_common/drivers/virtual_amiibo.cpp34
-rw-r--r--src/input_common/drivers/virtual_amiibo.h2
-rw-r--r--src/input_common/drivers/virtual_gamepad.cpp16
-rw-r--r--src/input_common/drivers/virtual_gamepad.h12
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp10
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp44
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp17
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.cpp61
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.h16
-rw-r--r--src/shader_recompiler/profile.h3
-rw-r--r--src/shader_recompiler/runtime_info.h2
-rw-r--r--src/video_core/CMakeLists.txt6
-rw-r--r--src/video_core/engines/maxwell_3d.cpp12
-rw-r--r--src/video_core/gpu.cpp1
-rw-r--r--src/video_core/gpu_thread.cpp2
-rw-r--r--src/video_core/renderer_base.cpp1
-rw-r--r--src/video_core/renderer_base.h5
-rw-r--r--src/video_core/renderer_null/renderer_null.cpp2
-rw-r--r--src/video_core/renderer_opengl/gl_shader_context.h1
-rw-r--r--src/video_core/renderer_vulkan/maxwell_to_vk.cpp8
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.cpp9
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.h6
-rw-r--r--src/video_core/renderer_vulkan/vk_blit_screen.cpp54
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp10
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h2
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp17
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.cpp52
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.h15
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp18
-rw-r--r--src/video_core/renderer_vulkan/vk_scheduler.cpp14
-rw-r--r--src/video_core/renderer_vulkan/vk_swapchain.cpp10
-rw-r--r--src/video_core/renderer_vulkan/vk_swapchain.h4
-rw-r--r--src/video_core/renderer_vulkan/vk_turbo_mode.cpp21
-rw-r--r--src/video_core/renderer_vulkan/vk_turbo_mode.h2
-rw-r--r--src/video_core/renderer_vulkan/vk_update_descriptor.h2
-rw-r--r--src/video_core/vulkan_common/vulkan_debug_callback.cpp28
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp103
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h14
-rw-r--r--src/video_core/vulkan_common/vulkan_library.cpp18
-rw-r--r--src/video_core/vulkan_common/vulkan_library.h6
-rw-r--r--src/yuzu/bootmanager.cpp1
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp4
-rw-r--r--src/yuzu/startup_checks.cpp4
-rw-r--r--src/yuzu_cmd/default_ini.h2
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl2.h2
328 files changed, 21104 insertions, 176 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5e3a74c0f..55b113297 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -195,3 +195,8 @@ endif()
195if (ENABLE_WEB_SERVICE) 195if (ENABLE_WEB_SERVICE)
196 add_subdirectory(web_service) 196 add_subdirectory(web_service)
197endif() 197endif()
198
199if (ANDROID)
200 add_subdirectory(android/app/src/main/jni)
201 target_include_directories(yuzu-android PRIVATE android/app/src/main)
202endif()
diff --git a/src/android/.gitignore b/src/android/.gitignore
new file mode 100644
index 000000000..121cc8484
--- /dev/null
+++ b/src/android/.gitignore
@@ -0,0 +1,65 @@
1# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2# SPDX-License-Identifier: GPL-3.0-or-later
3
4# Built application files
5*.apk
6*.ap_
7
8# Files for the ART/Dalvik VM
9*.dex
10
11# Java class files
12*.class
13
14# Generated files
15bin/
16gen/
17out/
18
19# Gradle files
20.gradle/
21build/
22
23# Local configuration file (sdk path, etc)
24local.properties
25
26# Proguard folder generated by Eclipse
27proguard/
28
29# Log Files
30*.log
31
32# Android Studio Navigation editor temp files
33.navigation/
34
35# Android Studio captures folder
36captures/
37
38# IntelliJ
39*.iml
40.idea/
41
42# Keystore files
43# Uncomment the following line if you do not want to check your keystore files in.
44#*.jks
45
46# External native build folder generated in Android Studio 2.2 and later
47.externalNativeBuild
48
49# CXX compile cache
50app/.cxx
51
52# Google Services (e.g. APIs or Firebase)
53google-services.json
54
55# Freeline
56freeline.py
57freeline/
58freeline_project_description.json
59
60# fastlane
61fastlane/report.xml
62fastlane/Preview.html
63fastlane/screenshots
64fastlane/test_output
65fastlane/readme.md
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
new file mode 100644
index 000000000..06f22fabe
--- /dev/null
+++ b/src/android/app/build.gradle.kts
@@ -0,0 +1,248 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4import android.annotation.SuppressLint
5
6plugins {
7 id("com.android.application")
8 id("org.jetbrains.kotlin.android")
9 id("kotlin-parcelize")
10 kotlin("plugin.serialization") version "1.8.21"
11}
12
13/**
14 * Use the number of seconds/10 since Jan 1 2016 as the versionCode.
15 * This lets us upload a new build at most every 10 seconds for the
16 * next 680 years.
17 */
18val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toInt()
19
20@Suppress("UnstableApiUsage")
21android {
22 namespace = "org.yuzu.yuzu_emu"
23
24 compileSdkVersion = "android-33"
25 ndkVersion = "25.2.9519653"
26
27 buildFeatures {
28 viewBinding = true
29 }
30
31 compileOptions {
32 sourceCompatibility = JavaVersion.VERSION_17
33 targetCompatibility = JavaVersion.VERSION_17
34 }
35
36 kotlinOptions {
37 jvmTarget = "17"
38 }
39
40 packaging {
41 // This is necessary for libadrenotools custom driver loading
42 jniLibs.useLegacyPackaging = true
43 }
44
45 lint {
46 // This is important as it will run lint but not abort on error
47 // Lint has some overly obnoxious "errors" that should really be warnings
48 abortOnError = false
49
50 //Uncomment disable lines for test builds...
51 //disable 'MissingTranslation'bin
52 //disable 'ExtraTranslation'
53 }
54
55 defaultConfig {
56 // TODO If this is ever modified, change application_id in strings.xml
57 applicationId = "org.yuzu.yuzu_emu"
58 minSdk = 30
59 targetSdk = 33
60 versionName = getGitVersion()
61
62 ndk {
63 @SuppressLint("ChromeOsAbiSupport")
64 abiFilters += listOf("arm64-v8a")
65 }
66
67 buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
68 buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
69 }
70
71 // Define build types, which are orthogonal to product flavors.
72 buildTypes {
73
74 // Signed by release key, allowing for upload to Play Store.
75 release {
76 signingConfig = signingConfigs.getByName("debug")
77 isMinifyEnabled = true
78 isDebuggable = false
79 proguardFiles(
80 getDefaultProguardFile("proguard-android.txt"),
81 "proguard-rules.pro"
82 )
83 }
84
85 register("relWithVersionCode") {
86 signingConfig = signingConfigs.getByName("debug")
87 isMinifyEnabled = true
88 isDebuggable = false
89 proguardFiles(
90 getDefaultProguardFile("proguard-android.txt"),
91 "proguard-rules.pro"
92 )
93 }
94
95 // builds a release build that doesn't need signing
96 // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
97 register("relWithDebInfo") {
98 signingConfig = signingConfigs.getByName("debug")
99 isMinifyEnabled = true
100 isDebuggable = true
101 proguardFiles(
102 getDefaultProguardFile("proguard-android.txt"),
103 "proguard-rules.pro"
104 )
105 versionNameSuffix = "-debug"
106 isJniDebuggable = true
107 }
108
109 // Signed by debug key disallowing distribution on Play Store.
110 // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
111 debug {
112 isDebuggable = true
113 isJniDebuggable = true
114 versionNameSuffix = "-debug"
115 }
116 }
117
118 flavorDimensions.add("version")
119 productFlavors {
120 create("mainline") {
121 dimension = "version"
122 buildConfigField("Boolean", "PREMIUM", "false")
123 }
124
125 create("ea") {
126 dimension = "version"
127 buildConfigField("Boolean", "PREMIUM", "true")
128 applicationIdSuffix = ".ea"
129 }
130 }
131
132 externalNativeBuild {
133 cmake {
134 version = "3.22.1"
135 path = file("../../../CMakeLists.txt")
136 }
137 }
138
139 defaultConfig {
140 externalNativeBuild {
141 cmake {
142 arguments(
143 "-DENABLE_QT=0", // Don't use QT
144 "-DENABLE_SDL2=0", // Don't use SDL
145 "-DENABLE_WEB_SERVICE=0", // Don't use telemetry
146 "-DBUNDLE_SPEEX=ON",
147 "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
148 "-DYUZU_USE_BUNDLED_VCPKG=ON",
149 "-DYUZU_USE_BUNDLED_FFMPEG=ON",
150 "-DYUZU_ENABLE_LTO=ON"
151 )
152
153 abiFilters("arm64-v8a", "x86_64")
154 }
155 }
156 }
157}
158
159dependencies {
160 implementation("androidx.core:core-ktx:1.10.1")
161 implementation("androidx.appcompat:appcompat:1.6.1")
162 implementation("androidx.recyclerview:recyclerview:1.3.0")
163 implementation("androidx.constraintlayout:constraintlayout:2.1.4")
164 implementation("androidx.fragment:fragment-ktx:1.5.7")
165 implementation("androidx.documentfile:documentfile:1.0.1")
166 implementation("com.google.android.material:material:1.9.0")
167 implementation("androidx.preference:preference:1.2.0")
168 implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
169 implementation("io.coil-kt:coil:2.2.2")
170 implementation("androidx.core:core-splashscreen:1.0.1")
171 implementation("androidx.window:window:1.0.0")
172 implementation("org.ini4j:ini4j:0.5.4")
173 implementation("androidx.constraintlayout:constraintlayout:2.1.4")
174 implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
175 implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
176 implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
177 implementation("info.debatty:java-string-similarity:2.0.0")
178 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
179}
180
181fun getGitVersion(): String {
182 var versionName = "0.0"
183
184 try {
185 versionName = ProcessBuilder("git", "describe", "--always", "--long")
186 .directory(project.rootDir)
187 .redirectOutput(ProcessBuilder.Redirect.PIPE)
188 .redirectError(ProcessBuilder.Redirect.PIPE)
189 .start().inputStream.bufferedReader().use { it.readText() }
190 .trim()
191 .replace(Regex("(-0)?-[^-]+$"), "")
192 } catch (e: Exception) {
193 logger.error("Cannot find git, defaulting to dummy version number")
194 }
195
196 if (System.getenv("GITHUB_ACTIONS") != null) {
197 val gitTag = System.getenv("GIT_TAG_NAME")
198 versionName = gitTag ?: versionName
199 }
200
201 return versionName
202}
203
204fun getGitHash(): String {
205 try {
206 val processBuilder = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
207 processBuilder.directory(project.rootDir)
208 val process = processBuilder.start()
209 val inputStream = process.inputStream
210 val errorStream = process.errorStream
211 process.waitFor()
212
213 return if (process.exitValue() == 0) {
214 inputStream.bufferedReader()
215 .use { it.readText().trim() } // return the value of gitHash
216 } else {
217 val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
218 logger.error("Error running git command: $errorMessage")
219 "dummy-hash" // return a dummy hash value in case of an error
220 }
221 } catch (e: Exception) {
222 logger.error("$e: Cannot find git, defaulting to dummy build hash")
223 return "dummy-hash" // return a dummy hash value in case of an error
224 }
225}
226
227fun getBranch(): String {
228 try {
229 val processBuilder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
230 processBuilder.directory(project.rootDir)
231 val process = processBuilder.start()
232 val inputStream = process.inputStream
233 val errorStream = process.errorStream
234 process.waitFor()
235
236 return if (process.exitValue() == 0) {
237 inputStream.bufferedReader()
238 .use { it.readText().trim() } // return the value of gitHash
239 } else {
240 val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
241 logger.error("Error running git command: $errorMessage")
242 "dummy-hash" // return a dummy hash value in case of an error
243 }
244 } catch (e: Exception) {
245 logger.error("$e: Cannot find git, defaulting to dummy build hash")
246 return "dummy-hash" // return a dummy hash value in case of an error
247 }
248}
diff --git a/src/android/app/proguard-rules.pro b/src/android/app/proguard-rules.pro
new file mode 100644
index 000000000..691e08fd0
--- /dev/null
+++ b/src/android/app/proguard-rules.pro
@@ -0,0 +1,24 @@
1# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2# SPDX-License-Identifier: GPL-3.0-or-later
3
4# To get usable stack traces
5-dontobfuscate
6
7# Prevents crashing when using Wini
8-keep class org.ini4j.spi.IniParser
9-keep class org.ini4j.spi.IniBuilder
10-keep class org.ini4j.spi.IniFormatter
11
12# Suppress warnings for R8
13-dontwarn org.bouncycastle.jsse.BCSSLParameters
14-dontwarn org.bouncycastle.jsse.BCSSLSocket
15-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
16-dontwarn org.conscrypt.Conscrypt$Version
17-dontwarn org.conscrypt.Conscrypt
18-dontwarn org.conscrypt.ConscryptHostnameVerifier
19-dontwarn org.openjsse.javax.net.ssl.SSLParameters
20-dontwarn org.openjsse.javax.net.ssl.SSLSocket
21-dontwarn org.openjsse.net.ssl.OpenJSSE
22-dontwarn java.beans.Introspector
23-dontwarn java.beans.VetoableChangeListener
24-dontwarn java.beans.VetoableChangeSupport
diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu.xml b/src/android/app/src/ea/res/drawable/ic_yuzu.xml
new file mode 100644
index 000000000..deb8ba53f
--- /dev/null
+++ b/src/android/app/src/ea/res/drawable/ic_yuzu.xml
@@ -0,0 +1,22 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="200dp"
3 android:height="200dp"
4 android:viewportWidth="500"
5 android:viewportHeight="500">
6 <path
7 android:fillColor="#C6C6C6"
8 android:fillType="nonZero"
9 android:pathData="M262.66,175.11L262.66,375.05C318.54,375.05 363.85,330.29 363.85,275.08C363.85,219.87 318.54,175.11 262.66,175.11M282.43,197.01C318.67,206 344.09,238.19 344.09,275.11C344.09,312.03 318.67,344.22 282.43,353.2L282.43,197.01"
10 android:strokeWidth="1.46"
11 android:strokeColor="#00000000"
12 android:strokeLineCap="butt"
13 android:strokeLineJoin="miter" />
14 <path
15 android:fillColor="#FFDC00"
16 android:fillType="nonZero"
17 android:pathData="M237.31,125.11C181.43,125.11 136.12,169.87 136.12,225.08C136.12,280.29 181.43,325.05 237.31,325.05ZM217.57,147.01L217.57,303.2C189.11,296.16 166.67,274.54 158.84,246.6C151.01,218.65 159,188.71 179.75,168.21C190.16,157.86 203.24,150.53 217.57,147.01"
18 android:strokeWidth="1.46"
19 android:strokeColor="#00000000"
20 android:strokeLineCap="butt"
21 android:strokeLineJoin="miter" />
22</vector>
diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml b/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml
new file mode 100644
index 000000000..4ef472876
--- /dev/null
+++ b/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml
@@ -0,0 +1,12 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="155.3dp"
3 android:height="172.55dp"
4 android:viewportWidth="155.3"
5 android:viewportHeight="172.55">
6 <path
7 android:fillColor="#C6C6C6"
8 android:pathData="M86.28,34.51v138a69,69 0,0 0,0 -138M99.76,49.63a55.57,55.57 0,0 1,0 107.8V49.63" />
9 <path
10 android:fillColor="#FFDC00"
11 android:pathData="M69,0a69,69 0,0 0,0 138ZM55.54,15.12v107.8A55.55,55.55 0,0 1,29.75 29.75,55.1 55.1,0 0,1 55.54,15.12" />
12</vector>
diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml b/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml
new file mode 100644
index 000000000..29d0cfced
--- /dev/null
+++ b/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml
@@ -0,0 +1,24 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="340.97dp"
3 android:height="389.85dp"
4 android:viewportWidth="340.97"
5 android:viewportHeight="389.85">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" />
9 <path
10 android:fillColor="?attr/colorOnSurface"
11 android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" />
12 <path
13 android:fillColor="?attr/colorOnSurface"
14 android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" />
15 <path
16 android:fillColor="?attr/colorOnSurface"
17 android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" />
18 <path
19 android:fillColor="#C6C6C6"
20 android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" />
21 <path
22 android:fillColor="#FFDC00"
23 android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" />
24</vector>
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..43087f2c0
--- /dev/null
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,91 @@
1<?xml version="1.0" encoding="utf-8"?>
2
3<!--
4SPDX-FileCopyrightText: 2023 yuzu Emulator Project
5SPDX-License-Identifier: GPL-3.0-or-later
6-->
7
8<manifest xmlns:android="http://schemas.android.com/apk/res/android">
9 <uses-feature
10 android:name="android.hardware.touchscreen"
11 android:required="false"/>
12 <uses-feature
13 android:name="android.hardware.gamepad"
14 android:required="false"/>
15
16 <uses-feature
17 android:name="android.hardware.vulkan.version"
18 android:version="0x401000"
19 android:required="true" />
20
21 <uses-permission android:name="android.permission.INTERNET" />
22 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
23 <uses-permission android:name="android.permission.NFC" />
24 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
25
26 <application
27 android:name="org.yuzu.yuzu_emu.YuzuApplication"
28 android:label="@string/app_name"
29 android:icon="@drawable/ic_launcher"
30 android:allowBackup="true"
31 android:hasFragileUserData="true"
32 android:supportsRtl="true"
33 android:isGame="true"
34 android:banner="@drawable/ic_launcher"
35 android:extractNativeLibs="true"
36 android:fullBackupContent="@xml/data_extraction_rules"
37 android:dataExtractionRules="@xml/data_extraction_rules_api_31"
38 android:enableOnBackInvokedCallback="true">
39
40 <activity
41 android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
42 android:exported="true"
43 android:theme="@style/Theme.Yuzu.Splash.Main">
44
45 <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
46 <intent-filter>
47 <action android:name="android.intent.action.MAIN"/>
48
49 <category android:name="android.intent.category.LAUNCHER"/>
50 </intent-filter>
51 </activity>
52
53 <activity
54 android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
55 android:theme="@style/Theme.Yuzu.Main"
56 android:label="@string/preferences_settings"/>
57
58 <activity
59 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
60 android:theme="@style/Theme.Yuzu.Main"
61 android:launchMode="singleTop"
62 android:screenOrientation="userLandscape"
63 android:exported="true">
64
65 <intent-filter>
66 <action android:name="android.nfc.action.TECH_DISCOVERED" />
67 <category android:name="android.intent.category.DEFAULT" />
68 <data android:mimeType="application/octet-stream" />
69 </intent-filter>
70
71 <meta-data
72 android:name="android.nfc.action.TECH_DISCOVERED"
73 android:resource="@xml/nfc_tech_filter" />
74 </activity>
75
76 <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
77
78 <provider
79 android:name=".features.DocumentProvider"
80 android:authorities="${applicationId}.user"
81 android:grantUriPermissions="true"
82 android:exported="true"
83 android:permission="android.permission.MANAGE_DOCUMENTS">
84 <intent-filter>
85 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
86 </intent-filter>
87 </provider>
88
89 </application>
90
91</manifest>
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
new file mode 100644
index 000000000..c11b6bc16
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -0,0 +1,508 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import android.text.Html
10import android.text.method.LinkMovementMethod
11import android.view.Surface
12import android.view.View
13import android.widget.TextView
14import androidx.annotation.Keep
15import androidx.fragment.app.DialogFragment
16import com.google.android.material.dialog.MaterialAlertDialogBuilder
17import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext
18import org.yuzu.yuzu_emu.activities.EmulationActivity
19import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
20import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
21import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
22import org.yuzu.yuzu_emu.utils.Log.error
23import org.yuzu.yuzu_emu.utils.Log.verbose
24import org.yuzu.yuzu_emu.utils.Log.warning
25import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
26import java.lang.ref.WeakReference
27
28/**
29 * Class which contains methods that interact
30 * with the native side of the Yuzu code.
31 */
32object NativeLibrary {
33 /**
34 * Default controller id for each device
35 */
36 const val Player1Device = 0
37 const val Player2Device = 1
38 const val Player3Device = 2
39 const val Player4Device = 3
40 const val Player5Device = 4
41 const val Player6Device = 5
42 const val Player7Device = 6
43 const val Player8Device = 7
44 const val ConsoleDevice = 8
45
46 /**
47 * Controller type for each device
48 */
49 const val ProController = 3
50 const val Handheld = 4
51 const val JoyconDual = 5
52 const val JoyconLeft = 6
53 const val JoyconRight = 7
54 const val GameCube = 8
55 const val Pokeball = 9
56 const val NES = 10
57 const val SNES = 11
58 const val N64 = 12
59 const val SegaGenesis = 13
60
61 @JvmField
62 var sEmulationActivity = WeakReference<EmulationActivity?>(null)
63
64 init {
65 try {
66 System.loadLibrary("yuzu-android")
67 } catch (ex: UnsatisfiedLinkError) {
68 error("[NativeLibrary] $ex")
69 }
70 }
71
72 @Keep
73 @JvmStatic
74 fun openContentUri(path: String?, openmode: String?): Int {
75 return if (isNativePath(path!!)) {
76 YuzuApplication.documentsTree!!.openContentUri(path, openmode)
77 } else openContentUri(appContext, path, openmode)
78 }
79
80 @Keep
81 @JvmStatic
82 fun getSize(path: String?): Long {
83 return if (isNativePath(path!!)) {
84 YuzuApplication.documentsTree!!.getFileSize(path)
85 } else getFileSize(appContext, path)
86 }
87
88 /**
89 * Returns true if pro controller isn't available and handheld is
90 */
91 external fun isHandheldOnly(): Boolean
92
93 /**
94 * Changes controller type for a specific device.
95 *
96 * @param Device The input descriptor of the gamepad.
97 * @param Type The NpadStyleIndex of the gamepad.
98 */
99 external fun setDeviceType(Device: Int, Type: Int): Boolean
100
101 /**
102 * Handles event when a gamepad is connected.
103 *
104 * @param Device The input descriptor of the gamepad.
105 */
106 external fun onGamePadConnectEvent(Device: Int): Boolean
107
108 /**
109 * Handles event when a gamepad is disconnected.
110 *
111 * @param Device The input descriptor of the gamepad.
112 */
113 external fun onGamePadDisconnectEvent(Device: Int): Boolean
114
115 /**
116 * Handles button press events for a gamepad.
117 *
118 * @param Device The input descriptor of the gamepad.
119 * @param Button Key code identifying which button was pressed.
120 * @param Action Mask identifying which action is happening (button pressed down, or button released).
121 * @return If we handled the button press.
122 */
123 external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
124
125 /**
126 * Handles joystick movement events.
127 *
128 * @param Device The device ID of the gamepad.
129 * @param Axis The axis ID
130 * @param x_axis The value of the x-axis represented by the given ID.
131 * @param y_axis The value of the y-axis represented by the given ID.
132 */
133 external fun onGamePadJoystickEvent(
134 Device: Int,
135 Axis: Int,
136 x_axis: Float,
137 y_axis: Float
138 ): Boolean
139
140 /**
141 * Handles motion events.
142 *
143 * @param delta_timestamp The finger id corresponding to this event
144 * @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor.
145 * @param accel_x,accel_y,accel_z The value of the y-axis
146 */
147 external fun onGamePadMotionEvent(
148 Device: Int,
149 delta_timestamp: Long,
150 gyro_x: Float,
151 gyro_y: Float,
152 gyro_z: Float,
153 accel_x: Float,
154 accel_y: Float,
155 accel_z: Float
156 ): Boolean
157
158 /**
159 * Signals and load a nfc tag
160 *
161 * @param data Byte array containing all the data from a nfc tag
162 */
163 external fun onReadNfcTag(data: ByteArray?): Boolean
164
165 /**
166 * Removes current loaded nfc tag
167 */
168 external fun onRemoveNfcTag(): Boolean
169
170 /**
171 * Handles touch press events.
172 *
173 * @param finger_id The finger id corresponding to this event
174 * @param x_axis The value of the x-axis.
175 * @param y_axis The value of the y-axis.
176 */
177 external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
178
179 /**
180 * Handles touch movement.
181 *
182 * @param x_axis The value of the instantaneous x-axis.
183 * @param y_axis The value of the instantaneous y-axis.
184 */
185 external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
186
187 /**
188 * Handles touch release events.
189 *
190 * @param finger_id The finger id corresponding to this event
191 */
192 external fun onTouchReleased(finger_id: Int)
193
194 external fun reloadSettings()
195
196 external fun getUserSetting(gameID: String?, Section: String?, Key: String?): String?
197
198 external fun setUserSetting(gameID: String?, Section: String?, Key: String?, Value: String?)
199
200 external fun initGameIni(gameID: String?)
201
202 /**
203 * Gets the embedded icon within the given ROM.
204 *
205 * @param filename the file path to the ROM.
206 * @return a byte array containing the JPEG data for the icon.
207 */
208 external fun getIcon(filename: String): ByteArray
209
210 /**
211 * Gets the embedded title of the given ISO/ROM.
212 *
213 * @param filename The file path to the ISO/ROM.
214 * @return the embedded title of the ISO/ROM.
215 */
216 external fun getTitle(filename: String): String
217
218 external fun getDescription(filename: String): String
219
220 external fun getGameId(filename: String): String
221
222 external fun getRegions(filename: String): String
223
224 external fun getCompany(filename: String): String
225
226 external fun setAppDirectory(directory: String)
227
228 external fun initializeGpuDriver(
229 hookLibDir: String?,
230 customDriverDir: String?,
231 customDriverName: String?,
232 fileRedirectDir: String?
233 )
234
235 external fun reloadKeys(): Boolean
236
237 external fun initializeEmulation()
238
239 external fun defaultCPUCore(): Int
240
241 /**
242 * Begins emulation.
243 */
244 external fun run(path: String?)
245
246 /**
247 * Begins emulation from the specified savestate.
248 */
249 external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
250
251 // Surface Handling
252 external fun surfaceChanged(surf: Surface?)
253
254 external fun surfaceDestroyed()
255
256 /**
257 * Unpauses emulation from a paused state.
258 */
259 external fun unPauseEmulation()
260
261 /**
262 * Pauses emulation.
263 */
264 external fun pauseEmulation()
265
266 /**
267 * Stops emulation.
268 */
269 external fun stopEmulation()
270
271 /**
272 * Resets the in-memory ROM metadata cache.
273 */
274 external fun resetRomMetadata()
275
276 /**
277 * Returns true if emulation is running (or is paused).
278 */
279 external fun isRunning(): Boolean
280
281 /**
282 * Returns the performance stats for the current game
283 */
284 external fun getPerfStats(): DoubleArray
285
286 /**
287 * Notifies the core emulation that the orientation has changed.
288 */
289 external fun notifyOrientationChange(layout_option: Int, rotation: Int)
290
291 enum class CoreError {
292 ErrorSystemFiles,
293 ErrorSavestate,
294 ErrorUnknown
295 }
296
297 private var coreErrorAlertResult = false
298 private val coreErrorAlertLock = Object()
299
300 class CoreErrorDialogFragment : DialogFragment() {
301 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
302 val title = requireArguments().serializable<String>("title")
303 val message = requireArguments().serializable<String>("message")
304
305 return MaterialAlertDialogBuilder(requireActivity())
306 .setTitle(title)
307 .setMessage(message)
308 .setPositiveButton(R.string.continue_button, null)
309 .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
310 coreErrorAlertResult = false
311 synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
312 }
313 .create()
314 }
315
316 override fun onDismiss(dialog: DialogInterface) {
317 coreErrorAlertResult = true
318 synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
319 }
320
321 companion object {
322 fun newInstance(title: String?, message: String?): CoreErrorDialogFragment {
323 val frag = CoreErrorDialogFragment()
324 val args = Bundle()
325 args.putString("title", title)
326 args.putString("message", message)
327 frag.arguments = args
328 return frag
329 }
330 }
331 }
332
333 private fun onCoreErrorImpl(title: String, message: String) {
334 val emulationActivity = sEmulationActivity.get()
335 if (emulationActivity == null) {
336 error("[NativeLibrary] EmulationActivity not present")
337 return
338 }
339
340 val fragment = CoreErrorDialogFragment.newInstance(title, message)
341 fragment.show(emulationActivity.supportFragmentManager, "coreError")
342 }
343
344 /**
345 * Handles a core error.
346 *
347 * @return true: continue; false: abort
348 */
349 fun onCoreError(error: CoreError?, details: String): Boolean {
350 val emulationActivity = sEmulationActivity.get()
351 if (emulationActivity == null) {
352 error("[NativeLibrary] EmulationActivity not present")
353 return false
354 }
355
356 val title: String
357 val message: String
358 when (error) {
359 CoreError.ErrorSystemFiles -> {
360 title = emulationActivity.getString(R.string.system_archive_not_found)
361 message = emulationActivity.getString(
362 R.string.system_archive_not_found_message,
363 details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
364 )
365 }
366 CoreError.ErrorSavestate -> {
367 title = emulationActivity.getString(R.string.save_load_error)
368 message = details
369 }
370 CoreError.ErrorUnknown -> {
371 title = emulationActivity.getString(R.string.fatal_error)
372 message = emulationActivity.getString(R.string.fatal_error_message)
373 }
374 else -> {
375 return true
376 }
377 }
378
379 // Show the AlertDialog on the main thread.
380 emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
381
382 // Wait for the lock to notify that it is complete.
383 synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
384
385 return coreErrorAlertResult
386 }
387
388 @Keep
389 @JvmStatic
390 fun exitEmulationActivity(resultCode: Int) {
391 val Success = 0
392 val ErrorNotInitialized = 1
393 val ErrorGetLoader = 2
394 val ErrorSystemFiles = 3
395 val ErrorSharedFont = 4
396 val ErrorVideoCore = 5
397 val ErrorUnknown = 6
398 val ErrorLoader = 7
399
400 val captionId: Int
401 var descriptionId: Int
402 when (resultCode) {
403 ErrorVideoCore -> {
404 captionId = R.string.loader_error_video_core
405 descriptionId = R.string.loader_error_video_core_description
406 }
407 else -> {
408 captionId = R.string.loader_error_encrypted
409 descriptionId = R.string.loader_error_encrypted_roms_description
410 if (!reloadKeys()) {
411 descriptionId = R.string.loader_error_encrypted_keys_description
412 }
413 }
414 }
415
416 val emulationActivity = sEmulationActivity.get()
417 if (emulationActivity == null) {
418 warning("[NativeLibrary] EmulationActivity is null, can't exit.")
419 return
420 }
421
422 val builder = MaterialAlertDialogBuilder(emulationActivity)
423 .setTitle(captionId)
424 .setMessage(
425 Html.fromHtml(
426 emulationActivity.getString(descriptionId),
427 Html.FROM_HTML_MODE_LEGACY
428 )
429 )
430 .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> emulationActivity.finish() }
431 .setOnDismissListener { emulationActivity.finish() }
432 emulationActivity.runOnUiThread {
433 val alert = builder.create()
434 alert.show()
435 (alert.findViewById<View>(android.R.id.message) as TextView).movementMethod =
436 LinkMovementMethod.getInstance()
437 }
438 }
439
440 fun setEmulationActivity(emulationActivity: EmulationActivity?) {
441 verbose("[NativeLibrary] Registering EmulationActivity.")
442 sEmulationActivity = WeakReference(emulationActivity)
443 }
444
445 fun clearEmulationActivity() {
446 verbose("[NativeLibrary] Unregistering EmulationActivity.")
447 sEmulationActivity.clear()
448 }
449
450 /**
451 * Logs the Yuzu version, Android version and, CPU.
452 */
453 external fun logDeviceInfo()
454
455 /**
456 * Submits inline keyboard text. Called on input for buttons that result text.
457 * @param text Text to submit to the inline software keyboard implementation.
458 */
459 external fun submitInlineKeyboardText(text: String?)
460
461 /**
462 * Submits inline keyboard input. Used to indicate keys pressed that are not text.
463 * @param key_code Android Key Code associated with the keyboard input.
464 */
465 external fun submitInlineKeyboardInput(key_code: Int)
466
467 /**
468 * Button type for use in onTouchEvent
469 */
470 object ButtonType {
471 const val BUTTON_A = 0
472 const val BUTTON_B = 1
473 const val BUTTON_X = 2
474 const val BUTTON_Y = 3
475 const val STICK_L = 4
476 const val STICK_R = 5
477 const val TRIGGER_L = 6
478 const val TRIGGER_R = 7
479 const val TRIGGER_ZL = 8
480 const val TRIGGER_ZR = 9
481 const val BUTTON_PLUS = 10
482 const val BUTTON_MINUS = 11
483 const val DPAD_LEFT = 12
484 const val DPAD_UP = 13
485 const val DPAD_RIGHT = 14
486 const val DPAD_DOWN = 15
487 const val BUTTON_SL = 16
488 const val BUTTON_SR = 17
489 const val BUTTON_HOME = 18
490 const val BUTTON_CAPTURE = 19
491 }
492
493 /**
494 * Stick type for use in onTouchEvent
495 */
496 object StickType {
497 const val STICK_L = 0
498 const val STICK_R = 1
499 }
500
501 /**
502 * Button states
503 */
504 object ButtonState {
505 const val RELEASED = 0
506 const val PRESSED = 1
507 }
508}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
new file mode 100644
index 000000000..4c947b786
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -0,0 +1,61 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu
5
6import android.app.Application
7import android.app.NotificationChannel
8import android.app.NotificationManager
9import android.content.Context
10import org.yuzu.yuzu_emu.utils.DirectoryInitialization
11import org.yuzu.yuzu_emu.utils.DocumentsTree
12import org.yuzu.yuzu_emu.utils.GpuDriverHelper
13import java.io.File
14
15fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
16
17class YuzuApplication : Application() {
18 private fun createNotificationChannels() {
19 val emulationChannel = NotificationChannel(
20 getString(R.string.emulation_notification_channel_id),
21 getString(R.string.emulation_notification_channel_name),
22 NotificationManager.IMPORTANCE_LOW
23 )
24 emulationChannel.description = getString(R.string.emulation_notification_channel_description)
25 emulationChannel.setSound(null, null)
26 emulationChannel.vibrationPattern = null
27
28 val noticeChannel = NotificationChannel(
29 getString(R.string.notice_notification_channel_id),
30 getString(R.string.notice_notification_channel_name),
31 NotificationManager.IMPORTANCE_HIGH
32 )
33 noticeChannel.description = getString(R.string.notice_notification_channel_description)
34 noticeChannel.setSound(null, null)
35
36 // Register the channel with the system; you can't change the importance
37 // or other notification behaviors after this
38 val notificationManager = getSystemService(NotificationManager::class.java)
39 notificationManager.createNotificationChannel(emulationChannel)
40 notificationManager.createNotificationChannel(noticeChannel)
41 }
42
43 override fun onCreate() {
44 super.onCreate()
45 application = this
46 documentsTree = DocumentsTree()
47 DirectoryInitialization.start(applicationContext)
48 GpuDriverHelper.initializeDriverParameters(applicationContext)
49 NativeLibrary.logDeviceInfo()
50
51 createNotificationChannels();
52 }
53
54 companion object {
55 var documentsTree: DocumentsTree? = null
56 lateinit var application: YuzuApplication
57
58 val appContext: Context
59 get() = application.applicationContext
60 }
61}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
new file mode 100644
index 000000000..94d5156cf
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -0,0 +1,333 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.activities
5
6import android.app.Activity
7import android.content.Context
8import android.content.Intent
9import android.content.res.Configuration
10import android.graphics.Rect
11import android.hardware.Sensor
12import android.hardware.SensorEvent
13import android.hardware.SensorEventListener
14import android.hardware.SensorManager
15import android.hardware.display.DisplayManager
16import android.os.Bundle
17import android.view.Display
18import android.view.InputDevice
19import android.view.KeyEvent
20import android.view.MotionEvent
21import android.view.Surface
22import android.view.View
23import android.view.inputmethod.InputMethodManager
24import androidx.activity.viewModels
25import androidx.appcompat.app.AppCompatActivity
26import androidx.core.content.getSystemService
27import androidx.core.view.WindowCompat
28import androidx.core.view.WindowInsetsCompat
29import androidx.core.view.WindowInsetsControllerCompat
30import androidx.lifecycle.Lifecycle
31import androidx.lifecycle.lifecycleScope
32import androidx.lifecycle.repeatOnLifecycle
33import androidx.window.layout.WindowInfoTracker
34import kotlinx.coroutines.Dispatchers
35import kotlinx.coroutines.launch
36import org.yuzu.yuzu_emu.NativeLibrary
37import org.yuzu.yuzu_emu.R
38import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
39import org.yuzu.yuzu_emu.fragments.EmulationFragment
40import org.yuzu.yuzu_emu.model.Game
41import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
42import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
43import org.yuzu.yuzu_emu.utils.ForegroundService
44import org.yuzu.yuzu_emu.utils.InputHandler
45import org.yuzu.yuzu_emu.utils.NfcReader
46import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
47import org.yuzu.yuzu_emu.utils.ThemeHelper
48import kotlin.math.roundToInt
49
50class EmulationActivity : AppCompatActivity(), SensorEventListener {
51 private var controllerMappingHelper: ControllerMappingHelper? = null
52
53 var isActivityRecreated = false
54 private var emulationFragment: EmulationFragment? = null
55 private lateinit var nfcReader: NfcReader
56 private lateinit var inputHandler: InputHandler
57
58 private val gyro = FloatArray(3)
59 private val accel = FloatArray(3)
60 private var motionTimestamp: Long = 0
61 private var flipMotionOrientation: Boolean = false
62
63 private lateinit var game: Game
64
65 private val settingsViewModel: SettingsViewModel by viewModels()
66
67 override fun onDestroy() {
68 stopForegroundService(this)
69 super.onDestroy()
70 }
71
72 override fun onCreate(savedInstanceState: Bundle?) {
73 ThemeHelper.setTheme(this)
74
75 settingsViewModel.settings.loadSettings()
76
77 super.onCreate(savedInstanceState)
78 if (savedInstanceState == null) {
79 // Get params we were passed
80 game = intent.parcelable(EXTRA_SELECTED_GAME)!!
81 isActivityRecreated = false
82 } else {
83 isActivityRecreated = true
84 restoreState(savedInstanceState)
85 }
86 controllerMappingHelper = ControllerMappingHelper()
87
88 // Set these options now so that the SurfaceView the game renders into is the right size.
89 enableFullscreenImmersive()
90
91 setContentView(R.layout.activity_emulation)
92 window.decorView.setBackgroundColor(getColor(android.R.color.black))
93
94 // Find or create the EmulationFragment
95 emulationFragment =
96 supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
97 if (emulationFragment == null) {
98 emulationFragment = EmulationFragment.newInstance(game)
99 supportFragmentManager.beginTransaction()
100 .add(R.id.frame_emulation_fragment, emulationFragment!!)
101 .commit()
102 }
103 title = game.title
104
105 nfcReader = NfcReader(this)
106 nfcReader.initialize()
107
108 inputHandler = InputHandler()
109 inputHandler.initialize()
110
111 lifecycleScope.launch(Dispatchers.Main) {
112 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
113 WindowInfoTracker.getOrCreate(this@EmulationActivity)
114 .windowLayoutInfo(this@EmulationActivity)
115 .collect { emulationFragment?.updateCurrentLayout(this@EmulationActivity, it) }
116 }
117 }
118
119 // Start a foreground service to prevent the app from getting killed in the background
120 val startIntent = Intent(this, ForegroundService::class.java)
121 startForegroundService(startIntent)
122 }
123
124 override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
125 if (event.action == KeyEvent.ACTION_DOWN) {
126 if (keyCode == KeyEvent.KEYCODE_ENTER) {
127 // Special case, we do not support multiline input, dismiss the keyboard.
128 val overlayView: View =
129 this.findViewById(R.id.surface_input_overlay)
130 val im =
131 overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
132 im.hideSoftInputFromWindow(overlayView.windowToken, 0)
133 } else {
134 val textChar = event.unicodeChar
135 if (textChar == 0) {
136 // No text, button input.
137 NativeLibrary.submitInlineKeyboardInput(keyCode)
138 } else {
139 // Text submitted.
140 NativeLibrary.submitInlineKeyboardText(textChar.toChar().toString())
141 }
142 }
143 }
144 return super.onKeyDown(keyCode, event)
145 }
146
147 override fun onResume() {
148 super.onResume()
149 nfcReader.startScanning()
150 startMotionSensorListener()
151
152 NativeLibrary.notifyOrientationChange(
153 EmulationMenuSettings.landscapeScreenLayout,
154 getAdjustedRotation()
155 )
156 }
157
158 override fun onPause() {
159 super.onPause()
160 nfcReader.stopScanning()
161 stopMotionSensorListener()
162 }
163
164 override fun onNewIntent(intent: Intent) {
165 super.onNewIntent(intent)
166 setIntent(intent)
167 nfcReader.onNewIntent(intent)
168 }
169
170 override fun onSaveInstanceState(outState: Bundle) {
171 outState.putParcelable(EXTRA_SELECTED_GAME, game)
172 super.onSaveInstanceState(outState)
173 }
174
175 override fun dispatchKeyEvent(event: KeyEvent): Boolean {
176 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
177 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
178 ) {
179 return super.dispatchKeyEvent(event)
180 }
181
182 return inputHandler.dispatchKeyEvent(event)
183 }
184
185 override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
186 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
187 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
188 ) {
189 return super.dispatchGenericMotionEvent(event)
190 }
191
192 // Don't attempt to do anything if we are disconnecting a device.
193 if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
194 return true
195 }
196
197 return inputHandler.dispatchGenericMotionEvent(event)
198 }
199
200 override fun onSensorChanged(event: SensorEvent) {
201 val rotation = this.display?.rotation
202 if (rotation == Surface.ROTATION_90) {
203 flipMotionOrientation = true
204 }
205 if (rotation == Surface.ROTATION_270) {
206 flipMotionOrientation = false
207 }
208
209 if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
210 if (flipMotionOrientation) {
211 accel[0] = event.values[1] / SensorManager.GRAVITY_EARTH
212 accel[1] = -event.values[0] / SensorManager.GRAVITY_EARTH
213 } else {
214 accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH
215 accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH
216 }
217 accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH
218 }
219 if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
220 // Investigate why sensor value is off by 6x
221 if (flipMotionOrientation) {
222 gyro[0] = -event.values[1] / 6.0f
223 gyro[1] = event.values[0] / 6.0f
224 } else {
225 gyro[0] = event.values[1] / 6.0f
226 gyro[1] = -event.values[0] / 6.0f
227 }
228 gyro[2] = event.values[2] / 6.0f
229 }
230
231 // Only update state on accelerometer data
232 if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) {
233 return
234 }
235 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
236 motionTimestamp = event.timestamp
237 NativeLibrary.onGamePadMotionEvent(
238 NativeLibrary.Player1Device,
239 deltaTimestamp,
240 gyro[0],
241 gyro[1],
242 gyro[2],
243 accel[0],
244 accel[1],
245 accel[2]
246 )
247 NativeLibrary.onGamePadMotionEvent(
248 NativeLibrary.ConsoleDevice,
249 deltaTimestamp,
250 gyro[0],
251 gyro[1],
252 gyro[2],
253 accel[0],
254 accel[1],
255 accel[2]
256 )
257 }
258
259 override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
260
261 private fun getAdjustedRotation():Int {
262 val rotation = getSystemService<DisplayManager>()!!.getDisplay(Display.DEFAULT_DISPLAY).rotation
263 val config: Configuration = resources.configuration
264
265 if ((config.screenLayout and Configuration.SCREENLAYOUT_LONG_YES) != 0 ||
266 (config.screenLayout and Configuration.SCREENLAYOUT_LONG_NO) == 0) {
267 return rotation
268 }
269 when (rotation) {
270 Surface.ROTATION_0 -> return Surface.ROTATION_90
271 Surface.ROTATION_90 -> return Surface.ROTATION_0
272 Surface.ROTATION_180 -> return Surface.ROTATION_270
273 Surface.ROTATION_270 -> return Surface.ROTATION_180
274 }
275 return rotation
276 }
277
278 private fun restoreState(savedInstanceState: Bundle) {
279 game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
280 }
281
282 private fun enableFullscreenImmersive() {
283 WindowCompat.setDecorFitsSystemWindows(window, false)
284
285 WindowInsetsControllerCompat(window, window.decorView).let { controller ->
286 controller.hide(WindowInsetsCompat.Type.systemBars())
287 controller.systemBarsBehavior =
288 WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
289 }
290 }
291
292 private fun startMotionSensorListener() {
293 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
294 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
295 val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
296 sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME)
297 sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME)
298 }
299
300 private fun stopMotionSensorListener() {
301 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
302 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
303 val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
304
305 sensorManager.unregisterListener(this, gyroSensor)
306 sensorManager.unregisterListener(this, accelSensor)
307 }
308
309 companion object {
310 const val EXTRA_SELECTED_GAME = "SelectedGame"
311
312 fun launch(activity: AppCompatActivity, game: Game) {
313 val launcher = Intent(activity, EmulationActivity::class.java)
314 launcher.putExtra(EXTRA_SELECTED_GAME, game)
315 activity.startActivity(launcher)
316 }
317
318 fun stopForegroundService(activity: Activity) {
319 val startIntent = Intent(activity, ForegroundService::class.java)
320 startIntent.action = ForegroundService.ACTION_STOP
321 activity.startForegroundService(startIntent)
322 }
323
324 private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
325 if (view == null) {
326 return true
327 }
328 val viewBounds = Rect()
329 view.getGlobalVisibleRect(viewBounds)
330 return !viewBounds.contains(x.roundToInt(), y.roundToInt())
331 }
332 }
333}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
new file mode 100644
index 000000000..7f9e2e2d4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -0,0 +1,134 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.graphics.Bitmap
7import android.graphics.BitmapFactory
8import android.net.Uri
9import android.text.TextUtils
10import android.view.LayoutInflater
11import android.view.View
12import android.view.ViewGroup
13import android.widget.ImageView
14import android.widget.Toast
15import androidx.appcompat.app.AppCompatActivity
16import androidx.documentfile.provider.DocumentFile
17import androidx.lifecycle.ViewModelProvider
18import androidx.lifecycle.lifecycleScope
19import androidx.preference.PreferenceManager
20import androidx.recyclerview.widget.AsyncDifferConfig
21import androidx.recyclerview.widget.DiffUtil
22import androidx.recyclerview.widget.ListAdapter
23import androidx.recyclerview.widget.RecyclerView
24import coil.load
25import kotlinx.coroutines.launch
26import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.YuzuApplication
29import org.yuzu.yuzu_emu.databinding.CardGameBinding
30import org.yuzu.yuzu_emu.activities.EmulationActivity
31import org.yuzu.yuzu_emu.model.Game
32import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
33import org.yuzu.yuzu_emu.model.GamesViewModel
34
35class GameAdapter(private val activity: AppCompatActivity) :
36 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
37 View.OnClickListener {
38 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
39 // Create a new view.
40 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
41 binding.cardGame.setOnClickListener(this)
42
43 // Use that view to create a ViewHolder.
44 return GameViewHolder(binding)
45 }
46
47 override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
48 holder.bind(currentList[position])
49 }
50
51 override fun getItemCount(): Int = currentList.size
52
53 /**
54 * Launches the game that was clicked on.
55 *
56 * @param view The card representing the game the user wants to play.
57 */
58 override fun onClick(view: View) {
59 val holder = view.tag as GameViewHolder
60
61 val gameExists = DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(holder.game.path))?.exists() == true
62 if (!gameExists) {
63 Toast.makeText(
64 YuzuApplication.appContext,
65 R.string.loader_error_file_not_found,
66 Toast.LENGTH_LONG
67 ).show()
68
69 ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
70 return
71 }
72
73 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
74 preferences.edit()
75 .putLong(
76 holder.game.keyLastPlayedTime,
77 System.currentTimeMillis()
78 )
79 .apply()
80
81 EmulationActivity.launch(activity, holder.game)
82 }
83
84 inner class GameViewHolder(val binding: CardGameBinding) :
85 RecyclerView.ViewHolder(binding.root) {
86 lateinit var game: Game
87
88 init {
89 binding.cardGame.tag = this
90 }
91
92 fun bind(game: Game) {
93 this.game = game
94
95 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
96 activity.lifecycleScope.launch {
97 val bitmap = decodeGameIcon(game.path)
98 binding.imageGameScreen.load(bitmap) {
99 error(R.drawable.default_icon)
100 }
101 }
102
103 binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
104
105 binding.textGameTitle.postDelayed(
106 {
107 binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
108 binding.textGameTitle.isSelected = true
109 },
110 3000
111 )
112 }
113 }
114
115 private class DiffCallback : DiffUtil.ItemCallback<Game>() {
116 override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
117 return oldItem.gameId == newItem.gameId
118 }
119
120 override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
121 return oldItem == newItem
122 }
123 }
124
125 private fun decodeGameIcon(uri: String): Bitmap? {
126 val data = NativeLibrary.getIcon(uri)
127 return BitmapFactory.decodeByteArray(
128 data,
129 0,
130 data.size,
131 BitmapFactory.Options()
132 )
133 }
134}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
new file mode 100644
index 000000000..b719dd539
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -0,0 +1,69 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity
10import androidx.core.content.ContextCompat
11import androidx.core.content.res.ResourcesCompat
12import androidx.recyclerview.widget.RecyclerView
13import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
15import org.yuzu.yuzu_emu.model.HomeSetting
16
17class HomeSettingAdapter(private val activity: AppCompatActivity, var options: List<HomeSetting>) :
18 RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
19 View.OnClickListener {
20 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
21 val binding =
22 CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
23 binding.root.setOnClickListener(this)
24 return HomeOptionViewHolder(binding)
25 }
26
27 override fun getItemCount(): Int {
28 return options.size
29 }
30
31 override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
32 holder.bind(options[position])
33 }
34
35 override fun onClick(view: View) {
36 val holder = view.tag as HomeOptionViewHolder
37 holder.option.onClick.invoke()
38 }
39
40 inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
41 RecyclerView.ViewHolder(binding.root) {
42 lateinit var option: HomeSetting
43
44 init {
45 itemView.tag = this
46 }
47
48 fun bind(option: HomeSetting) {
49 this.option = option
50 binding.optionTitle.text = activity.resources.getString(option.titleId)
51 binding.optionDescription.text = activity.resources.getString(option.descriptionId)
52 binding.optionIcon.setImageDrawable(
53 ResourcesCompat.getDrawable(
54 activity.resources,
55 option.iconId,
56 activity.theme
57 )
58 )
59
60 when (option.titleId) {
61 R.string.get_early_access -> binding.optionLayout.background =
62 ContextCompat.getDrawable(
63 binding.optionCard.context,
64 R.drawable.premium_background
65 )
66 }
67 }
68 }
69}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt
new file mode 100644
index 000000000..7006651d0
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt
@@ -0,0 +1,54 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity
10import androidx.recyclerview.widget.RecyclerView
11import androidx.recyclerview.widget.RecyclerView.ViewHolder
12import org.yuzu.yuzu_emu.YuzuApplication
13import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
14import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
15import org.yuzu.yuzu_emu.model.License
16
17class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
18 RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
19 View.OnClickListener {
20 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
21 val binding =
22 ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
23 binding.root.setOnClickListener(this)
24 return LicenseViewHolder(binding)
25 }
26
27 override fun getItemCount(): Int = licenses.size
28
29 override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
30 holder.bind(licenses[position])
31 }
32
33 override fun onClick(view: View) {
34 val license = (view.tag as LicenseViewHolder).license
35 LicenseBottomSheetDialogFragment.newInstance(license)
36 .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
37 }
38
39 inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
40 lateinit var license: License
41
42 init {
43 itemView.tag = this
44 }
45
46 fun bind(license: License) {
47 this.license = license
48
49 val context = YuzuApplication.appContext
50 binding.textSettingName.text = context.getString(license.titleId)
51 binding.textSettingDescription.text = context.getString(license.descriptionId)
52 }
53 }
54}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
new file mode 100644
index 000000000..481ddd5a5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
@@ -0,0 +1,70 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.text.Html
7import android.view.LayoutInflater
8import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity
10import androidx.core.content.res.ResourcesCompat
11import androidx.recyclerview.widget.RecyclerView
12import com.google.android.material.button.MaterialButton
13import org.yuzu.yuzu_emu.databinding.PageSetupBinding
14import org.yuzu.yuzu_emu.model.SetupPage
15
16class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
17 RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
18 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
19 val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 return SetupPageViewHolder(binding)
21 }
22
23 override fun getItemCount(): Int = pages.size
24
25 override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
26 holder.bind(pages[position])
27
28 inner class SetupPageViewHolder(val binding: PageSetupBinding) :
29 RecyclerView.ViewHolder(binding.root) {
30 lateinit var page: SetupPage
31
32 init {
33 itemView.tag = this
34 }
35
36 fun bind(page: SetupPage) {
37 this.page = page
38 binding.icon.setImageDrawable(
39 ResourcesCompat.getDrawable(
40 activity.resources,
41 page.iconId,
42 activity.theme
43 )
44 )
45 binding.textTitle.text = activity.resources.getString(page.titleId)
46 binding.textDescription.text =
47 Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
48
49 binding.buttonAction.apply {
50 text = activity.resources.getString(page.buttonTextId)
51 if (page.buttonIconId != 0) {
52 icon = ResourcesCompat.getDrawable(
53 activity.resources,
54 page.buttonIconId,
55 activity.theme
56 )
57 }
58 iconGravity =
59 if (page.leftAlignedIcon) {
60 MaterialButton.ICON_GRAVITY_START
61 } else {
62 MaterialButton.ICON_GRAVITY_END
63 }
64 setOnClickListener {
65 page.buttonAction.invoke()
66 }
67 }
68 }
69 }
70}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt
new file mode 100644
index 000000000..82a6712b6
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt
@@ -0,0 +1,121 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.applets.keyboard
5
6import android.content.Context
7import android.os.Handler
8import android.os.Looper
9import android.view.KeyEvent
10import android.view.View
11import android.view.WindowInsets
12import android.view.inputmethod.InputMethodManager
13import androidx.annotation.Keep
14import androidx.core.view.ViewCompat
15import org.yuzu.yuzu_emu.NativeLibrary
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment
18import java.io.Serializable
19
20@Keep
21object SoftwareKeyboard {
22 lateinit var data: KeyboardData
23 val dataLock = Object()
24
25 private fun executeNormalImpl(config: KeyboardConfig) {
26 val emulationActivity = NativeLibrary.sEmulationActivity.get()
27 data = KeyboardData(SwkbdResult.Cancel.ordinal, "")
28 val fragment = KeyboardDialogFragment.newInstance(config)
29 fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
30 }
31
32 private fun executeInlineImpl(config: KeyboardConfig) {
33 val emulationActivity = NativeLibrary.sEmulationActivity.get()
34
35 val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay)
36 val im =
37 overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
38 im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED)
39
40 // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
41 val handler = Handler(Looper.myLooper()!!)
42 val delayMs = 500
43 handler.postDelayed(object : Runnable {
44 override fun run() {
45 val insets = ViewCompat.getRootWindowInsets(overlayView)
46 val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime())
47 if (isKeyboardVisible) {
48 handler.postDelayed(this, delayMs.toLong())
49 return
50 }
51
52 // No longer visible, submit the result.
53 NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
54 }
55 }, delayMs.toLong())
56 }
57
58 @JvmStatic
59 fun executeNormal(config: KeyboardConfig): KeyboardData {
60 NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) }
61 synchronized(dataLock) {
62 dataLock.wait()
63 }
64 return data
65 }
66
67 @JvmStatic
68 fun executeInline(config: KeyboardConfig) {
69 NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) }
70 }
71
72 // Corresponds to Service::AM::Applets::SwkbdType
73 enum class SwkbdType {
74 Normal,
75 NumberPad,
76 Qwerty,
77 Unknown3,
78 Latin,
79 SimplifiedChinese,
80 TraditionalChinese,
81 Korean
82 }
83
84 // Corresponds to Service::AM::Applets::SwkbdPasswordMode
85 enum class SwkbdPasswordMode {
86 Disabled,
87 Enabled
88 }
89
90 // Corresponds to Service::AM::Applets::SwkbdResult
91 enum class SwkbdResult {
92 Ok,
93 Cancel
94 }
95
96 @Keep
97 data class KeyboardConfig(
98 var ok_text: String? = null,
99 var header_text: String? = null,
100 var sub_text: String? = null,
101 var guide_text: String? = null,
102 var initial_text: String? = null,
103 var left_optional_symbol_key: Short = 0,
104 var right_optional_symbol_key: Short = 0,
105 var max_text_length: Int = 0,
106 var min_text_length: Int = 0,
107 var initial_cursor_position: Int = 0,
108 var type: Int = 0,
109 var password_mode: Int = 0,
110 var text_draw_type: Int = 0,
111 var key_disable_flags: Int = 0,
112 var use_blur_background: Boolean = false,
113 var enable_backspace_button: Boolean = false,
114 var enable_return_button: Boolean = false,
115 var disable_cancel_button: Boolean = false
116 ) : Serializable
117
118 // Corresponds to Frontend::KeyboardData
119 @Keep
120 data class KeyboardData(var result: Int, var text: String)
121}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt
new file mode 100644
index 000000000..607a3d506
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt
@@ -0,0 +1,100 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.applets.keyboard.ui
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import android.text.InputFilter
10import android.text.InputType
11import androidx.fragment.app.DialogFragment
12import com.google.android.material.dialog.MaterialAlertDialogBuilder
13import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard
15import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig
16import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
17import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
18
19class KeyboardDialogFragment : DialogFragment() {
20 private lateinit var binding: DialogEditTextBinding
21 private lateinit var config: KeyboardConfig
22
23 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
24 binding = DialogEditTextBinding.inflate(layoutInflater)
25 config = requireArguments().serializable(CONFIG)!!
26
27 // Set up the input
28 binding.editText.hint = config.initial_text
29 binding.editText.isSingleLine = !config.enable_return_button
30 binding.editText.filters =
31 arrayOf<InputFilter>(InputFilter.LengthFilter(config.max_text_length))
32
33 // Handle input type
34 var inputType: Int
35 when (config.type) {
36 SoftwareKeyboard.SwkbdType.Normal.ordinal,
37 SoftwareKeyboard.SwkbdType.Qwerty.ordinal,
38 SoftwareKeyboard.SwkbdType.Unknown3.ordinal,
39 SoftwareKeyboard.SwkbdType.Latin.ordinal,
40 SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal,
41 SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal,
42 SoftwareKeyboard.SwkbdType.Korean.ordinal -> {
43 inputType = InputType.TYPE_CLASS_TEXT
44 if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
45 inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
46 }
47 }
48 SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> {
49 inputType = InputType.TYPE_CLASS_NUMBER
50 if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
51 inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD
52 }
53 }
54 else -> {
55 inputType = InputType.TYPE_CLASS_TEXT
56 if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
57 inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
58 }
59 }
60 }
61 binding.editText.inputType = inputType
62
63 val headerText =
64 config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) }
65 val okText =
66 config.ok_text!!.ifEmpty { resources.getString(R.string.submit) }
67
68 return MaterialAlertDialogBuilder(requireContext())
69 .setTitle(headerText)
70 .setView(binding.root)
71 .setPositiveButton(okText) { _, _ ->
72 SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal
73 SoftwareKeyboard.data.text = binding.editText.text.toString()
74 }
75 .setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ ->
76 SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal
77 }
78 .create()
79 }
80
81 override fun onDismiss(dialog: DialogInterface) {
82 super.onDismiss(dialog)
83 synchronized(SoftwareKeyboard.dataLock) {
84 SoftwareKeyboard.dataLock.notifyAll()
85 }
86 }
87
88 companion object {
89 const val TAG = "KeyboardDialogFragment"
90 const val CONFIG = "keyboard_config"
91
92 fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment {
93 val frag = KeyboardDialogFragment()
94 val args = Bundle()
95 args.putSerializable(CONFIG, config)
96 frag.arguments = args
97 return frag
98 }
99 }
100}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt
new file mode 100644
index 000000000..3b1559c80
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt
@@ -0,0 +1,48 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.disk_shader_cache
5
6import androidx.annotation.Keep
7import org.yuzu.yuzu_emu.NativeLibrary
8import org.yuzu.yuzu_emu.R
9import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment
10
11@Keep
12object DiskShaderCacheProgress {
13 val finishLock = Object()
14 private lateinit var fragment: ShaderProgressDialogFragment
15
16 private fun prepareDialog() {
17 val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
18 emulationActivity.runOnUiThread {
19 fragment = ShaderProgressDialogFragment.newInstance(
20 emulationActivity.getString(R.string.loading),
21 emulationActivity.getString(R.string.preparing_shaders)
22 )
23 fragment.show(emulationActivity.supportFragmentManager, ShaderProgressDialogFragment.TAG)
24 }
25 synchronized(finishLock) { finishLock.wait() }
26 }
27
28 @JvmStatic
29 fun loadProgress(stage: Int, progress: Int, max: Int) {
30 val emulationActivity = NativeLibrary.sEmulationActivity.get()
31 ?: error("[DiskShaderCacheProgress] EmulationActivity not present")
32
33 when (LoadCallbackStage.values()[stage]) {
34 LoadCallbackStage.Prepare -> prepareDialog()
35 LoadCallbackStage.Build -> fragment.onUpdateProgress(
36 emulationActivity.getString(R.string.building_shaders),
37 progress,
38 max
39 )
40 LoadCallbackStage.Complete -> fragment.dismiss()
41 }
42 }
43
44 // Equivalent to VideoCore::LoadCallbackStage
45 enum class LoadCallbackStage {
46 Prepare, Build, Complete
47 }
48}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt
new file mode 100644
index 000000000..bf6f0366d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt
@@ -0,0 +1,31 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.disk_shader_cache
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.ViewModel
9
10class ShaderProgressViewModel : ViewModel() {
11 private val _progress = MutableLiveData(0)
12 val progress: LiveData<Int> get() = _progress
13
14 private val _max = MutableLiveData(0)
15 val max: LiveData<Int> get() = _max
16
17 private val _message = MutableLiveData("")
18 val message: LiveData<String> get() = _message
19
20 fun setProgress(progress: Int) {
21 _progress.postValue(progress)
22 }
23
24 fun setMax(max: Int) {
25 _max.postValue(max)
26 }
27
28 fun setMessage(msg: String) {
29 _message.postValue(msg)
30 }
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt
new file mode 100644
index 000000000..2c68c9ac3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt
@@ -0,0 +1,101 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.disk_shader_cache.ui
5
6import android.app.Dialog
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.appcompat.app.AlertDialog
12import androidx.fragment.app.DialogFragment
13import androidx.lifecycle.ViewModelProvider
14import com.google.android.material.dialog.MaterialAlertDialogBuilder
15import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
16import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
17import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
18
19class ShaderProgressDialogFragment : DialogFragment() {
20 private var _binding: DialogProgressBarBinding? = null
21 private val binding get() = _binding!!
22
23 private lateinit var alertDialog: AlertDialog
24
25 private lateinit var shaderProgressViewModel: ShaderProgressViewModel
26
27 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
28 _binding = DialogProgressBarBinding.inflate(layoutInflater)
29 shaderProgressViewModel =
30 ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
31
32 val title = requireArguments().getString(TITLE)
33 val message = requireArguments().getString(MESSAGE)
34
35 isCancelable = false
36 alertDialog = MaterialAlertDialogBuilder(requireActivity())
37 .setView(binding.root)
38 .setTitle(title)
39 .setMessage(message)
40 .create()
41 return alertDialog
42 }
43
44 override fun onCreateView(
45 inflater: LayoutInflater,
46 container: ViewGroup?,
47 savedInstanceState: Bundle?
48 ): View {
49 return binding.root
50 }
51
52 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53 super.onViewCreated(view, savedInstanceState)
54 shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
55 binding.progressBar.progress = progress
56 setUpdateText()
57 }
58 shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
59 binding.progressBar.max = max
60 setUpdateText()
61 }
62 shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
63 alertDialog.setMessage(msg)
64 }
65 synchronized(DiskShaderCacheProgress.finishLock) { DiskShaderCacheProgress.finishLock.notifyAll() }
66 }
67
68 override fun onDestroyView() {
69 super.onDestroyView()
70 _binding = null
71 }
72
73 fun onUpdateProgress(msg: String, progress: Int, max: Int) {
74 shaderProgressViewModel.setProgress(progress)
75 shaderProgressViewModel.setMax(max)
76 shaderProgressViewModel.setMessage(msg)
77 }
78
79 private fun setUpdateText() {
80 binding.progressText.text = String.format(
81 "%d/%d",
82 shaderProgressViewModel.progress.value,
83 shaderProgressViewModel.max.value
84 )
85 }
86
87 companion object {
88 const val TAG = "ProgressDialogFragment"
89 const val TITLE = "title"
90 const val MESSAGE = "message"
91
92 fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
93 val frag = ShaderProgressDialogFragment()
94 val args = Bundle()
95 args.putString(TITLE, title)
96 args.putString(MESSAGE, message)
97 frag.arguments = args
98 return frag
99 }
100 }
101}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt
new file mode 100644
index 000000000..4c3a9ca80
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt
@@ -0,0 +1,302 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4// SPDX-License-Identifier: MPL-2.0
5// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
6
7package org.yuzu.yuzu_emu.features
8
9import android.database.Cursor
10import android.database.MatrixCursor
11import android.os.CancellationSignal
12import android.os.ParcelFileDescriptor
13import android.provider.DocumentsContract
14import android.provider.DocumentsProvider
15import android.webkit.MimeTypeMap
16import org.yuzu.yuzu_emu.BuildConfig
17import org.yuzu.yuzu_emu.R
18import org.yuzu.yuzu_emu.YuzuApplication
19import org.yuzu.yuzu_emu.getPublicFilesDir
20import java.io.*
21
22class DocumentProvider : DocumentsProvider() {
23 private val baseDirectory: File
24 get() = File(YuzuApplication.application.getPublicFilesDir().canonicalPath)
25
26 companion object {
27 private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
28 DocumentsContract.Root.COLUMN_ROOT_ID,
29 DocumentsContract.Root.COLUMN_MIME_TYPES,
30 DocumentsContract.Root.COLUMN_FLAGS,
31 DocumentsContract.Root.COLUMN_ICON,
32 DocumentsContract.Root.COLUMN_TITLE,
33 DocumentsContract.Root.COLUMN_SUMMARY,
34 DocumentsContract.Root.COLUMN_DOCUMENT_ID,
35 DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
36 )
37
38 private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
39 DocumentsContract.Document.COLUMN_DOCUMENT_ID,
40 DocumentsContract.Document.COLUMN_MIME_TYPE,
41 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
42 DocumentsContract.Document.COLUMN_LAST_MODIFIED,
43 DocumentsContract.Document.COLUMN_FLAGS,
44 DocumentsContract.Document.COLUMN_SIZE
45 )
46
47 const val AUTHORITY : String = BuildConfig.APPLICATION_ID + ".user"
48 const val ROOT_ID: String = "root"
49 }
50
51 override fun onCreate(): Boolean {
52 return true
53 }
54
55 /**
56 * @return The [File] that corresponds to the document ID supplied by [getDocumentId]
57 */
58 private fun getFile(documentId: String): File {
59 if (documentId.startsWith(ROOT_ID)) {
60 val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
61 if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
62 return file
63 } else {
64 throw FileNotFoundException("'$documentId' is not in any known root")
65 }
66 }
67
68 /**
69 * @return A unique ID for the provided [File]
70 */
71 private fun getDocumentId(file: File): String {
72 return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
73 }
74
75 override fun queryRoots(projection: Array<out String>?): Cursor {
76 val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
77
78 cursor.newRow().apply {
79 add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
80 add(DocumentsContract.Root.COLUMN_SUMMARY, null)
81 add(
82 DocumentsContract.Root.COLUMN_FLAGS,
83 DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
84 )
85 add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
86 add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
87 add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
88 add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
89 add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
90 }
91
92 return cursor
93 }
94
95 override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
96 val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
97 return includeFile(cursor, documentId, null)
98 }
99
100 override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
101 return documentId?.startsWith(parentDocumentId!!) ?: false
102 }
103
104 /**
105 * @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
106 */
107 private fun File.resolveWithoutConflict(name: String): File {
108 var file = resolve(name)
109 if (file.exists()) {
110 var noConflictId =
111 1 // Makes sure two files don't have the same name by adding a number to the end
112 val extension = name.substringAfterLast('.')
113 val baseName = name.substringBeforeLast('.')
114 while (file.exists())
115 file = resolve("$baseName (${noConflictId++}).$extension")
116 }
117 return file
118 }
119
120 override fun createDocument(
121 parentDocumentId: String?,
122 mimeType: String?,
123 displayName: String
124 ): String {
125 val parentFile = getFile(parentDocumentId!!)
126 val newFile = parentFile.resolveWithoutConflict(displayName)
127
128 try {
129 if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
130 if (!newFile.mkdir())
131 throw IOException("Failed to create directory")
132 } else {
133 if (!newFile.createNewFile())
134 throw IOException("Failed to create file")
135 }
136 } catch (e: IOException) {
137 throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
138 }
139
140 return getDocumentId(newFile)
141 }
142
143 override fun deleteDocument(documentId: String?) {
144 val file = getFile(documentId!!)
145 if (!file.delete())
146 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
147 }
148
149 override fun removeDocument(documentId: String, parentDocumentId: String?) {
150 val parent = getFile(parentDocumentId!!)
151 val file = getFile(documentId)
152
153 if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
154 if (!file.delete())
155 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
156 } else {
157 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
158 }
159 }
160
161 override fun renameDocument(documentId: String?, displayName: String?): String {
162 if (displayName == null)
163 throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
164
165 val sourceFile = getFile(documentId!!)
166 val sourceParentFile = sourceFile.parentFile
167 ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
168 val destFile = sourceParentFile.resolve(displayName)
169
170 try {
171 if (!sourceFile.renameTo(destFile))
172 throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
173 } catch (e: Exception) {
174 throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
175 }
176
177 return getDocumentId(destFile)
178 }
179
180 private fun copyDocument(
181 sourceDocumentId: String, sourceParentDocumentId: String,
182 targetParentDocumentId: String?
183 ): String {
184 if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
185 throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
186
187 return copyDocument(sourceDocumentId, targetParentDocumentId)
188 }
189
190 override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
191 val parent = getFile(targetParentDocumentId!!)
192 val oldFile = getFile(sourceDocumentId)
193 val newFile = parent.resolveWithoutConflict(oldFile.name)
194
195 try {
196 if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
197 throw IOException("Couldn't create new file")
198
199 FileInputStream(oldFile).use { inStream ->
200 FileOutputStream(newFile).use { outStream ->
201 inStream.copyTo(outStream)
202 }
203 }
204 } catch (e: IOException) {
205 throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
206 }
207
208 return getDocumentId(newFile)
209 }
210
211 override fun moveDocument(
212 sourceDocumentId: String, sourceParentDocumentId: String?,
213 targetParentDocumentId: String?
214 ): String {
215 try {
216 val newDocumentId = copyDocument(
217 sourceDocumentId, sourceParentDocumentId!!,
218 targetParentDocumentId
219 )
220 removeDocument(sourceDocumentId, sourceParentDocumentId)
221 return newDocumentId
222 } catch (e: FileNotFoundException) {
223 throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
224 }
225 }
226
227 private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
228 val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
229 val localFile = file ?: getFile(documentId!!)
230
231 var flags = 0
232 if (localFile.isDirectory && localFile.canWrite()) {
233 flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
234 } else if (localFile.canWrite()) {
235 flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
236 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
237
238 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
239 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
240 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
241 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
242 }
243
244 cursor.newRow().apply {
245 add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
246 add(
247 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
248 if (localFile == baseDirectory) context!!.getString(R.string.app_name) else localFile.name
249 )
250 add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
251 add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
252 add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
253 add(DocumentsContract.Document.COLUMN_FLAGS, flags)
254 if (localFile == baseDirectory)
255 add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
256 }
257
258 return cursor
259 }
260
261 private fun getTypeForFile(file: File): Any {
262 return if (file.isDirectory)
263 DocumentsContract.Document.MIME_TYPE_DIR
264 else
265 getTypeForName(file.name)
266 }
267
268 private fun getTypeForName(name: String): Any {
269 val lastDot = name.lastIndexOf('.')
270 if (lastDot >= 0) {
271 val extension = name.substring(lastDot + 1)
272 val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
273 if (mime != null)
274 return mime
275 }
276 return "application/octect-stream"
277 }
278
279 override fun queryChildDocuments(
280 parentDocumentId: String?,
281 projection: Array<out String>?,
282 sortOrder: String?
283 ): Cursor {
284 var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
285
286 val parent = getFile(parentDocumentId!!)
287 for (file in parent.listFiles()!!)
288 cursor = includeFile(cursor, null, file)
289
290 return cursor
291 }
292
293 override fun openDocument(
294 documentId: String?,
295 mode: String?,
296 signal: CancellationSignal?
297 ): ParcelFileDescriptor {
298 val file = documentId?.let { getFile(it) }
299 val accessMode = ParcelFileDescriptor.parseMode(mode)
300 return ParcelFileDescriptor.open(file, accessMode)
301 }
302}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
new file mode 100644
index 000000000..a6e9833ee
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
@@ -0,0 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractBooleanSetting : AbstractSetting {
7 var boolean: Boolean
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
new file mode 100644
index 000000000..6fe4bc263
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
@@ -0,0 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractFloatSetting : AbstractSetting {
7 var float: Float
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
new file mode 100644
index 000000000..892b7dcfe
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
@@ -0,0 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractIntSetting : AbstractSetting {
7 var int: Int
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
new file mode 100644
index 000000000..258580209
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
@@ -0,0 +1,12 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractSetting {
7 val key: String?
8 val section: String?
9 val isRuntimeEditable: Boolean
10 val valueAsString: String
11 val defaultValue: Any
12}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
new file mode 100644
index 000000000..0d02c5997
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
@@ -0,0 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractStringSetting : AbstractSetting {
7 var string: String
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
new file mode 100644
index 000000000..3dfd66779
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -0,0 +1,38 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6enum class BooleanSetting(
7 override val key: String,
8 override val section: String,
9 override val defaultValue: Boolean
10) : AbstractBooleanSetting {
11 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
12
13 override var boolean: Boolean = defaultValue
14
15 override val valueAsString: String
16 get() = boolean.toString()
17
18 override val isRuntimeEditable: Boolean
19 get() {
20 for (setting in NOT_RUNTIME_EDITABLE) {
21 if (setting == this) {
22 return false
23 }
24 }
25 return true
26 }
27
28 companion object {
29 private val NOT_RUNTIME_EDITABLE = listOf(
30 USE_CUSTOM_RTC
31 )
32
33 fun from(key: String): BooleanSetting? =
34 BooleanSetting.values().firstOrNull { it.key == key }
35
36 fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
37 }
38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
new file mode 100644
index 000000000..e5545a916
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
@@ -0,0 +1,36 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6enum class FloatSetting(
7 override val key: String,
8 override val section: String,
9 override val defaultValue: Float
10) : AbstractFloatSetting {
11 // No float settings currently exist
12 EMPTY_SETTING("", "", 0f);
13
14 override var float: Float = defaultValue
15
16 override val valueAsString: String
17 get() = float.toString()
18
19 override val isRuntimeEditable: Boolean
20 get() {
21 for (setting in NOT_RUNTIME_EDITABLE) {
22 if (setting == this) {
23 return false
24 }
25 }
26 return true
27 }
28
29 companion object {
30 private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
31
32 fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
33
34 fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
35 }
36}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
new file mode 100644
index 000000000..c5722a5a1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -0,0 +1,131 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6enum class IntSetting(
7 override val key: String,
8 override val section: String,
9 override val defaultValue: Int
10) : AbstractIntSetting {
11 RENDERER_USE_SPEED_LIMIT(
12 "use_speed_limit",
13 Settings.SECTION_RENDERER,
14 1
15 ),
16 USE_DOCKED_MODE(
17 "use_docked_mode",
18 Settings.SECTION_SYSTEM,
19 0
20 ),
21 RENDERER_USE_DISK_SHADER_CACHE(
22 "use_disk_shader_cache",
23 Settings.SECTION_RENDERER,
24 1
25 ),
26 RENDERER_FORCE_MAX_CLOCK(
27 "force_max_clock",
28 Settings.SECTION_RENDERER,
29 1
30 ),
31 RENDERER_ASYNCHRONOUS_SHADERS(
32 "use_asynchronous_shaders",
33 Settings.SECTION_RENDERER,
34 0
35 ),
36 RENDERER_DEBUG(
37 "debug",
38 Settings.SECTION_RENDERER,
39 0
40 ),
41 RENDERER_SPEED_LIMIT(
42 "speed_limit",
43 Settings.SECTION_RENDERER,
44 100
45 ),
46 CPU_ACCURACY(
47 "cpu_accuracy",
48 Settings.SECTION_CPU,
49 0
50 ),
51 REGION_INDEX(
52 "region_index",
53 Settings.SECTION_SYSTEM,
54 -1
55 ),
56 LANGUAGE_INDEX(
57 "language_index",
58 Settings.SECTION_SYSTEM,
59 1
60 ),
61 RENDERER_BACKEND(
62 "backend",
63 Settings.SECTION_RENDERER,
64 1
65 ),
66 RENDERER_ACCURACY(
67 "gpu_accuracy",
68 Settings.SECTION_RENDERER,
69 0
70 ),
71 RENDERER_RESOLUTION(
72 "resolution_setup",
73 Settings.SECTION_RENDERER,
74 2
75 ),
76 RENDERER_VSYNC(
77 "use_vsync",
78 Settings.SECTION_RENDERER,
79 0
80 ),
81 RENDERER_SCALING_FILTER(
82 "scaling_filter",
83 Settings.SECTION_RENDERER,
84 1
85 ),
86 RENDERER_ANTI_ALIASING(
87 "anti_aliasing",
88 Settings.SECTION_RENDERER,
89 0
90 ),
91 RENDERER_ASPECT_RATIO(
92 "aspect_ratio",
93 Settings.SECTION_RENDERER,
94 0
95 ),
96 AUDIO_VOLUME(
97 "volume",
98 Settings.SECTION_AUDIO,
99 100
100 );
101
102 override var int: Int = defaultValue
103
104 override val valueAsString: String
105 get() = int.toString()
106
107 override val isRuntimeEditable: Boolean
108 get() {
109 for (setting in NOT_RUNTIME_EDITABLE) {
110 if (setting == this) {
111 return false
112 }
113 }
114 return true
115 }
116
117 companion object {
118 private val NOT_RUNTIME_EDITABLE = listOf(
119 RENDERER_USE_DISK_SHADER_CACHE,
120 RENDERER_ASYNCHRONOUS_SHADERS,
121 RENDERER_DEBUG,
122 RENDERER_BACKEND,
123 RENDERER_RESOLUTION,
124 RENDERER_VSYNC
125 )
126
127 fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
128
129 fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
130 }
131}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt
new file mode 100644
index 000000000..474f598a9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt
@@ -0,0 +1,37 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6/**
7 * A semantically-related group of Settings objects. These Settings are
8 * internally stored as a HashMap.
9 */
10class SettingSection(val name: String) {
11 val settings = HashMap<String, AbstractSetting>()
12
13 /**
14 * Convenience method; inserts a value directly into the backing HashMap.
15 *
16 * @param setting The Setting to be inserted.
17 */
18 fun putSetting(setting: AbstractSetting) {
19 settings[setting.key!!] = setting
20 }
21
22 /**
23 * Convenience method; gets a value directly from the backing HashMap.
24 *
25 * @param key Used to retrieve the Setting.
26 * @return A Setting object (you should probably cast this before using)
27 */
28 fun getSetting(key: String): AbstractSetting? {
29 return settings[key]
30 }
31
32 fun mergeSection(settingSection: SettingSection) {
33 for (setting in settingSection.settings.values) {
34 putSetting(setting)
35 }
36 }
37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
new file mode 100644
index 000000000..8df20b928
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -0,0 +1,158 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6import android.text.TextUtils
7import org.yuzu.yuzu_emu.R
8import org.yuzu.yuzu_emu.YuzuApplication
9import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
10import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
11import java.util.*
12
13class Settings {
14 private var gameId: String? = null
15
16 var isLoaded = false
17
18 /**
19 * A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
20 * when getting a key not already in the map
21 */
22 class SettingsSectionMap : HashMap<String, SettingSection?>() {
23 override operator fun get(key: String): SettingSection? {
24 if (!super.containsKey(key)) {
25 val section = SettingSection(key)
26 super.put(key, section)
27 return section
28 }
29 return super.get(key)
30 }
31 }
32
33 var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
34
35 fun getSection(sectionName: String): SettingSection? {
36 return sections[sectionName]
37 }
38
39 val isEmpty: Boolean
40 get() = sections.isEmpty()
41
42 fun loadSettings(view: SettingsActivityView? = null) {
43 sections = SettingsSectionMap()
44 loadYuzuSettings(view)
45 if (!TextUtils.isEmpty(gameId)) {
46 loadCustomGameSettings(gameId!!, view)
47 }
48 isLoaded = true
49 }
50
51 private fun loadYuzuSettings(view: SettingsActivityView?) {
52 for ((fileName) in configFileSectionsMap) {
53 sections.putAll(SettingsFile.readFile(fileName, view))
54 }
55 }
56
57 private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
58 // Custom game settings
59 mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
60 }
61
62 private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
63 for ((key, updatedSection) in updatedSections) {
64 if (sections.containsKey(key)) {
65 val originalSection = sections[key]
66 originalSection!!.mergeSection(updatedSection!!)
67 } else {
68 sections[key] = updatedSection
69 }
70 }
71 }
72
73 fun loadSettings(gameId: String, view: SettingsActivityView) {
74 this.gameId = gameId
75 loadSettings(view)
76 }
77
78 fun saveSettings(view: SettingsActivityView) {
79 if (TextUtils.isEmpty(gameId)) {
80 view.showToastMessage(
81 YuzuApplication.appContext.getString(R.string.ini_saved),
82 false
83 )
84
85 for ((fileName, sectionNames) in configFileSectionsMap) {
86 val iniSections = TreeMap<String, SettingSection>()
87 for (section in sectionNames) {
88 iniSections[section] = sections[section]!!
89 }
90
91 SettingsFile.saveFile(fileName, iniSections, view)
92 }
93 } else {
94 // Custom game settings
95 view.showToastMessage(
96 YuzuApplication.appContext.getString(R.string.gameid_saved, gameId),
97 false
98 )
99
100 SettingsFile.saveCustomGameSettings(gameId, sections)
101 }
102 }
103
104 companion object {
105 const val SECTION_GENERAL = "General"
106 const val SECTION_SYSTEM = "System"
107 const val SECTION_RENDERER = "Renderer"
108 const val SECTION_AUDIO = "Audio"
109 const val SECTION_CPU = "Cpu"
110 const val SECTION_THEME = "Theme"
111 const val SECTION_DEBUG = "Debug"
112
113 const val PREF_OVERLAY_INIT = "OverlayInit"
114 const val PREF_CONTROL_SCALE = "controlScale"
115 const val PREF_CONTROL_OPACITY = "controlOpacity"
116 const val PREF_TOUCH_ENABLED = "isTouchEnabled"
117 const val PREF_BUTTON_TOGGLE_0 = "buttonToggle0"
118 const val PREF_BUTTON_TOGGLE_1 = "buttonToggle1"
119 const val PREF_BUTTON_TOGGLE_2 = "buttonToggle2"
120 const val PREF_BUTTON_TOGGLE_3 = "buttonToggle3"
121 const val PREF_BUTTON_TOGGLE_4 = "buttonToggle4"
122 const val PREF_BUTTON_TOGGLE_5 = "buttonToggle5"
123 const val PREF_BUTTON_TOGGLE_6 = "buttonToggle6"
124 const val PREF_BUTTON_TOGGLE_7 = "buttonToggle7"
125 const val PREF_BUTTON_TOGGLE_8 = "buttonToggle8"
126 const val PREF_BUTTON_TOGGLE_9 = "buttonToggle9"
127 const val PREF_BUTTON_TOGGLE_10 = "buttonToggle10"
128 const val PREF_BUTTON_TOGGLE_11 = "buttonToggle11"
129 const val PREF_BUTTON_TOGGLE_12 = "buttonToggle12"
130 const val PREF_BUTTON_TOGGLE_13 = "buttonToggle13"
131 const val PREF_BUTTON_TOGGLE_14 = "buttonToggle14"
132
133 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
134 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
135 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
136 const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
137 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
138 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
139
140 const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
141 const val PREF_THEME = "Theme"
142 const val PREF_THEME_MODE = "ThemeMode"
143 const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
144
145 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
146
147 init {
148 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
149 listOf(
150 SECTION_GENERAL,
151 SECTION_SYSTEM,
152 SECTION_RENDERER,
153 SECTION_AUDIO,
154 SECTION_CPU
155 )
156 }
157 }
158}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt
new file mode 100644
index 000000000..bd9233d62
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt
@@ -0,0 +1,10 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6import androidx.lifecycle.ViewModel
7
8class SettingsViewModel : ViewModel() {
9 val settings = Settings()
10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
new file mode 100644
index 000000000..63f95690c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
@@ -0,0 +1,37 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6enum class StringSetting(
7 override val key: String,
8 override val section: String,
9 override val defaultValue: String
10) : AbstractStringSetting {
11 CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0");
12
13 override var string: String = defaultValue
14
15 override val valueAsString: String
16 get() = string
17
18 override val isRuntimeEditable: Boolean
19 get() {
20 for (setting in NOT_RUNTIME_EDITABLE) {
21 if (setting == this) {
22 return false
23 }
24 }
25 return true
26 }
27
28 companion object {
29 private val NOT_RUNTIME_EDITABLE = listOf(
30 CUSTOM_RTC
31 )
32
33 fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
34
35 fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
36 }
37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
new file mode 100644
index 000000000..bc0bf7788
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -0,0 +1,31 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
8
9class DateTimeSetting(
10 setting: AbstractSetting?,
11 titleId: Int,
12 descriptionId: Int,
13 val key: String? = null,
14 private val defaultValue: String? = null
15) : SettingsItem(setting, titleId, descriptionId) {
16 override val type = TYPE_DATETIME_SETTING
17
18 val value: String
19 get() = if (setting != null) {
20 val setting = setting as AbstractStringSetting
21 setting.string
22 } else {
23 defaultValue!!
24 }
25
26 fun setSelectedValue(datetime: String): AbstractStringSetting {
27 val stringSetting = setting as AbstractStringSetting
28 stringSetting.string = datetime
29 return stringSetting
30 }
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
new file mode 100644
index 000000000..0f8edbfb0
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7
8class HeaderSetting(
9 setting: AbstractSetting?,
10 titleId: Int,
11 descriptionId: Int
12) : SettingsItem(setting, titleId, descriptionId) {
13 override val type = TYPE_HEADER
14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
new file mode 100644
index 000000000..caaab50d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
@@ -0,0 +1,13 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6class RunnableSetting(
7 titleId: Int,
8 descriptionId: Int,
9 val isRuntimeRunnable: Boolean,
10 val runnable: () -> Unit
11) : SettingsItem(null, titleId, descriptionId) {
12 override val type = TYPE_RUNNABLE
13}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
new file mode 100644
index 000000000..07520849e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -0,0 +1,39 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.NativeLibrary
7import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
8
9/**
10 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
11 * Each one corresponds to a [AbstractSetting] object, so this class's subclasses
12 * should vaguely correspond to those subclasses. There are a few with multiple analogues
13 * and a few with none (Headers, for example, do not correspond to anything in the ini
14 * file.)
15 */
16abstract class SettingsItem(
17 var setting: AbstractSetting?,
18 val nameId: Int,
19 val descriptionId: Int
20) {
21 abstract val type: Int
22
23 val isEditable: Boolean
24 get() {
25 if (!NativeLibrary.isRunning()) return true
26 return setting?.isRuntimeEditable ?: false
27 }
28
29 companion object {
30 const val TYPE_HEADER = 0
31 const val TYPE_SWITCH = 1
32 const val TYPE_SINGLE_CHOICE = 2
33 const val TYPE_SLIDER = 3
34 const val TYPE_SUBMENU = 4
35 const val TYPE_STRING_SINGLE_CHOICE = 5
36 const val TYPE_DATETIME_SETTING = 6
37 const val TYPE_RUNNABLE = 7
38 }
39}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
new file mode 100644
index 000000000..9eac9904e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -0,0 +1,40 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
7import org.yuzu.yuzu_emu.features.settings.model.IntSetting
8
9class SingleChoiceSetting(
10 setting: AbstractIntSetting?,
11 titleId: Int,
12 descriptionId: Int,
13 val choicesId: Int,
14 val valuesId: Int,
15 val key: String? = null,
16 val defaultValue: Int? = null
17) : SettingsItem(setting, titleId, descriptionId) {
18 override val type = TYPE_SINGLE_CHOICE
19
20 val selectedValue: Int
21 get() = if (setting != null) {
22 val setting = setting as AbstractIntSetting
23 setting.int
24 } else {
25 defaultValue!!
26 }
27
28 /**
29 * Write a value to the backing int. If that int was previously null,
30 * initializes a new one and returns it, so it can be added to the Hashmap.
31 *
32 * @param selection New value of the int.
33 * @return the existing setting with the new value applied.
34 */
35 fun setSelectedValue(selection: Int): AbstractIntSetting {
36 val intSetting = setting as AbstractIntSetting
37 intSetting.int = selection
38 return intSetting
39 }
40}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
new file mode 100644
index 000000000..842648ce4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -0,0 +1,64 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
8import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
9import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
10import org.yuzu.yuzu_emu.features.settings.model.IntSetting
11import org.yuzu.yuzu_emu.utils.Log
12import kotlin.math.roundToInt
13
14class SliderSetting(
15 setting: AbstractSetting?,
16 titleId: Int,
17 descriptionId: Int,
18 val min: Int,
19 val max: Int,
20 val units: String,
21 val key: String? = null,
22 val defaultValue: Int? = null,
23) : SettingsItem(setting, titleId, descriptionId) {
24 override val type = TYPE_SLIDER
25
26 val selectedValue: Int
27 get() {
28 val setting = setting ?: return defaultValue!!
29 return when (setting) {
30 is AbstractIntSetting -> setting.int
31 is AbstractFloatSetting -> setting.float.roundToInt()
32 else -> {
33 Log.error("[SliderSetting] Error casting setting type.")
34 -1
35 }
36 }
37 }
38
39 /**
40 * Write a value to the backing int. If that int was previously null,
41 * initializes a new one and returns it, so it can be added to the Hashmap.
42 *
43 * @param selection New value of the int.
44 * @return the existing setting with the new value applied.
45 */
46 fun setSelectedValue(selection: Int): AbstractIntSetting {
47 val intSetting = setting as AbstractIntSetting
48 intSetting.int = selection
49 return intSetting
50 }
51
52 /**
53 * Write a value to the backing float. If that float was previously null,
54 * initializes a new one and returns it, so it can be added to the Hashmap.
55 *
56 * @param selection New value of the float.
57 * @return the existing setting with the new value applied.
58 */
59 fun setSelectedValue(selection: Float): AbstractFloatSetting {
60 val floatSetting = setting as AbstractFloatSetting
61 floatSetting.float = selection
62 return floatSetting
63 }
64}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
new file mode 100644
index 000000000..9e9b00d10
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -0,0 +1,58 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
8import org.yuzu.yuzu_emu.features.settings.model.StringSetting
9
10class StringSingleChoiceSetting(
11 val key: String? = null,
12 setting: AbstractSetting?,
13 titleId: Int,
14 descriptionId: Int,
15 val choicesId: Array<String>,
16 private val valuesId: Array<String>?,
17 private val defaultValue: String? = null
18) : SettingsItem(setting, titleId, descriptionId) {
19 override val type = TYPE_STRING_SINGLE_CHOICE
20
21 fun getValueAt(index: Int): String? {
22 if (valuesId == null) return null
23 return if (index >= 0 && index < valuesId.size) {
24 valuesId[index]
25 } else ""
26 }
27
28 val selectedValue: String
29 get() = if (setting != null) {
30 val setting = setting as AbstractStringSetting
31 setting.string
32 } else {
33 defaultValue!!
34 }
35 val selectValueIndex: Int
36 get() {
37 val selectedValue = selectedValue
38 for (i in valuesId!!.indices) {
39 if (valuesId[i] == selectedValue) {
40 return i
41 }
42 }
43 return -1
44 }
45
46 /**
47 * Write a value to the backing int. If that int was previously null,
48 * initializes a new one and returns it, so it can be added to the Hashmap.
49 *
50 * @param selection New value of the int.
51 * @return the existing setting with the new value applied.
52 */
53 fun setSelectedValue(selection: String): AbstractStringSetting {
54 val stringSetting = setting as AbstractStringSetting
55 stringSetting.string = selection
56 return stringSetting
57 }
58}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
new file mode 100644
index 000000000..a3ef59c2f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7
8class SubmenuSetting(
9 titleId: Int,
10 descriptionId: Int,
11 val menuKey: String
12) : SettingsItem(null, titleId, descriptionId) {
13 override val type = TYPE_SUBMENU
14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
new file mode 100644
index 000000000..90b198718
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -0,0 +1,62 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
8import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
9
10class SwitchSetting(
11 setting: AbstractSetting,
12 titleId: Int,
13 descriptionId: Int,
14 val key: String? = null,
15 val defaultValue: Any? = null
16) : SettingsItem(setting, titleId, descriptionId) {
17 override val type = TYPE_SWITCH
18
19 val isChecked: Boolean
20 get() {
21 if (setting == null) {
22 return defaultValue as Boolean
23 }
24
25 // Try integer setting
26 try {
27 val setting = setting as AbstractIntSetting
28 return setting.int == 1
29 } catch (_: ClassCastException) {
30 }
31
32 // Try boolean setting
33 try {
34 val setting = setting as AbstractBooleanSetting
35 return setting.boolean
36 } catch (_: ClassCastException) {
37 }
38 return defaultValue as Boolean
39 }
40
41 /**
42 * Write a value to the backing boolean. If that boolean was previously null,
43 * initializes a new one and returns it, so it can be added to the Hashmap.
44 *
45 * @param checked Pretty self explanatory.
46 * @return the existing setting with the new value applied.
47 */
48 fun setChecked(checked: Boolean): AbstractSetting {
49 // Try integer setting
50 try {
51 val setting = setting as AbstractIntSetting
52 setting.int = if (checked) 1 else 0
53 return setting
54 } catch (_: ClassCastException) {
55 }
56
57 // Try boolean setting
58 val setting = setting as AbstractBooleanSetting
59 setting.boolean = checked
60 return setting
61 }
62}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
new file mode 100644
index 000000000..72e2cce2a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -0,0 +1,243 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.content.Intent
8import android.os.Bundle
9import android.view.Menu
10import android.view.View
11import android.widget.Toast
12import androidx.activity.viewModels
13import androidx.appcompat.app.AppCompatActivity
14import androidx.core.view.ViewCompat
15import androidx.core.view.WindowCompat
16import androidx.core.view.WindowInsetsCompat
17import android.view.ViewGroup.MarginLayoutParams
18import androidx.activity.OnBackPressedCallback
19import androidx.core.view.updatePadding
20import com.google.android.material.color.MaterialColors
21import org.yuzu.yuzu_emu.NativeLibrary
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
24import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
25import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
26import org.yuzu.yuzu_emu.features.settings.model.IntSetting
27import org.yuzu.yuzu_emu.features.settings.model.Settings
28import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
29import org.yuzu.yuzu_emu.features.settings.model.StringSetting
30import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
31import org.yuzu.yuzu_emu.utils.*
32import java.io.IOException
33
34class SettingsActivity : AppCompatActivity(), SettingsActivityView {
35 private val presenter = SettingsActivityPresenter(this)
36
37 private lateinit var binding: ActivitySettingsBinding
38
39 private val settingsViewModel: SettingsViewModel by viewModels()
40
41 override val settings: Settings get() = settingsViewModel.settings
42
43 override fun onCreate(savedInstanceState: Bundle?) {
44 ThemeHelper.setTheme(this)
45
46 super.onCreate(savedInstanceState)
47
48 binding = ActivitySettingsBinding.inflate(layoutInflater)
49 setContentView(binding.root)
50
51 WindowCompat.setDecorFitsSystemWindows(window, false)
52
53 val launcher = intent
54 val gameID = launcher.getStringExtra(ARG_GAME_ID)
55 val menuTag = launcher.getStringExtra(ARG_MENU_TAG)
56 presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
57
58 // Show "Back" button in the action bar for navigation
59 setSupportActionBar(binding.toolbarSettings)
60 supportActionBar!!.setDisplayHomeAsUpEnabled(true)
61
62 if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) {
63 binding.navigationBarShade.setBackgroundColor(
64 ThemeHelper.getColorWithOpacity(
65 MaterialColors.getColor(
66 binding.navigationBarShade,
67 com.google.android.material.R.attr.colorSurface
68 ),
69 ThemeHelper.SYSTEM_BAR_ALPHA
70 )
71 )
72 }
73
74 onBackPressedDispatcher.addCallback(
75 this,
76 object : OnBackPressedCallback(true) {
77 override fun handleOnBackPressed() = navigateBack()
78 })
79
80 setInsets()
81 }
82
83 override fun onSupportNavigateUp(): Boolean {
84 navigateBack()
85 return true
86 }
87
88 private fun navigateBack() {
89 if (supportFragmentManager.backStackEntryCount > 0) {
90 supportFragmentManager.popBackStack()
91 } else {
92 finish()
93 }
94 }
95
96 override fun onCreateOptionsMenu(menu: Menu): Boolean {
97 val inflater = menuInflater
98 inflater.inflate(R.menu.menu_settings, menu)
99 return true
100 }
101
102 override fun onSaveInstanceState(outState: Bundle) {
103 // Critical: If super method is not called, rotations will be busted.
104 super.onSaveInstanceState(outState)
105 presenter.saveState(outState)
106 }
107
108 override fun onStart() {
109 super.onStart()
110 presenter.onStart()
111 }
112
113 /**
114 * If this is called, the user has left the settings screen (potentially through the
115 * home button) and will expect their changes to be persisted. So we kick off an
116 * IntentService which will do so on a background thread.
117 */
118 override fun onStop() {
119 super.onStop()
120 presenter.onStop(isFinishing)
121 }
122
123 override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
124 if (!addToStack && settingsFragment != null) {
125 return
126 }
127
128 val transaction = supportFragmentManager.beginTransaction()
129 if (addToStack) {
130 if (areSystemAnimationsEnabled()) {
131 transaction.setCustomAnimations(
132 R.anim.anim_settings_fragment_in,
133 R.anim.anim_settings_fragment_out,
134 0,
135 R.anim.anim_pop_settings_fragment_out
136 )
137 }
138 transaction.addToBackStack(null)
139 }
140 transaction.replace(
141 R.id.frame_content,
142 SettingsFragment.newInstance(menuTag, gameId),
143 FRAGMENT_TAG
144 )
145 transaction.commit()
146 }
147
148 private fun areSystemAnimationsEnabled(): Boolean {
149 val duration = android.provider.Settings.Global.getFloat(
150 contentResolver,
151 android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
152 )
153 val transition = android.provider.Settings.Global.getFloat(
154 contentResolver,
155 android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
156 )
157 return duration != 0f && transition != 0f
158 }
159
160 override fun onSettingsFileLoaded() {
161 val fragment: SettingsFragmentView? = settingsFragment
162 fragment?.loadSettingsList()
163 }
164
165 override fun onSettingsFileNotFound() {
166 val fragment: SettingsFragmentView? = settingsFragment
167 fragment?.loadSettingsList()
168 }
169
170 override fun showToastMessage(message: String, is_long: Boolean) {
171 Toast.makeText(
172 this,
173 message,
174 if (is_long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
175 ).show()
176 }
177
178 override fun onSettingChanged() {
179 presenter.onSettingChanged()
180 }
181
182 fun onSettingsReset() {
183 // Prevents saving to a non-existent settings file
184 presenter.onSettingsReset()
185
186 // Reset the static memory representation of each setting
187 BooleanSetting.clear()
188 FloatSetting.clear()
189 IntSetting.clear()
190 StringSetting.clear()
191
192 // Delete settings file because the user may have changed values that do not exist in the UI
193 val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
194 if (!settingsFile.delete()) {
195 throw IOException("Failed to delete $settingsFile")
196 }
197
198 showToastMessage(getString(R.string.settings_reset), true)
199 finish()
200 }
201
202 fun setToolbarTitle(title: String) {
203 binding.toolbarSettingsLayout.title = title
204 }
205
206 private val settingsFragment: SettingsFragment?
207 get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
208
209 private fun setInsets() {
210 ViewCompat.setOnApplyWindowInsetsListener(binding.frameContent) { view: View, windowInsets: WindowInsetsCompat ->
211 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
212 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
213 view.updatePadding(
214 left = barInsets.left + cutoutInsets.left,
215 right = barInsets.right + cutoutInsets.right
216 )
217
218 val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
219 mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
220 mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
221 binding.appbarSettings.layoutParams = mlpAppBar
222
223 val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
224 mlpShade.height = barInsets.bottom
225 binding.navigationBarShade.layoutParams = mlpShade
226
227 windowInsets
228 }
229 }
230
231 companion object {
232 private const val ARG_MENU_TAG = "menu_tag"
233 private const val ARG_GAME_ID = "game_id"
234 private const val FRAGMENT_TAG = "settings"
235
236 fun launch(context: Context, menuTag: String?, gameId: String?) {
237 val settings = Intent(context, SettingsActivity::class.java)
238 settings.putExtra(ARG_MENU_TAG, menuTag)
239 settings.putExtra(ARG_GAME_ID, gameId)
240 context.startActivity(settings)
241 }
242 }
243}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt
new file mode 100644
index 000000000..4361d95fb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt
@@ -0,0 +1,84 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.os.Bundle
8import android.text.TextUtils
9import org.yuzu.yuzu_emu.NativeLibrary
10import org.yuzu.yuzu_emu.features.settings.model.Settings
11import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
12import org.yuzu.yuzu_emu.utils.DirectoryInitialization
13import org.yuzu.yuzu_emu.utils.Log
14import java.io.File
15
16class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
17 val settings: Settings get() = activityView.settings
18
19 private var shouldSave = false
20 private lateinit var menuTag: String
21 private lateinit var gameId: String
22
23 fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
24 this.menuTag = menuTag
25 this.gameId = gameId
26 if (savedInstanceState != null) {
27 shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
28 }
29 }
30
31 fun onStart() {
32 prepareDirectoriesIfNeeded()
33 }
34
35 private fun loadSettingsUI() {
36 if (!settings.isLoaded) {
37 if (!TextUtils.isEmpty(gameId)) {
38 settings.loadSettings(gameId, activityView)
39 } else {
40 settings.loadSettings(activityView)
41 }
42 }
43 activityView.showSettingsFragment(menuTag, false, gameId)
44 activityView.onSettingsFileLoaded()
45 }
46
47 private fun prepareDirectoriesIfNeeded() {
48 val configFile =
49 File(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini")
50 if (!configFile.exists()) {
51 Log.error(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini")
52 Log.error("yuzu config file could not be found!")
53 }
54
55 if (!DirectoryInitialization.areDirectoriesReady) {
56 DirectoryInitialization.start(activityView as Context)
57 }
58 loadSettingsUI()
59 }
60
61 fun onStop(finishing: Boolean) {
62 if (finishing && shouldSave) {
63 Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
64 settings.saveSettings(activityView)
65 }
66 NativeLibrary.reloadSettings()
67 }
68
69 fun onSettingChanged() {
70 shouldSave = true
71 }
72
73 fun onSettingsReset() {
74 shouldSave = false
75 }
76
77 fun saveState(outState: Bundle) {
78 outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
79 }
80
81 companion object {
82 private const val KEY_SHOULD_SAVE = "should_save"
83 }
84}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt
new file mode 100644
index 000000000..c186fc388
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt
@@ -0,0 +1,57 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import org.yuzu.yuzu_emu.features.settings.model.Settings
7
8/**
9 * Abstraction for the Activity that manages SettingsFragments.
10 */
11interface SettingsActivityView {
12 /**
13 * Show a new SettingsFragment.
14 *
15 * @param menuTag Identifier for the settings group that should be displayed.
16 * @param addToStack Whether or not this fragment should replace a previous one.
17 */
18 fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
19
20 /**
21 * Called by a contained Fragment to get access to the Setting HashMap
22 * loaded from disk, so that each Fragment doesn't need to perform its own
23 * read operation.
24 *
25 * @return A HashMap of Settings.
26 */
27 val settings: Settings
28
29 /**
30 * Called when a load operation completes.
31 */
32 fun onSettingsFileLoaded()
33
34 /**
35 * Called when a load operation fails.
36 */
37 fun onSettingsFileNotFound()
38
39 /**
40 * Display a popup text message on screen.
41 *
42 * @param message The contents of the onscreen message.
43 * @param is_long Whether this should be a long Toast or short one.
44 */
45 fun showToastMessage(message: String, is_long: Boolean)
46
47 /**
48 * End the activity.
49 */
50 fun finish()
51
52 /**
53 * Called by a containing Fragment to tell the Activity that a setting was changed;
54 * unless this has been called, the Activity will not save to disk.
55 */
56 fun onSettingChanged()
57}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
new file mode 100644
index 000000000..1eb4899fc
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -0,0 +1,340 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.content.DialogInterface
8import android.icu.util.Calendar
9import android.icu.util.TimeZone
10import android.text.format.DateFormat
11import android.view.LayoutInflater
12import android.view.ViewGroup
13import android.widget.TextView
14import androidx.appcompat.app.AlertDialog
15import androidx.appcompat.app.AppCompatActivity
16import androidx.fragment.app.setFragmentResultListener
17import androidx.recyclerview.widget.RecyclerView
18import com.google.android.material.datepicker.MaterialDatePicker
19import com.google.android.material.dialog.MaterialAlertDialogBuilder
20import com.google.android.material.slider.Slider
21import com.google.android.material.timepicker.MaterialTimePicker
22import com.google.android.material.timepicker.TimeFormat
23import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
25import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
26import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
27import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
28import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
29import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
30import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
31import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
32import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
33import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
34import org.yuzu.yuzu_emu.features.settings.model.view.*
35import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
36
37class SettingsAdapter(
38 private val fragmentView: SettingsFragmentView,
39 private val context: Context
40) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
41 private var settings: ArrayList<SettingsItem>? = null
42 private var clickedItem: SettingsItem? = null
43 private var clickedPosition: Int
44 private var dialog: AlertDialog? = null
45 private var sliderProgress = 0
46 private var textSliderValue: TextView? = null
47
48 private var defaultCancelListener =
49 DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
50
51 init {
52 clickedPosition = -1
53 }
54
55 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
56 val inflater = LayoutInflater.from(parent.context)
57 return when (viewType) {
58 SettingsItem.TYPE_HEADER -> {
59 HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
60 }
61
62 SettingsItem.TYPE_SWITCH -> {
63 SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
64 }
65
66 SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
67 SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
68 }
69
70 SettingsItem.TYPE_SLIDER -> {
71 SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
72 }
73
74 SettingsItem.TYPE_SUBMENU -> {
75 SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
76 }
77
78 SettingsItem.TYPE_DATETIME_SETTING -> {
79 DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this)
80 }
81
82 SettingsItem.TYPE_RUNNABLE -> {
83 RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
84 }
85
86 else -> {
87 // TODO: Create an error view since we can't return null now
88 HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
89 }
90 }
91 }
92
93 override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
94 holder.bind(getItem(position))
95 }
96
97 private fun getItem(position: Int): SettingsItem {
98 return settings!![position]
99 }
100
101 override fun getItemCount(): Int {
102 return if (settings != null) {
103 settings!!.size
104 } else {
105 0
106 }
107 }
108
109 override fun getItemViewType(position: Int): Int {
110 return getItem(position).type
111 }
112
113 fun setSettingsList(settings: ArrayList<SettingsItem>?) {
114 this.settings = settings
115 notifyDataSetChanged()
116 }
117
118 fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
119 val setting = item.setChecked(checked)
120 fragmentView.putSetting(setting)
121 fragmentView.onSettingChanged()
122 }
123
124 private fun onSingleChoiceClick(item: SingleChoiceSetting) {
125 clickedItem = item
126 val value = getSelectionForSingleChoiceValue(item)
127 dialog = MaterialAlertDialogBuilder(context)
128 .setTitle(item.nameId)
129 .setSingleChoiceItems(item.choicesId, value, this)
130 .show()
131 }
132
133 fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
134 clickedPosition = position
135 onSingleChoiceClick(item)
136 }
137
138 private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
139 clickedItem = item
140 dialog = MaterialAlertDialogBuilder(context)
141 .setTitle(item.nameId)
142 .setSingleChoiceItems(item.choicesId, item.selectValueIndex, this)
143 .show()
144 }
145
146 fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
147 clickedPosition = position
148 onStringSingleChoiceClick(item)
149 }
150
151 fun onDateTimeClick(item: DateTimeSetting, position: Int) {
152 clickedItem = item
153 clickedPosition = position
154 val storedTime = java.lang.Long.decode(item.value) * 1000
155
156 // Helper to extract hour and minute from epoch time
157 val calendar: Calendar = Calendar.getInstance()
158 calendar.timeInMillis = storedTime
159 calendar.timeZone = TimeZone.getTimeZone("UTC")
160
161 var timeFormat: Int = TimeFormat.CLOCK_12H
162 if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) {
163 timeFormat = TimeFormat.CLOCK_24H
164 }
165
166 val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker()
167 .setSelection(storedTime)
168 .setTitleText(R.string.select_rtc_date)
169 .build()
170 val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
171 .setTimeFormat(timeFormat)
172 .setHour(calendar.get(Calendar.HOUR_OF_DAY))
173 .setMinute(calendar.get(Calendar.MINUTE))
174 .setTitleText(R.string.select_rtc_time)
175 .build()
176
177 datePicker.addOnPositiveButtonClickListener {
178 timePicker.show(
179 (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
180 "TimePicker"
181 )
182 }
183 timePicker.addOnPositiveButtonClickListener {
184 var epochTime: Long = datePicker.selection!! / 1000
185 epochTime += timePicker.hour.toLong() * 60 * 60
186 epochTime += timePicker.minute.toLong() * 60
187 val rtcString = epochTime.toString()
188 if (item.value != rtcString) {
189 fragmentView.onSettingChanged()
190 }
191 notifyItemChanged(clickedPosition)
192 val setting = item.setSelectedValue(rtcString)
193 fragmentView.putSetting(setting)
194 clickedItem = null
195 }
196 datePicker.show(
197 (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
198 "DatePicker"
199 )
200 }
201
202 fun onSliderClick(item: SliderSetting, position: Int) {
203 clickedItem = item
204 clickedPosition = position
205 sliderProgress = item.selectedValue
206
207 val inflater = LayoutInflater.from(context)
208 val sliderBinding = DialogSliderBinding.inflate(inflater)
209
210 textSliderValue = sliderBinding.textValue
211 textSliderValue!!.text = sliderProgress.toString()
212 sliderBinding.textUnits.text = item.units
213
214 sliderBinding.slider.apply {
215 valueFrom = item.min.toFloat()
216 valueTo = item.max.toFloat()
217 value = sliderProgress.toFloat()
218 addOnChangeListener { _: Slider, value: Float, _: Boolean ->
219 sliderProgress = value.toInt()
220 textSliderValue!!.text = sliderProgress.toString()
221 }
222 }
223
224 dialog = MaterialAlertDialogBuilder(context)
225 .setTitle(item.nameId)
226 .setView(sliderBinding.root)
227 .setPositiveButton(android.R.string.ok, this)
228 .setNegativeButton(android.R.string.cancel, defaultCancelListener)
229 .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
230 sliderBinding.slider.value = item.defaultValue!!.toFloat()
231 onClick(dialog, which)
232 }
233 .show()
234 }
235
236 fun onSubmenuClick(item: SubmenuSetting) {
237 fragmentView.loadSubMenu(item.menuKey)
238 }
239
240 override fun onClick(dialog: DialogInterface, which: Int) {
241 when (clickedItem) {
242 is SingleChoiceSetting -> {
243 val scSetting = clickedItem as SingleChoiceSetting
244 val value = getValueForSingleChoiceSelection(scSetting, which)
245 if (scSetting.selectedValue != value) {
246 fragmentView.onSettingChanged()
247 }
248
249 // Get the backing Setting, which may be null (if for example it was missing from the file)
250 val setting = scSetting.setSelectedValue(value)
251 fragmentView.putSetting(setting)
252 closeDialog()
253 }
254
255 is StringSingleChoiceSetting -> {
256 val scSetting = clickedItem as StringSingleChoiceSetting
257 val value = scSetting.getValueAt(which)
258 if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
259 val setting = scSetting.setSelectedValue(value!!)
260 fragmentView.putSetting(setting)
261 closeDialog()
262 }
263
264 is SliderSetting -> {
265 val sliderSetting = clickedItem as SliderSetting
266 if (sliderSetting.selectedValue != sliderProgress) {
267 fragmentView.onSettingChanged()
268 }
269 if (sliderSetting.setting is FloatSetting) {
270 val value = sliderProgress.toFloat()
271 val setting = sliderSetting.setSelectedValue(value)
272 fragmentView.putSetting(setting)
273 } else {
274 val setting = sliderSetting.setSelectedValue(sliderProgress)
275 fragmentView.putSetting(setting)
276 }
277 closeDialog()
278 }
279 }
280 clickedItem = null
281 sliderProgress = -1
282 }
283
284 fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
285 MaterialAlertDialogBuilder(context)
286 .setMessage(R.string.reset_setting_confirmation)
287 .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
288 when (setting) {
289 is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
290 is AbstractFloatSetting -> setting.float = setting.defaultValue as Float
291 is AbstractIntSetting -> setting.int = setting.defaultValue as Int
292 is AbstractStringSetting -> setting.string = setting.defaultValue as String
293 }
294 notifyItemChanged(position)
295 fragmentView.onSettingChanged()
296 }
297 .setNegativeButton(android.R.string.cancel, null)
298 .show()
299
300 return true
301 }
302
303 fun closeDialog() {
304 if (dialog != null) {
305 if (clickedPosition != -1) {
306 notifyItemChanged(clickedPosition)
307 clickedPosition = -1
308 }
309 dialog!!.dismiss()
310 dialog = null
311 }
312 }
313
314 private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
315 val valuesId = item.valuesId
316 return if (valuesId > 0) {
317 val valuesArray = context.resources.getIntArray(valuesId)
318 valuesArray[which]
319 } else {
320 which
321 }
322 }
323
324 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
325 val value = item.selectedValue
326 val valuesId = item.valuesId
327 if (valuesId > 0) {
328 val valuesArray = context.resources.getIntArray(valuesId)
329 for (index in valuesArray.indices) {
330 val current = valuesArray[index]
331 if (current == value) {
332 return index
333 }
334 }
335 } else {
336 return value
337 }
338 return -1
339 }
340}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
new file mode 100644
index 000000000..867147950
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -0,0 +1,122 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.core.view.ViewCompat
12import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment
15import androidx.recyclerview.widget.LinearLayoutManager
16import com.google.android.material.divider.MaterialDividerItemDecoration
17import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
18import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
19import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
20
21class SettingsFragment : Fragment(), SettingsFragmentView {
22 override var activityView: SettingsActivityView? = null
23
24 private val fragmentPresenter = SettingsFragmentPresenter(this)
25 private var settingsAdapter: SettingsAdapter? = null
26
27 private var _binding: FragmentSettingsBinding? = null
28 private val binding get() = _binding!!
29
30 override fun onAttach(context: Context) {
31 super.onAttach(context)
32 activityView = requireActivity() as SettingsActivityView
33 }
34
35 override fun onCreate(savedInstanceState: Bundle?) {
36 super.onCreate(savedInstanceState)
37 val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
38 val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
39 fragmentPresenter.onCreate(menuTag!!, gameId!!)
40 }
41
42 override fun onCreateView(
43 inflater: LayoutInflater,
44 container: ViewGroup?,
45 savedInstanceState: Bundle?
46 ): View {
47 _binding = FragmentSettingsBinding.inflate(layoutInflater)
48 return binding.root
49 }
50
51 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
52 settingsAdapter = SettingsAdapter(this, requireActivity())
53 val dividerDecoration = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
54 dividerDecoration.isLastItemDecorated = false
55 binding.listSettings.apply {
56 adapter = settingsAdapter
57 layoutManager = LinearLayoutManager(activity)
58 addItemDecoration(dividerDecoration)
59 }
60 fragmentPresenter.onViewCreated()
61
62 setInsets()
63 }
64
65 override fun onDetach() {
66 super.onDetach()
67 activityView = null
68 if (settingsAdapter != null) {
69 settingsAdapter!!.closeDialog()
70 }
71 }
72
73 override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
74 settingsAdapter!!.setSettingsList(settingsList)
75 }
76
77 override fun loadSettingsList() {
78 fragmentPresenter.loadSettingsList()
79 }
80
81 override fun loadSubMenu(menuKey: String) {
82 activityView!!.showSettingsFragment(
83 menuKey,
84 true,
85 requireArguments().getString(ARGUMENT_GAME_ID)!!
86 )
87 }
88
89 override fun showToastMessage(message: String?, is_long: Boolean) {
90 activityView!!.showToastMessage(message!!, is_long)
91 }
92
93 override fun putSetting(setting: AbstractSetting) {
94 fragmentPresenter.putSetting(setting)
95 }
96
97 override fun onSettingChanged() {
98 activityView!!.onSettingChanged()
99 }
100
101 private fun setInsets() {
102 ViewCompat.setOnApplyWindowInsetsListener(binding.listSettings) { view: View, windowInsets: WindowInsetsCompat ->
103 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
104 view.updatePadding(bottom = insets.bottom)
105 windowInsets
106 }
107 }
108
109 companion object {
110 private const val ARGUMENT_MENU_TAG = "menu_tag"
111 private const val ARGUMENT_GAME_ID = "game_id"
112
113 fun newInstance(menuTag: String?, gameId: String?): Fragment {
114 val fragment = SettingsFragment()
115 val arguments = Bundle()
116 arguments.putString(ARGUMENT_MENU_TAG, menuTag)
117 arguments.putString(ARGUMENT_GAME_ID, gameId)
118 fragment.arguments = arguments
119 return fragment
120 }
121 }
122}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
new file mode 100644
index 000000000..061046b2e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -0,0 +1,465 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.SharedPreferences
7import android.os.Build
8import android.text.TextUtils
9import androidx.preference.PreferenceManager
10import com.google.android.material.dialog.MaterialAlertDialogBuilder
11import org.yuzu.yuzu_emu.R
12import org.yuzu.yuzu_emu.YuzuApplication
13import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
14import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
15import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
16import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
17import org.yuzu.yuzu_emu.features.settings.model.IntSetting
18import org.yuzu.yuzu_emu.features.settings.model.Settings
19import org.yuzu.yuzu_emu.features.settings.model.StringSetting
20import org.yuzu.yuzu_emu.features.settings.model.view.*
21import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
22import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
23import org.yuzu.yuzu_emu.utils.ThemeHelper
24
25class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
26 private var menuTag: String? = null
27 private lateinit var gameId: String
28 private var settingsList: ArrayList<SettingsItem>? = null
29
30 private val settingsActivity get() = fragmentView.activityView as SettingsActivity
31 private val settings get() = fragmentView.activityView!!.settings
32
33 private lateinit var preferences: SharedPreferences
34
35 fun onCreate(menuTag: String, gameId: String) {
36 this.gameId = gameId
37 this.menuTag = menuTag
38 }
39
40 fun onViewCreated() {
41 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
42 loadSettingsList()
43 }
44
45 fun putSetting(setting: AbstractSetting) {
46 if (setting.section == null) {
47 return
48 }
49
50 val section = settings.getSection(setting.section!!)!!
51 if (section.getSetting(setting.key!!) == null) {
52 section.putSetting(setting)
53 }
54 }
55
56 fun loadSettingsList() {
57 if (!TextUtils.isEmpty(gameId)) {
58 settingsActivity.setToolbarTitle("Game Settings: $gameId")
59 }
60 val sl = ArrayList<SettingsItem>()
61 if (menuTag == null) {
62 return
63 }
64 when (menuTag) {
65 SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
66 Settings.SECTION_GENERAL -> addGeneralSettings(sl)
67 Settings.SECTION_SYSTEM -> addSystemSettings(sl)
68 Settings.SECTION_RENDERER -> addGraphicsSettings(sl)
69 Settings.SECTION_AUDIO -> addAudioSettings(sl)
70 Settings.SECTION_THEME -> addThemeSettings(sl)
71 Settings.SECTION_DEBUG -> addDebugSettings(sl)
72 else -> {
73 fragmentView.showToastMessage("Unimplemented menu", false)
74 return
75 }
76 }
77 settingsList = sl
78 fragmentView.showSettingsList(settingsList!!)
79 }
80
81 private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
82 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings))
83 sl.apply {
84 add(
85 SubmenuSetting(
86 R.string.preferences_general,
87 0,
88 Settings.SECTION_GENERAL
89 )
90 )
91 add(
92 SubmenuSetting(
93 R.string.preferences_system,
94 0,
95 Settings.SECTION_SYSTEM
96 )
97 )
98 add(
99 SubmenuSetting(
100 R.string.preferences_graphics,
101 0,
102 Settings.SECTION_RENDERER
103 )
104 )
105 add(
106 SubmenuSetting(
107 R.string.preferences_audio,
108 0,
109 Settings.SECTION_AUDIO
110 )
111 )
112 add(
113 SubmenuSetting(
114 R.string.preferences_debug,
115 0,
116 Settings.SECTION_DEBUG
117 )
118 )
119 add(
120 RunnableSetting(
121 R.string.reset_to_default,
122 0,
123 false
124 ) {
125 ResetSettingsDialogFragment().show(
126 settingsActivity.supportFragmentManager,
127 ResetSettingsDialogFragment.TAG
128 )
129 }
130 )
131 }
132 }
133
134 private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
135 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general))
136 sl.apply {
137 add(
138 SwitchSetting(
139 IntSetting.RENDERER_USE_SPEED_LIMIT,
140 R.string.frame_limit_enable,
141 R.string.frame_limit_enable_description,
142 IntSetting.RENDERER_USE_SPEED_LIMIT.key,
143 IntSetting.RENDERER_USE_SPEED_LIMIT.defaultValue
144 )
145 )
146 add(
147 SliderSetting(
148 IntSetting.RENDERER_SPEED_LIMIT,
149 R.string.frame_limit_slider,
150 R.string.frame_limit_slider_description,
151 1,
152 200,
153 "%",
154 IntSetting.RENDERER_SPEED_LIMIT.key,
155 IntSetting.RENDERER_SPEED_LIMIT.defaultValue
156 )
157 )
158 add(
159 SingleChoiceSetting(
160 IntSetting.CPU_ACCURACY,
161 R.string.cpu_accuracy,
162 0,
163 R.array.cpuAccuracyNames,
164 R.array.cpuAccuracyValues,
165 IntSetting.CPU_ACCURACY.key,
166 IntSetting.CPU_ACCURACY.defaultValue
167 )
168 )
169 }
170 }
171
172 private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
173 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system))
174 sl.apply {
175 add(
176 SwitchSetting(
177 IntSetting.USE_DOCKED_MODE,
178 R.string.use_docked_mode,
179 R.string.use_docked_mode_description,
180 IntSetting.USE_DOCKED_MODE.key,
181 IntSetting.USE_DOCKED_MODE.defaultValue
182 )
183 )
184 add(
185 SingleChoiceSetting(
186 IntSetting.REGION_INDEX,
187 R.string.emulated_region,
188 0,
189 R.array.regionNames,
190 R.array.regionValues,
191 IntSetting.REGION_INDEX.key,
192 IntSetting.REGION_INDEX.defaultValue
193 )
194 )
195 add(
196 SingleChoiceSetting(
197 IntSetting.LANGUAGE_INDEX,
198 R.string.emulated_language,
199 0,
200 R.array.languageNames,
201 R.array.languageValues,
202 IntSetting.LANGUAGE_INDEX.key,
203 IntSetting.LANGUAGE_INDEX.defaultValue
204 )
205 )
206 add(
207 SwitchSetting(
208 BooleanSetting.USE_CUSTOM_RTC,
209 R.string.use_custom_rtc,
210 R.string.use_custom_rtc_description,
211 BooleanSetting.USE_CUSTOM_RTC.key,
212 BooleanSetting.USE_CUSTOM_RTC.defaultValue
213 )
214 )
215 add(
216 DateTimeSetting(
217 StringSetting.CUSTOM_RTC,
218 R.string.set_custom_rtc,
219 0,
220 StringSetting.CUSTOM_RTC.key,
221 StringSetting.CUSTOM_RTC.defaultValue
222 )
223 )
224 }
225 }
226
227 private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
228 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
229 sl.apply {
230
231 add(
232 SingleChoiceSetting(
233 IntSetting.RENDERER_ACCURACY,
234 R.string.renderer_accuracy,
235 0,
236 R.array.rendererAccuracyNames,
237 R.array.rendererAccuracyValues,
238 IntSetting.RENDERER_ACCURACY.key,
239 IntSetting.RENDERER_ACCURACY.defaultValue
240 )
241 )
242 add(
243 SingleChoiceSetting(
244 IntSetting.RENDERER_RESOLUTION,
245 R.string.renderer_resolution,
246 0,
247 R.array.rendererResolutionNames,
248 R.array.rendererResolutionValues,
249 IntSetting.RENDERER_RESOLUTION.key,
250 IntSetting.RENDERER_RESOLUTION.defaultValue
251 )
252 )
253 add(
254 SingleChoiceSetting(
255 IntSetting.RENDERER_VSYNC,
256 R.string.renderer_vsync,
257 0,
258 R.array.rendererVSyncNames,
259 R.array.rendererVSyncValues,
260 IntSetting.RENDERER_VSYNC.key,
261 IntSetting.RENDERER_VSYNC.defaultValue
262 )
263 )
264 add(
265 SingleChoiceSetting(
266 IntSetting.RENDERER_SCALING_FILTER,
267 R.string.renderer_scaling_filter,
268 0,
269 R.array.rendererScalingFilterNames,
270 R.array.rendererScalingFilterValues,
271 IntSetting.RENDERER_SCALING_FILTER.key,
272 IntSetting.RENDERER_SCALING_FILTER.defaultValue
273 )
274 )
275 add(
276 SingleChoiceSetting(
277 IntSetting.RENDERER_ANTI_ALIASING,
278 R.string.renderer_anti_aliasing,
279 0,
280 R.array.rendererAntiAliasingNames,
281 R.array.rendererAntiAliasingValues,
282 IntSetting.RENDERER_ANTI_ALIASING.key,
283 IntSetting.RENDERER_ANTI_ALIASING.defaultValue
284 )
285 )
286 add(
287 SingleChoiceSetting(
288 IntSetting.RENDERER_ASPECT_RATIO,
289 R.string.renderer_aspect_ratio,
290 0,
291 R.array.rendererAspectRatioNames,
292 R.array.rendererAspectRatioValues,
293 IntSetting.RENDERER_ASPECT_RATIO.key,
294 IntSetting.RENDERER_ASPECT_RATIO.defaultValue
295 )
296 )
297 add(
298 SwitchSetting(
299 IntSetting.RENDERER_USE_DISK_SHADER_CACHE,
300 R.string.use_disk_shader_cache,
301 R.string.use_disk_shader_cache_description,
302 IntSetting.RENDERER_USE_DISK_SHADER_CACHE.key,
303 IntSetting.RENDERER_USE_DISK_SHADER_CACHE.defaultValue
304 )
305 )
306 add(
307 SwitchSetting(
308 IntSetting.RENDERER_FORCE_MAX_CLOCK,
309 R.string.renderer_force_max_clock,
310 R.string.renderer_force_max_clock_description,
311 IntSetting.RENDERER_FORCE_MAX_CLOCK.key,
312 IntSetting.RENDERER_FORCE_MAX_CLOCK.defaultValue
313 )
314 )
315 add(
316 SwitchSetting(
317 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS,
318 R.string.renderer_asynchronous_shaders,
319 R.string.renderer_asynchronous_shaders_description,
320 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.key,
321 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.defaultValue
322 )
323 )
324 }
325 }
326
327 private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
328 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio))
329 sl.add(
330 SliderSetting(
331 IntSetting.AUDIO_VOLUME,
332 R.string.audio_volume,
333 R.string.audio_volume_description,
334 0,
335 100,
336 "%",
337 IntSetting.AUDIO_VOLUME.key,
338 IntSetting.AUDIO_VOLUME.defaultValue
339 )
340 )
341 }
342
343 private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
344 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme))
345 sl.apply {
346 val theme: AbstractIntSetting = object : AbstractIntSetting {
347 override var int: Int
348 get() = preferences.getInt(Settings.PREF_THEME, 0)
349 set(value) {
350 preferences.edit()
351 .putInt(Settings.PREF_THEME, value)
352 .apply()
353 settingsActivity.recreate()
354 }
355 override val key: String? = null
356 override val section: String? = null
357 override val isRuntimeEditable: Boolean = false
358 override val valueAsString: String
359 get() = preferences.getInt(Settings.PREF_THEME, 0).toString()
360 override val defaultValue: Any = 0
361 }
362
363 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
364 add(
365 SingleChoiceSetting(
366 theme,
367 R.string.change_app_theme,
368 0,
369 R.array.themeEntriesA12,
370 R.array.themeValuesA12
371 )
372 )
373 } else {
374 add(
375 SingleChoiceSetting(
376 theme,
377 R.string.change_app_theme,
378 0,
379 R.array.themeEntries,
380 R.array.themeValues
381 )
382 )
383 }
384
385 val themeMode: AbstractIntSetting = object : AbstractIntSetting {
386 override var int: Int
387 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
388 set(value) {
389 preferences.edit()
390 .putInt(Settings.PREF_THEME_MODE, value)
391 .apply()
392 ThemeHelper.setThemeMode(settingsActivity)
393 }
394 override val key: String? = null
395 override val section: String? = null
396 override val isRuntimeEditable: Boolean = false
397 override val valueAsString: String
398 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString()
399 override val defaultValue: Any = -1
400 }
401
402 add(
403 SingleChoiceSetting(
404 themeMode,
405 R.string.change_theme_mode,
406 0,
407 R.array.themeModeEntries,
408 R.array.themeModeValues
409 )
410 )
411
412 val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
413 override var boolean: Boolean
414 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
415 set(value) {
416 preferences.edit()
417 .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
418 .apply()
419 settingsActivity.recreate()
420 }
421 override val key: String? = null
422 override val section: String? = null
423 override val isRuntimeEditable: Boolean = false
424 override val valueAsString: String
425 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
426 .toString()
427 override val defaultValue: Any = false
428 }
429
430 add(
431 SwitchSetting(
432 blackBackgrounds,
433 R.string.use_black_backgrounds,
434 R.string.use_black_backgrounds_description
435 )
436 )
437 }
438 }
439
440 private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
441 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug))
442 sl.apply {
443 add(
444 SingleChoiceSetting(
445 IntSetting.RENDERER_BACKEND,
446 R.string.renderer_api,
447 0,
448 R.array.rendererApiNames,
449 R.array.rendererApiValues,
450 IntSetting.RENDERER_BACKEND.key,
451 IntSetting.RENDERER_BACKEND.defaultValue
452 )
453 )
454 add(
455 SwitchSetting(
456 IntSetting.RENDERER_DEBUG,
457 R.string.renderer_debug,
458 R.string.renderer_debug_description,
459 IntSetting.RENDERER_DEBUG.key,
460 IntSetting.RENDERER_DEBUG.defaultValue
461 )
462 )
463 }
464 }
465}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt
new file mode 100644
index 000000000..1ebe35eaa
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt
@@ -0,0 +1,58 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
8
9/**
10 * Abstraction for a screen showing a list of settings. Instances of
11 * this type of view will each display a layer of the setting hierarchy.
12 */
13interface SettingsFragmentView {
14 /**
15 * Pass an ArrayList to the View so that it can be displayed on screen.
16 *
17 * @param settingsList The result of converting the HashMap to an ArrayList
18 */
19 fun showSettingsList(settingsList: ArrayList<SettingsItem>)
20
21 /**
22 * Instructs the Fragment to load the settings screen.
23 */
24 fun loadSettingsList()
25
26 /**
27 * @return The Fragment's containing activity.
28 */
29 val activityView: SettingsActivityView?
30
31 /**
32 * Tell the Fragment to tell the containing Activity to show a new
33 * Fragment containing a submenu of settings.
34 *
35 * @param menuKey Identifier for the settings group that should be shown.
36 */
37 fun loadSubMenu(menuKey: String)
38
39 /**
40 * Tell the Fragment to tell the containing activity to display a toast message.
41 *
42 * @param message Text to be shown in the Toast
43 * @param is_long Whether this should be a long Toast or short one.
44 */
45 fun showToastMessage(message: String?, is_long: Boolean)
46
47 /**
48 * Have the fragment add a setting to the HashMap.
49 *
50 * @param setting The (possibly previously missing) new setting.
51 */
52 fun putSetting(setting: AbstractSetting)
53
54 /**
55 * Have the fragment tell the containing Activity that a setting was modified.
56 */
57 fun onSettingChanged()
58}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
new file mode 100644
index 000000000..04c045e77
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -0,0 +1,48 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
11import java.time.Instant
12import java.time.ZoneId
13import java.time.ZonedDateTime
14import java.time.format.DateTimeFormatter
15import java.time.format.FormatStyle
16
17class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
18 SettingViewHolder(binding.root, adapter) {
19 private lateinit var setting: DateTimeSetting
20
21 override fun bind(item: SettingsItem) {
22 setting = item as DateTimeSetting
23 binding.textSettingName.setText(item.nameId)
24 if (item.descriptionId != 0) {
25 binding.textSettingDescription.setText(item.descriptionId)
26 binding.textSettingDescription.visibility = View.VISIBLE
27 } else {
28 val epochTime = setting.value.toLong()
29 val instant = Instant.ofEpochMilli(epochTime * 1000)
30 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
31 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
32 binding.textSettingDescription.text = dateFormatter.format(zonedTime)
33 }
34 }
35
36 override fun onClick(clicked: View) {
37 if (setting.isEditable) {
38 adapter.onDateTimeClick(setting, bindingAdapterPosition)
39 }
40 }
41
42 override fun onLongClick(clicked: View): Boolean {
43 if (setting.isEditable) {
44 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
45 }
46 return false
47 }
48}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
new file mode 100644
index 000000000..f5bcf705c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
@@ -0,0 +1,30 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
10
11class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) :
12 SettingViewHolder(binding.root, adapter) {
13
14 init {
15 itemView.setOnClickListener(null)
16 }
17
18 override fun bind(item: SettingsItem) {
19 binding.textHeaderName.setText(item.nameId)
20 }
21
22 override fun onClick(clicked: View) {
23 // no-op
24 }
25
26 override fun onLongClick(clicked: View): Boolean {
27 // no-op
28 return true
29 }
30}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
new file mode 100644
index 000000000..5dad5945f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -0,0 +1,38 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.NativeLibrary
8import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
9import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12
13class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) {
15 private lateinit var setting: RunnableSetting
16
17 override fun bind(item: SettingsItem) {
18 setting = item as RunnableSetting
19 binding.textSettingName.setText(item.nameId)
20 if (item.descriptionId != 0) {
21 binding.textSettingDescription.setText(item.descriptionId)
22 binding.textSettingDescription.visibility = View.VISIBLE
23 } else {
24 binding.textSettingDescription.visibility = View.GONE
25 }
26 }
27
28 override fun onClick(clicked: View) {
29 if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
30 setting.runnable.invoke()
31 }
32 }
33
34 override fun onLongClick(clicked: View): Boolean {
35 // no-op
36 return true
37 }
38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
new file mode 100644
index 000000000..f56460893
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
@@ -0,0 +1,36 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import androidx.recyclerview.widget.RecyclerView
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
10
11abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) :
12 RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
13
14 init {
15 itemView.setOnClickListener(this)
16 itemView.setOnLongClickListener(this)
17 }
18
19 /**
20 * Called by the adapter to set this ViewHolder's child views to display the list item
21 * it must now represent.
22 *
23 * @param item The list item that should be represented by this ViewHolder.
24 */
25 abstract fun bind(item: SettingsItem)
26
27 /**
28 * Called when this ViewHolder's view is clicked on. Implementations should usually pass
29 * this event up to the adapter.
30 *
31 * @param clicked The view that was clicked on.
32 */
33 abstract override fun onClick(clicked: View)
34
35 abstract override fun onLongClick(clicked: View): Boolean
36}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
new file mode 100644
index 000000000..de764a27f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -0,0 +1,60 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12
13class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) {
15 private lateinit var setting: SettingsItem
16
17 override fun bind(item: SettingsItem) {
18 setting = item
19 binding.textSettingName.setText(item.nameId)
20 binding.textSettingDescription.visibility = View.VISIBLE
21 if (item.descriptionId != 0) {
22 binding.textSettingDescription.setText(item.descriptionId)
23 } else if (item is SingleChoiceSetting) {
24 val resMgr = binding.textSettingDescription.context.resources
25 val values = resMgr.getIntArray(item.valuesId)
26 for (i in values.indices) {
27 if (values[i] == item.selectedValue) {
28 binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i]
29 }
30 }
31 } else {
32 binding.textSettingDescription.visibility = View.GONE
33 }
34 }
35
36 override fun onClick(clicked: View) {
37 if (!setting.isEditable) {
38 return
39 }
40
41 if (setting is SingleChoiceSetting) {
42 adapter.onSingleChoiceClick(
43 (setting as SingleChoiceSetting),
44 bindingAdapterPosition
45 )
46 } else if (setting is StringSingleChoiceSetting) {
47 adapter.onStringSingleChoiceClick(
48 (setting as StringSingleChoiceSetting),
49 bindingAdapterPosition
50 )
51 }
52 }
53
54 override fun onLongClick(clicked: View): Boolean {
55 if (setting.isEditable) {
56 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
57 }
58 return false
59 }
60}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
new file mode 100644
index 000000000..cc3f39aa5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -0,0 +1,39 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
10import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
11
12class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
13 SettingViewHolder(binding.root, adapter) {
14 private lateinit var setting: SliderSetting
15
16 override fun bind(item: SettingsItem) {
17 setting = item as SliderSetting
18 binding.textSettingName.setText(item.nameId)
19 if (item.descriptionId != 0) {
20 binding.textSettingDescription.setText(item.descriptionId)
21 binding.textSettingDescription.visibility = View.VISIBLE
22 } else {
23 binding.textSettingDescription.visibility = View.GONE
24 }
25 }
26
27 override fun onClick(clicked: View) {
28 if (setting.isEditable) {
29 adapter.onSliderClick(setting, bindingAdapterPosition)
30 }
31 }
32
33 override fun onLongClick(clicked: View): Boolean {
34 if (setting.isEditable) {
35 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
36 }
37 return false
38 }
39}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
new file mode 100644
index 000000000..c545b4174
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -0,0 +1,35 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting
10import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
11
12class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
13 SettingViewHolder(binding.root, adapter) {
14 private lateinit var item: SubmenuSetting
15
16 override fun bind(item: SettingsItem) {
17 this.item = item as SubmenuSetting
18 binding.textSettingName.setText(item.nameId)
19 if (item.descriptionId != 0) {
20 binding.textSettingDescription.setText(item.descriptionId)
21 binding.textSettingDescription.visibility = View.VISIBLE
22 } else {
23 binding.textSettingDescription.visibility = View.GONE
24 }
25 }
26
27 override fun onClick(clicked: View) {
28 adapter.onSubmenuClick(item)
29 }
30
31 override fun onLongClick(clicked: View): Boolean {
32 // no-op
33 return true
34 }
35}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
new file mode 100644
index 000000000..b163bd6ca
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -0,0 +1,48 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import android.widget.CompoundButton
8import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
9import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12
13class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) {
15
16 private lateinit var setting: SwitchSetting
17
18 override fun bind(item: SettingsItem) {
19 setting = item as SwitchSetting
20 binding.textSettingName.setText(item.nameId)
21 if (item.descriptionId != 0) {
22 binding.textSettingDescription.setText(item.descriptionId)
23 binding.textSettingDescription.visibility = View.VISIBLE
24 } else {
25 binding.textSettingDescription.text = ""
26 binding.textSettingDescription.visibility = View.GONE
27 }
28 binding.switchWidget.isChecked = setting.isChecked
29 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
30 adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
31 }
32
33 binding.switchWidget.isEnabled = setting.isEditable
34 }
35
36 override fun onClick(clicked: View) {
37 if (setting.isEditable) {
38 binding.switchWidget.toggle()
39 }
40 }
41
42 override fun onLongClick(clicked: View): Boolean {
43 if (setting.isEditable) {
44 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
45 }
46 return false
47 }
48}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
new file mode 100644
index 000000000..e29bca11d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
@@ -0,0 +1,241 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.utils
5
6import org.ini4j.Wini
7import org.yuzu.yuzu_emu.NativeLibrary
8import org.yuzu.yuzu_emu.R
9import org.yuzu.yuzu_emu.YuzuApplication
10import org.yuzu.yuzu_emu.features.settings.model.*
11import org.yuzu.yuzu_emu.features.settings.model.Settings.SettingsSectionMap
12import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
13import org.yuzu.yuzu_emu.utils.BiMap
14import org.yuzu.yuzu_emu.utils.DirectoryInitialization
15import org.yuzu.yuzu_emu.utils.Log
16import java.io.*
17import java.util.*
18
19/**
20 * Contains static methods for interacting with .ini files in which settings are stored.
21 */
22object SettingsFile {
23 const val FILE_NAME_CONFIG = "config"
24
25 private var sectionsMap = BiMap<String?, String?>()
26
27 /**
28 * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
29 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
30 * failed.
31 *
32 * @param ini The ini file to load the settings from
33 * @param isCustomGame
34 * @param view The current view.
35 * @return An Observable that emits a HashMap of the file's contents, then completes.
36 */
37 private fun readFile(
38 ini: File?,
39 isCustomGame: Boolean,
40 view: SettingsActivityView? = null
41 ): HashMap<String, SettingSection?> {
42 val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
43 var reader: BufferedReader? = null
44 try {
45 reader = BufferedReader(FileReader(ini))
46 var current: SettingSection? = null
47 var line: String?
48 while (reader.readLine().also { line = it } != null) {
49 if (line!!.startsWith("[") && line!!.endsWith("]")) {
50 current = sectionFromLine(line!!, isCustomGame)
51 sections[current.name] = current
52 } else if (current != null) {
53 val setting = settingFromLine(line!!)
54 if (setting != null) {
55 current.putSetting(setting)
56 }
57 }
58 }
59 } catch (e: FileNotFoundException) {
60 Log.error("[SettingsFile] File not found: " + e.message)
61 view?.onSettingsFileNotFound()
62 } catch (e: IOException) {
63 Log.error("[SettingsFile] Error reading from: " + e.message)
64 view?.onSettingsFileNotFound()
65 } finally {
66 if (reader != null) {
67 try {
68 reader.close()
69 } catch (e: IOException) {
70 Log.error("[SettingsFile] Error closing: " + e.message)
71 }
72 }
73 }
74 return sections
75 }
76
77 fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
78 return readFile(getSettingsFile(fileName), false, view)
79 }
80
81 fun readFile(fileName: String): HashMap<String, SettingSection?> =
82 readFile(getSettingsFile(fileName), false)
83
84 /**
85 * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
86 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
87 * failed.
88 *
89 * @param gameId the id of the game to load it's settings.
90 * @param view The current view.
91 */
92 fun readCustomGameSettings(
93 gameId: String,
94 view: SettingsActivityView?
95 ): HashMap<String, SettingSection?> {
96 return readFile(getCustomGameSettingsFile(gameId), true, view)
97 }
98
99 /**
100 * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
101 * telling why it failed.
102 *
103 * @param fileName The target filename without a path or extension.
104 * @param sections The HashMap containing the Settings we want to serialize.
105 * @param view The current view.
106 */
107 fun saveFile(
108 fileName: String,
109 sections: TreeMap<String, SettingSection>,
110 view: SettingsActivityView
111 ) {
112 val ini = getSettingsFile(fileName)
113 try {
114 val writer = Wini(ini)
115 val keySet: Set<String> = sections.keys
116 for (key in keySet) {
117 val section = sections[key]
118 writeSection(writer, section!!)
119 }
120 writer.store()
121 } catch (e: IOException) {
122 Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message)
123 view.showToastMessage(
124 YuzuApplication.appContext
125 .getString(R.string.error_saving, fileName, e.message),
126 false
127 )
128 }
129 }
130
131 fun saveCustomGameSettings(gameId: String?, sections: HashMap<String, SettingSection?>) {
132 val sortedSections: Set<String> = TreeSet(sections.keys)
133 for (sectionKey in sortedSections) {
134 val section = sections[sectionKey]
135 val settings = section!!.settings
136 val sortedKeySet: Set<String> = TreeSet(settings.keys)
137 for (settingKey in sortedKeySet) {
138 val setting = settings[settingKey]
139 NativeLibrary.setUserSetting(
140 gameId, mapSectionNameFromIni(
141 section.name
142 ), setting!!.key, setting.valueAsString
143 )
144 }
145 }
146 }
147
148 private fun mapSectionNameFromIni(generalSectionName: String): String? {
149 return if (sectionsMap.getForward(generalSectionName) != null) {
150 sectionsMap.getForward(generalSectionName)
151 } else generalSectionName
152 }
153
154 private fun mapSectionNameToIni(generalSectionName: String): String {
155 return if (sectionsMap.getBackward(generalSectionName) != null) {
156 sectionsMap.getBackward(generalSectionName).toString()
157 } else generalSectionName
158 }
159
160 fun getSettingsFile(fileName: String): File {
161 return File(
162 DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini"
163 )
164 }
165
166 private fun getCustomGameSettingsFile(gameId: String): File {
167 return File(DirectoryInitialization.userDirectory + "/GameSettings/" + gameId + ".ini")
168 }
169
170 private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
171 var sectionName: String = line.substring(1, line.length - 1)
172 if (isCustomGame) {
173 sectionName = mapSectionNameToIni(sectionName)
174 }
175 return SettingSection(sectionName)
176 }
177
178 /**
179 * For a line of text, determines what type of data is being represented, and returns
180 * a Setting object containing this data.
181 *
182 * @param line The line of text being parsed.
183 * @return A typed Setting containing the key/value contained in the line.
184 */
185 private fun settingFromLine(line: String): AbstractSetting? {
186 val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
187 if (splitLine.size != 2) {
188 return null
189 }
190 val key = splitLine[0].trim { it <= ' ' }
191 val value = splitLine[1].trim { it <= ' ' }
192 if (value.isEmpty()) {
193 return null
194 }
195
196 val booleanSetting = BooleanSetting.from(key)
197 if (booleanSetting != null) {
198 booleanSetting.boolean = value.toBoolean()
199 return booleanSetting
200 }
201
202 val intSetting = IntSetting.from(key)
203 if (intSetting != null) {
204 intSetting.int = value.toInt()
205 return intSetting
206 }
207
208 val floatSetting = FloatSetting.from(key)
209 if (floatSetting != null) {
210 floatSetting.float = value.toFloat()
211 return floatSetting
212 }
213
214 val stringSetting = StringSetting.from(key)
215 if (stringSetting != null) {
216 stringSetting.string = value
217 return stringSetting
218 }
219
220 return null
221 }
222
223 /**
224 * Writes the contents of a Section HashMap to disk.
225 *
226 * @param parser A Wini pointed at a file on disk.
227 * @param section A section containing settings to be written to the file.
228 */
229 private fun writeSection(parser: Wini, section: SettingSection) {
230 // Write the section header.
231 val header = section.name
232
233 // Write this section's values.
234 val settings = section.settings
235 val keySet: Set<String> = settings.keys
236 for (key in keySet) {
237 val setting = settings[key]
238 parser.put(header, setting!!.key, setting.valueAsString)
239 }
240 }
241}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
new file mode 100644
index 000000000..c92e2755c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
@@ -0,0 +1,125 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.ClipData
7import android.content.ClipboardManager
8import android.content.Context
9import android.content.Intent
10import android.net.Uri
11import android.os.Build
12import android.os.Bundle
13import android.view.LayoutInflater
14import android.view.View
15import android.view.ViewGroup
16import android.view.ViewGroup.MarginLayoutParams
17import android.widget.Toast
18import androidx.core.view.ViewCompat
19import androidx.core.view.WindowInsetsCompat
20import androidx.core.view.updatePadding
21import androidx.fragment.app.Fragment
22import androidx.fragment.app.activityViewModels
23import androidx.navigation.findNavController
24import com.google.android.material.transition.MaterialSharedAxis
25import org.yuzu.yuzu_emu.BuildConfig
26import org.yuzu.yuzu_emu.R
27import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
28import org.yuzu.yuzu_emu.model.HomeViewModel
29
30class AboutFragment : Fragment() {
31 private var _binding: FragmentAboutBinding? = null
32 private val binding get() = _binding!!
33
34 private val homeViewModel: HomeViewModel by activityViewModels()
35
36 override fun onCreate(savedInstanceState: Bundle?) {
37 super.onCreate(savedInstanceState)
38 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
39 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
40 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
41 }
42
43 override fun onCreateView(
44 inflater: LayoutInflater,
45 container: ViewGroup?,
46 savedInstanceState: Bundle?
47 ): View {
48 _binding = FragmentAboutBinding.inflate(layoutInflater)
49 return binding.root
50 }
51
52 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53 homeViewModel.setNavigationVisibility(visible = false, animated = true)
54 homeViewModel.setStatusBarShadeVisibility(visible = false)
55
56 binding.toolbarAbout.setNavigationOnClickListener {
57 binding.root.findNavController().popBackStack()
58 }
59
60 binding.imageLogo.setOnLongClickListener {
61 Toast.makeText(
62 requireContext(),
63 R.string.gaia_is_not_real,
64 Toast.LENGTH_SHORT
65 ).show()
66 true
67 }
68
69 binding.buttonContributors.setOnClickListener { openLink(getString(R.string.contributors_link)) }
70 binding.buttonLicenses.setOnClickListener {
71 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
72 binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
73 }
74
75 binding.textBuildHash.text = BuildConfig.GIT_HASH
76 binding.buttonBuildHash.setOnClickListener {
77 val clipBoard =
78 requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
79 val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH)
80 clipBoard.setPrimaryClip(clip)
81
82 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
83 Toast.makeText(
84 requireContext(),
85 R.string.copied_to_clipboard,
86 Toast.LENGTH_SHORT
87 ).show()
88 }
89 }
90
91 binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
92 binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
93 binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
94
95 setInsets()
96 }
97
98 private fun openLink(link: String) {
99 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
100 startActivity(intent)
101 }
102
103 private fun setInsets() =
104 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
105 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
106 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
107
108 val leftInsets = barInsets.left + cutoutInsets.left
109 val rightInsets = barInsets.right + cutoutInsets.right
110
111 val mlpAppBar = binding.appbarAbout.layoutParams as MarginLayoutParams
112 mlpAppBar.leftMargin = leftInsets
113 mlpAppBar.rightMargin = rightInsets
114 binding.appbarAbout.layoutParams = mlpAppBar
115
116 val mlpScrollAbout = binding.scrollAbout.layoutParams as MarginLayoutParams
117 mlpScrollAbout.leftMargin = leftInsets
118 mlpScrollAbout.rightMargin = rightInsets
119 binding.scrollAbout.layoutParams = mlpScrollAbout
120
121 binding.contentAbout.updatePadding(bottom = barInsets.bottom)
122
123 windowInsets
124 }
125}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt
new file mode 100644
index 000000000..d8bbc1ce4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt
@@ -0,0 +1,83 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.Intent
7import android.net.Uri
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels
17import androidx.navigation.fragment.findNavController
18import com.google.android.material.transition.MaterialSharedAxis
19import org.yuzu.yuzu_emu.R
20import org.yuzu.yuzu_emu.databinding.FragmentEarlyAccessBinding
21import org.yuzu.yuzu_emu.model.HomeViewModel
22
23class EarlyAccessFragment : Fragment() {
24 private var _binding: FragmentEarlyAccessBinding? = null
25 private val binding get() = _binding!!
26
27 private val homeViewModel: HomeViewModel by activityViewModels()
28
29 override fun onCreate(savedInstanceState: Bundle?) {
30 super.onCreate(savedInstanceState)
31 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
32 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
33 }
34
35 override fun onCreateView(
36 inflater: LayoutInflater,
37 container: ViewGroup?,
38 savedInstanceState: Bundle?
39 ): View {
40 _binding = FragmentEarlyAccessBinding.inflate(layoutInflater)
41 return binding.root
42 }
43
44 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
45 homeViewModel.setNavigationVisibility(visible = false, animated = true)
46 homeViewModel.setStatusBarShadeVisibility(visible = false)
47
48 binding.toolbarAbout.setNavigationOnClickListener {
49 parentFragmentManager.primaryNavigationFragment?.findNavController()?.popBackStack()
50 }
51
52 binding.getEarlyAccessButton.setOnClickListener { openLink(getString(R.string.play_store_link)) }
53
54 setInsets()
55 }
56
57 private fun openLink(link: String) {
58 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
59 startActivity(intent)
60 }
61
62 private fun setInsets() =
63 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
64 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
65 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
66
67 val leftInsets = barInsets.left + cutoutInsets.left
68 val rightInsets = barInsets.right + cutoutInsets.right
69
70 val mlpAppBar = binding.appbarEa.layoutParams as ViewGroup.MarginLayoutParams
71 mlpAppBar.leftMargin = leftInsets
72 mlpAppBar.rightMargin = rightInsets
73 binding.appbarEa.layoutParams = mlpAppBar
74
75 binding.scrollEa.updatePadding(
76 left = leftInsets,
77 right = rightInsets,
78 bottom = barInsets.bottom
79 )
80
81 windowInsets
82 }
83}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
new file mode 100644
index 000000000..9523381cd
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -0,0 +1,613 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.annotation.SuppressLint
7import android.app.AlertDialog
8import android.content.Context
9import android.content.DialogInterface
10import android.content.SharedPreferences
11import android.content.pm.ActivityInfo
12import android.content.res.Resources
13import android.graphics.Color
14import android.os.Bundle
15import android.os.Handler
16import android.os.Looper
17import android.util.Rational
18import android.util.TypedValue
19import android.view.*
20import android.widget.TextView
21import androidx.activity.OnBackPressedCallback
22import androidx.appcompat.widget.PopupMenu
23import androidx.core.content.res.ResourcesCompat
24import androidx.core.graphics.Insets
25import androidx.core.view.ViewCompat
26import androidx.core.view.WindowInsetsCompat
27import androidx.core.view.updatePadding
28import androidx.fragment.app.Fragment
29import androidx.preference.PreferenceManager
30import androidx.window.layout.FoldingFeature
31import androidx.window.layout.WindowLayoutInfo
32import com.google.android.material.dialog.MaterialAlertDialogBuilder
33import com.google.android.material.slider.Slider
34import org.yuzu.yuzu_emu.NativeLibrary
35import org.yuzu.yuzu_emu.R
36import org.yuzu.yuzu_emu.YuzuApplication
37import org.yuzu.yuzu_emu.activities.EmulationActivity
38import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
39import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
40import org.yuzu.yuzu_emu.features.settings.model.IntSetting
41import org.yuzu.yuzu_emu.features.settings.model.Settings
42import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
43import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
44import org.yuzu.yuzu_emu.model.Game
45import org.yuzu.yuzu_emu.utils.*
46import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
47
48class EmulationFragment : Fragment(), SurfaceHolder.Callback {
49 private lateinit var preferences: SharedPreferences
50 private lateinit var emulationState: EmulationState
51 private var emulationActivity: EmulationActivity? = null
52 private var perfStatsUpdater: (() -> Unit)? = null
53
54 private var _binding: FragmentEmulationBinding? = null
55 private val binding get() = _binding!!
56
57 private lateinit var game: Game
58
59 override fun onAttach(context: Context) {
60 super.onAttach(context)
61 if (context is EmulationActivity) {
62 emulationActivity = context
63 NativeLibrary.setEmulationActivity(context)
64 } else {
65 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
66 }
67 }
68
69 /**
70 * Initialize anything that doesn't depend on the layout / views in here.
71 */
72 override fun onCreate(savedInstanceState: Bundle?) {
73 super.onCreate(savedInstanceState)
74
75 // So this fragment doesn't restart on configuration changes; i.e. rotation.
76 retainInstance = true
77 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
78 game = requireArguments().parcelable(EmulationActivity.EXTRA_SELECTED_GAME)!!
79 emulationState = EmulationState(game.path)
80 }
81
82 /**
83 * Initialize the UI and start emulation in here.
84 */
85 override fun onCreateView(
86 inflater: LayoutInflater,
87 container: ViewGroup?,
88 savedInstanceState: Bundle?
89 ): View {
90 _binding = FragmentEmulationBinding.inflate(layoutInflater)
91 return binding.root
92 }
93
94 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
95 binding.surfaceEmulation.holder.addCallback(this)
96 binding.showFpsText.setTextColor(Color.YELLOW)
97 binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
98
99 // Setup overlay.
100 updateShowFpsOverlay()
101
102 binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
103 game.title
104 binding.inGameMenu.setNavigationItemSelectedListener {
105 when (it.itemId) {
106 R.id.menu_pause_emulation -> {
107 if (emulationState.isPaused) {
108 emulationState.run(false)
109 it.title = resources.getString(R.string.emulation_pause)
110 it.icon = ResourcesCompat.getDrawable(
111 resources,
112 R.drawable.ic_pause,
113 requireContext().theme
114 )
115 } else {
116 emulationState.pause()
117 it.title = resources.getString(R.string.emulation_unpause)
118 it.icon = ResourcesCompat.getDrawable(
119 resources,
120 R.drawable.ic_play,
121 requireContext().theme
122 )
123 }
124 true
125 }
126
127 R.id.menu_settings -> {
128 SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
129 true
130 }
131
132 R.id.menu_overlay_controls -> {
133 showOverlayOptions()
134 true
135 }
136
137 R.id.menu_exit -> {
138 emulationState.stop()
139 requireActivity().finish()
140 true
141 }
142
143 else -> true
144 }
145 }
146
147 setInsets()
148
149 requireActivity().onBackPressedDispatcher.addCallback(
150 requireActivity(),
151 object : OnBackPressedCallback(true) {
152 override fun handleOnBackPressed() {
153 if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open()
154 }
155 })
156 }
157
158 override fun onResume() {
159 super.onResume()
160 if (!DirectoryInitialization.areDirectoriesReady) {
161 DirectoryInitialization.start(requireContext())
162 }
163
164 binding.surfaceEmulation.setAspectRatio(
165 when (IntSetting.RENDERER_ASPECT_RATIO.int) {
166 0 -> Rational(16, 9)
167 1 -> Rational(4, 3)
168 2 -> Rational(21, 9)
169 3 -> Rational(16, 10)
170 4 -> null // Stretch
171 else -> Rational(16, 9)
172 }
173 )
174
175 emulationState.run(emulationActivity!!.isActivityRecreated)
176 }
177
178 override fun onPause() {
179 if (emulationState.isRunning) {
180 emulationState.pause()
181 }
182 super.onPause()
183 }
184
185 override fun onDestroyView() {
186 super.onDestroyView()
187 _binding = null
188 }
189
190 override fun onDetach() {
191 NativeLibrary.clearEmulationActivity()
192 super.onDetach()
193 }
194
195 private fun refreshInputOverlay() {
196 binding.surfaceInputOverlay.refreshControls()
197 }
198
199 private fun resetInputOverlay() {
200 preferences.edit()
201 .remove(Settings.PREF_CONTROL_SCALE)
202 .remove(Settings.PREF_CONTROL_OPACITY)
203 .apply()
204 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() }
205 }
206
207 private fun updateShowFpsOverlay() {
208 if (EmulationMenuSettings.showFps) {
209 val SYSTEM_FPS = 0
210 val FPS = 1
211 val FRAMETIME = 2
212 val SPEED = 3
213 perfStatsUpdater = {
214 val perfStats = NativeLibrary.getPerfStats()
215 if (perfStats[FPS] > 0 && _binding != null) {
216 binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
217 }
218
219 if (!emulationState.isStopped) {
220 perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
221 }
222 }
223 perfStatsUpdateHandler.post(perfStatsUpdater!!)
224 binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
225 binding.showFpsText.visibility = View.VISIBLE
226 } else {
227 if (perfStatsUpdater != null) {
228 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
229 }
230 binding.showFpsText.visibility = View.GONE
231 }
232 }
233
234 private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
235
236 fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
237 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
238 if (it.isSeparating) {
239 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
240 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
241 binding.surfaceEmulation.layoutParams.height = it.bounds.top
242 binding.inGameMenu.layoutParams.height = it.bounds.bottom
243 binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
244 binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
245 }
246 }
247 it.isSeparating
248 } ?: false
249 if (!isFolding) {
250 binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
251 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
252 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
253 binding.overlayContainer.updatePadding(0, 0, 0, 0)
254 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
255 }
256 binding.surfaceInputOverlay.requestLayout()
257 binding.inGameMenu.requestLayout()
258 binding.overlayContainer.requestLayout()
259 }
260
261 override fun surfaceCreated(holder: SurfaceHolder) {
262 // We purposely don't do anything here.
263 // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
264 }
265
266 override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
267 Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height)
268 emulationState.newSurface(holder.surface)
269 }
270
271 override fun surfaceDestroyed(holder: SurfaceHolder) {
272 emulationState.clearSurface()
273 }
274
275 private fun showOverlayOptions() {
276 val anchor = binding.inGameMenu.findViewById<View>(R.id.menu_overlay_controls)
277 val popup = PopupMenu(requireContext(), anchor)
278
279 popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
280
281 popup.menu.apply {
282 findItem(R.id.menu_toggle_fps).isChecked = EmulationMenuSettings.showFps
283 findItem(R.id.menu_rel_stick_center).isChecked = EmulationMenuSettings.joystickRelCenter
284 findItem(R.id.menu_dpad_slide).isChecked = EmulationMenuSettings.dpadSlide
285 findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
286 findItem(R.id.menu_haptics).isChecked = EmulationMenuSettings.hapticFeedback
287 }
288
289 popup.setOnMenuItemClickListener {
290 when (it.itemId) {
291 R.id.menu_toggle_fps -> {
292 it.isChecked = !it.isChecked
293 EmulationMenuSettings.showFps = it.isChecked
294 updateShowFpsOverlay()
295 true
296 }
297
298 R.id.menu_edit_overlay -> {
299 binding.drawerLayout.close()
300 binding.surfaceInputOverlay.requestFocus()
301 startConfiguringControls()
302 true
303 }
304
305 R.id.menu_adjust_overlay -> {
306 adjustOverlay()
307 true
308 }
309
310 R.id.menu_toggle_controls -> {
311 val preferences =
312 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
313 val optionsArray = BooleanArray(15)
314 for (i in 0..14) {
315 optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 13)
316 }
317
318 val dialog = MaterialAlertDialogBuilder(requireContext())
319 .setTitle(R.string.emulation_toggle_controls)
320 .setMultiChoiceItems(
321 R.array.gamepadButtons,
322 optionsArray
323 ) { _, indexSelected, isChecked ->
324 preferences.edit()
325 .putBoolean("buttonToggle$indexSelected", isChecked)
326 .apply()
327 }
328 .setPositiveButton(android.R.string.ok) { _, _ ->
329 refreshInputOverlay()
330 }
331 .setNegativeButton(android.R.string.cancel, null)
332 .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
333 .show()
334
335 // Override normal behaviour so the dialog doesn't close
336 dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
337 .setOnClickListener {
338 val isChecked = !optionsArray[0]
339 for (i in 0..14) {
340 optionsArray[i] = isChecked
341 dialog.listView.setItemChecked(i, isChecked)
342 preferences.edit()
343 .putBoolean("buttonToggle$i", isChecked)
344 .apply()
345 }
346 }
347 true
348 }
349
350 R.id.menu_show_overlay -> {
351 it.isChecked = !it.isChecked
352 EmulationMenuSettings.showOverlay = it.isChecked
353 refreshInputOverlay()
354 true
355 }
356
357 R.id.menu_rel_stick_center -> {
358 it.isChecked = !it.isChecked
359 EmulationMenuSettings.joystickRelCenter = it.isChecked
360 true
361 }
362
363 R.id.menu_dpad_slide -> {
364 it.isChecked = !it.isChecked
365 EmulationMenuSettings.dpadSlide = it.isChecked
366 true
367 }
368
369 R.id.menu_haptics -> {
370 it.isChecked = !it.isChecked
371 EmulationMenuSettings.hapticFeedback = it.isChecked
372 true
373 }
374
375 R.id.menu_reset_overlay -> {
376 binding.drawerLayout.close()
377 resetInputOverlay()
378 true
379 }
380
381 else -> true
382 }
383 }
384
385 popup.show()
386 }
387
388 private fun startConfiguringControls() {
389 binding.doneControlConfig.visibility = View.VISIBLE
390 binding.surfaceInputOverlay.setIsInEditMode(true)
391 }
392
393 private fun stopConfiguringControls() {
394 binding.doneControlConfig.visibility = View.GONE
395 binding.surfaceInputOverlay.setIsInEditMode(false)
396 }
397
398 @SuppressLint("SetTextI18n")
399 private fun adjustOverlay() {
400 val adjustBinding = DialogOverlayAdjustBinding.inflate(layoutInflater)
401 adjustBinding.apply {
402 inputScaleSlider.apply {
403 valueTo = 150F
404 value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
405 addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
406 inputScaleValue.text = "${value.toInt()}%"
407 setControlScale(value.toInt())
408 })
409 }
410 inputOpacitySlider.apply {
411 valueTo = 100F
412 value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
413 addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
414 inputOpacityValue.text = "${value.toInt()}%"
415 setControlOpacity(value.toInt())
416 })
417 }
418 inputScaleValue.text = "${inputScaleSlider.value.toInt()}%"
419 inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%"
420 }
421
422 MaterialAlertDialogBuilder(requireContext())
423 .setTitle(R.string.emulation_control_adjust)
424 .setView(adjustBinding.root)
425 .setPositiveButton(android.R.string.ok, null)
426 .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
427 setControlScale(50)
428 setControlOpacity(100)
429 }
430 .show()
431 }
432
433 private fun setControlScale(scale: Int) {
434 preferences.edit()
435 .putInt(Settings.PREF_CONTROL_SCALE, scale)
436 .apply()
437 refreshInputOverlay()
438 }
439
440 private fun setControlOpacity(opacity: Int) {
441 preferences.edit()
442 .putInt(Settings.PREF_CONTROL_OPACITY, opacity)
443 .apply()
444 refreshInputOverlay()
445 }
446
447 private fun setInsets() {
448 ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat ->
449 val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
450 var left = 0
451 var right = 0
452 if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
453 left = cutInsets.left
454 } else {
455 right = cutInsets.right
456 }
457
458 v.setPadding(left, cutInsets.top, right, 0)
459
460 // Ensure FPS text doesn't get cut off by rounded display corners
461 val sidePadding = resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
462 if (cutInsets.left == 0) {
463 binding.showFpsText.setPadding(
464 sidePadding,
465 cutInsets.top,
466 cutInsets.right,
467 cutInsets.bottom
468 )
469 } else {
470 binding.showFpsText.setPadding(
471 cutInsets.left,
472 cutInsets.top,
473 cutInsets.right,
474 cutInsets.bottom
475 )
476 }
477 windowInsets
478 }
479 }
480
481 private class EmulationState(private val gamePath: String) {
482 private var state: State
483 private var surface: Surface? = null
484 private var runWhenSurfaceIsValid = false
485
486 init {
487 // Starting state is stopped.
488 state = State.STOPPED
489 }
490
491 @get:Synchronized
492 val isStopped: Boolean
493 get() = state == State.STOPPED
494
495 // Getters for the current state
496 @get:Synchronized
497 val isPaused: Boolean
498 get() = state == State.PAUSED
499
500 @get:Synchronized
501 val isRunning: Boolean
502 get() = state == State.RUNNING
503
504 @Synchronized
505 fun stop() {
506 if (state != State.STOPPED) {
507 Log.debug("[EmulationFragment] Stopping emulation.")
508 NativeLibrary.stopEmulation()
509 state = State.STOPPED
510 } else {
511 Log.warning("[EmulationFragment] Stop called while already stopped.")
512 }
513 }
514
515 // State changing methods
516 @Synchronized
517 fun pause() {
518 if (state != State.PAUSED) {
519 Log.debug("[EmulationFragment] Pausing emulation.")
520
521 NativeLibrary.pauseEmulation()
522
523 state = State.PAUSED
524 } else {
525 Log.warning("[EmulationFragment] Pause called while already paused.")
526 }
527 }
528
529 @Synchronized
530 fun run(isActivityRecreated: Boolean) {
531 if (isActivityRecreated) {
532 if (NativeLibrary.isRunning()) {
533 state = State.PAUSED
534 }
535 } else {
536 Log.debug("[EmulationFragment] activity resumed or fresh start")
537 }
538
539 // If the surface is set, run now. Otherwise, wait for it to get set.
540 if (surface != null) {
541 runWithValidSurface()
542 } else {
543 runWhenSurfaceIsValid = true
544 }
545 }
546
547 // Surface callbacks
548 @Synchronized
549 fun newSurface(surface: Surface?) {
550 this.surface = surface
551 if (runWhenSurfaceIsValid) {
552 runWithValidSurface()
553 }
554 }
555
556 @Synchronized
557 fun clearSurface() {
558 if (surface == null) {
559 Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
560 } else {
561 surface = null
562 Log.debug("[EmulationFragment] Surface destroyed.")
563 when (state) {
564 State.RUNNING -> {
565 state = State.PAUSED
566 }
567
568 State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.")
569 else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.")
570 }
571 }
572 }
573
574 private fun runWithValidSurface() {
575 runWhenSurfaceIsValid = false
576 when (state) {
577 State.STOPPED -> {
578 NativeLibrary.surfaceChanged(surface)
579 val emulationThread = Thread({
580 Log.debug("[EmulationFragment] Starting emulation thread.")
581 NativeLibrary.run(gamePath)
582 }, "NativeEmulation")
583 emulationThread.start()
584 }
585
586 State.PAUSED -> {
587 Log.debug("[EmulationFragment] Resuming emulation.")
588 NativeLibrary.surfaceChanged(surface)
589 NativeLibrary.unPauseEmulation()
590 }
591
592 else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
593 }
594 state = State.RUNNING
595 }
596
597 private enum class State {
598 STOPPED, RUNNING, PAUSED
599 }
600 }
601
602 companion object {
603 private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
604
605 fun newInstance(game: Game): EmulationFragment {
606 val args = Bundle()
607 args.putParcelable(EmulationActivity.EXTRA_SELECTED_GAME, game)
608 val fragment = EmulationFragment()
609 fragment.arguments = args
610 return fragment
611 }
612 }
613}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
new file mode 100644
index 000000000..bdc337501
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -0,0 +1,330 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.Manifest
7import android.content.ActivityNotFoundException
8import android.content.DialogInterface
9import android.content.Intent
10import android.content.pm.PackageManager
11import android.os.Bundle
12import android.provider.DocumentsContract
13import android.view.LayoutInflater
14import android.view.View
15import android.view.ViewGroup
16import android.view.ViewGroup.MarginLayoutParams
17import android.widget.Toast
18import androidx.appcompat.app.AppCompatActivity
19import androidx.core.app.ActivityCompat
20import androidx.core.app.NotificationCompat
21import androidx.core.app.NotificationManagerCompat
22import androidx.core.view.ViewCompat
23import androidx.core.view.WindowInsetsCompat
24import androidx.core.view.updatePadding
25import androidx.documentfile.provider.DocumentFile
26import androidx.fragment.app.Fragment
27import androidx.fragment.app.activityViewModels
28import androidx.navigation.fragment.findNavController
29import androidx.recyclerview.widget.LinearLayoutManager
30import com.google.android.material.dialog.MaterialAlertDialogBuilder
31import com.google.android.material.transition.MaterialSharedAxis
32import org.yuzu.yuzu_emu.BuildConfig
33import org.yuzu.yuzu_emu.R
34import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
35import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
36import org.yuzu.yuzu_emu.features.DocumentProvider
37import org.yuzu.yuzu_emu.features.settings.model.Settings
38import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
39import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
40import org.yuzu.yuzu_emu.model.HomeSetting
41import org.yuzu.yuzu_emu.model.HomeViewModel
42import org.yuzu.yuzu_emu.ui.main.MainActivity
43import org.yuzu.yuzu_emu.utils.FileUtil
44import org.yuzu.yuzu_emu.utils.GpuDriverHelper
45
46class HomeSettingsFragment : Fragment() {
47 private var _binding: FragmentHomeSettingsBinding? = null
48 private val binding get() = _binding!!
49
50 private lateinit var mainActivity: MainActivity
51
52 private val homeViewModel: HomeViewModel by activityViewModels()
53
54 override fun onCreate(savedInstanceState: Bundle?) {
55 super.onCreate(savedInstanceState)
56 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
57 }
58
59 override fun onCreateView(
60 inflater: LayoutInflater,
61 container: ViewGroup?,
62 savedInstanceState: Bundle?
63 ): View {
64 _binding = FragmentHomeSettingsBinding.inflate(layoutInflater)
65 return binding.root
66 }
67
68 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
69 mainActivity = requireActivity() as MainActivity
70
71 val optionsList: MutableList<HomeSetting> = mutableListOf(
72 HomeSetting(
73 R.string.advanced_settings,
74 R.string.settings_description,
75 R.drawable.ic_settings
76 ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") },
77 HomeSetting(
78 R.string.open_user_folder,
79 R.string.open_user_folder_description,
80 R.drawable.ic_folder_open
81 ) { openFileManager() },
82 HomeSetting(
83 R.string.preferences_theme,
84 R.string.theme_and_color_description,
85 R.drawable.ic_palette
86 ) { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") },
87 HomeSetting(
88 R.string.install_gpu_driver,
89 R.string.install_gpu_driver_description,
90 R.drawable.ic_exit
91 ) { driverInstaller() },
92 HomeSetting(
93 R.string.install_amiibo_keys,
94 R.string.install_amiibo_keys_description,
95 R.drawable.ic_nfc
96 ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) },
97 HomeSetting(
98 R.string.select_games_folder,
99 R.string.select_games_folder_description,
100 R.drawable.ic_add
101 ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
102 HomeSetting(
103 R.string.manage_save_data,
104 R.string.import_export_saves_description,
105 R.drawable.ic_save
106 ) { ImportExportSavesFragment().show(parentFragmentManager, ImportExportSavesFragment.TAG) },
107 HomeSetting(
108 R.string.install_prod_keys,
109 R.string.install_prod_keys_description,
110 R.drawable.ic_unlock
111 ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
112 HomeSetting(
113 R.string.install_firmware,
114 R.string.install_firmware_description,
115 R.drawable.ic_firmware
116 ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) },
117 HomeSetting(
118 R.string.share_log,
119 R.string.share_log_description,
120 R.drawable.ic_log
121 ) { shareLog() },
122 HomeSetting(
123 R.string.about,
124 R.string.about_description,
125 R.drawable.ic_info_outline
126 ) {
127 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
128 parentFragmentManager.primaryNavigationFragment?.findNavController()
129 ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
130 }
131 )
132
133 if (!BuildConfig.PREMIUM) {
134 optionsList.add(
135 0,
136 HomeSetting(
137 R.string.get_early_access,
138 R.string.get_early_access_description,
139 R.drawable.ic_diamond
140 ) {
141 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
142 parentFragmentManager.primaryNavigationFragment?.findNavController()
143 ?.navigate(R.id.action_homeSettingsFragment_to_earlyAccessFragment)
144 }
145 )
146 }
147
148 binding.homeSettingsList.apply {
149 layoutManager = LinearLayoutManager(requireContext())
150 adapter = HomeSettingAdapter(requireActivity() as AppCompatActivity, optionsList)
151 }
152
153 setInsets()
154 }
155
156 override fun onStart() {
157 super.onStart()
158 exitTransition = null
159 homeViewModel.setNavigationVisibility(visible = true, animated = true)
160 homeViewModel.setStatusBarShadeVisibility(visible = true)
161 }
162
163 override fun onDestroyView() {
164 super.onDestroyView()
165 _binding = null
166 }
167
168 private fun openFileManager() {
169 // First, try to open the user data folder directly
170 try {
171 startActivity(getFileManagerIntentOnDocumentProvider(Intent.ACTION_VIEW))
172 return
173 } catch (_: ActivityNotFoundException) {
174 }
175
176 try {
177 startActivity(getFileManagerIntentOnDocumentProvider("android.provider.action.BROWSE"))
178 return
179 } catch (_: ActivityNotFoundException) {
180 }
181
182 // Just try to open the file manager, try the package name used on "normal" phones
183 try {
184 startActivity(getFileManagerIntent("com.google.android.documentsui"))
185 showNoLinkNotification()
186 return
187 } catch (_: ActivityNotFoundException) {
188 }
189
190 try {
191 // Next, try the AOSP package name
192 startActivity(getFileManagerIntent("com.android.documentsui"))
193 showNoLinkNotification()
194 return
195 } catch (_: ActivityNotFoundException) {
196 }
197
198 Toast.makeText(
199 requireContext(),
200 resources.getString(R.string.no_file_manager),
201 Toast.LENGTH_LONG
202 ).show()
203 }
204
205 private fun getFileManagerIntent(packageName: String): Intent {
206 // Fragile, but some phones don't expose the system file manager in any better way
207 val intent = Intent(Intent.ACTION_MAIN)
208 intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
209 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
210 return intent
211 }
212
213 private fun getFileManagerIntentOnDocumentProvider(action: String): Intent {
214 val authority = "${requireContext().packageName}.user"
215 val intent = Intent(action)
216 intent.addCategory(Intent.CATEGORY_DEFAULT)
217 intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID)
218 intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
219 return intent
220 }
221
222 private fun showNoLinkNotification() {
223 val builder = NotificationCompat.Builder(
224 requireContext(),
225 getString(R.string.notice_notification_channel_id)
226 )
227 .setSmallIcon(R.drawable.ic_stat_notification_logo)
228 .setContentTitle(getString(R.string.notification_no_directory_link))
229 .setContentText(getString(R.string.notification_no_directory_link_description))
230 .setPriority(NotificationCompat.PRIORITY_HIGH)
231 .setAutoCancel(true)
232 // TODO: Make the click action for this notification lead to a help article
233
234 with(NotificationManagerCompat.from(requireContext())) {
235 if (ActivityCompat.checkSelfPermission(
236 requireContext(),
237 Manifest.permission.POST_NOTIFICATIONS
238 ) != PackageManager.PERMISSION_GRANTED
239 ) {
240 Toast.makeText(
241 requireContext(),
242 resources.getString(R.string.notification_permission_not_granted),
243 Toast.LENGTH_LONG
244 ).show()
245 return
246 }
247 notify(0, builder.build())
248 }
249 }
250
251 private fun driverInstaller() {
252 // Get the driver name for the dialog message.
253 var driverName = GpuDriverHelper.customDriverName
254 if (driverName == null) {
255 driverName = getString(R.string.system_gpu_driver)
256 }
257
258 MaterialAlertDialogBuilder(requireContext())
259 .setTitle(getString(R.string.select_gpu_driver_title))
260 .setMessage(driverName)
261 .setNegativeButton(android.R.string.cancel, null)
262 .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
263 GpuDriverHelper.installDefaultDriver(requireContext())
264 Toast.makeText(
265 requireContext(),
266 R.string.select_gpu_driver_use_default,
267 Toast.LENGTH_SHORT
268 ).show()
269 }
270 .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
271 mainActivity.getDriver.launch(arrayOf("application/zip"))
272 }
273 .show()
274 }
275
276 private fun shareLog() {
277 val file = DocumentFile.fromSingleUri(
278 mainActivity,
279 DocumentsContract.buildDocumentUri(
280 DocumentProvider.AUTHORITY,
281 "${DocumentProvider.ROOT_ID}/log/yuzu_log.txt"
282 )
283 )!!
284 if (file.exists()) {
285 val intent = Intent(Intent.ACTION_SEND)
286 .setDataAndType(file.uri, FileUtil.TEXT_PLAIN)
287 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
288 .putExtra(Intent.EXTRA_STREAM, file.uri)
289 startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
290 } else {
291 Toast.makeText(
292 requireContext(),
293 getText(R.string.share_log_missing),
294 Toast.LENGTH_SHORT
295 ).show()
296 }
297 }
298
299 private fun setInsets() =
300 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
301 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
302 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
303 val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
304 val spacingNavigationRail =
305 resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
306
307 val leftInsets = barInsets.left + cutoutInsets.left
308 val rightInsets = barInsets.right + cutoutInsets.right
309
310 binding.scrollViewSettings.updatePadding(
311 top = barInsets.top,
312 bottom = barInsets.bottom
313 )
314
315 val mlpScrollSettings = binding.scrollViewSettings.layoutParams as MarginLayoutParams
316 mlpScrollSettings.leftMargin = leftInsets
317 mlpScrollSettings.rightMargin = rightInsets
318 binding.scrollViewSettings.layoutParams = mlpScrollSettings
319
320 binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation)
321
322 if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
323 binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail)
324 } else {
325 binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
326 }
327
328 windowInsets
329 }
330}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
new file mode 100644
index 000000000..36e63bb9e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
@@ -0,0 +1,210 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.Intent
8import android.net.Uri
9import android.os.Bundle
10import android.provider.DocumentsContract
11import android.widget.Toast
12import androidx.activity.result.ActivityResultLauncher
13import androidx.activity.result.contract.ActivityResultContracts
14import androidx.appcompat.app.AppCompatActivity
15import androidx.documentfile.provider.DocumentFile
16import androidx.fragment.app.DialogFragment
17import com.google.android.material.dialog.MaterialAlertDialogBuilder
18import kotlinx.coroutines.CoroutineScope
19import kotlinx.coroutines.Dispatchers
20import kotlinx.coroutines.launch
21import kotlinx.coroutines.withContext
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.YuzuApplication
24import org.yuzu.yuzu_emu.features.DocumentProvider
25import org.yuzu.yuzu_emu.getPublicFilesDir
26import org.yuzu.yuzu_emu.utils.FileUtil
27import java.io.BufferedOutputStream
28import java.io.File
29import java.io.FileOutputStream
30import java.io.FilenameFilter
31import java.time.LocalDateTime
32import java.time.format.DateTimeFormatter
33import java.util.zip.ZipEntry
34import java.util.zip.ZipOutputStream
35
36class ImportExportSavesFragment : DialogFragment() {
37 private val context = YuzuApplication.appContext
38 private val savesFolder =
39 "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
40
41 // Get first subfolder in saves folder (should be the user folder)
42 private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
43 private var lastZipCreated: File? = null
44
45 private lateinit var startForResultExportSave: ActivityResultLauncher<Intent>
46 private lateinit var documentPicker: ActivityResultLauncher<Array<String>>
47
48 override fun onCreate(savedInstanceState: Bundle?) {
49 super.onCreate(savedInstanceState)
50 val activity = requireActivity() as AppCompatActivity
51
52 val activityResultRegistry = requireActivity().activityResultRegistry
53 startForResultExportSave = activityResultRegistry.register(
54 "startForResultExportSaveKey",
55 ActivityResultContracts.StartActivityForResult()
56 ) {
57 File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
58 }
59 documentPicker = activityResultRegistry.register(
60 "documentPickerKey",
61 ActivityResultContracts.OpenDocument()
62 ) {
63 it?.let { uri -> importSave(uri, activity) }
64 }
65 }
66
67 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
68 return if (savesFolderRoot == "") {
69 MaterialAlertDialogBuilder(requireContext())
70 .setTitle(R.string.manage_save_data)
71 .setMessage(R.string.import_export_saves_no_profile)
72 .setPositiveButton(android.R.string.ok, null)
73 .show()
74 } else {
75 MaterialAlertDialogBuilder(requireContext())
76 .setTitle(R.string.manage_save_data)
77 .setMessage(R.string.manage_save_data_description)
78 .setNegativeButton(R.string.export_saves) { _, _ ->
79 exportSave()
80 }
81 .setPositiveButton(R.string.import_saves) { _, _ ->
82 documentPicker.launch(arrayOf("application/zip"))
83 }
84 .setNeutralButton(android.R.string.cancel, null)
85 .show()
86 }
87 }
88
89 /**
90 * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
91 * @return true if the zip file is successfully created, false otherwise.
92 */
93 private fun zipSave(): Boolean {
94 try {
95 val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp")
96 tempFolder.mkdirs()
97 val saveFolder = File(savesFolderRoot)
98 val outputZipFile = File(
99 tempFolder,
100 "yuzu saves - ${
101 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
102 }.zip"
103 )
104 outputZipFile.createNewFile()
105 ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
106 saveFolder.walkTopDown().forEach { file ->
107 val zipFileName =
108 file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
109 if (zipFileName == "")
110 return@forEach
111 val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
112 zos.putNextEntry(entry)
113 if (file.isFile)
114 file.inputStream().use { fis -> fis.copyTo(zos) }
115 }
116 }
117 lastZipCreated = outputZipFile
118 } catch (e: Exception) {
119 return false
120 }
121 return true
122 }
123
124 /**
125 * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
126 */
127 private fun exportSave() {
128 CoroutineScope(Dispatchers.IO).launch {
129 val wasZipCreated = zipSave()
130 val lastZipFile = lastZipCreated
131 if (!wasZipCreated || lastZipFile == null) {
132 withContext(Dispatchers.Main) {
133 Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show()
134 }
135 return@launch
136 }
137
138 withContext(Dispatchers.Main) {
139 val file = DocumentFile.fromSingleUri(
140 context, DocumentsContract.buildDocumentUri(
141 DocumentProvider.AUTHORITY,
142 "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
143 )
144 )!!
145 val intent = Intent(Intent.ACTION_SEND)
146 .setDataAndType(file.uri, "application/zip")
147 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
148 .putExtra(Intent.EXTRA_STREAM, file.uri)
149 startForResultExportSave.launch(Intent.createChooser(intent, "Share save file"))
150 }
151 }
152 }
153
154 /**
155 * Imports the save files contained in the zip file, and replaces any existing ones with the new save file.
156 * @param zipUri The Uri of the zip file containing the save file(s) to import.
157 */
158 private fun importSave(zipUri: Uri, activity: AppCompatActivity) {
159 val inputZip = context.contentResolver.openInputStream(zipUri)
160 // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
161 var validZip = false
162 val savesFolder = File(savesFolderRoot)
163 val cacheSaveDir = File("${context.cacheDir.path}/saves/")
164 cacheSaveDir.mkdir()
165
166 if (inputZip == null) {
167 Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
168 .show()
169 return
170 }
171
172 val filterTitleId =
173 FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
174
175 try {
176 CoroutineScope(Dispatchers.IO).launch {
177 FileUtil.unzip(inputZip, cacheSaveDir)
178 cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
179 File(savesFolder, savePath).deleteRecursively()
180 File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
181 validZip = true
182 }
183
184 withContext(Dispatchers.Main) {
185 if (!validZip) {
186 MessageDialogFragment.newInstance(
187 R.string.save_file_invalid_zip_structure,
188 R.string.save_file_invalid_zip_structure_description
189 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
190 return@withContext
191 }
192 Toast.makeText(
193 context,
194 context.getString(R.string.save_file_imported_success),
195 Toast.LENGTH_LONG
196 ).show()
197 }
198
199 cacheSaveDir.deleteRecursively()
200 }
201 } catch (e: Exception) {
202 Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
203 .show()
204 }
205 }
206
207 companion object {
208 const val TAG = "ImportExportSavesFragment"
209 }
210}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
new file mode 100644
index 000000000..c7880d8cc
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -0,0 +1,70 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.os.Bundle
8import android.widget.Toast
9import androidx.appcompat.app.AppCompatActivity
10import androidx.fragment.app.DialogFragment
11import androidx.fragment.app.activityViewModels
12import androidx.lifecycle.ViewModelProvider
13import com.google.android.material.dialog.MaterialAlertDialogBuilder
14import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
15import org.yuzu.yuzu_emu.model.TaskViewModel
16
17
18class IndeterminateProgressDialogFragment : DialogFragment() {
19 private val taskViewModel: TaskViewModel by activityViewModels()
20
21 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
22 val titleId = requireArguments().getInt(TITLE)
23
24 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
25 progressBinding.progressBar.isIndeterminate = true
26 val dialog = MaterialAlertDialogBuilder(requireContext())
27 .setTitle(titleId)
28 .setView(progressBinding.root)
29 .create()
30 dialog.setCanceledOnTouchOutside(false)
31
32 taskViewModel.isComplete.observe(this) { complete ->
33 if (complete) {
34 dialog.dismiss()
35 when (val result = taskViewModel.result.value) {
36 is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
37 is MessageDialogFragment -> result.show(
38 parentFragmentManager,
39 MessageDialogFragment.TAG
40 )
41 }
42 taskViewModel.clear()
43 }
44 }
45
46 if (taskViewModel.isRunning.value == false) {
47 taskViewModel.runTask()
48 }
49 return dialog
50 }
51
52 companion object {
53 const val TAG = "IndeterminateProgressDialogFragment"
54
55 private const val TITLE = "Title"
56
57 fun newInstance(
58 activity: AppCompatActivity,
59 titleId: Int,
60 task: () -> Any
61 ): IndeterminateProgressDialogFragment {
62 val dialog = IndeterminateProgressDialogFragment()
63 val args = Bundle()
64 ViewModelProvider(activity)[TaskViewModel::class.java].task = task
65 args.putInt(TITLE, titleId)
66 dialog.arguments = args
67 return dialog
68 }
69 }
70}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicenseBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicenseBottomSheetDialogFragment.kt
new file mode 100644
index 000000000..78419191c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicenseBottomSheetDialogFragment.kt
@@ -0,0 +1,59 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import com.google.android.material.bottomsheet.BottomSheetBehavior
11import com.google.android.material.bottomsheet.BottomSheetDialogFragment
12import org.yuzu.yuzu_emu.databinding.DialogLicenseBinding
13import org.yuzu.yuzu_emu.model.License
14import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
15
16class LicenseBottomSheetDialogFragment : BottomSheetDialogFragment() {
17 private var _binding: DialogLicenseBinding? = null
18 private val binding get() = _binding!!
19
20 override fun onCreateView(
21 inflater: LayoutInflater,
22 container: ViewGroup?,
23 savedInstanceState: Bundle?
24 ): View {
25 _binding = DialogLicenseBinding.inflate(layoutInflater)
26 return binding.root
27 }
28
29 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
30 super.onViewCreated(view, savedInstanceState)
31 BottomSheetBehavior.from<View>(view.parent as View).state =
32 BottomSheetBehavior.STATE_HALF_EXPANDED
33
34 val license = requireArguments().parcelable<License>(LICENSE)!!
35
36 binding.apply {
37 textTitle.setText(license.titleId)
38 textLink.setText(license.linkId)
39 textCopyright.setText(license.copyrightId)
40 textLicense.setText(license.licenseId)
41 }
42 }
43
44 companion object {
45 const val TAG = "LicenseBottomSheetDialogFragment"
46
47 const val LICENSE = "License"
48
49 fun newInstance(
50 license: License
51 ): LicenseBottomSheetDialogFragment {
52 val dialog = LicenseBottomSheetDialogFragment()
53 val bundle = Bundle()
54 bundle.putParcelable(LICENSE, license)
55 dialog.arguments = bundle
56 return dialog
57 }
58 }
59}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt
new file mode 100644
index 000000000..59141e823
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt
@@ -0,0 +1,137 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import android.view.ViewGroup.MarginLayoutParams
11import androidx.appcompat.app.AppCompatActivity
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels
17import androidx.navigation.findNavController
18import androidx.recyclerview.widget.LinearLayoutManager
19import com.google.android.material.transition.MaterialSharedAxis
20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.adapters.LicenseAdapter
22import org.yuzu.yuzu_emu.databinding.FragmentLicensesBinding
23import org.yuzu.yuzu_emu.model.HomeViewModel
24import org.yuzu.yuzu_emu.model.License
25
26class LicensesFragment : Fragment() {
27 private var _binding: FragmentLicensesBinding? = null
28 private val binding get() = _binding!!
29
30 private val homeViewModel: HomeViewModel by activityViewModels()
31
32 override fun onCreate(savedInstanceState: Bundle?) {
33 super.onCreate(savedInstanceState)
34 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
35 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
36 }
37
38 override fun onCreateView(
39 inflater: LayoutInflater,
40 container: ViewGroup?,
41 savedInstanceState: Bundle?
42 ): View {
43 _binding = FragmentLicensesBinding.inflate(layoutInflater)
44 return binding.root
45 }
46
47 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
48 homeViewModel.setNavigationVisibility(visible = false, animated = true)
49 homeViewModel.setStatusBarShadeVisibility(visible = false)
50
51 binding.toolbarLicenses.setNavigationOnClickListener {
52 binding.root.findNavController().popBackStack()
53 }
54
55 val licenses = listOf(
56 License(
57 R.string.license_fidelityfx_fsr,
58 R.string.license_fidelityfx_fsr_description,
59 R.string.license_fidelityfx_fsr_link,
60 R.string.license_fidelityfx_fsr_copyright,
61 R.string.license_fidelityfx_fsr_text
62 ),
63 License(
64 R.string.license_cubeb,
65 R.string.license_cubeb_description,
66 R.string.license_cubeb_link,
67 R.string.license_cubeb_copyright,
68 R.string.license_cubeb_text
69 ),
70 License(
71 R.string.license_dynarmic,
72 R.string.license_dynarmic_description,
73 R.string.license_dynarmic_link,
74 R.string.license_dynarmic_copyright,
75 R.string.license_dynarmic_text
76 ),
77 License(
78 R.string.license_ffmpeg,
79 R.string.license_ffmpeg_description,
80 R.string.license_ffmpeg_link,
81 R.string.license_ffmpeg_copyright,
82 R.string.license_ffmpeg_text
83 ),
84 License(
85 R.string.license_opus,
86 R.string.license_opus_description,
87 R.string.license_opus_link,
88 R.string.license_opus_copyright,
89 R.string.license_opus_text
90 ),
91 License(
92 R.string.license_sirit,
93 R.string.license_sirit_description,
94 R.string.license_sirit_link,
95 R.string.license_sirit_copyright,
96 R.string.license_sirit_text
97 ),
98 License(
99 R.string.license_adreno_tools,
100 R.string.license_adreno_tools_description,
101 R.string.license_adreno_tools_link,
102 R.string.license_adreno_tools_copyright,
103 R.string.license_adreno_tools_text
104 )
105 )
106
107 binding.listLicenses.apply {
108 layoutManager = LinearLayoutManager(requireContext())
109 adapter = LicenseAdapter(requireActivity() as AppCompatActivity, licenses)
110 }
111
112 setInsets()
113 }
114
115 private fun setInsets() =
116 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
117 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
118 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
119
120 val leftInsets = barInsets.left + cutoutInsets.left
121 val rightInsets = barInsets.right + cutoutInsets.right
122
123 val mlpAppBar = binding.appbarLicenses.layoutParams as MarginLayoutParams
124 mlpAppBar.leftMargin = leftInsets
125 mlpAppBar.rightMargin = rightInsets
126 binding.appbarLicenses.layoutParams = mlpAppBar
127
128 val mlpScrollAbout = binding.listLicenses.layoutParams as MarginLayoutParams
129 mlpScrollAbout.leftMargin = leftInsets
130 mlpScrollAbout.rightMargin = rightInsets
131 binding.listLicenses.layoutParams = mlpScrollAbout
132
133 binding.listLicenses.updatePadding(bottom = barInsets.bottom)
134
135 windowInsets
136 }
137}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
new file mode 100644
index 000000000..2db38fdc2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -0,0 +1,62 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.Intent
8import android.net.Uri
9import android.os.Bundle
10import androidx.fragment.app.DialogFragment
11import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.R
13
14class MessageDialogFragment : DialogFragment() {
15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
16 val titleId = requireArguments().getInt(TITLE)
17 val descriptionId = requireArguments().getInt(DESCRIPTION)
18 val helpLinkId = requireArguments().getInt(HELP_LINK)
19
20 val dialog = MaterialAlertDialogBuilder(requireContext())
21 .setPositiveButton(R.string.close, null)
22 .setTitle(titleId)
23 .setMessage(descriptionId)
24
25 if (helpLinkId != 0) {
26 dialog.setNeutralButton(R.string.learn_more) { _, _ ->
27 openLink(getString(helpLinkId))
28 }
29 }
30
31 return dialog.show()
32 }
33
34 private fun openLink(link: String) {
35 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
36 startActivity(intent)
37 }
38
39 companion object {
40 const val TAG = "MessageDialogFragment"
41
42 private const val TITLE = "Title"
43 private const val DESCRIPTION = "Description"
44 private const val HELP_LINK = "Link"
45
46 fun newInstance(
47 titleId: Int,
48 descriptionId: Int,
49 helpLinkId: Int = 0
50 ): MessageDialogFragment {
51 val dialog = MessageDialogFragment()
52 val bundle = Bundle()
53 bundle.apply {
54 putInt(TITLE, titleId)
55 putInt(DESCRIPTION, descriptionId)
56 putInt(HELP_LINK, helpLinkId)
57 }
58 dialog.arguments = bundle
59 return dialog
60 }
61 }
62}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/PermissionDeniedDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/PermissionDeniedDialogFragment.kt
new file mode 100644
index 000000000..3478b9250
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/PermissionDeniedDialogFragment.kt
@@ -0,0 +1,38 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.content.Intent
9import android.net.Uri
10import android.os.Bundle
11import android.provider.Settings
12import androidx.fragment.app.DialogFragment
13import com.google.android.material.dialog.MaterialAlertDialogBuilder
14import org.yuzu.yuzu_emu.R
15
16class PermissionDeniedDialogFragment : DialogFragment() {
17 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
18 return MaterialAlertDialogBuilder(requireContext())
19 .setPositiveButton(R.string.home_settings) { _: DialogInterface?, _: Int ->
20 openSettings()
21 }
22 .setNegativeButton(android.R.string.cancel, null)
23 .setTitle(R.string.permission_denied)
24 .setMessage(R.string.permission_denied_description)
25 .show()
26 }
27
28 private fun openSettings() {
29 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
30 val uri = Uri.fromParts("package", requireActivity().packageName, null)
31 intent.data = uri
32 startActivity(intent)
33 }
34
35 companion object {
36 const val TAG = "PermissionDeniedDialogFragment"
37 }
38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ResetSettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ResetSettingsDialogFragment.kt
new file mode 100644
index 000000000..1b4b93ab8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ResetSettingsDialogFragment.kt
@@ -0,0 +1,30 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.os.Bundle
8import androidx.fragment.app.DialogFragment
9import com.google.android.material.dialog.MaterialAlertDialogBuilder
10import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
12
13class ResetSettingsDialogFragment : DialogFragment() {
14 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
15 val settingsActivity = requireActivity() as SettingsActivity
16
17 return MaterialAlertDialogBuilder(requireContext())
18 .setTitle(R.string.reset_all_settings)
19 .setMessage(R.string.reset_all_settings_description)
20 .setPositiveButton(android.R.string.ok) { _, _ ->
21 settingsActivity.onSettingsReset()
22 }
23 .setNegativeButton(android.R.string.cancel, null)
24 .show()
25 }
26
27 companion object {
28 const val TAG = "ResetSettingsDialogFragment"
29 }
30}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
new file mode 100644
index 000000000..ebc0f164a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -0,0 +1,236 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.Context
7import android.content.SharedPreferences
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import android.view.inputmethod.InputMethodManager
13import androidx.appcompat.app.AppCompatActivity
14import androidx.core.view.ViewCompat
15import androidx.core.view.WindowInsetsCompat
16import androidx.core.view.updatePadding
17import androidx.core.widget.doOnTextChanged
18import androidx.fragment.app.Fragment
19import androidx.fragment.app.activityViewModels
20import androidx.preference.PreferenceManager
21import info.debatty.java.stringsimilarity.Jaccard
22import info.debatty.java.stringsimilarity.JaroWinkler
23import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.YuzuApplication
25import org.yuzu.yuzu_emu.adapters.GameAdapter
26import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding
27import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
28import org.yuzu.yuzu_emu.model.Game
29import org.yuzu.yuzu_emu.model.GamesViewModel
30import org.yuzu.yuzu_emu.model.HomeViewModel
31import org.yuzu.yuzu_emu.utils.FileUtil
32import org.yuzu.yuzu_emu.utils.Log
33import java.util.Locale
34
35class SearchFragment : Fragment() {
36 private var _binding: FragmentSearchBinding? = null
37 private val binding get() = _binding!!
38
39 private val gamesViewModel: GamesViewModel by activityViewModels()
40 private val homeViewModel: HomeViewModel by activityViewModels()
41
42 private lateinit var preferences: SharedPreferences
43
44 companion object {
45 private const val SEARCH_TEXT = "SearchText"
46 }
47
48 override fun onCreateView(
49 inflater: LayoutInflater,
50 container: ViewGroup?,
51 savedInstanceState: Bundle?
52 ): View {
53 _binding = FragmentSearchBinding.inflate(layoutInflater)
54 return binding.root
55 }
56
57 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
58 homeViewModel.setNavigationVisibility(visible = true, animated = false)
59 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
60
61 if (savedInstanceState != null) {
62 binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
63 }
64
65 binding.gridGamesSearch.apply {
66 layoutManager = AutofitGridLayoutManager(
67 requireContext(),
68 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
69 )
70 adapter = GameAdapter(requireActivity() as AppCompatActivity)
71 }
72
73 binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
74
75 binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
76 if (text.toString().isNotEmpty()) {
77 binding.clearButton.visibility = View.VISIBLE
78 } else {
79 binding.clearButton.visibility = View.INVISIBLE
80 }
81 filterAndSearch()
82 }
83
84 gamesViewModel.apply {
85 searchFocused.observe(viewLifecycleOwner) { searchFocused ->
86 if (searchFocused) {
87 focusSearch()
88 gamesViewModel.setSearchFocused(false)
89 }
90 }
91
92 games.observe(viewLifecycleOwner) { filterAndSearch() }
93 searchedGames.observe(viewLifecycleOwner) {
94 (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
95 if (it.isEmpty()) {
96 binding.noResultsView.visibility = View.VISIBLE
97 } else {
98 binding.noResultsView.visibility = View.GONE
99 }
100 }
101 }
102
103 binding.clearButton.setOnClickListener { binding.searchText.setText("") }
104
105 binding.searchBackground.setOnClickListener { focusSearch() }
106
107 setInsets()
108 filterAndSearch()
109 }
110
111 private inner class ScoredGame(val score: Double, val item: Game)
112
113 private fun filterAndSearch() {
114 val baseList = gamesViewModel.games.value!!
115 val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
116 R.id.chip_recently_played -> {
117 baseList.filter {
118 val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
119 lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
120 }
121 }
122
123 R.id.chip_recently_added -> {
124 baseList.filter {
125 val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
126 addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
127 }
128 }
129
130 R.id.chip_homebrew -> {
131 baseList.filter {
132 Log.error("Guh - ${it.path}")
133 FileUtil.hasExtension(it.path, "nro")
134 || FileUtil.hasExtension(it.path, "nso")
135 }
136 }
137
138 R.id.chip_retail -> baseList.filter {
139 FileUtil.hasExtension(it.path, "xci")
140 || FileUtil.hasExtension(it.path, "nsp")
141 }
142
143 else -> baseList
144 }
145
146 if (binding.searchText.text.toString().isEmpty()
147 && binding.chipGroup.checkedChipId != View.NO_ID
148 ) {
149 gamesViewModel.setSearchedGames(filteredList)
150 return
151 }
152
153 val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
154 val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
155 val sortedList: List<Game> = filteredList.mapNotNull { game ->
156 val title = game.title.lowercase(Locale.getDefault())
157 val score = searchAlgorithm.similarity(searchTerm, title)
158 if (score > 0.03) {
159 ScoredGame(score, game)
160 } else {
161 null
162 }
163 }.sortedByDescending { it.score }.map { it.item }
164 gamesViewModel.setSearchedGames(sortedList)
165 }
166
167 override fun onDestroyView() {
168 super.onDestroyView()
169 _binding = null
170 }
171
172 override fun onSaveInstanceState(outState: Bundle) {
173 super.onSaveInstanceState(outState)
174 if (_binding != null) {
175 outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
176 }
177 }
178
179 private fun focusSearch() {
180 if (_binding != null) {
181 binding.searchText.requestFocus()
182 val imm =
183 requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
184 imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
185 }
186 }
187
188 private fun setInsets() =
189 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
190 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
191 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
192 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
193 val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
194 val spacingNavigationRail =
195 resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
196 val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
197
198 binding.constraintSearch.updatePadding(
199 left = barInsets.left + cutoutInsets.left,
200 top = barInsets.top,
201 right = barInsets.right + cutoutInsets.right
202 )
203
204 binding.gridGamesSearch.updatePadding(
205 top = extraListSpacing,
206 bottom = barInsets.bottom + spacingNavigation + extraListSpacing
207 )
208 binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
209
210 val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
211 if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
212 binding.frameSearch.updatePadding(left = spacingNavigationRail)
213 binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
214 binding.noResultsView.updatePadding(left = spacingNavigationRail)
215 binding.chipGroup.updatePadding(
216 left = chipSpacing + spacingNavigationRail,
217 right = chipSpacing
218 )
219 mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
220 mlpDivider.rightMargin = chipSpacing
221 } else {
222 binding.frameSearch.updatePadding(right = spacingNavigationRail)
223 binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
224 binding.noResultsView.updatePadding(right = spacingNavigationRail)
225 binding.chipGroup.updatePadding(
226 left = chipSpacing,
227 right = chipSpacing + spacingNavigationRail
228 )
229 mlpDivider.leftMargin = chipSpacing
230 mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
231 }
232 binding.divider.layoutParams = mlpDivider
233
234 windowInsets
235 }
236}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
new file mode 100644
index 000000000..258773380
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -0,0 +1,329 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.Manifest
7import android.content.Intent
8import android.os.Build
9import android.os.Bundle
10import android.view.LayoutInflater
11import android.view.View
12import android.view.ViewGroup
13import androidx.activity.OnBackPressedCallback
14import androidx.activity.result.contract.ActivityResultContracts
15import androidx.annotation.RequiresApi
16import androidx.appcompat.app.AppCompatActivity
17import androidx.core.app.NotificationManagerCompat
18import androidx.core.content.ContextCompat
19import androidx.core.view.ViewCompat
20import androidx.core.view.WindowInsetsCompat
21import androidx.core.view.isVisible
22import androidx.fragment.app.Fragment
23import androidx.fragment.app.activityViewModels
24import androidx.navigation.findNavController
25import androidx.preference.PreferenceManager
26import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
27import com.google.android.material.transition.MaterialFadeThrough
28import org.yuzu.yuzu_emu.R
29import org.yuzu.yuzu_emu.YuzuApplication
30import org.yuzu.yuzu_emu.adapters.SetupAdapter
31import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
32import org.yuzu.yuzu_emu.features.settings.model.Settings
33import org.yuzu.yuzu_emu.model.HomeViewModel
34import org.yuzu.yuzu_emu.model.SetupPage
35import org.yuzu.yuzu_emu.ui.main.MainActivity
36import org.yuzu.yuzu_emu.utils.DirectoryInitialization
37import org.yuzu.yuzu_emu.utils.GameHelper
38import java.io.File
39
40class SetupFragment : Fragment() {
41 private var _binding: FragmentSetupBinding? = null
42 private val binding get() = _binding!!
43
44 private val homeViewModel: HomeViewModel by activityViewModels()
45
46 private lateinit var mainActivity: MainActivity
47
48 private lateinit var hasBeenWarned: BooleanArray
49
50 companion object {
51 const val KEY_NEXT_VISIBILITY = "NextButtonVisibility"
52 const val KEY_BACK_VISIBILITY = "BackButtonVisibility"
53 const val KEY_HAS_BEEN_WARNED = "HasBeenWarned"
54 }
55
56 override fun onCreate(savedInstanceState: Bundle?) {
57 super.onCreate(savedInstanceState)
58 exitTransition = MaterialFadeThrough()
59 }
60
61 override fun onCreateView(
62 inflater: LayoutInflater,
63 container: ViewGroup?,
64 savedInstanceState: Bundle?
65 ): View {
66 _binding = FragmentSetupBinding.inflate(layoutInflater)
67 return binding.root
68 }
69
70 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
71 mainActivity = requireActivity() as MainActivity
72
73 homeViewModel.setNavigationVisibility(visible = false, animated = false)
74
75 requireActivity().onBackPressedDispatcher.addCallback(
76 viewLifecycleOwner,
77 object : OnBackPressedCallback(true) {
78 override fun handleOnBackPressed() {
79 if (binding.viewPager2.currentItem > 0) {
80 pageBackward()
81 } else {
82 requireActivity().finish()
83 }
84 }
85 })
86
87 requireActivity().window.navigationBarColor =
88 ContextCompat.getColor(requireContext(), android.R.color.transparent)
89
90 val pages = mutableListOf<SetupPage>()
91 pages.apply {
92 add(
93 SetupPage(
94 R.drawable.ic_yuzu_title,
95 R.string.welcome,
96 R.string.welcome_description,
97 0,
98 true,
99 R.string.get_started,
100 { pageForward() },
101 false
102 )
103 )
104
105 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
106 add(
107 SetupPage(
108 R.drawable.ic_notification,
109 R.string.notifications,
110 R.string.notifications_description,
111 0,
112 false,
113 R.string.give_permission,
114 { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) },
115 true,
116 R.string.notification_warning,
117 R.string.notification_warning_description,
118 0,
119 {
120 NotificationManagerCompat.from(requireContext())
121 .areNotificationsEnabled()
122 }
123 )
124 )
125 }
126
127 add(
128 SetupPage(
129 R.drawable.ic_key,
130 R.string.keys,
131 R.string.keys_description,
132 R.drawable.ic_add,
133 true,
134 R.string.select_keys,
135 { mainActivity.getProdKey.launch(arrayOf("*/*")) },
136 true,
137 R.string.install_prod_keys_warning,
138 R.string.install_prod_keys_warning_description,
139 R.string.install_prod_keys_warning_help,
140 { File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() }
141 )
142 )
143 add(
144 SetupPage(
145 R.drawable.ic_controller,
146 R.string.games,
147 R.string.games_description,
148 R.drawable.ic_add,
149 true,
150 R.string.add_games,
151 { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
152 true,
153 R.string.add_games_warning,
154 R.string.add_games_warning_description,
155 R.string.add_games_warning_help,
156 {
157 val preferences =
158 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
159 preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
160 }
161 )
162 )
163 add(
164 SetupPage(
165 R.drawable.ic_check,
166 R.string.done,
167 R.string.done_description,
168 R.drawable.ic_arrow_forward,
169 false,
170 R.string.text_continue,
171 { finishSetup() },
172 false
173 )
174 )
175 }
176
177 binding.viewPager2.apply {
178 adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
179 offscreenPageLimit = 2
180 isUserInputEnabled = false
181 }
182
183 binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
184 var previousPosition: Int = 0
185
186 override fun onPageSelected(position: Int) {
187 super.onPageSelected(position)
188
189 if (position == 1 && previousPosition == 0) {
190 showView(binding.buttonNext)
191 showView(binding.buttonBack)
192 } else if (position == 0 && previousPosition == 1) {
193 hideView(binding.buttonBack)
194 hideView(binding.buttonNext)
195 } else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
196 hideView(binding.buttonNext)
197 } else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
198 showView(binding.buttonNext)
199 }
200
201 previousPosition = position
202 }
203 })
204
205 binding.buttonNext.setOnClickListener {
206 val index = binding.viewPager2.currentItem
207 val currentPage = pages[index]
208
209 // Checks if the user has completed the task on the current page
210 if (currentPage.hasWarning) {
211 if (currentPage.taskCompleted.invoke()) {
212 pageForward()
213 return@setOnClickListener
214 }
215
216 if (!hasBeenWarned[index]) {
217 SetupWarningDialogFragment.newInstance(
218 currentPage.warningTitleId,
219 currentPage.warningDescriptionId,
220 currentPage.warningHelpLinkId,
221 index
222 ).show(childFragmentManager, SetupWarningDialogFragment.TAG)
223 return@setOnClickListener
224 }
225 }
226 pageForward()
227 }
228 binding.buttonBack.setOnClickListener { pageBackward() }
229
230 if (savedInstanceState != null) {
231 val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
232 val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
233 hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
234
235 if (nextIsVisible) {
236 binding.buttonNext.visibility = View.VISIBLE
237 }
238 if (backIsVisible) {
239 binding.buttonBack.visibility = View.VISIBLE
240 }
241 } else {
242 hasBeenWarned = BooleanArray(pages.size)
243 }
244
245 setInsets()
246 }
247
248 override fun onSaveInstanceState(outState: Bundle) {
249 super.onSaveInstanceState(outState)
250 outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
251 outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
252 outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
253 }
254
255 override fun onDestroyView() {
256 super.onDestroyView()
257 _binding = null
258 }
259
260 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
261 private val permissionLauncher =
262 registerForActivityResult(ActivityResultContracts.RequestPermission()) {
263 if (!it && !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
264 PermissionDeniedDialogFragment().show(
265 childFragmentManager,
266 PermissionDeniedDialogFragment.TAG
267 )
268 }
269 }
270
271 private fun finishSetup() {
272 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
273 .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
274 .apply()
275 mainActivity.finishSetup(binding.root.findNavController())
276 }
277
278 private fun showView(view: View) {
279 view.apply {
280 alpha = 0f
281 visibility = View.VISIBLE
282 isClickable = true
283 }.animate().apply {
284 duration = 300
285 alpha(1f)
286 }.start()
287 }
288
289 private fun hideView(view: View) {
290 if (view.visibility == View.INVISIBLE) {
291 return
292 }
293
294 view.apply {
295 alpha = 1f
296 isClickable = false
297 }.animate().apply {
298 duration = 300
299 alpha(0f)
300 }.withEndAction {
301 view.visibility = View.INVISIBLE
302 }
303 }
304
305 fun pageForward() {
306 binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
307 }
308
309 fun pageBackward() {
310 binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
311 }
312
313 fun setPageWarned(page: Int) {
314 hasBeenWarned[page] = true
315 }
316
317 private fun setInsets() =
318 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
319 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
320 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
321 view.setPadding(
322 barInsets.left + cutoutInsets.left,
323 barInsets.top + cutoutInsets.top,
324 barInsets.right + cutoutInsets.right,
325 barInsets.bottom + cutoutInsets.bottom
326 )
327 windowInsets
328 }
329}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt
new file mode 100644
index 000000000..b2c1d54af
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt
@@ -0,0 +1,86 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.content.Intent
9import android.net.Uri
10import android.os.Bundle
11import androidx.fragment.app.DialogFragment
12import com.google.android.material.dialog.MaterialAlertDialogBuilder
13import org.yuzu.yuzu_emu.R
14
15class SetupWarningDialogFragment : DialogFragment() {
16 private var titleId: Int = 0
17 private var descriptionId: Int = 0
18 private var helpLinkId: Int = 0
19 private var page: Int = 0
20
21 private lateinit var setupFragment: SetupFragment
22
23 override fun onCreate(savedInstanceState: Bundle?) {
24 super.onCreate(savedInstanceState)
25 titleId = requireArguments().getInt(TITLE)
26 descriptionId = requireArguments().getInt(DESCRIPTION)
27 helpLinkId = requireArguments().getInt(HELP_LINK)
28 page = requireArguments().getInt(PAGE)
29
30 setupFragment = requireParentFragment() as SetupFragment
31 }
32
33 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
34 val builder = MaterialAlertDialogBuilder(requireContext())
35 .setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
36 setupFragment.pageForward()
37 setupFragment.setPageWarned(page)
38 }
39 .setNegativeButton(R.string.warning_cancel, null)
40
41 if (titleId != 0) {
42 builder.setTitle(titleId)
43 } else {
44 builder.setTitle("")
45 }
46 if (descriptionId != 0) {
47 builder.setMessage(descriptionId)
48 }
49 if (helpLinkId != 0) {
50 builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
51 val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
52 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
53 startActivity(intent)
54 }
55 }
56
57 return builder.show()
58 }
59
60 companion object {
61 const val TAG = "SetupWarningDialogFragment"
62
63 private const val TITLE = "Title"
64 private const val DESCRIPTION = "Description"
65 private const val HELP_LINK = "HelpLink"
66 private const val PAGE = "Page"
67
68 fun newInstance(
69 titleId: Int,
70 descriptionId: Int,
71 helpLinkId: Int,
72 page: Int
73 ): SetupWarningDialogFragment {
74 val dialog = SetupWarningDialogFragment()
75 val bundle = Bundle()
76 bundle.apply {
77 putInt(TITLE, titleId)
78 putInt(DESCRIPTION, descriptionId)
79 putInt(HELP_LINK, helpLinkId)
80 putInt(PAGE, page)
81 }
82 dialog.arguments = bundle
83 return dialog
84 }
85 }
86}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt
new file mode 100644
index 000000000..be5e4c86c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt
@@ -0,0 +1,61 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.layout
5
6import android.content.Context
7import androidx.recyclerview.widget.GridLayoutManager
8import androidx.recyclerview.widget.RecyclerView
9import androidx.recyclerview.widget.RecyclerView.Recycler
10import org.yuzu.yuzu_emu.R
11
12/**
13 * Cut down version of the solution provided here
14 * https://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
15 */
16class AutofitGridLayoutManager(
17 context: Context,
18 columnWidth: Int
19) : GridLayoutManager(context, 1) {
20 private var columnWidth = 0
21 private var isColumnWidthChanged = true
22 private var lastWidth = 0
23 private var lastHeight = 0
24
25 init {
26 setColumnWidth(checkedColumnWidth(context, columnWidth))
27 }
28
29 private fun checkedColumnWidth(context: Context, columnWidth: Int): Int {
30 var newColumnWidth = columnWidth
31 if (newColumnWidth <= 0) {
32 newColumnWidth = context.resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
33 }
34 return newColumnWidth
35 }
36
37 private fun setColumnWidth(newColumnWidth: Int) {
38 if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
39 columnWidth = newColumnWidth
40 isColumnWidthChanged = true
41 }
42 }
43
44 override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
45 val width = width
46 val height = height
47 if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
48 val totalSpace: Int = if (orientation == VERTICAL) {
49 width - paddingRight - paddingLeft
50 } else {
51 height - paddingTop - paddingBottom
52 }
53 val spanCount = 1.coerceAtLeast(totalSpace / columnWidth)
54 setSpanCount(spanCount)
55 isColumnWidthChanged = false
56 }
57 lastWidth = width
58 lastHeight = height
59 super.onLayoutChildren(recycler, state)
60 }
61}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
new file mode 100644
index 000000000..2a17653b2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -0,0 +1,41 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.os.Parcelable
7import kotlinx.parcelize.Parcelize
8import kotlinx.serialization.Serializable
9import java.util.HashSet
10
11@Parcelize
12@Serializable
13class Game(
14 val title: String,
15 val description: String,
16 val regions: String,
17 val path: String,
18 val gameId: String,
19 val company: String
20) : Parcelable {
21 val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
22 val keyLastPlayedTime get() = "${gameId}_LastPlayed"
23
24 override fun equals(other: Any?): Boolean {
25 if (other !is Game)
26 return false
27
28 return title == other.title
29 && description == other.description
30 && regions == other.regions
31 && path == other.path
32 && gameId == other.gameId
33 && company == other.company
34 }
35
36 companion object {
37 val extensions: Set<String> = HashSet(
38 listOf(".xci", ".nsp", ".nca", ".nro")
39 )
40 }
41}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
new file mode 100644
index 000000000..7059856f1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -0,0 +1,109 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.net.Uri
7import androidx.documentfile.provider.DocumentFile
8import androidx.lifecycle.LiveData
9import androidx.lifecycle.MutableLiveData
10import androidx.lifecycle.ViewModel
11import androidx.lifecycle.viewModelScope
12import androidx.preference.PreferenceManager
13import kotlinx.coroutines.Dispatchers
14import kotlinx.coroutines.launch
15import kotlinx.coroutines.withContext
16import kotlinx.serialization.decodeFromString
17import kotlinx.serialization.json.Json
18import org.yuzu.yuzu_emu.NativeLibrary
19import org.yuzu.yuzu_emu.YuzuApplication
20import org.yuzu.yuzu_emu.utils.GameHelper
21import java.util.Locale
22
23class GamesViewModel : ViewModel() {
24 private val _games = MutableLiveData<List<Game>>(emptyList())
25 val games: LiveData<List<Game>> get() = _games
26
27 private val _searchedGames = MutableLiveData<List<Game>>(emptyList())
28 val searchedGames: LiveData<List<Game>> get() = _searchedGames
29
30 private val _isReloading = MutableLiveData(false)
31 val isReloading: LiveData<Boolean> get() = _isReloading
32
33 private val _shouldSwapData = MutableLiveData(false)
34 val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData
35
36 private val _shouldScrollToTop = MutableLiveData(false)
37 val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
38
39 private val _searchFocused = MutableLiveData(false)
40 val searchFocused: LiveData<Boolean> get() = _searchFocused
41
42 init {
43 // Ensure keys are loaded so that ROM metadata can be decrypted.
44 NativeLibrary.reloadKeys()
45
46 // Retrieve list of cached games
47 val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
48 .getStringSet(GameHelper.KEY_GAMES, emptySet())
49 if (storedGames!!.isNotEmpty()) {
50 val deserializedGames = mutableSetOf<Game>()
51 storedGames.forEach {
52 val game: Game = Json.decodeFromString(it)
53 val gameExists =
54 DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(game.path))
55 ?.exists()
56 if (gameExists == true) {
57 deserializedGames.add(game)
58 }
59 }
60 setGames(deserializedGames.toList())
61 }
62 reloadGames(false)
63 }
64
65 fun setGames(games: List<Game>) {
66 val sortedList = games.sortedWith(
67 compareBy(
68 { it.title.lowercase(Locale.getDefault()) },
69 { it.path }
70 )
71 )
72
73 _games.postValue(sortedList)
74 }
75
76 fun setSearchedGames(games: List<Game>) {
77 _searchedGames.postValue(games)
78 }
79
80 fun setShouldSwapData(shouldSwap: Boolean) {
81 _shouldSwapData.postValue(shouldSwap)
82 }
83
84 fun setShouldScrollToTop(shouldScroll: Boolean) {
85 _shouldScrollToTop.postValue(shouldScroll)
86 }
87
88 fun setSearchFocused(searchFocused: Boolean) {
89 _searchFocused.postValue(searchFocused)
90 }
91
92 fun reloadGames(directoryChanged: Boolean) {
93 if (isReloading.value == true)
94 return
95 _isReloading.postValue(true)
96
97 viewModelScope.launch {
98 withContext(Dispatchers.IO) {
99 NativeLibrary.resetRomMetadata()
100 setGames(GameHelper.getGames())
101 _isReloading.postValue(false)
102
103 if (directoryChanged) {
104 setShouldSwapData(true)
105 }
106 }
107 }
108 }
109}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt
new file mode 100644
index 000000000..7049f2fa5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt
@@ -0,0 +1,11 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6data class HomeSetting(
7 val titleId: Int,
8 val descriptionId: Int,
9 val iconId: Int,
10 val onClick: () -> Unit
11)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
new file mode 100644
index 000000000..263ee7144
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -0,0 +1,36 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.ViewModel
9
10class HomeViewModel : ViewModel() {
11 private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
12 val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible
13
14 private val _statusBarShadeVisible = MutableLiveData(true)
15 val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
16
17 var navigatedToSetup = false
18
19 init {
20 _navigationVisible.value = Pair(false, false)
21 }
22
23 fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
24 if (_navigationVisible.value?.first == visible) {
25 return
26 }
27 _navigationVisible.value = Pair(visible, animated)
28 }
29
30 fun setStatusBarShadeVisibility(visible: Boolean) {
31 if (_statusBarShadeVisible.value == visible) {
32 return
33 }
34 _statusBarShadeVisible.value = visible
35 }
36}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/License.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/License.kt
new file mode 100644
index 000000000..f24d5cf34
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/License.kt
@@ -0,0 +1,16 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.os.Parcelable
7import kotlinx.parcelize.Parcelize
8
9@Parcelize
10data class License(
11 val titleId: Int,
12 val descriptionId: Int,
13 val linkId: Int,
14 val copyrightId: Int,
15 val licenseId: Int
16) : Parcelable
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.kt
new file mode 100644
index 000000000..b4b78e42d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.kt
@@ -0,0 +1,11 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import android.net.Uri
7import android.provider.DocumentsContract
8
9class MinimalDocumentFile(val filename: String, mimeType: String, val uri: Uri) {
10 val isDirectory: Boolean = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
11}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
new file mode 100644
index 000000000..a0c878e1c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
@@ -0,0 +1,19 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6data class SetupPage(
7 val iconId: Int,
8 val titleId: Int,
9 val descriptionId: Int,
10 val buttonIconId: Int,
11 val leftAlignedIcon: Boolean,
12 val buttonTextId: Int,
13 val buttonAction: () -> Unit,
14 val hasWarning: Boolean,
15 val warningTitleId: Int = 0,
16 val warningDescriptionId: Int = 0,
17 val warningHelpLinkId: Int = 0,
18 val taskCompleted: () -> Boolean = { true }
19)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
new file mode 100644
index 000000000..27ea725a5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -0,0 +1,47 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.ViewModel
9import androidx.lifecycle.viewModelScope
10import kotlinx.coroutines.Dispatchers
11import kotlinx.coroutines.launch
12
13class TaskViewModel : ViewModel() {
14 private val _result = MutableLiveData<Any>()
15 val result: LiveData<Any> = _result
16
17 private val _isComplete = MutableLiveData<Boolean>()
18 val isComplete: LiveData<Boolean> = _isComplete
19
20 private val _isRunning = MutableLiveData<Boolean>()
21 val isRunning: LiveData<Boolean> = _isRunning
22
23 lateinit var task: () -> Any
24
25 init {
26 clear()
27 }
28
29 fun clear() {
30 _result.value = Any()
31 _isComplete.value = false
32 _isRunning.value = false
33 }
34
35 fun runTask() {
36 if (_isRunning.value == true) {
37 return
38 }
39 _isRunning.value = true
40
41 viewModelScope.launch(Dispatchers.IO) {
42 val res = task()
43 _result.postValue(res)
44 _isComplete.postValue(true)
45 }
46 }
47}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
new file mode 100644
index 000000000..c9f5797ac
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -0,0 +1,1064 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.overlay
5
6import android.app.Activity
7import android.content.Context
8import android.content.SharedPreferences
9import android.content.res.Configuration
10import android.graphics.Bitmap
11import android.graphics.Canvas
12import android.graphics.Point
13import android.graphics.Rect
14import android.graphics.drawable.Drawable
15import android.graphics.drawable.VectorDrawable
16import android.os.Build
17import android.util.AttributeSet
18import android.view.HapticFeedbackConstants
19import android.view.MotionEvent
20import android.view.SurfaceView
21import android.view.View
22import android.view.View.OnTouchListener
23import android.view.WindowInsets
24import androidx.core.content.ContextCompat
25import androidx.preference.PreferenceManager
26import androidx.window.layout.WindowMetricsCalculator
27import org.yuzu.yuzu_emu.NativeLibrary
28import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
29import org.yuzu.yuzu_emu.NativeLibrary.StickType
30import org.yuzu.yuzu_emu.R
31import org.yuzu.yuzu_emu.YuzuApplication
32import org.yuzu.yuzu_emu.features.settings.model.Settings
33import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
34import kotlin.math.max
35import kotlin.math.min
36
37/**
38 * Draws the interactive input overlay on top of the
39 * [SurfaceView] that is rendering emulation.
40 */
41class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs),
42 OnTouchListener {
43 private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
44 private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
45 private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
46
47 private var inEditMode = false
48 private var buttonBeingConfigured: InputOverlayDrawableButton? = null
49 private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
50 private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
51
52 private lateinit var windowInsets: WindowInsets
53
54 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
55 super.onLayout(changed, left, top, right, bottom)
56
57 windowInsets = rootWindowInsets
58
59 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
60 defaultOverlay()
61 }
62
63 // Load the controls.
64 refreshControls()
65
66 // Set the on touch listener.
67 setOnTouchListener(this)
68
69 // Force draw
70 setWillNotDraw(false)
71
72 // Request focus for the overlay so it has priority on presses.
73 requestFocus()
74 }
75
76 override fun draw(canvas: Canvas) {
77 super.draw(canvas)
78 for (button in overlayButtons) {
79 button.draw(canvas)
80 }
81 for (dpad in overlayDpads) {
82 dpad.draw(canvas)
83 }
84 for (joystick in overlayJoysticks) {
85 joystick.draw(canvas)
86 }
87 }
88
89 override fun onTouch(v: View, event: MotionEvent): Boolean {
90 if (inEditMode) {
91 return onTouchWhileEditing(event)
92 }
93
94 var shouldUpdateView = false
95 val playerIndex =
96 if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
97
98 for (button in overlayButtons) {
99 if (!button.updateStatus(event)) {
100 continue
101 }
102 NativeLibrary.onGamePadButtonEvent(
103 playerIndex,
104 button.buttonId,
105 button.status
106 )
107 playHaptics(event)
108 shouldUpdateView = true
109 }
110
111 for (dpad in overlayDpads) {
112 if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) {
113 continue
114 }
115 NativeLibrary.onGamePadButtonEvent(
116 playerIndex,
117 dpad.upId,
118 dpad.upStatus
119 )
120 NativeLibrary.onGamePadButtonEvent(
121 playerIndex,
122 dpad.downId,
123 dpad.downStatus
124 )
125 NativeLibrary.onGamePadButtonEvent(
126 playerIndex,
127 dpad.leftId,
128 dpad.leftStatus
129 )
130 NativeLibrary.onGamePadButtonEvent(
131 playerIndex,
132 dpad.rightId,
133 dpad.rightStatus
134 )
135 playHaptics(event)
136 shouldUpdateView = true
137 }
138
139 for (joystick in overlayJoysticks) {
140 if (!joystick.updateStatus(event)) {
141 continue
142 }
143 val axisID = joystick.joystickId
144 NativeLibrary.onGamePadJoystickEvent(
145 playerIndex,
146 axisID,
147 joystick.xAxis,
148 joystick.realYAxis
149 )
150 NativeLibrary.onGamePadButtonEvent(
151 playerIndex,
152 joystick.buttonId,
153 joystick.buttonStatus
154 )
155 playHaptics(event)
156 shouldUpdateView = true
157 }
158
159 if (shouldUpdateView)
160 invalidate()
161
162 if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) {
163 return true
164 }
165
166 val pointerIndex = event.actionIndex
167 val xPosition = event.getX(pointerIndex).toInt()
168 val yPosition = event.getY(pointerIndex).toInt()
169 val pointerId = event.getPointerId(pointerIndex)
170 val motionEvent = event.action and MotionEvent.ACTION_MASK
171 val isActionDown =
172 motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
173 val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
174 val isActionUp =
175 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
176
177 if (isActionDown && !isTouchInputConsumed(pointerId)) {
178 NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
179 }
180
181 if (isActionMove) {
182 for (i in 0 until event.pointerCount) {
183 val fingerId = event.getPointerId(i)
184 if (isTouchInputConsumed(fingerId)) {
185 continue
186 }
187 NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i))
188 }
189 }
190
191 if (isActionUp && !isTouchInputConsumed(pointerId)) {
192 NativeLibrary.onTouchReleased(pointerId)
193 }
194
195 return true
196 }
197
198 private fun playHaptics(event: MotionEvent) {
199 if (EmulationMenuSettings.hapticFeedback) {
200 when (event.actionMasked) {
201 MotionEvent.ACTION_DOWN,
202 MotionEvent.ACTION_POINTER_DOWN ->
203 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
204
205 MotionEvent.ACTION_UP,
206 MotionEvent.ACTION_POINTER_UP ->
207 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
208 }
209 }
210 }
211
212 private fun isTouchInputConsumed(track_id: Int): Boolean {
213 for (button in overlayButtons) {
214 if (button.trackId == track_id) {
215 return true
216 }
217 }
218 for (dpad in overlayDpads) {
219 if (dpad.trackId == track_id) {
220 return true
221 }
222 }
223 for (joystick in overlayJoysticks) {
224 if (joystick.trackId == track_id) {
225 return true
226 }
227 }
228 return false
229 }
230
231 private fun onTouchWhileEditing(event: MotionEvent): Boolean {
232 val pointerIndex = event.actionIndex
233 val fingerPositionX = event.getX(pointerIndex).toInt()
234 val fingerPositionY = event.getY(pointerIndex).toInt()
235
236 // TODO: Provide support for portrait layout
237 //val orientation =
238 // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
239
240 for (button in overlayButtons) {
241 // Determine the button state to apply based on the MotionEvent action flag.
242 when (event.action and MotionEvent.ACTION_MASK) {
243 MotionEvent.ACTION_DOWN,
244 MotionEvent.ACTION_POINTER_DOWN ->
245 // If no button is being moved now, remember the currently touched button to move.
246 if (buttonBeingConfigured == null &&
247 button.bounds.contains(
248 fingerPositionX,
249 fingerPositionY
250 )
251 ) {
252 buttonBeingConfigured = button
253 buttonBeingConfigured!!.onConfigureTouch(event)
254 }
255
256 MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
257 buttonBeingConfigured!!.onConfigureTouch(event)
258 invalidate()
259 return true
260 }
261
262 MotionEvent.ACTION_UP,
263 MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
264 // Persist button position by saving new place.
265 saveControlPosition(
266 buttonBeingConfigured!!.buttonId,
267 buttonBeingConfigured!!.bounds.centerX(),
268 buttonBeingConfigured!!.bounds.centerY(),
269 ""
270 )
271 buttonBeingConfigured = null
272 }
273 }
274 }
275
276 for (dpad in overlayDpads) {
277 // Determine the button state to apply based on the MotionEvent action flag.
278 when (event.action and MotionEvent.ACTION_MASK) {
279 MotionEvent.ACTION_DOWN,
280 MotionEvent.ACTION_POINTER_DOWN ->
281 // If no button is being moved now, remember the currently touched button to move.
282 if (buttonBeingConfigured == null &&
283 dpad.bounds.contains(fingerPositionX, fingerPositionY)
284 ) {
285 dpadBeingConfigured = dpad
286 dpadBeingConfigured!!.onConfigureTouch(event)
287 }
288
289 MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
290 dpadBeingConfigured!!.onConfigureTouch(event)
291 invalidate()
292 return true
293 }
294
295 MotionEvent.ACTION_UP,
296 MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
297 // Persist button position by saving new place.
298 saveControlPosition(
299 dpadBeingConfigured!!.upId,
300 dpadBeingConfigured!!.bounds.centerX(),
301 dpadBeingConfigured!!.bounds.centerY(),
302 ""
303 )
304 dpadBeingConfigured = null
305 }
306 }
307 }
308
309 for (joystick in overlayJoysticks) {
310 when (event.action) {
311 MotionEvent.ACTION_DOWN,
312 MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
313 joystick.bounds.contains(
314 fingerPositionX,
315 fingerPositionY
316 )
317 ) {
318 joystickBeingConfigured = joystick
319 joystickBeingConfigured!!.onConfigureTouch(event)
320 }
321
322 MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
323 joystickBeingConfigured!!.onConfigureTouch(event)
324 invalidate()
325 }
326
327 MotionEvent.ACTION_UP,
328 MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
329 saveControlPosition(
330 joystickBeingConfigured!!.buttonId,
331 joystickBeingConfigured!!.bounds.centerX(),
332 joystickBeingConfigured!!.bounds.centerY(),
333 ""
334 )
335 joystickBeingConfigured = null
336 }
337 }
338 }
339
340 return true
341 }
342
343 private fun addOverlayControls(orientation: String) {
344 val windowSize = getSafeScreenSize(context)
345 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
346 overlayButtons.add(
347 initializeOverlayButton(
348 context,
349 windowSize,
350 R.drawable.facebutton_a,
351 R.drawable.facebutton_a_depressed,
352 ButtonType.BUTTON_A,
353 orientation
354 )
355 )
356 }
357 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) {
358 overlayButtons.add(
359 initializeOverlayButton(
360 context,
361 windowSize,
362 R.drawable.facebutton_b,
363 R.drawable.facebutton_b_depressed,
364 ButtonType.BUTTON_B,
365 orientation
366 )
367 )
368 }
369 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) {
370 overlayButtons.add(
371 initializeOverlayButton(
372 context,
373 windowSize,
374 R.drawable.facebutton_x,
375 R.drawable.facebutton_x_depressed,
376 ButtonType.BUTTON_X,
377 orientation
378 )
379 )
380 }
381 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) {
382 overlayButtons.add(
383 initializeOverlayButton(
384 context,
385 windowSize,
386 R.drawable.facebutton_y,
387 R.drawable.facebutton_y_depressed,
388 ButtonType.BUTTON_Y,
389 orientation
390 )
391 )
392 }
393 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) {
394 overlayButtons.add(
395 initializeOverlayButton(
396 context,
397 windowSize,
398 R.drawable.l_shoulder,
399 R.drawable.l_shoulder_depressed,
400 ButtonType.TRIGGER_L,
401 orientation
402 )
403 )
404 }
405 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) {
406 overlayButtons.add(
407 initializeOverlayButton(
408 context,
409 windowSize,
410 R.drawable.r_shoulder,
411 R.drawable.r_shoulder_depressed,
412 ButtonType.TRIGGER_R,
413 orientation
414 )
415 )
416 }
417 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) {
418 overlayButtons.add(
419 initializeOverlayButton(
420 context,
421 windowSize,
422 R.drawable.zl_trigger,
423 R.drawable.zl_trigger_depressed,
424 ButtonType.TRIGGER_ZL,
425 orientation
426 )
427 )
428 }
429 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) {
430 overlayButtons.add(
431 initializeOverlayButton(
432 context,
433 windowSize,
434 R.drawable.zr_trigger,
435 R.drawable.zr_trigger_depressed,
436 ButtonType.TRIGGER_ZR,
437 orientation
438 )
439 )
440 }
441 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) {
442 overlayButtons.add(
443 initializeOverlayButton(
444 context,
445 windowSize,
446 R.drawable.facebutton_plus,
447 R.drawable.facebutton_plus_depressed,
448 ButtonType.BUTTON_PLUS,
449 orientation
450 )
451 )
452 }
453 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) {
454 overlayButtons.add(
455 initializeOverlayButton(
456 context,
457 windowSize,
458 R.drawable.facebutton_minus,
459 R.drawable.facebutton_minus_depressed,
460 ButtonType.BUTTON_MINUS,
461 orientation
462 )
463 )
464 }
465 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) {
466 overlayDpads.add(
467 initializeOverlayDpad(
468 context,
469 windowSize,
470 R.drawable.dpad_standard,
471 R.drawable.dpad_standard_cardinal_depressed,
472 R.drawable.dpad_standard_diagonal_depressed,
473 orientation
474 )
475 )
476 }
477 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) {
478 overlayJoysticks.add(
479 initializeOverlayJoystick(
480 context,
481 windowSize,
482 R.drawable.joystick_range,
483 R.drawable.joystick,
484 R.drawable.joystick_depressed,
485 StickType.STICK_L,
486 ButtonType.STICK_L,
487 orientation
488 )
489 )
490 }
491 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) {
492 overlayJoysticks.add(
493 initializeOverlayJoystick(
494 context,
495 windowSize,
496 R.drawable.joystick_range,
497 R.drawable.joystick,
498 R.drawable.joystick_depressed,
499 StickType.STICK_R,
500 ButtonType.STICK_R,
501 orientation
502 )
503 )
504 }
505 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) {
506 overlayButtons.add(
507 initializeOverlayButton(
508 context,
509 windowSize,
510 R.drawable.facebutton_home,
511 R.drawable.facebutton_home_depressed,
512 ButtonType.BUTTON_HOME,
513 orientation
514 )
515 )
516 }
517 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) {
518 overlayButtons.add(
519 initializeOverlayButton(
520 context,
521 windowSize,
522 R.drawable.facebutton_screenshot,
523 R.drawable.facebutton_screenshot_depressed,
524 ButtonType.BUTTON_CAPTURE,
525 orientation
526 )
527 )
528 }
529 }
530
531 fun refreshControls() {
532 // Remove all the overlay buttons from the HashSet.
533 overlayButtons.clear()
534 overlayDpads.clear()
535 overlayJoysticks.clear()
536 val orientation =
537 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
538
539 // Add all the enabled overlay items back to the HashSet.
540 if (EmulationMenuSettings.showOverlay) {
541 addOverlayControls(orientation)
542 }
543 invalidate()
544 }
545
546 private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
547 val windowSize = getSafeScreenSize(context)
548 val min = windowSize.first
549 val max = windowSize.second
550 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
551 .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
552 .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
553 .apply()
554 }
555
556 fun setIsInEditMode(editMode: Boolean) {
557 inEditMode = editMode
558 }
559
560 private fun defaultOverlay() {
561 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
562 defaultOverlayLandscape()
563 }
564
565 resetButtonPlacement()
566 preferences.edit()
567 .putBoolean(Settings.PREF_OVERLAY_INIT, true)
568 .apply()
569 }
570
571 fun resetButtonPlacement() {
572 defaultOverlayLandscape()
573 refreshControls()
574 }
575
576 private fun defaultOverlayLandscape() {
577 // Each value represents the position of the button in relation to the screen size without insets.
578 preferences.edit()
579 .putFloat(
580 ButtonType.BUTTON_A.toString() + "-X",
581 resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
582 )
583 .putFloat(
584 ButtonType.BUTTON_A.toString() + "-Y",
585 resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
586 )
587 .putFloat(
588 ButtonType.BUTTON_B.toString() + "-X",
589 resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
590 )
591 .putFloat(
592 ButtonType.BUTTON_B.toString() + "-Y",
593 resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
594 )
595 .putFloat(
596 ButtonType.BUTTON_X.toString() + "-X",
597 resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
598 )
599 .putFloat(
600 ButtonType.BUTTON_X.toString() + "-Y",
601 resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
602 )
603 .putFloat(
604 ButtonType.BUTTON_Y.toString() + "-X",
605 resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
606 )
607 .putFloat(
608 ButtonType.BUTTON_Y.toString() + "-Y",
609 resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
610 )
611 .putFloat(
612 ButtonType.TRIGGER_ZL.toString() + "-X",
613 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
614 )
615 .putFloat(
616 ButtonType.TRIGGER_ZL.toString() + "-Y",
617 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
618 )
619 .putFloat(
620 ButtonType.TRIGGER_ZR.toString() + "-X",
621 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
622 )
623 .putFloat(
624 ButtonType.TRIGGER_ZR.toString() + "-Y",
625 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
626 )
627 .putFloat(
628 ButtonType.DPAD_UP.toString() + "-X",
629 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
630 )
631 .putFloat(
632 ButtonType.DPAD_UP.toString() + "-Y",
633 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
634 )
635 .putFloat(
636 ButtonType.TRIGGER_L.toString() + "-X",
637 resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
638 )
639 .putFloat(
640 ButtonType.TRIGGER_L.toString() + "-Y",
641 resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
642 )
643 .putFloat(
644 ButtonType.TRIGGER_R.toString() + "-X",
645 resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
646 )
647 .putFloat(
648 ButtonType.TRIGGER_R.toString() + "-Y",
649 resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
650 )
651 .putFloat(
652 ButtonType.BUTTON_PLUS.toString() + "-X",
653 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
654 )
655 .putFloat(
656 ButtonType.BUTTON_PLUS.toString() + "-Y",
657 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
658 )
659 .putFloat(
660 ButtonType.BUTTON_MINUS.toString() + "-X",
661 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
662 )
663 .putFloat(
664 ButtonType.BUTTON_MINUS.toString() + "-Y",
665 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
666 )
667 .putFloat(
668 ButtonType.BUTTON_HOME.toString() + "-X",
669 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
670 )
671 .putFloat(
672 ButtonType.BUTTON_HOME.toString() + "-Y",
673 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
674 )
675 .putFloat(
676 ButtonType.BUTTON_CAPTURE.toString() + "-X",
677 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
678 .toFloat() / 1000
679 )
680 .putFloat(
681 ButtonType.BUTTON_CAPTURE.toString() + "-Y",
682 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
683 .toFloat() / 1000
684 )
685 .putFloat(
686 ButtonType.STICK_R.toString() + "-X",
687 resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
688 )
689 .putFloat(
690 ButtonType.STICK_R.toString() + "-Y",
691 resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
692 )
693 .putFloat(
694 ButtonType.STICK_L.toString() + "-X",
695 resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
696 )
697 .putFloat(
698 ButtonType.STICK_L.toString() + "-Y",
699 resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
700 )
701 .apply()
702 }
703
704 override fun isInEditMode(): Boolean {
705 return inEditMode
706 }
707
708 companion object {
709 private val preferences: SharedPreferences =
710 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
711
712 /**
713 * Resizes a [Bitmap] by a given scale factor
714 *
715 * @param context Context for getting the vector drawable
716 * @param drawableId The ID of the drawable to scale.
717 * @param scale The scale factor for the bitmap.
718 * @return The scaled [Bitmap]
719 */
720 private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
721 val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
722
723 val bitmap = Bitmap.createBitmap(
724 (vectorDrawable.intrinsicWidth * scale).toInt(),
725 (vectorDrawable.intrinsicHeight * scale).toInt(),
726 Bitmap.Config.ARGB_8888
727 )
728
729 val dm = context.resources.displayMetrics
730 val minScreenDimension = min(dm.widthPixels, dm.heightPixels)
731
732 val maxBitmapDimension = max(bitmap.width, bitmap.height)
733 val bitmapScale = scale * minScreenDimension / maxBitmapDimension
734
735 val scaledBitmap = Bitmap.createScaledBitmap(
736 bitmap,
737 (bitmap.width * bitmapScale).toInt(),
738 (bitmap.height * bitmapScale).toInt(),
739 true
740 )
741
742 val canvas = Canvas(scaledBitmap)
743 vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
744 vectorDrawable.draw(canvas)
745 return scaledBitmap
746 }
747
748 /**
749 * Gets the safe screen size for drawing the overlay
750 *
751 * @param context Context for getting the window metrics
752 * @return A pair of points, the first being the top left corner of the safe area,
753 * the second being the bottom right corner of the safe area
754 */
755 private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
756 // Get screen size
757 val windowMetrics =
758 WindowMetricsCalculator.getOrCreate()
759 .computeCurrentWindowMetrics(context as Activity)
760 var maxY = windowMetrics.bounds.height().toFloat()
761 var maxX = windowMetrics.bounds.width().toFloat()
762 var minY = 0
763 var minX = 0
764
765 // If we have API access, calculate the safe area to draw the overlay
766 var cutoutLeft = 0
767 var cutoutBottom = 0
768 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
769 if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
770 if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
771 insets.boundingRectTop.bottom.toFloat() else maxY
772 if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
773 insets.boundingRectRight.left.toFloat() else maxX
774
775 minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
776 minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
777
778 cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
779 cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
780 }
781
782 // This makes sure that if we have an inset on one side of the screen, we mirror it on
783 // the other side. Since removing space from one of the max values messes with the scale,
784 // we also have to account for it using our min values.
785 if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
786 if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
787 if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
788 maxX -= (minX * 2)
789 } else if (minX > 0) {
790 maxX -= minX
791 }
792 if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
793 maxY -= (minY * 2)
794 } else if (minY > 0) {
795 maxY -= minY
796 }
797
798 return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
799 }
800
801 /**
802 * Initializes an InputOverlayDrawableButton, given by resId, with all of the
803 * parameters set for it to be properly shown on the InputOverlay.
804 *
805 *
806 * This works due to the way the X and Y coordinates are stored within
807 * the [SharedPreferences].
808 *
809 *
810 * In the input overlay configuration menu,
811 * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
812 * the X and Y coordinates of the button at the END of its touch event
813 * (when you remove your finger/stylus from the touchscreen) are then stored
814 * within a SharedPreferences instance so that those values can be retrieved here.
815 *
816 *
817 * This has a few benefits over the conventional way of storing the values
818 * (ie. within the yuzu ini file).
819 *
820 * * No native calls
821 * * Keeps Android-only values inside the Android environment
822 *
823 *
824 *
825 * Technically no modifications should need to be performed on the returned
826 * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
827 * for Android to call the onDraw method.
828 *
829 * @param context The current [Context].
830 * @param windowSize The size of the window to draw the overlay on.
831 * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
832 * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
833 * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
834 * @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
835 */
836 private fun initializeOverlayButton(
837 context: Context,
838 windowSize: Pair<Point, Point>,
839 defaultResId: Int,
840 pressedResId: Int,
841 buttonId: Int,
842 orientation: String
843 ): InputOverlayDrawableButton {
844 // Resources handle for fetching the initial Drawable resource.
845 val res = context.resources
846
847 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
848 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
849
850 // Decide scale based on button ID and user preference
851 var scale: Float = when (buttonId) {
852 ButtonType.BUTTON_HOME,
853 ButtonType.BUTTON_CAPTURE,
854 ButtonType.BUTTON_PLUS,
855 ButtonType.BUTTON_MINUS -> 0.07f
856
857 ButtonType.TRIGGER_L,
858 ButtonType.TRIGGER_R,
859 ButtonType.TRIGGER_ZL,
860 ButtonType.TRIGGER_ZR -> 0.26f
861
862 else -> 0.11f
863 }
864 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
865 scale /= 100f
866
867 // Initialize the InputOverlayDrawableButton.
868 val defaultStateBitmap = getBitmap(context, defaultResId, scale)
869 val pressedStateBitmap = getBitmap(context, pressedResId, scale)
870 val overlayDrawable =
871 InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
872
873 // Get the minimum and maximum coordinates of the screen where the button can be placed.
874 val min = windowSize.first
875 val max = windowSize.second
876
877 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
878 // These were set in the input overlay configuration menu.
879 val xKey = "$buttonId$orientation-X"
880 val yKey = "$buttonId$orientation-Y"
881 val drawableXPercent = sPrefs.getFloat(xKey, 0f)
882 val drawableYPercent = sPrefs.getFloat(yKey, 0f)
883 val drawableX = (drawableXPercent * max.x + min.x).toInt()
884 val drawableY = (drawableYPercent * max.y + min.y).toInt()
885 val width = overlayDrawable.width
886 val height = overlayDrawable.height
887
888 // Now set the bounds for the InputOverlayDrawableButton.
889 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
890 overlayDrawable.setBounds(
891 drawableX - (width / 2),
892 drawableY - (height / 2),
893 drawableX + (width / 2),
894 drawableY + (height / 2)
895 )
896
897 // Need to set the image's position
898 overlayDrawable.setPosition(
899 drawableX - (width / 2),
900 drawableY - (height / 2)
901 )
902 val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
903 overlayDrawable.setOpacity(savedOpacity * 255 / 100)
904 return overlayDrawable
905 }
906
907 /**
908 * Initializes an [InputOverlayDrawableDpad]
909 *
910 * @param context The current [Context].
911 * @param windowSize The size of the window to draw the overlay on.
912 * @param defaultResId The [Bitmap] resource ID of the default state.
913 * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
914 * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
915 * @return the initialized [InputOverlayDrawableDpad]
916 */
917 private fun initializeOverlayDpad(
918 context: Context,
919 windowSize: Pair<Point, Point>,
920 defaultResId: Int,
921 pressedOneDirectionResId: Int,
922 pressedTwoDirectionsResId: Int,
923 orientation: String
924 ): InputOverlayDrawableDpad {
925 // Resources handle for fetching the initial Drawable resource.
926 val res = context.resources
927
928 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
929 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
930
931 // Decide scale based on button ID and user preference
932 var scale = 0.25f
933 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
934 scale /= 100f
935
936 // Initialize the InputOverlayDrawableDpad.
937 val defaultStateBitmap =
938 getBitmap(context, defaultResId, scale)
939 val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
940 val pressedTwoDirectionsStateBitmap =
941 getBitmap(context, pressedTwoDirectionsResId, scale)
942
943 val overlayDrawable = InputOverlayDrawableDpad(
944 res,
945 defaultStateBitmap,
946 pressedOneDirectionStateBitmap,
947 pressedTwoDirectionsStateBitmap,
948 ButtonType.DPAD_UP,
949 ButtonType.DPAD_DOWN,
950 ButtonType.DPAD_LEFT,
951 ButtonType.DPAD_RIGHT
952 )
953
954 // Get the minimum and maximum coordinates of the screen where the button can be placed.
955 val min = windowSize.first
956 val max = windowSize.second
957
958 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
959 // These were set in the input overlay configuration menu.
960 val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f)
961 val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
962 val drawableX = (drawableXPercent * max.x + min.x).toInt()
963 val drawableY = (drawableYPercent * max.y + min.y).toInt()
964 val width = overlayDrawable.width
965 val height = overlayDrawable.height
966
967 // Now set the bounds for the InputOverlayDrawableDpad.
968 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
969 overlayDrawable.setBounds(
970 drawableX - (width / 2),
971 drawableY - (height / 2),
972 drawableX + (width / 2),
973 drawableY + (height / 2)
974 )
975
976 // Need to set the image's position
977 overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
978 val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
979 overlayDrawable.setOpacity(savedOpacity * 255 / 100)
980 return overlayDrawable
981 }
982
983 /**
984 * Initializes an [InputOverlayDrawableJoystick]
985 *
986 * @param context The current [Context]
987 * @param windowSize The size of the window to draw the overlay on.
988 * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
989 * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
990 * @param pressedResInner Resource ID for the pressed inner image of the joystick.
991 * @param joystick Identifier for which joystick this is.
992 * @param button Identifier for which joystick button this is.
993 * @return the initialized [InputOverlayDrawableJoystick].
994 */
995 private fun initializeOverlayJoystick(
996 context: Context,
997 windowSize: Pair<Point, Point>,
998 resOuter: Int,
999 defaultResInner: Int,
1000 pressedResInner: Int,
1001 joystick: Int,
1002 button: Int,
1003 orientation: String
1004 ): InputOverlayDrawableJoystick {
1005 // Resources handle for fetching the initial Drawable resource.
1006 val res = context.resources
1007
1008 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
1009 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
1010
1011 // Decide scale based on user preference
1012 var scale = 0.3f
1013 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
1014 scale /= 100f
1015
1016 // Initialize the InputOverlayDrawableJoystick.
1017 val bitmapOuter = getBitmap(context, resOuter, scale)
1018 val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
1019 val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
1020
1021 // Get the minimum and maximum coordinates of the screen where the button can be placed.
1022 val min = windowSize.first
1023 val max = windowSize.second
1024
1025 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
1026 // These were set in the input overlay configuration menu.
1027 val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f)
1028 val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
1029 val drawableX = (drawableXPercent * max.x + min.x).toInt()
1030 val drawableY = (drawableYPercent * max.y + min.y).toInt()
1031 val outerScale = 1.66f
1032
1033 // Now set the bounds for the InputOverlayDrawableJoystick.
1034 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
1035 val outerSize = bitmapOuter.width
1036 val outerRect = Rect(
1037 drawableX - (outerSize / 2),
1038 drawableY - (outerSize / 2),
1039 drawableX + (outerSize / 2),
1040 drawableY + (outerSize / 2)
1041 )
1042 val innerRect =
1043 Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
1044
1045 // Send the drawableId to the joystick so it can be referenced when saving control position.
1046 val overlayDrawable = InputOverlayDrawableJoystick(
1047 res,
1048 bitmapOuter,
1049 bitmapInnerDefault,
1050 bitmapInnerPressed,
1051 outerRect,
1052 innerRect,
1053 joystick,
1054 button
1055 )
1056
1057 // Need to set the image's position
1058 overlayDrawable.setPosition(drawableX, drawableY)
1059 val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
1060 overlayDrawable.setOpacity(savedOpacity * 255 / 100)
1061 return overlayDrawable
1062 }
1063 }
1064}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
new file mode 100644
index 000000000..4a93e0b14
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
@@ -0,0 +1,148 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.overlay
5
6import android.content.res.Resources
7import android.graphics.Bitmap
8import android.graphics.Canvas
9import android.graphics.Rect
10import android.graphics.drawable.BitmapDrawable
11import android.view.MotionEvent
12import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
13
14/**
15 * Custom [BitmapDrawable] that is capable
16 * of storing it's own ID.
17 *
18 * @param res [Resources] instance.
19 * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
20 * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
21 * @param buttonId Identifier for this type of button.
22 */
23class InputOverlayDrawableButton(
24 res: Resources,
25 defaultStateBitmap: Bitmap,
26 pressedStateBitmap: Bitmap,
27 val buttonId: Int
28) {
29 // The ID value what motion event is tracking
30 var trackId: Int
31
32 // The drawable position on the screen
33 private var buttonPositionX = 0
34 private var buttonPositionY = 0
35
36 val width: Int
37 val height: Int
38
39 private val defaultStateBitmap: BitmapDrawable
40 private val pressedStateBitmap: BitmapDrawable
41 private var pressedState = false
42
43 private var previousTouchX = 0
44 private var previousTouchY = 0
45 var controlPositionX = 0
46 var controlPositionY = 0
47
48 init {
49 this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
50 this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap)
51 trackId = -1
52 width = this.defaultStateBitmap.intrinsicWidth
53 height = this.defaultStateBitmap.intrinsicHeight
54 }
55
56 /**
57 * Updates button status based on the motion event.
58 *
59 * @return true if value was changed
60 */
61 fun updateStatus(event: MotionEvent): Boolean {
62 val pointerIndex = event.actionIndex
63 val xPosition = event.getX(pointerIndex).toInt()
64 val yPosition = event.getY(pointerIndex).toInt()
65 val pointerId = event.getPointerId(pointerIndex)
66 val motionEvent = event.action and MotionEvent.ACTION_MASK
67 val isActionDown =
68 motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
69 val isActionUp =
70 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
71
72 if (isActionDown) {
73 if (!bounds.contains(xPosition, yPosition)) {
74 return false
75 }
76 pressedState = true
77 trackId = pointerId
78 return true
79 }
80
81 if (isActionUp) {
82 if (trackId != pointerId) {
83 return false
84 }
85 pressedState = false
86 trackId = -1
87 return true
88 }
89
90 return false
91 }
92
93 fun setPosition(x: Int, y: Int) {
94 buttonPositionX = x
95 buttonPositionY = y
96 }
97
98 fun draw(canvas: Canvas?) {
99 currentStateBitmapDrawable.draw(canvas!!)
100 }
101
102 private val currentStateBitmapDrawable: BitmapDrawable
103 get() = if (pressedState) pressedStateBitmap else defaultStateBitmap
104
105 fun onConfigureTouch(event: MotionEvent): Boolean {
106 val pointerIndex = event.actionIndex
107 val fingerPositionX = event.getX(pointerIndex).toInt()
108 val fingerPositionY = event.getY(pointerIndex).toInt()
109
110 when (event.action) {
111 MotionEvent.ACTION_DOWN -> {
112 previousTouchX = fingerPositionX
113 previousTouchY = fingerPositionY
114 controlPositionX = fingerPositionX - (width / 2)
115 controlPositionY = fingerPositionY - (height / 2)
116 }
117
118 MotionEvent.ACTION_MOVE -> {
119 controlPositionX += fingerPositionX - previousTouchX
120 controlPositionY += fingerPositionY - previousTouchY
121 setBounds(
122 controlPositionX,
123 controlPositionY,
124 width + controlPositionX,
125 height + controlPositionY
126 )
127 previousTouchX = fingerPositionX
128 previousTouchY = fingerPositionY
129 }
130 }
131 return true
132 }
133
134 fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
135 defaultStateBitmap.setBounds(left, top, right, bottom)
136 pressedStateBitmap.setBounds(left, top, right, bottom)
137 }
138
139 fun setOpacity(value: Int) {
140 defaultStateBitmap.alpha = value
141 pressedStateBitmap.alpha = value
142 }
143
144 val status: Int
145 get() = if (pressedState) ButtonState.PRESSED else ButtonState.RELEASED
146 val bounds: Rect
147 get() = defaultStateBitmap.bounds
148}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
new file mode 100644
index 000000000..43d664d21
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
@@ -0,0 +1,274 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.overlay
5
6import android.content.res.Resources
7import android.graphics.Bitmap
8import android.graphics.Canvas
9import android.graphics.Rect
10import android.graphics.drawable.BitmapDrawable
11import android.view.MotionEvent
12import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
13
14/**
15 * Custom [BitmapDrawable] that is capable
16 * of storing it's own ID.
17 *
18 * @param res [Resources] instance.
19 * @param defaultStateBitmap [Bitmap] of the default state.
20 * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction.
21 * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
22 * @param buttonUp Identifier for the up button.
23 * @param buttonDown Identifier for the down button.
24 * @param buttonLeft Identifier for the left button.
25 * @param buttonRight Identifier for the right button.
26 */
27class InputOverlayDrawableDpad(
28 res: Resources,
29 defaultStateBitmap: Bitmap,
30 pressedOneDirectionStateBitmap: Bitmap,
31 pressedTwoDirectionsStateBitmap: Bitmap,
32 buttonUp: Int,
33 buttonDown: Int,
34 buttonLeft: Int,
35 buttonRight: Int
36) {
37 /**
38 * Gets one of the InputOverlayDrawableDpad's button IDs.
39 *
40 * @return the requested InputOverlayDrawableDpad's button ID.
41 */
42 // The ID identifying what type of button this Drawable represents.
43 val upId: Int
44 val downId: Int
45 val leftId: Int
46 val rightId: Int
47 var trackId: Int
48
49 val width: Int
50 val height: Int
51
52 private val defaultStateBitmap: BitmapDrawable
53 private val pressedOneDirectionStateBitmap: BitmapDrawable
54 private val pressedTwoDirectionsStateBitmap: BitmapDrawable
55
56 private var previousTouchX = 0
57 private var previousTouchY = 0
58 private var controlPositionX = 0
59 private var controlPositionY = 0
60
61 private var upButtonState = false
62 private var downButtonState = false
63 private var leftButtonState = false
64 private var rightButtonState = false
65
66 init {
67 this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
68 this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap)
69 this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
70 width = this.defaultStateBitmap.intrinsicWidth
71 height = this.defaultStateBitmap.intrinsicHeight
72 upId = buttonUp
73 downId = buttonDown
74 leftId = buttonLeft
75 rightId = buttonRight
76 trackId = -1
77 }
78
79 fun updateStatus(event: MotionEvent, dpad_slide: Boolean): Boolean {
80 val pointerIndex = event.actionIndex
81 val xPosition = event.getX(pointerIndex).toInt()
82 val yPosition = event.getY(pointerIndex).toInt()
83 val pointerId = event.getPointerId(pointerIndex)
84 val motionEvent = event.action and MotionEvent.ACTION_MASK
85 val isActionDown =
86 motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
87 val isActionUp =
88 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
89 if (isActionDown) {
90 if (!bounds.contains(xPosition, yPosition)) {
91 return false
92 }
93 trackId = pointerId
94 }
95 if (isActionUp) {
96 if (trackId != pointerId) {
97 return false
98 }
99 trackId = -1
100 upButtonState = false
101 downButtonState = false
102 leftButtonState = false
103 rightButtonState = false
104 return true
105 }
106 if (trackId == -1) {
107 return false
108 }
109 if (!dpad_slide && !isActionDown) {
110 return false
111 }
112 for (i in 0 until event.pointerCount) {
113 if (trackId != event.getPointerId(i)) {
114 continue
115 }
116
117 var touchX = event.getX(i)
118 var touchY = event.getY(i)
119 var maxY = bounds.bottom.toFloat()
120 var maxX = bounds.right.toFloat()
121 touchX -= bounds.centerX().toFloat()
122 maxX -= bounds.centerX().toFloat()
123 touchY -= bounds.centerY().toFloat()
124 maxY -= bounds.centerY().toFloat()
125 val axisX = touchX / maxX
126 val axisY = touchY / maxY
127 val oldUpState = upButtonState
128 val oldDownState = downButtonState
129 val oldLeftState = leftButtonState
130 val oldRightState = rightButtonState
131
132 upButtonState = axisY < -VIRT_AXIS_DEADZONE
133 downButtonState = axisY > VIRT_AXIS_DEADZONE
134 leftButtonState = axisX < -VIRT_AXIS_DEADZONE
135 rightButtonState = axisX > VIRT_AXIS_DEADZONE
136 return oldUpState != upButtonState || oldDownState != downButtonState || oldLeftState != leftButtonState || oldRightState != rightButtonState
137 }
138 return false
139 }
140
141 fun draw(canvas: Canvas) {
142 val px = controlPositionX + width / 2
143 val py = controlPositionY + height / 2
144
145 // Pressed up
146 if (upButtonState && !leftButtonState && !rightButtonState) {
147 pressedOneDirectionStateBitmap.draw(canvas)
148 return
149 }
150
151 // Pressed down
152 if (downButtonState && !leftButtonState && !rightButtonState) {
153 canvas.save()
154 canvas.rotate(180f, px.toFloat(), py.toFloat())
155 pressedOneDirectionStateBitmap.draw(canvas)
156 canvas.restore()
157 return
158 }
159
160 // Pressed left
161 if (leftButtonState && !upButtonState && !downButtonState) {
162 canvas.save()
163 canvas.rotate(270f, px.toFloat(), py.toFloat())
164 pressedOneDirectionStateBitmap.draw(canvas)
165 canvas.restore()
166 return
167 }
168
169 // Pressed right
170 if (rightButtonState && !upButtonState && !downButtonState) {
171 canvas.save()
172 canvas.rotate(90f, px.toFloat(), py.toFloat())
173 pressedOneDirectionStateBitmap.draw(canvas)
174 canvas.restore()
175 return
176 }
177
178 // Pressed up left
179 if (upButtonState && leftButtonState && !rightButtonState) {
180 pressedTwoDirectionsStateBitmap.draw(canvas)
181 return
182 }
183
184 // Pressed up right
185 if (upButtonState && !leftButtonState && rightButtonState) {
186 canvas.save()
187 canvas.rotate(90f, px.toFloat(), py.toFloat())
188 pressedTwoDirectionsStateBitmap.draw(canvas)
189 canvas.restore()
190 return
191 }
192
193 // Pressed down right
194 if (downButtonState && !leftButtonState && rightButtonState) {
195 canvas.save()
196 canvas.rotate(180f, px.toFloat(), py.toFloat())
197 pressedTwoDirectionsStateBitmap.draw(canvas)
198 canvas.restore()
199 return
200 }
201
202 // Pressed down left
203 if (downButtonState && leftButtonState && !rightButtonState) {
204 canvas.save()
205 canvas.rotate(270f, px.toFloat(), py.toFloat())
206 pressedTwoDirectionsStateBitmap.draw(canvas)
207 canvas.restore()
208 return
209 }
210
211 // Not pressed
212 defaultStateBitmap.draw(canvas)
213 }
214
215 val upStatus: Int
216 get() = if (upButtonState) ButtonState.PRESSED else ButtonState.RELEASED
217 val downStatus: Int
218 get() = if (downButtonState) ButtonState.PRESSED else ButtonState.RELEASED
219 val leftStatus: Int
220 get() = if (leftButtonState) ButtonState.PRESSED else ButtonState.RELEASED
221 val rightStatus: Int
222 get() = if (rightButtonState) ButtonState.PRESSED else ButtonState.RELEASED
223
224 fun onConfigureTouch(event: MotionEvent): Boolean {
225 val pointerIndex = event.actionIndex
226 val fingerPositionX = event.getX(pointerIndex).toInt()
227 val fingerPositionY = event.getY(pointerIndex).toInt()
228
229 when (event.action) {
230 MotionEvent.ACTION_DOWN -> {
231 previousTouchX = fingerPositionX
232 previousTouchY = fingerPositionY
233 }
234
235 MotionEvent.ACTION_MOVE -> {
236 controlPositionX += fingerPositionX - previousTouchX
237 controlPositionY += fingerPositionY - previousTouchY
238 setBounds(
239 controlPositionX,
240 controlPositionY,
241 width + controlPositionX,
242 height + controlPositionY
243 )
244 previousTouchX = fingerPositionX
245 previousTouchY = fingerPositionY
246 }
247 }
248 return true
249 }
250
251 fun setPosition(x: Int, y: Int) {
252 controlPositionX = x
253 controlPositionY = y
254 }
255
256 fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
257 defaultStateBitmap.setBounds(left, top, right, bottom)
258 pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom)
259 pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom)
260 }
261
262 fun setOpacity(value: Int) {
263 defaultStateBitmap.alpha = value
264 pressedOneDirectionStateBitmap.alpha = value
265 pressedTwoDirectionsStateBitmap.alpha = value
266 }
267
268 val bounds: Rect
269 get() = defaultStateBitmap.bounds
270
271 companion object {
272 const val VIRT_AXIS_DEADZONE = 0.5f
273 }
274}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
new file mode 100644
index 000000000..f1d32192a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -0,0 +1,282 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.overlay
5
6import android.content.res.Resources
7import android.graphics.Bitmap
8import android.graphics.Canvas
9import android.graphics.Rect
10import android.graphics.drawable.BitmapDrawable
11import android.view.MotionEvent
12import org.yuzu.yuzu_emu.NativeLibrary
13import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
14import kotlin.math.atan2
15import kotlin.math.cos
16import kotlin.math.sin
17import kotlin.math.sqrt
18
19/**
20 * Custom [BitmapDrawable] that is capable
21 * of storing it's own ID.
22 *
23 * @param res [Resources] instance.
24 * @param bitmapOuter [Bitmap] which represents the outer non-movable part of the joystick.
25 * @param bitmapInnerDefault [Bitmap] which represents the default inner movable part of the joystick.
26 * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
27 * @param rectOuter [Rect] which represents the outer joystick bounds.
28 * @param rectInner [Rect] which represents the inner joystick bounds.
29 * @param joystickId The ID value what type of joystick this Drawable represents.
30 * @param buttonId The ID value what type of button this Drawable represents.
31 */
32class InputOverlayDrawableJoystick(
33 res: Resources,
34 bitmapOuter: Bitmap,
35 bitmapInnerDefault: Bitmap,
36 bitmapInnerPressed: Bitmap,
37 rectOuter: Rect,
38 rectInner: Rect,
39 val joystickId: Int,
40 val buttonId: Int
41) {
42 // The ID value what motion event is tracking
43 var trackId = -1
44
45 var xAxis = 0f
46 private var yAxis = 0f
47
48 val width: Int
49 val height: Int
50
51 private var opacity: Int = 0
52
53 private var virtBounds: Rect
54 private var origBounds: Rect
55
56 private val outerBitmap: BitmapDrawable
57 private val defaultStateInnerBitmap: BitmapDrawable
58 private val pressedStateInnerBitmap: BitmapDrawable
59
60 private var previousTouchX = 0
61 private var previousTouchY = 0
62 var controlPositionX = 0
63 var controlPositionY = 0
64
65 private val boundsBoxBitmap: BitmapDrawable
66
67 private var pressedState = false
68
69 // TODO: Add button support
70 val buttonStatus: Int
71 get() =
72 NativeLibrary.ButtonState.RELEASED
73 var bounds: Rect
74 get() = outerBitmap.bounds
75 set(bounds) {
76 outerBitmap.bounds = bounds
77 }
78
79 // Nintendo joysticks have y axis inverted
80 val realYAxis: Float
81 get() = -yAxis
82
83 private val currentStateBitmapDrawable: BitmapDrawable
84 get() = if (pressedState) pressedStateInnerBitmap else defaultStateInnerBitmap
85
86 init {
87 outerBitmap = BitmapDrawable(res, bitmapOuter)
88 defaultStateInnerBitmap = BitmapDrawable(res, bitmapInnerDefault)
89 pressedStateInnerBitmap = BitmapDrawable(res, bitmapInnerPressed)
90 boundsBoxBitmap = BitmapDrawable(res, bitmapOuter)
91 width = bitmapOuter.width
92 height = bitmapOuter.height
93 bounds = rectOuter
94 defaultStateInnerBitmap.bounds = rectInner
95 pressedStateInnerBitmap.bounds = rectInner
96 virtBounds = bounds
97 origBounds = outerBitmap.copyBounds()
98 boundsBoxBitmap.alpha = 0
99 boundsBoxBitmap.bounds = virtBounds
100 setInnerBounds()
101 }
102
103 fun draw(canvas: Canvas?) {
104 outerBitmap.draw(canvas!!)
105 currentStateBitmapDrawable.draw(canvas)
106 boundsBoxBitmap.draw(canvas)
107 }
108
109 fun updateStatus(event: MotionEvent): Boolean {
110 val pointerIndex = event.actionIndex
111 val xPosition = event.getX(pointerIndex).toInt()
112 val yPosition = event.getY(pointerIndex).toInt()
113 val pointerId = event.getPointerId(pointerIndex)
114 val motionEvent = event.action and MotionEvent.ACTION_MASK
115 val isActionDown =
116 motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
117 val isActionUp =
118 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
119
120 if (isActionDown) {
121 if (!bounds.contains(xPosition, yPosition)) {
122 return false
123 }
124 pressedState = true
125 outerBitmap.alpha = 0
126 boundsBoxBitmap.alpha = opacity
127 if (EmulationMenuSettings.joystickRelCenter) {
128 virtBounds.offset(
129 xPosition - virtBounds.centerX(),
130 yPosition - virtBounds.centerY()
131 )
132 }
133 boundsBoxBitmap.bounds = virtBounds
134 trackId = pointerId
135 }
136
137 if (isActionUp) {
138 if (trackId != pointerId) {
139 return false
140 }
141 pressedState = false
142 xAxis = 0.0f
143 yAxis = 0.0f
144 outerBitmap.alpha = opacity
145 boundsBoxBitmap.alpha = 0
146 virtBounds = Rect(
147 origBounds.left,
148 origBounds.top,
149 origBounds.right,
150 origBounds.bottom
151 )
152 bounds = Rect(
153 origBounds.left,
154 origBounds.top,
155 origBounds.right,
156 origBounds.bottom
157 )
158 setInnerBounds()
159 trackId = -1
160 return true
161 }
162
163 if (trackId == -1) return false
164
165 for (i in 0 until event.pointerCount) {
166 if (trackId != event.getPointerId(i)) {
167 continue
168 }
169 var touchX = event.getX(i)
170 var touchY = event.getY(i)
171 var maxY = virtBounds.bottom.toFloat()
172 var maxX = virtBounds.right.toFloat()
173 touchX -= virtBounds.centerX().toFloat()
174 maxX -= virtBounds.centerX().toFloat()
175 touchY -= virtBounds.centerY().toFloat()
176 maxY -= virtBounds.centerY().toFloat()
177 val axisX = touchX / maxX
178 val axisY = touchY / maxY
179 val oldXAxis = xAxis
180 val oldYAxis = yAxis
181
182 // Clamp the circle pad input to a circle
183 val angle = atan2(axisY.toDouble(), axisX.toDouble()).toFloat()
184 var radius = sqrt((axisX * axisX + axisY * axisY).toDouble()).toFloat()
185 if (radius > 1.0f) {
186 radius = 1.0f
187 }
188 xAxis = cos(angle.toDouble()).toFloat() * radius
189 yAxis = sin(angle.toDouble()).toFloat() * radius
190 setInnerBounds()
191 return oldXAxis != xAxis && oldYAxis != yAxis
192 }
193 return false
194 }
195
196 fun onConfigureTouch(event: MotionEvent): Boolean {
197 val pointerIndex = event.actionIndex
198 val fingerPositionX = event.getX(pointerIndex).toInt()
199 val fingerPositionY = event.getY(pointerIndex).toInt()
200
201 when (event.action) {
202 MotionEvent.ACTION_DOWN -> {
203 previousTouchX = fingerPositionX
204 previousTouchY = fingerPositionY
205 controlPositionX = fingerPositionX - (width / 2)
206 controlPositionY = fingerPositionY - (height / 2)
207 }
208
209 MotionEvent.ACTION_MOVE -> {
210 controlPositionX += fingerPositionX - previousTouchX
211 controlPositionY += fingerPositionY - previousTouchY
212 bounds = Rect(
213 controlPositionX,
214 controlPositionY,
215 outerBitmap.intrinsicWidth + controlPositionX,
216 outerBitmap.intrinsicHeight + controlPositionY
217 )
218 virtBounds = Rect(
219 controlPositionX,
220 controlPositionY,
221 outerBitmap.intrinsicWidth + controlPositionX,
222 outerBitmap.intrinsicHeight + controlPositionY
223 )
224 setInnerBounds()
225 bounds = Rect(
226 Rect(
227 controlPositionX,
228 controlPositionY,
229 outerBitmap.intrinsicWidth + controlPositionX,
230 outerBitmap.intrinsicHeight + controlPositionY
231 )
232 )
233 previousTouchX = fingerPositionX
234 previousTouchY = fingerPositionY
235 }
236 }
237 origBounds = outerBitmap.copyBounds()
238 return true
239 }
240
241 private fun setInnerBounds() {
242 var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt()
243 var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt()
244 if (x > virtBounds.centerX() + virtBounds.width() / 2) x =
245 virtBounds.centerX() + virtBounds.width() / 2
246 if (x < virtBounds.centerX() - virtBounds.width() / 2) x =
247 virtBounds.centerX() - virtBounds.width() / 2
248 if (y > virtBounds.centerY() + virtBounds.height() / 2) y =
249 virtBounds.centerY() + virtBounds.height() / 2
250 if (y < virtBounds.centerY() - virtBounds.height() / 2) y =
251 virtBounds.centerY() - virtBounds.height() / 2
252 val width = pressedStateInnerBitmap.bounds.width() / 2
253 val height = pressedStateInnerBitmap.bounds.height() / 2
254 defaultStateInnerBitmap.setBounds(
255 x - width,
256 y - height,
257 x + width,
258 y + height
259 )
260 pressedStateInnerBitmap.bounds = defaultStateInnerBitmap.bounds
261 }
262
263 fun setPosition(x: Int, y: Int) {
264 controlPositionX = x
265 controlPositionY = y
266 }
267
268 fun setOpacity(value: Int) {
269 opacity = value
270
271 defaultStateInnerBitmap.alpha = value
272 pressedStateInnerBitmap.alpha = value
273
274 if (trackId == -1) {
275 outerBitmap.alpha = value
276 boundsBoxBitmap.alpha = 0
277 } else {
278 outerBitmap.alpha = 0
279 boundsBoxBitmap.alpha = value
280 }
281 }
282}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
new file mode 100644
index 000000000..97eef40d2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -0,0 +1,165 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import android.view.ViewGroup.MarginLayoutParams
11import androidx.appcompat.app.AppCompatActivity
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels
17import com.google.android.material.color.MaterialColors
18import com.google.android.material.transition.MaterialFadeThrough
19import org.yuzu.yuzu_emu.R
20import org.yuzu.yuzu_emu.adapters.GameAdapter
21import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
22import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
23import org.yuzu.yuzu_emu.model.GamesViewModel
24import org.yuzu.yuzu_emu.model.HomeViewModel
25
26class GamesFragment : Fragment() {
27 private var _binding: FragmentGamesBinding? = null
28 private val binding get() = _binding!!
29
30 private val gamesViewModel: GamesViewModel by activityViewModels()
31 private val homeViewModel: HomeViewModel by activityViewModels()
32
33 override fun onCreate(savedInstanceState: Bundle?) {
34 super.onCreate(savedInstanceState)
35 enterTransition = MaterialFadeThrough()
36 }
37
38 override fun onCreateView(
39 inflater: LayoutInflater,
40 container: ViewGroup?,
41 savedInstanceState: Bundle?
42 ): View {
43 _binding = FragmentGamesBinding.inflate(inflater)
44 return binding.root
45 }
46
47 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
48 homeViewModel.setNavigationVisibility(visible = true, animated = false)
49
50 binding.gridGames.apply {
51 layoutManager = AutofitGridLayoutManager(
52 requireContext(),
53 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
54 )
55 adapter = GameAdapter(requireActivity() as AppCompatActivity)
56 }
57
58 binding.swipeRefresh.apply {
59 // Add swipe down to refresh gesture
60 setOnRefreshListener {
61 gamesViewModel.reloadGames(false)
62 }
63
64 // Set theme color to the refresh animation's background
65 setProgressBackgroundColorSchemeColor(
66 MaterialColors.getColor(
67 binding.swipeRefresh,
68 com.google.android.material.R.attr.colorPrimary
69 )
70 )
71 setColorSchemeColors(
72 MaterialColors.getColor(
73 binding.swipeRefresh,
74 com.google.android.material.R.attr.colorOnPrimary
75 )
76 )
77
78 // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn
79 post {
80 if (_binding == null) {
81 return@post
82 }
83 binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!!
84 }
85 }
86
87 gamesViewModel.apply {
88 // Watch for when we get updates to any of our games lists
89 isReloading.observe(viewLifecycleOwner) { isReloading ->
90 binding.swipeRefresh.isRefreshing = isReloading
91 }
92 games.observe(viewLifecycleOwner) {
93 (binding.gridGames.adapter as GameAdapter).submitList(it)
94 if (it.isEmpty()) {
95 binding.noticeText.visibility = View.VISIBLE
96 } else {
97 binding.noticeText.visibility = View.GONE
98 }
99 }
100 shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
101 if (shouldSwapData) {
102 (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value!!)
103 gamesViewModel.setShouldSwapData(false)
104 }
105 }
106
107 // Check if the user reselected the games menu item and then scroll to top of the list
108 shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
109 if (shouldScroll) {
110 scrollToTop()
111 gamesViewModel.setShouldScrollToTop(false)
112 }
113 }
114 }
115
116 setInsets()
117 }
118
119 override fun onDestroyView() {
120 super.onDestroyView()
121 _binding = null
122 }
123
124 private fun scrollToTop() {
125 if (_binding != null) {
126 binding.gridGames.smoothScrollToPosition(0)
127 }
128 }
129
130 private fun setInsets() =
131 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
132 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
133 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
134 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
135 val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
136 val spacingNavigationRail =
137 resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
138
139 binding.gridGames.updatePadding(
140 top = barInsets.top + extraListSpacing,
141 bottom = barInsets.bottom + spacingNavigation + extraListSpacing
142 )
143
144 binding.swipeRefresh.setProgressViewEndTarget(
145 false,
146 barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
147 )
148
149 val leftInsets = barInsets.left + cutoutInsets.left
150 val rightInsets = barInsets.right + cutoutInsets.right
151 val mlpSwipe = binding.swipeRefresh.layoutParams as MarginLayoutParams
152 if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
153 mlpSwipe.leftMargin = leftInsets + spacingNavigationRail
154 mlpSwipe.rightMargin = rightInsets
155 } else {
156 mlpSwipe.leftMargin = leftInsets
157 mlpSwipe.rightMargin = rightInsets + spacingNavigationRail
158 }
159 binding.swipeRefresh.layoutParams = mlpSwipe
160
161 binding.noticeText.updatePadding(bottom = spacingNavigation)
162
163 windowInsets
164 }
165}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
new file mode 100644
index 000000000..124f62f08
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -0,0 +1,470 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.main
5
6import android.content.Intent
7import android.os.Bundle
8import android.view.View
9import android.view.ViewGroup.MarginLayoutParams
10import android.view.WindowManager
11import android.view.animation.PathInterpolator
12import android.widget.Toast
13import androidx.activity.result.contract.ActivityResultContracts
14import androidx.activity.viewModels
15import androidx.appcompat.app.AppCompatActivity
16import androidx.core.content.ContextCompat
17import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
18import androidx.core.view.ViewCompat
19import androidx.core.view.WindowCompat
20import androidx.core.view.WindowInsetsCompat
21import androidx.lifecycle.lifecycleScope
22import androidx.navigation.NavController
23import androidx.navigation.fragment.NavHostFragment
24import androidx.navigation.ui.setupWithNavController
25import androidx.preference.PreferenceManager
26import com.google.android.material.color.MaterialColors
27import com.google.android.material.dialog.MaterialAlertDialogBuilder
28import com.google.android.material.navigation.NavigationBarView
29import kotlinx.coroutines.Dispatchers
30import kotlinx.coroutines.launch
31import kotlinx.coroutines.withContext
32import org.yuzu.yuzu_emu.NativeLibrary
33import org.yuzu.yuzu_emu.R
34import org.yuzu.yuzu_emu.activities.EmulationActivity
35import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
36import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
37import org.yuzu.yuzu_emu.features.settings.model.Settings
38import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
39import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
40import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
43import org.yuzu.yuzu_emu.model.GamesViewModel
44import org.yuzu.yuzu_emu.model.HomeViewModel
45import org.yuzu.yuzu_emu.utils.*
46import java.io.File
47import java.io.FilenameFilter
48import java.io.IOException
49
50class MainActivity : AppCompatActivity(), ThemeProvider {
51 private lateinit var binding: ActivityMainBinding
52
53 private val homeViewModel: HomeViewModel by viewModels()
54 private val gamesViewModel: GamesViewModel by viewModels()
55 private val settingsViewModel: SettingsViewModel by viewModels()
56
57 override var themeId: Int = 0
58
59 override fun onCreate(savedInstanceState: Bundle?) {
60 val splashScreen = installSplashScreen()
61 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
62
63 settingsViewModel.settings.loadSettings()
64
65 ThemeHelper.setTheme(this)
66
67 super.onCreate(savedInstanceState)
68
69 binding = ActivityMainBinding.inflate(layoutInflater)
70 setContentView(binding.root)
71
72 WindowCompat.setDecorFitsSystemWindows(window, false)
73 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
74
75 window.statusBarColor =
76 ContextCompat.getColor(applicationContext, android.R.color.transparent)
77 window.navigationBarColor =
78 ContextCompat.getColor(applicationContext, android.R.color.transparent)
79
80 binding.statusBarShade.setBackgroundColor(
81 ThemeHelper.getColorWithOpacity(
82 MaterialColors.getColor(
83 binding.root,
84 com.google.android.material.R.attr.colorSurface
85 ),
86 ThemeHelper.SYSTEM_BAR_ALPHA
87 )
88 )
89 if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) {
90 binding.navigationBarShade.setBackgroundColor(
91 ThemeHelper.getColorWithOpacity(
92 MaterialColors.getColor(
93 binding.root,
94 com.google.android.material.R.attr.colorSurface
95 ),
96 ThemeHelper.SYSTEM_BAR_ALPHA
97 )
98 )
99 }
100
101 val navHostFragment =
102 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
103 setUpNavigation(navHostFragment.navController)
104 (binding.navigationView as NavigationBarView).setOnItemReselectedListener {
105 when (it.itemId) {
106 R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
107 R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
108 R.id.homeSettingsFragment -> SettingsActivity.launch(
109 this,
110 SettingsFile.FILE_NAME_CONFIG,
111 ""
112 )
113 }
114 }
115
116 // Prevents navigation from being drawn for a short time on recreation if set to hidden
117 if (!homeViewModel.navigationVisible.value?.first!!) {
118 binding.navigationView.visibility = View.INVISIBLE
119 binding.statusBarShade.visibility = View.INVISIBLE
120 }
121
122 homeViewModel.navigationVisible.observe(this) {
123 showNavigation(it.first, it.second)
124 }
125 homeViewModel.statusBarShadeVisible.observe(this) { visible ->
126 showStatusBarShade(visible)
127 }
128
129 // Dismiss previous notifications (should not happen unless a crash occurred)
130 EmulationActivity.stopForegroundService(this)
131
132 setInsets()
133 }
134
135 fun finishSetup(navController: NavController) {
136 navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
137 (binding.navigationView as NavigationBarView).setupWithNavController(navController)
138 showNavigation(visible = true, animated = true)
139 }
140
141 private fun setUpNavigation(navController: NavController) {
142 val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
143 .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
144
145 if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
146 navController.navigate(R.id.firstTimeSetupFragment)
147 homeViewModel.navigatedToSetup = true
148 } else {
149 (binding.navigationView as NavigationBarView).setupWithNavController(navController)
150 }
151 }
152
153 private fun showNavigation(visible: Boolean, animated: Boolean) {
154 if (!animated) {
155 if (visible) {
156 binding.navigationView.visibility = View.VISIBLE
157 } else {
158 binding.navigationView.visibility = View.INVISIBLE
159 }
160 return
161 }
162
163 val smallLayout = resources.getBoolean(R.bool.small_layout)
164 binding.navigationView.animate().apply {
165 if (visible) {
166 binding.navigationView.visibility = View.VISIBLE
167 duration = 300
168 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
169
170 if (smallLayout) {
171 binding.navigationView.translationY =
172 binding.navigationView.height.toFloat() * 2
173 translationY(0f)
174 } else {
175 if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) {
176 binding.navigationView.translationX =
177 binding.navigationView.width.toFloat() * -2
178 translationX(0f)
179 } else {
180 binding.navigationView.translationX =
181 binding.navigationView.width.toFloat() * 2
182 translationX(0f)
183 }
184 }
185 } else {
186 duration = 300
187 interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
188
189 if (smallLayout) {
190 translationY(binding.navigationView.height.toFloat() * 2)
191 } else {
192 if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) {
193 translationX(binding.navigationView.width.toFloat() * -2)
194 } else {
195 translationX(binding.navigationView.width.toFloat() * 2)
196 }
197 }
198 }
199 }.withEndAction {
200 if (!visible) {
201 binding.navigationView.visibility = View.INVISIBLE
202 }
203 }.start()
204 }
205
206 private fun showStatusBarShade(visible: Boolean) {
207 binding.statusBarShade.animate().apply {
208 if (visible) {
209 binding.statusBarShade.visibility = View.VISIBLE
210 binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
211 duration = 300
212 translationY(0f)
213 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
214 } else {
215 duration = 300
216 translationY(binding.navigationView.height.toFloat() * -2)
217 interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
218 }
219 }.withEndAction {
220 if (!visible) {
221 binding.statusBarShade.visibility = View.INVISIBLE
222 }
223 }.start()
224 }
225
226 override fun onResume() {
227 ThemeHelper.setCorrectTheme(this)
228 super.onResume()
229 }
230
231 override fun onDestroy() {
232 EmulationActivity.stopForegroundService(this)
233 super.onDestroy()
234 }
235
236 private fun setInsets() =
237 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
238 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
239 val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams
240 mlpStatusShade.height = insets.top
241 binding.statusBarShade.layoutParams = mlpStatusShade
242
243 // The only situation where we care to have a nav bar shade is when it's at the bottom
244 // of the screen where scrolling list elements can go behind it.
245 val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
246 mlpNavShade.height = insets.bottom
247 binding.navigationBarShade.layoutParams = mlpNavShade
248
249 windowInsets
250 }
251
252 override fun setTheme(resId: Int) {
253 super.setTheme(resId)
254 themeId = resId
255 }
256
257 val getGamesDirectory =
258 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
259 if (result == null)
260 return@registerForActivityResult
261
262 contentResolver.takePersistableUriPermission(
263 result,
264 Intent.FLAG_GRANT_READ_URI_PERMISSION
265 )
266
267 // When a new directory is picked, we currently will reset the existing games
268 // database. This effectively means that only one game directory is supported.
269 PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
270 .putString(GameHelper.KEY_GAME_PATH, result.toString())
271 .apply()
272
273 Toast.makeText(
274 applicationContext,
275 R.string.games_dir_selected,
276 Toast.LENGTH_LONG
277 ).show()
278
279 gamesViewModel.reloadGames(true)
280 }
281
282 val getProdKey =
283 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
284 if (result == null)
285 return@registerForActivityResult
286
287 if (!FileUtil.hasExtension(result.toString(), "keys")) {
288 MessageDialogFragment.newInstance(
289 R.string.reading_keys_failure,
290 R.string.install_keys_failure_extension_description
291 ).show(supportFragmentManager, MessageDialogFragment.TAG)
292 return@registerForActivityResult
293 }
294
295 contentResolver.takePersistableUriPermission(
296 result,
297 Intent.FLAG_GRANT_READ_URI_PERMISSION
298 )
299
300 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
301 if (FileUtil.copyUriToInternalStorage(
302 applicationContext,
303 result,
304 dstPath,
305 "prod.keys"
306 )
307 ) {
308 if (NativeLibrary.reloadKeys()) {
309 Toast.makeText(
310 applicationContext,
311 R.string.install_keys_success,
312 Toast.LENGTH_SHORT
313 ).show()
314 gamesViewModel.reloadGames(true)
315 } else {
316 MessageDialogFragment.newInstance(
317 R.string.invalid_keys_error,
318 R.string.install_keys_failure_description,
319 R.string.dumping_keys_quickstart_link
320 ).show(supportFragmentManager, MessageDialogFragment.TAG)
321 }
322 }
323 }
324
325 val getFirmware =
326 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
327 if (result == null)
328 return@registerForActivityResult
329
330 val inputZip = contentResolver.openInputStream(result)
331 if (inputZip == null) {
332 Toast.makeText(
333 applicationContext,
334 getString(R.string.fatal_error),
335 Toast.LENGTH_LONG
336 ).show()
337 return@registerForActivityResult
338 }
339
340 val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
341
342 val firmwarePath =
343 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
344 val cacheFirmwareDir = File("${cacheDir.path}/registered/")
345
346 val task: () -> Any = {
347 var messageToShow: Any
348 try {
349 FileUtil.unzip(inputZip, cacheFirmwareDir)
350 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
351 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
352 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
353 MessageDialogFragment.newInstance(
354 R.string.firmware_installed_failure,
355 R.string.firmware_installed_failure_description
356 )
357 } else {
358 firmwarePath.deleteRecursively()
359 cacheFirmwareDir.copyRecursively(firmwarePath, true)
360 getString(R.string.save_file_imported_success)
361 }
362 } catch (e: Exception) {
363 messageToShow = getString(R.string.fatal_error)
364 } finally {
365 cacheFirmwareDir.deleteRecursively()
366 }
367 messageToShow
368 }
369
370 IndeterminateProgressDialogFragment.newInstance(
371 this,
372 R.string.firmware_installing,
373 task
374 ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
375 }
376
377 val getAmiiboKey =
378 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
379 if (result == null)
380 return@registerForActivityResult
381
382 if (!FileUtil.hasExtension(result.toString(), "bin")) {
383 MessageDialogFragment.newInstance(
384 R.string.reading_keys_failure,
385 R.string.install_keys_failure_extension_description
386 ).show(supportFragmentManager, MessageDialogFragment.TAG)
387 return@registerForActivityResult
388 }
389
390 contentResolver.takePersistableUriPermission(
391 result,
392 Intent.FLAG_GRANT_READ_URI_PERMISSION
393 )
394
395 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
396 if (FileUtil.copyUriToInternalStorage(
397 applicationContext,
398 result,
399 dstPath,
400 "key_retail.bin"
401 )
402 ) {
403 if (NativeLibrary.reloadKeys()) {
404 Toast.makeText(
405 applicationContext,
406 R.string.install_keys_success,
407 Toast.LENGTH_SHORT
408 ).show()
409 } else {
410 MessageDialogFragment.newInstance(
411 R.string.invalid_keys_error,
412 R.string.install_keys_failure_description,
413 R.string.dumping_keys_quickstart_link
414 ).show(supportFragmentManager, MessageDialogFragment.TAG)
415 }
416 }
417 }
418
419 val getDriver =
420 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
421 if (result == null)
422 return@registerForActivityResult
423
424 val takeFlags =
425 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
426 contentResolver.takePersistableUriPermission(
427 result,
428 takeFlags
429 )
430
431 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
432 progressBinding.progressBar.isIndeterminate = true
433 val installationDialog = MaterialAlertDialogBuilder(this)
434 .setTitle(R.string.installing_driver)
435 .setView(progressBinding.root)
436 .show()
437
438 lifecycleScope.launch {
439 withContext(Dispatchers.IO) {
440 // Ignore file exceptions when a user selects an invalid zip
441 try {
442 GpuDriverHelper.installCustomDriver(applicationContext, result)
443 } catch (_: IOException) {
444 }
445
446 withContext(Dispatchers.Main) {
447 installationDialog.dismiss()
448
449 val driverName = GpuDriverHelper.customDriverName
450 if (driverName != null) {
451 Toast.makeText(
452 applicationContext,
453 getString(
454 R.string.select_gpu_driver_install_success,
455 driverName
456 ),
457 Toast.LENGTH_SHORT
458 ).show()
459 } else {
460 Toast.makeText(
461 applicationContext,
462 R.string.select_gpu_driver_error,
463 Toast.LENGTH_LONG
464 ).show()
465 }
466 }
467 }
468 }
469 }
470}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/ThemeProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/ThemeProvider.kt
new file mode 100644
index 000000000..511a6e4fa
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/ThemeProvider.kt
@@ -0,0 +1,11 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.main
5
6interface ThemeProvider {
7 /**
8 * Provides theme ID by overriding an activity's 'setTheme' method and returning that result
9 */
10 var themeId: Int
11}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt
new file mode 100644
index 000000000..9cfda74ee
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt
@@ -0,0 +1,25 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6class BiMap<K, V> {
7 private val forward: MutableMap<K, V> = HashMap()
8 private val backward: MutableMap<V, K> = HashMap()
9
10 @Synchronized
11 fun add(key: K, value: V) {
12 forward[key] = value
13 backward[value] = key
14 }
15
16 @Synchronized
17 fun getForward(key: K): V? {
18 return forward[key]
19 }
20
21 @Synchronized
22 fun getBackward(key: V): K? {
23 return backward[key]
24 }
25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt
new file mode 100644
index 000000000..791cea904
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt
@@ -0,0 +1,68 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.view.InputDevice
7import android.view.KeyEvent
8import android.view.MotionEvent
9
10/**
11 * Some controllers have incorrect mappings. This class has special-case fixes for them.
12 */
13class ControllerMappingHelper {
14 /**
15 * Some controllers report extra button presses that can be ignored.
16 */
17 fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean {
18 return if (isDualShock4(inputDevice)) {
19 // The two analog triggers generate analog motion events as well as a keycode.
20 // We always prefer to use the analog values, so throw away the button press
21 keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2
22 } else false
23 }
24
25 /**
26 * Scale an axis to be zero-centered with a proper range.
27 */
28 fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float {
29 if (isDualShock4(inputDevice)) {
30 // Android doesn't have correct mappings for this controller's triggers. It reports them
31 // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
32 // Scale them to properly zero-centered with a range of [0.0, 1.0].
33 if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
34 return (value + 1) / 2.0f
35 }
36 } else if (isXboxOneWireless(inputDevice)) {
37 // Same as the DualShock 4, the mappings are missing.
38 if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
39 return (value + 1) / 2.0f
40 }
41 if (axis == MotionEvent.AXIS_GENERIC_1) {
42 // This axis is stuck at ~.5. Ignore it.
43 return 0.0f
44 }
45 } else if (isMogaPro2Hid(inputDevice)) {
46 // This controller has a broken axis that reports a constant value. Ignore it.
47 if (axis == MotionEvent.AXIS_GENERIC_1) {
48 return 0.0f
49 }
50 }
51 return value
52 }
53
54 // Sony DualShock 4 controller
55 private fun isDualShock4(inputDevice: InputDevice): Boolean {
56 return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc
57 }
58
59 // Microsoft Xbox One controller
60 private fun isXboxOneWireless(inputDevice: InputDevice): Boolean {
61 return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0
62 }
63
64 // Moga Pro 2 HID
65 private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean {
66 return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271
67 }
68}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
new file mode 100644
index 000000000..36c479e6c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -0,0 +1,37 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.Context
7import org.yuzu.yuzu_emu.NativeLibrary
8import java.io.IOException
9
10object DirectoryInitialization {
11 private var userPath: String? = null
12
13 var areDirectoriesReady: Boolean = false
14
15 fun start(context: Context) {
16 if (!areDirectoriesReady) {
17 initializeInternalStorage(context)
18 NativeLibrary.initializeEmulation()
19 areDirectoriesReady = true
20 }
21 }
22
23 val userDirectory: String?
24 get() {
25 check(areDirectoriesReady) { "Directory initialization is not ready!" }
26 return userPath
27 }
28
29 private fun initializeInternalStorage(context: Context) {
30 try {
31 userPath = context.getExternalFilesDir(null)!!.canonicalPath
32 NativeLibrary.setAppDirectory(userPath!!)
33 } catch (e: IOException) {
34 e.printStackTrace()
35 }
36 }
37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
new file mode 100644
index 000000000..cc8ea6b9d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
@@ -0,0 +1,112 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.net.Uri
7import androidx.documentfile.provider.DocumentFile
8import org.yuzu.yuzu_emu.YuzuApplication
9import org.yuzu.yuzu_emu.model.MinimalDocumentFile
10import java.io.File
11import java.util.*
12
13class DocumentsTree {
14 private var root: DocumentsNode? = null
15
16 fun setRoot(rootUri: Uri?) {
17 root = null
18 root = DocumentsNode()
19 root!!.uri = rootUri
20 root!!.isDirectory = true
21 }
22
23 fun openContentUri(filepath: String, openMode: String?): Int {
24 val node = resolvePath(filepath) ?: return -1
25 return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode)
26 }
27
28 fun getFileSize(filepath: String): Long {
29 val node = resolvePath(filepath)
30 return if (node == null || node.isDirectory) {
31 0
32 } else FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString())
33 }
34
35 fun exists(filepath: String): Boolean {
36 return resolvePath(filepath) != null
37 }
38
39 private fun resolvePath(filepath: String): DocumentsNode? {
40 val tokens = StringTokenizer(filepath, File.separator, false)
41 var iterator = root
42 while (tokens.hasMoreTokens()) {
43 val token = tokens.nextToken()
44 if (token.isEmpty()) continue
45 iterator = find(iterator, token)
46 if (iterator == null) return null
47 }
48 return iterator
49 }
50
51 private fun find(parent: DocumentsNode?, filename: String): DocumentsNode? {
52 if (parent!!.isDirectory && !parent.loaded) {
53 structTree(parent)
54 }
55 return parent.children[filename]
56 }
57
58 /**
59 * Construct current level directory tree
60 * @param parent parent node of this level
61 */
62 private fun structTree(parent: DocumentsNode) {
63 val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!)
64 for (document in documents) {
65 val node = DocumentsNode(document)
66 node.parent = parent
67 parent.children[node.name] = node
68 }
69 parent.loaded = true
70 }
71
72 private class DocumentsNode {
73 var parent: DocumentsNode? = null
74 val children: MutableMap<String?, DocumentsNode> = HashMap()
75 var name: String? = null
76 var uri: Uri? = null
77 var loaded = false
78 var isDirectory = false
79
80 constructor()
81 constructor(document: MinimalDocumentFile) {
82 name = document.filename
83 uri = document.uri
84 isDirectory = document.isDirectory
85 loaded = !isDirectory
86 }
87
88 private constructor(document: DocumentFile, isCreateDir: Boolean) {
89 name = document.name
90 uri = document.uri
91 isDirectory = isCreateDir
92 loaded = true
93 }
94
95 private fun rename(name: String) {
96 if (parent == null) {
97 return
98 }
99 parent!!.children.remove(this.name)
100 this.name = name
101 parent!!.children[name] = this
102 }
103 }
104
105 companion object {
106 fun isNativePath(path: String): Boolean {
107 return if (path.isNotEmpty()) {
108 path[0] == '/'
109 } else false
110 }
111 }
112}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
new file mode 100644
index 000000000..e1e7a59d7
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
@@ -0,0 +1,68 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import androidx.preference.PreferenceManager
7import org.yuzu.yuzu_emu.YuzuApplication
8import org.yuzu.yuzu_emu.features.settings.model.Settings
9
10object EmulationMenuSettings {
11 private val preferences =
12 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
13
14 // These must match what is defined in src/core/settings.h
15 const val LayoutOption_Default = 0
16 const val LayoutOption_SingleScreen = 1
17 const val LayoutOption_LargeScreen = 2
18 const val LayoutOption_SideScreen = 3
19 const val LayoutOption_MobilePortrait = 4
20 const val LayoutOption_MobileLandscape = 5
21
22 var joystickRelCenter: Boolean
23 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
24 set(value) {
25 preferences.edit()
26 .putBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, value)
27 .apply()
28 }
29 var dpadSlide: Boolean
30 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, true)
31 set(value) {
32 preferences.edit()
33 .putBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, value)
34 .apply()
35 }
36 var hapticFeedback: Boolean
37 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, false)
38 set(value) {
39 preferences.edit()
40 .putBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, value)
41 .apply()
42 }
43
44 var landscapeScreenLayout: Int
45 get() = preferences.getInt(
46 Settings.PREF_MENU_SETTINGS_LANDSCAPE,
47 LayoutOption_MobileLandscape
48 )
49 set(value) {
50 preferences.edit()
51 .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
52 .apply()
53 }
54 var showFps: Boolean
55 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
56 set(value) {
57 preferences.edit()
58 .putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, value)
59 .apply()
60 }
61 var showOverlay: Boolean
62 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, true)
63 set(value) {
64 preferences.edit()
65 .putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, value)
66 .apply()
67 }
68}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
new file mode 100644
index 000000000..593dad8d3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -0,0 +1,330 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.Context
7import android.database.Cursor
8import android.net.Uri
9import android.provider.DocumentsContract
10import androidx.documentfile.provider.DocumentFile
11import org.yuzu.yuzu_emu.model.MinimalDocumentFile
12import java.io.BufferedInputStream
13import java.io.File
14import java.io.FileOutputStream
15import java.io.IOException
16import java.io.InputStream
17import java.net.URLDecoder
18import java.util.zip.ZipEntry
19import java.util.zip.ZipInputStream
20
21object FileUtil {
22 const val PATH_TREE = "tree"
23 const val DECODE_METHOD = "UTF-8"
24 const val APPLICATION_OCTET_STREAM = "application/octet-stream"
25 const val TEXT_PLAIN = "text/plain"
26
27 /**
28 * Create a file from directory with filename.
29 * @param context Application context
30 * @param directory parent path for file.
31 * @param filename file display name.
32 * @return boolean
33 */
34 fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? {
35 var decodedFilename = filename
36 try {
37 val directoryUri = Uri.parse(directory)
38 val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
39 decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
40 var mimeType = APPLICATION_OCTET_STREAM
41 if (decodedFilename.endsWith(".txt")) {
42 mimeType = TEXT_PLAIN
43 }
44 val exists = parent.findFile(decodedFilename)
45 return exists ?: parent.createFile(mimeType, decodedFilename)
46 } catch (e: Exception) {
47 Log.error("[FileUtil]: Cannot create file, error: " + e.message)
48 }
49 return null
50 }
51
52 /**
53 * Create a directory from directory with filename.
54 * @param context Application context
55 * @param directory parent path for directory.
56 * @param directoryName directory display name.
57 * @return boolean
58 */
59 fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? {
60 var decodedDirectoryName = directoryName
61 try {
62 val directoryUri = Uri.parse(directory)
63 val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
64 decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
65 val isExist = parent.findFile(decodedDirectoryName)
66 return isExist ?: parent.createDirectory(decodedDirectoryName)
67 } catch (e: Exception) {
68 Log.error("[FileUtil]: Cannot create file, error: " + e.message)
69 }
70 return null
71 }
72
73 /**
74 * Open content uri and return file descriptor to JNI.
75 * @param context Application context
76 * @param path Native content uri path
77 * @param openMode will be one of "r", "r", "rw", "wa", "rwa"
78 * @return file descriptor
79 */
80 @JvmStatic
81 fun openContentUri(context: Context, path: String, openMode: String?): Int {
82 try {
83 val uri = Uri.parse(path)
84 val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
85 if (parcelFileDescriptor == null) {
86 Log.error("[FileUtil]: Cannot get the file descriptor from uri: $path")
87 return -1
88 }
89 val fileDescriptor = parcelFileDescriptor.detachFd()
90 parcelFileDescriptor.close()
91 return fileDescriptor
92 } catch (e: Exception) {
93 Log.error("[FileUtil]: Cannot open content uri, error: " + e.message)
94 }
95 return -1
96 }
97
98 /**
99 * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
100 * This function will be faster than DoucmentFile.listFiles
101 * @param context Application context
102 * @param uri Directory uri.
103 * @return CheapDocument lists.
104 */
105 fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> {
106 val resolver = context.contentResolver
107 val columns = arrayOf(
108 DocumentsContract.Document.COLUMN_DOCUMENT_ID,
109 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
110 DocumentsContract.Document.COLUMN_MIME_TYPE
111 )
112 var c: Cursor? = null
113 val results: MutableList<MinimalDocumentFile> = ArrayList()
114 try {
115 val docId: String = if (isRootTreeUri(uri)) {
116 DocumentsContract.getTreeDocumentId(uri)
117 } else {
118 DocumentsContract.getDocumentId(uri)
119 }
120 val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
121 c = resolver.query(childrenUri, columns, null, null, null)
122 while (c!!.moveToNext()) {
123 val documentId = c.getString(0)
124 val documentName = c.getString(1)
125 val documentMimeType = c.getString(2)
126 val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId)
127 val document = MinimalDocumentFile(documentName, documentMimeType, documentUri)
128 results.add(document)
129 }
130 } catch (e: Exception) {
131 Log.error("[FileUtil]: Cannot list file error: " + e.message)
132 } finally {
133 closeQuietly(c)
134 }
135 return results.toTypedArray()
136 }
137
138 /**
139 * Check whether given path exists.
140 * @param path Native content uri path
141 * @return bool
142 */
143 fun exists(context: Context, path: String?): Boolean {
144 var c: Cursor? = null
145 try {
146 val mUri = Uri.parse(path)
147 val columns = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
148 c = context.contentResolver.query(mUri, columns, null, null, null)
149 return c!!.count > 0
150 } catch (e: Exception) {
151 Log.info("[FileUtil] Cannot find file from given path, error: " + e.message)
152 } finally {
153 closeQuietly(c)
154 }
155 return false
156 }
157
158 /**
159 * Check whether given path is a directory
160 * @param path content uri path
161 * @return bool
162 */
163 fun isDirectory(context: Context, path: String): Boolean {
164 val resolver = context.contentResolver
165 val columns = arrayOf(
166 DocumentsContract.Document.COLUMN_MIME_TYPE
167 )
168 var isDirectory = false
169 var c: Cursor? = null
170 try {
171 val mUri = Uri.parse(path)
172 c = resolver.query(mUri, columns, null, null, null)
173 c!!.moveToNext()
174 val mimeType = c.getString(0)
175 isDirectory = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
176 } catch (e: Exception) {
177 Log.error("[FileUtil]: Cannot list files, error: " + e.message)
178 } finally {
179 closeQuietly(c)
180 }
181 return isDirectory
182 }
183
184 /**
185 * Get file display name from given path
186 * @param path content uri path
187 * @return String display name
188 */
189 fun getFilename(context: Context, path: String): String {
190 val resolver = context.contentResolver
191 val columns = arrayOf(
192 DocumentsContract.Document.COLUMN_DISPLAY_NAME
193 )
194 var filename = ""
195 var c: Cursor? = null
196 try {
197 val mUri = Uri.parse(path)
198 c = resolver.query(mUri, columns, null, null, null)
199 c!!.moveToNext()
200 filename = c.getString(0)
201 } catch (e: Exception) {
202 Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
203 } finally {
204 closeQuietly(c)
205 }
206 return filename
207 }
208
209 fun getFilesName(context: Context, path: String): Array<String> {
210 val uri = Uri.parse(path)
211 val files: MutableList<String> = ArrayList()
212 for (file in listFiles(context, uri)) {
213 files.add(file.filename)
214 }
215 return files.toTypedArray()
216 }
217
218 /**
219 * Get file size from given path.
220 * @param path content uri path
221 * @return long file size
222 */
223 @JvmStatic
224 fun getFileSize(context: Context, path: String): Long {
225 val resolver = context.contentResolver
226 val columns = arrayOf(
227 DocumentsContract.Document.COLUMN_SIZE
228 )
229 var size: Long = 0
230 var c: Cursor? = null
231 try {
232 val mUri = Uri.parse(path)
233 c = resolver.query(mUri, columns, null, null, null)
234 c!!.moveToNext()
235 size = c.getLong(0)
236 } catch (e: Exception) {
237 Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
238 } finally {
239 closeQuietly(c)
240 }
241 return size
242 }
243
244 fun copyUriToInternalStorage(
245 context: Context,
246 sourceUri: Uri?,
247 destinationParentPath: String,
248 destinationFilename: String
249 ): Boolean {
250 var input: InputStream? = null
251 var output: FileOutputStream? = null
252 try {
253 input = context.contentResolver.openInputStream(sourceUri!!)
254 output = FileOutputStream("$destinationParentPath/$destinationFilename")
255 val buffer = ByteArray(1024)
256 var len: Int
257 while (input!!.read(buffer).also { len = it } != -1) {
258 output.write(buffer, 0, len)
259 }
260 output.flush()
261 return true
262 } catch (e: Exception) {
263 Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
264 } finally {
265 if (input != null) {
266 try {
267 input.close()
268 } catch (e: IOException) {
269 Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
270 }
271 }
272 if (output != null) {
273 try {
274 output.close()
275 } catch (e: IOException) {
276 Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
277 }
278 }
279 }
280 return false
281 }
282
283 /**
284 * Extracts the given zip file into the given directory.
285 * @exception IOException if the file was being created outside of the target directory
286 */
287 @Throws(SecurityException::class)
288 fun unzip(zipStream: InputStream, destDir: File): Boolean {
289 ZipInputStream(BufferedInputStream(zipStream)).use { zis ->
290 var entry: ZipEntry? = zis.nextEntry
291 while (entry != null) {
292 val entryName = entry.name
293 val entryFile = File(destDir, entryName)
294 if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
295 throw SecurityException("Entry is outside of the target dir: " + entryFile.name)
296 }
297 if (entry.isDirectory) {
298 entryFile.mkdirs()
299 } else {
300 entryFile.parentFile?.mkdirs()
301 entryFile.createNewFile()
302 entryFile.outputStream().use { fos -> zis.copyTo(fos) }
303 }
304 entry = zis.nextEntry
305 }
306 }
307
308 return true
309 }
310
311 fun isRootTreeUri(uri: Uri): Boolean {
312 val paths = uri.pathSegments
313 return paths.size == 2 && PATH_TREE == paths[0]
314 }
315
316 fun closeQuietly(closeable: AutoCloseable?) {
317 if (closeable != null) {
318 try {
319 closeable.close()
320 } catch (rethrown: RuntimeException) {
321 throw rethrown
322 } catch (ignored: Exception) {
323 }
324 }
325 }
326
327 fun hasExtension(path: String, extension: String): Boolean {
328 return path.substring(path.lastIndexOf(".") + 1).contains(extension)
329 }
330}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt
new file mode 100644
index 000000000..dc9b7c744
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt
@@ -0,0 +1,70 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.app.PendingIntent
7import android.app.Service
8import android.content.Intent
9import android.os.IBinder
10import androidx.core.app.NotificationCompat
11import androidx.core.app.NotificationManagerCompat
12import org.yuzu.yuzu_emu.R
13import org.yuzu.yuzu_emu.activities.EmulationActivity
14
15/**
16 * A service that shows a permanent notification in the background to avoid the app getting
17 * cleared from memory by the system.
18 */
19class ForegroundService : Service() {
20 companion object {
21 const val EMULATION_RUNNING_NOTIFICATION = 0x1000
22
23 const val ACTION_STOP = "stop"
24 }
25
26 private fun showRunningNotification() {
27 // Intent is used to resume emulation if the notification is clicked
28 val contentIntent = PendingIntent.getActivity(
29 this,
30 0,
31 Intent(this, EmulationActivity::class.java),
32 PendingIntent.FLAG_IMMUTABLE
33 )
34 val builder =
35 NotificationCompat.Builder(this, getString(R.string.emulation_notification_channel_id))
36 .setSmallIcon(R.drawable.ic_stat_notification_logo)
37 .setContentTitle(getString(R.string.app_name))
38 .setContentText(getString(R.string.emulation_notification_running))
39 .setPriority(NotificationCompat.PRIORITY_LOW)
40 .setOngoing(true)
41 .setVibrate(null)
42 .setSound(null)
43 .setContentIntent(contentIntent)
44 startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build())
45 }
46
47 override fun onBind(intent: Intent): IBinder? {
48 return null
49 }
50
51 override fun onCreate() {
52 showRunningNotification()
53 }
54
55 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
56 if (intent == null) {
57 return START_NOT_STICKY;
58 }
59 if (intent.action == ACTION_STOP) {
60 NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
61 stopForeground(STOP_FOREGROUND_REMOVE)
62 stopSelfResult(startId)
63 }
64 return START_STICKY
65 }
66
67 override fun onDestroy() {
68 NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
69 }
70}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
new file mode 100644
index 000000000..ba6b5783e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -0,0 +1,98 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.SharedPreferences
7import android.net.Uri
8import androidx.preference.PreferenceManager
9import kotlinx.serialization.decodeFromString
10import kotlinx.serialization.encodeToString
11import kotlinx.serialization.json.Json
12import org.yuzu.yuzu_emu.NativeLibrary
13import org.yuzu.yuzu_emu.YuzuApplication
14import org.yuzu.yuzu_emu.model.Game
15import java.util.*
16
17object GameHelper {
18 const val KEY_GAME_PATH = "game_path"
19 const val KEY_GAMES = "Games"
20
21 private lateinit var preferences: SharedPreferences
22
23 fun getGames(): List<Game> {
24 val games = mutableListOf<Game>()
25 val context = YuzuApplication.appContext
26 val gamesDir =
27 PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
28 val gamesUri = Uri.parse(gamesDir)
29 preferences = PreferenceManager.getDefaultSharedPreferences(context)
30
31 // Ensure keys are loaded so that ROM metadata can be decrypted.
32 NativeLibrary.reloadKeys()
33
34 val children = FileUtil.listFiles(context, gamesUri)
35 for (file in children) {
36 if (!file.isDirectory) {
37 val filename = file.uri.toString()
38 val extensionStart = filename.lastIndexOf('.')
39 if (extensionStart > 0) {
40 val fileExtension = filename.substring(extensionStart)
41
42 // Check that the file has an extension we care about before trying to read out of it.
43 if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
44 games.add(getGame(filename))
45 }
46 }
47 }
48 }
49
50 // Cache list of games found on disk
51 val serializedGames = mutableSetOf<String>()
52 games.forEach {
53 serializedGames.add(Json.encodeToString(it))
54 }
55 preferences.edit()
56 .remove(KEY_GAMES)
57 .putStringSet(KEY_GAMES, serializedGames)
58 .apply()
59
60 return games.toList()
61 }
62
63 private fun getGame(filePath: String): Game {
64 var name = NativeLibrary.getTitle(filePath)
65
66 // If the game's title field is empty, use the filename.
67 if (name.isEmpty()) {
68 name = filePath.substring(filePath.lastIndexOf("/") + 1)
69 }
70 var gameId = NativeLibrary.getGameId(filePath)
71
72 // If the game's ID field is empty, use the filename without extension.
73 if (gameId.isEmpty()) {
74 gameId = filePath.substring(
75 filePath.lastIndexOf("/") + 1,
76 filePath.lastIndexOf(".")
77 )
78 }
79
80 val newGame = Game(
81 name,
82 NativeLibrary.getDescription(filePath).replace("\n", " "),
83 NativeLibrary.getRegions(filePath),
84 filePath,
85 gameId,
86 NativeLibrary.getCompany(filePath)
87 )
88
89 val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
90 if (addedTime == 0L) {
91 preferences.edit()
92 .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
93 .apply()
94 }
95
96 return newGame
97 }
98}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
new file mode 100644
index 000000000..528011d7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -0,0 +1,152 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.Context
7import android.net.Uri
8import org.yuzu.yuzu_emu.NativeLibrary
9import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
10import java.io.BufferedInputStream
11import java.io.File
12import java.io.FileInputStream
13import java.io.FileOutputStream
14import java.io.IOException
15import java.util.zip.ZipInputStream
16
17object GpuDriverHelper {
18 private const val META_JSON_FILENAME = "meta.json"
19 private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
20 private var fileRedirectionPath: String? = null
21 private var driverInstallationPath: String? = null
22 private var hookLibPath: String? = null
23
24 @Throws(IOException::class)
25 private fun unzip(zipFilePath: String, destDir: String) {
26 val dir = File(destDir)
27
28 // Create output directory if it doesn't exist
29 if (!dir.exists()) dir.mkdirs()
30
31 // Unpack the files.
32 val inputStream = FileInputStream(zipFilePath)
33 val zis = ZipInputStream(BufferedInputStream(inputStream))
34 val buffer = ByteArray(1024)
35 var ze = zis.nextEntry
36 while (ze != null) {
37 val newFile = File(destDir, ze.name)
38 val canonicalPath = newFile.canonicalPath
39 if (!canonicalPath.startsWith(destDir + ze.name)) {
40 throw SecurityException("Zip file attempted path traversal! " + ze.name)
41 }
42
43 newFile.parentFile!!.mkdirs()
44 val fos = FileOutputStream(newFile)
45 var len: Int
46 while (zis.read(buffer).also { len = it } > 0) {
47 fos.write(buffer, 0, len)
48 }
49 fos.close()
50 zis.closeEntry()
51 ze = zis.nextEntry
52 }
53 zis.closeEntry()
54 }
55
56 fun initializeDriverParameters(context: Context) {
57 try {
58 // Initialize the file redirection directory.
59 fileRedirectionPath =
60 context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
61
62 // Initialize the driver installation directory.
63 driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
64 } catch (e: IOException) {
65 throw RuntimeException(e)
66 }
67
68 // Initialize directories.
69 initializeDirectories()
70
71 // Initialize hook libraries directory.
72 hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
73
74 // Initialize GPU driver.
75 NativeLibrary.initializeGpuDriver(
76 hookLibPath,
77 driverInstallationPath,
78 customDriverLibraryName,
79 fileRedirectionPath
80 )
81 }
82
83 fun installDefaultDriver(context: Context) {
84 // Removing the installed driver will result in the backend using the default system driver.
85 val driverInstallationDir = File(driverInstallationPath!!)
86 deleteRecursive(driverInstallationDir)
87 initializeDriverParameters(context)
88 }
89
90 fun installCustomDriver(context: Context, driverPathUri: Uri?) {
91 // Revert to system default in the event the specified driver is bad.
92 installDefaultDriver(context)
93
94 // Ensure we have directories.
95 initializeDirectories()
96
97 // Copy the zip file URI into our private storage.
98 copyUriToInternalStorage(
99 context,
100 driverPathUri,
101 driverInstallationPath!!,
102 DRIVER_INTERNAL_FILENAME
103 )
104
105 // Unzip the driver.
106 try {
107 unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
108 } catch (e: SecurityException) {
109 return
110 }
111
112 // Initialize the driver parameters.
113 initializeDriverParameters(context)
114 }
115
116 // Parse the custom driver metadata to retrieve the name.
117 val customDriverName: String?
118 get() {
119 val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
120 return metadata.name
121 }
122
123 // Parse the custom driver metadata to retrieve the library name.
124 private val customDriverLibraryName: String?
125 get() {
126 // Parse the custom driver metadata to retrieve the library name.
127 val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
128 return metadata.libraryName
129 }
130
131 private fun initializeDirectories() {
132 // Ensure the file redirection directory exists.
133 val fileRedirectionDir = File(fileRedirectionPath!!)
134 if (!fileRedirectionDir.exists()) {
135 fileRedirectionDir.mkdirs()
136 }
137 // Ensure the driver installation directory exists.
138 val driverInstallationDir = File(driverInstallationPath!!)
139 if (!driverInstallationDir.exists()) {
140 driverInstallationDir.mkdirs()
141 }
142 }
143
144 private fun deleteRecursive(fileOrDirectory: File) {
145 if (fileOrDirectory.isDirectory) {
146 for (child in fileOrDirectory.listFiles()!!) {
147 deleteRecursive(child)
148 }
149 }
150 fileOrDirectory.delete()
151 }
152}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
new file mode 100644
index 000000000..70bdb4097
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
@@ -0,0 +1,47 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import org.json.JSONException
7import org.json.JSONObject
8import java.io.IOException
9import java.nio.charset.StandardCharsets
10import java.nio.file.Files
11import java.nio.file.Paths
12
13class GpuDriverMetadata(metadataFilePath: String) {
14 var name: String? = null
15 var description: String? = null
16 var author: String? = null
17 var vendor: String? = null
18 var driverVersion: String? = null
19 var minApi = 0
20 var libraryName: String? = null
21
22 init {
23 try {
24 val json = JSONObject(getStringFromFile(metadataFilePath))
25 name = json.getString("name")
26 description = json.getString("description")
27 author = json.getString("author")
28 vendor = json.getString("vendor")
29 driverVersion = json.getString("driverVersion")
30 minApi = json.getInt("minApi")
31 libraryName = json.getString("libraryName")
32 } catch (e: JSONException) {
33 // JSON is malformed, ignore and treat as unsupported metadata.
34 } catch (e: IOException) {
35 // File is inaccessible, ignore and treat as unsupported metadata.
36 }
37 }
38
39 companion object {
40 @Throws(IOException::class)
41 private fun getStringFromFile(filePath: String): String {
42 val path = Paths.get(filePath)
43 val bytes = Files.readAllBytes(path)
44 return String(bytes, StandardCharsets.UTF_8)
45 }
46 }
47}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
new file mode 100644
index 000000000..24e999b29
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
@@ -0,0 +1,360 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.view.KeyEvent
7import android.view.MotionEvent
8import org.yuzu.yuzu_emu.NativeLibrary
9import kotlin.math.sqrt
10
11class InputHandler {
12 fun initialize() {
13 // Connect first controller
14 NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
15 }
16
17 fun dispatchKeyEvent(event: KeyEvent): Boolean {
18 val button: Int = when (event.device.vendorId) {
19 0x045E -> getInputXboxButtonKey(event.keyCode)
20 0x054C -> getInputDS5ButtonKey(event.keyCode)
21 0x057E -> getInputJoyconButtonKey(event.keyCode)
22 0x1532 -> getInputRazerButtonKey(event.keyCode)
23 else -> getInputGenericButtonKey(event.keyCode)
24 }
25
26 val action = when (event.action) {
27 KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED
28 KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
29 else -> return false
30 }
31
32 // Ignore invalid buttons
33 if (button < 0) {
34 return false
35 }
36
37 return NativeLibrary.onGamePadButtonEvent(
38 getPlayerNumber(event.device.controllerNumber),
39 button,
40 action
41 )
42 }
43
44 fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
45 val device = event.device
46 // Check every axis input available on the controller
47 for (range in device.motionRanges) {
48 val axis = range.axis
49 when (device.vendorId) {
50 0x045E -> setGenericAxisInput(event, axis)
51 0x054C -> setGenericAxisInput(event, axis)
52 0x057E -> setJoyconAxisInput(event, axis)
53 0x1532 -> setRazerAxisInput(event, axis)
54 else -> setGenericAxisInput(event, axis)
55 }
56 }
57
58 return true
59 }
60
61 private fun getPlayerNumber(index: Int): Int {
62 // TODO: Joycons are handled as different controllers. Find a way to merge them.
63 return when (index) {
64 2 -> NativeLibrary.Player2Device
65 3 -> NativeLibrary.Player3Device
66 4 -> NativeLibrary.Player4Device
67 5 -> NativeLibrary.Player5Device
68 6 -> NativeLibrary.Player6Device
69 7 -> NativeLibrary.Player7Device
70 8 -> NativeLibrary.Player8Device
71 else -> if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
72 }
73 }
74
75 private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
76 // Calculate vector size
77 val r2 = xAxis * xAxis + yAxis * yAxis
78 var r = sqrt(r2.toDouble()).toFloat()
79
80 // Adjust range of joystick
81 val deadzone = 0.15f
82 var x = xAxis
83 var y = yAxis
84
85 if (r > deadzone) {
86 val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
87 x *= deadzoneFactor
88 y *= deadzoneFactor
89 r *= deadzoneFactor
90 } else {
91 x = 0.0f
92 y = 0.0f
93 }
94
95 // Normalize joystick
96 if (r > 1.0f) {
97 x /= r
98 y /= r
99 }
100
101 NativeLibrary.onGamePadJoystickEvent(
102 playerNumber,
103 index,
104 x,
105 -y
106 )
107 }
108
109 private fun getAxisToButton(axis: Float): Int {
110 return if (axis > 0.5f) NativeLibrary.ButtonState.PRESSED else NativeLibrary.ButtonState.RELEASED
111 }
112
113 private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) {
114 NativeLibrary.onGamePadButtonEvent(
115 playerNumber,
116 NativeLibrary.ButtonType.DPAD_UP,
117 getAxisToButton(-yAxis)
118 )
119 NativeLibrary.onGamePadButtonEvent(
120 playerNumber,
121 NativeLibrary.ButtonType.DPAD_DOWN,
122 getAxisToButton(yAxis)
123 )
124 NativeLibrary.onGamePadButtonEvent(
125 playerNumber,
126 NativeLibrary.ButtonType.DPAD_LEFT,
127 getAxisToButton(-xAxis)
128 )
129 NativeLibrary.onGamePadButtonEvent(
130 playerNumber,
131 NativeLibrary.ButtonType.DPAD_RIGHT,
132 getAxisToButton(xAxis)
133 )
134 }
135
136 private fun getInputDS5ButtonKey(key: Int): Int {
137 // The missing ds5 buttons are axis
138 return when (key) {
139 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
140 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
141 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
142 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
143 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
144 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
145 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
146 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
147 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
148 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
149 else -> -1
150 }
151 }
152
153 private fun getInputJoyconButtonKey(key: Int): Int {
154 // Joycon support is half dead. A lot of buttons can't be mapped
155 return when (key) {
156 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
157 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
158 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
159 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
160 KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
161 KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
162 KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
163 KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
164 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
165 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
166 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
167 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
168 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
169 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
170 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
171 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
172 else -> -1
173 }
174 }
175
176 private fun getInputXboxButtonKey(key: Int): Int {
177 // The missing xbox buttons are axis
178 return when (key) {
179 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
180 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
181 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
182 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
183 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
184 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
185 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
186 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
187 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
188 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
189 else -> -1
190 }
191 }
192
193 private fun getInputRazerButtonKey(key: Int): Int {
194 // The missing xbox buttons are axis
195 return when (key) {
196 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
197 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
198 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
199 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
200 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
201 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
202 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
203 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
204 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
205 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
206 else -> -1
207 }
208 }
209
210 private fun getInputGenericButtonKey(key: Int): Int {
211 return when (key) {
212 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
213 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
214 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
215 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
216 KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
217 KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
218 KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
219 KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
220 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
221 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
222 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
223 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
224 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
225 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
226 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
227 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
228 else -> -1
229 }
230 }
231
232 private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
233 val playerNumber = getPlayerNumber(event.device.controllerNumber)
234
235 when (axis) {
236 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
237 setStickState(
238 playerNumber,
239 NativeLibrary.StickType.STICK_L,
240 event.getAxisValue(MotionEvent.AXIS_X),
241 event.getAxisValue(MotionEvent.AXIS_Y)
242 )
243 MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
244 setStickState(
245 playerNumber,
246 NativeLibrary.StickType.STICK_R,
247 event.getAxisValue(MotionEvent.AXIS_RX),
248 event.getAxisValue(MotionEvent.AXIS_RY)
249 )
250 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
251 setStickState(
252 playerNumber,
253 NativeLibrary.StickType.STICK_R,
254 event.getAxisValue(MotionEvent.AXIS_Z),
255 event.getAxisValue(MotionEvent.AXIS_RZ)
256 )
257 MotionEvent.AXIS_LTRIGGER ->
258 NativeLibrary.onGamePadButtonEvent(
259 playerNumber,
260 NativeLibrary.ButtonType.TRIGGER_ZL,
261 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
262 )
263 MotionEvent.AXIS_BRAKE ->
264 NativeLibrary.onGamePadButtonEvent(
265 playerNumber,
266 NativeLibrary.ButtonType.TRIGGER_ZL,
267 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
268 )
269 MotionEvent.AXIS_RTRIGGER ->
270 NativeLibrary.onGamePadButtonEvent(
271 playerNumber,
272 NativeLibrary.ButtonType.TRIGGER_ZR,
273 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
274 )
275 MotionEvent.AXIS_GAS ->
276 NativeLibrary.onGamePadButtonEvent(
277 playerNumber,
278 NativeLibrary.ButtonType.TRIGGER_ZR,
279 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
280 )
281 MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
282 setAxisDpadState(
283 playerNumber,
284 event.getAxisValue(MotionEvent.AXIS_HAT_X),
285 event.getAxisValue(MotionEvent.AXIS_HAT_Y)
286 )
287 }
288 }
289
290
291 private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
292 // Joycon support is half dead. Right joystick doesn't work
293 val playerNumber = getPlayerNumber(event.device.controllerNumber)
294
295 when (axis) {
296 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
297 setStickState(
298 playerNumber,
299 NativeLibrary.StickType.STICK_L,
300 event.getAxisValue(MotionEvent.AXIS_X),
301 event.getAxisValue(MotionEvent.AXIS_Y)
302 )
303 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
304 setStickState(
305 playerNumber,
306 NativeLibrary.StickType.STICK_R,
307 event.getAxisValue(MotionEvent.AXIS_Z),
308 event.getAxisValue(MotionEvent.AXIS_RZ)
309 )
310 MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
311 setStickState(
312 playerNumber,
313 NativeLibrary.StickType.STICK_R,
314 event.getAxisValue(MotionEvent.AXIS_RX),
315 event.getAxisValue(MotionEvent.AXIS_RY)
316 )
317 }
318 }
319
320 private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
321 val playerNumber = getPlayerNumber(event.device.controllerNumber)
322
323 when (axis) {
324 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
325 setStickState(
326 playerNumber,
327 NativeLibrary.StickType.STICK_L,
328 event.getAxisValue(MotionEvent.AXIS_X),
329 event.getAxisValue(MotionEvent.AXIS_Y)
330 )
331 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
332 setStickState(
333 playerNumber,
334 NativeLibrary.StickType.STICK_R,
335 event.getAxisValue(MotionEvent.AXIS_Z),
336 event.getAxisValue(MotionEvent.AXIS_RZ)
337 )
338 MotionEvent.AXIS_BRAKE ->
339 NativeLibrary.onGamePadButtonEvent(
340 playerNumber,
341 NativeLibrary.ButtonType.TRIGGER_ZL,
342 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
343 )
344 MotionEvent.AXIS_GAS ->
345 NativeLibrary.onGamePadButtonEvent(
346 playerNumber,
347 NativeLibrary.ButtonType.TRIGGER_ZR,
348 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
349 )
350 MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
351 setAxisDpadState(
352 playerNumber,
353 event.getAxisValue(MotionEvent.AXIS_HAT_X),
354 event.getAxisValue(MotionEvent.AXIS_HAT_Y)
355 )
356 }
357 }
358
359
360} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt
new file mode 100644
index 000000000..19c53c481
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt
@@ -0,0 +1,31 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.annotation.SuppressLint
7import android.app.Activity
8import android.content.Context
9import android.graphics.Rect
10
11object InsetsHelper {
12 const val THREE_BUTTON_NAVIGATION = 0
13 const val TWO_BUTTON_NAVIGATION = 1
14 const val GESTURE_NAVIGATION = 2
15
16 @SuppressLint("DiscouragedApi")
17 fun getSystemGestureType(context: Context): Int {
18 val resources = context.resources
19 val resourceId =
20 resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
21 return if (resourceId != 0) {
22 resources.getInteger(resourceId)
23 } else 0
24 }
25
26 fun getBottomPaddingRequired(activity: Activity): Int {
27 val visibleFrame = Rect()
28 activity.window.decorView.getWindowVisibleDisplayFrame(visibleFrame)
29 return visibleFrame.bottom - visibleFrame.top - activity.resources.displayMetrics.heightPixels
30 }
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt
new file mode 100644
index 000000000..a193e82a4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt
@@ -0,0 +1,40 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.util.Log
7import org.yuzu.yuzu_emu.BuildConfig
8
9/**
10 * Contains methods that call through to [android.util.Log], but
11 * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
12 * levels in release builds.
13 */
14object Log {
15 private const val TAG = "Yuzu Frontend"
16
17 fun verbose(message: String) {
18 if (BuildConfig.DEBUG) {
19 Log.v(TAG, message)
20 }
21 }
22
23 fun debug(message: String) {
24 if (BuildConfig.DEBUG) {
25 Log.d(TAG, message)
26 }
27 }
28
29 fun info(message: String) {
30 Log.i(TAG, message)
31 }
32
33 fun warning(message: String) {
34 Log.w(TAG, message)
35 }
36
37 fun error(message: String) {
38 Log.e(TAG, message)
39 }
40}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
new file mode 100644
index 000000000..344dd8a0a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
@@ -0,0 +1,168 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.app.Activity
7import android.app.PendingIntent
8import android.content.Intent
9import android.content.IntentFilter
10import android.nfc.NfcAdapter
11import android.nfc.Tag
12import android.nfc.tech.NfcA
13import android.os.Build
14import android.os.Handler
15import android.os.Looper
16import org.yuzu.yuzu_emu.NativeLibrary
17import java.io.IOException
18
19class NfcReader(private val activity: Activity) {
20 private var nfcAdapter: NfcAdapter? = null
21 private var pendingIntent: PendingIntent? = null
22
23 fun initialize() {
24 nfcAdapter = NfcAdapter.getDefaultAdapter(activity) ?: return
25
26 pendingIntent = PendingIntent.getActivity(
27 activity,
28 0, Intent(activity, activity.javaClass),
29 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
30 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
31 else PendingIntent.FLAG_UPDATE_CURRENT
32 )
33
34 val tagDetected = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
35 tagDetected.addCategory(Intent.CATEGORY_DEFAULT)
36 }
37
38 fun startScanning() {
39 nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, null, null)
40 }
41
42 fun stopScanning() {
43 nfcAdapter?.disableForegroundDispatch(activity)
44 }
45
46 fun onNewIntent(intent: Intent) {
47 val action = intent.action
48 if (NfcAdapter.ACTION_TAG_DISCOVERED != action
49 && NfcAdapter.ACTION_TECH_DISCOVERED != action
50 && NfcAdapter.ACTION_NDEF_DISCOVERED != action
51 ) {
52 return
53 }
54
55 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
56 val tag =
57 intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) ?: return
58 readTagData(tag)
59 return
60 }
61
62 val tag =
63 intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
64 readTagData(tag)
65 }
66
67 private fun readTagData(tag: Tag) {
68 if (!tag.techList.contains("android.nfc.tech.NfcA")) {
69 return
70 }
71
72 val amiibo = NfcA.get(tag) ?: return
73 amiibo.connect()
74
75 val tagData = ntag215ReadAll(amiibo) ?: return
76 NativeLibrary.onReadNfcTag(tagData)
77
78 nfcAdapter?.ignore(
79 tag,
80 1000,
81 { NativeLibrary.onRemoveNfcTag() },
82 Handler(Looper.getMainLooper())
83 )
84 }
85
86 private fun ntag215ReadAll(amiibo: NfcA): ByteArray? {
87 val bufferSize = amiibo.maxTransceiveLength;
88 val tagSize = 0x21C
89 val pageSize = 4
90 val lastPage = tagSize / pageSize - 1
91 val tagData = ByteArray(tagSize)
92
93 // We need to read the ntag in steps otherwise we overflow the buffer
94 for (i in 0..tagSize step bufferSize - 1) {
95 val dataStart = i / pageSize
96 var dataEnd = (i + bufferSize) / pageSize
97
98 if (dataEnd > lastPage) {
99 dataEnd = lastPage
100 }
101
102 try {
103 val data = ntag215FastRead(amiibo, dataStart, dataEnd - 1)
104 System.arraycopy(data, 0, tagData, i, (dataEnd - dataStart) * pageSize)
105 } catch (e: IOException) {
106 return null;
107 }
108 }
109 return tagData
110 }
111
112 private fun ntag215Read(amiibo: NfcA, page: Int): ByteArray? {
113 return amiibo.transceive(
114 byteArrayOf(
115 0x30.toByte(),
116 (page and 0xFF).toByte()
117 )
118 )
119 }
120
121 private fun ntag215FastRead(amiibo: NfcA, start: Int, end: Int): ByteArray? {
122 return amiibo.transceive(
123 byteArrayOf(
124 0x3A.toByte(),
125 (start and 0xFF).toByte(),
126 (end and 0xFF).toByte()
127 )
128 )
129 }
130
131 private fun ntag215PWrite(
132 amiibo: NfcA,
133 page: Int,
134 data1: Int,
135 data2: Int,
136 data3: Int,
137 data4: Int
138 ): ByteArray? {
139 return amiibo.transceive(
140 byteArrayOf(
141 0xA2.toByte(),
142 (page and 0xFF).toByte(),
143 (data1 and 0xFF).toByte(),
144 (data2 and 0xFF).toByte(),
145 (data3 and 0xFF).toByte(),
146 (data4 and 0xFF).toByte()
147 )
148 )
149 }
150
151 private fun ntag215PwdAuth(
152 amiibo: NfcA,
153 data1: Int,
154 data2: Int,
155 data3: Int,
156 data4: Int
157 ): ByteArray? {
158 return amiibo.transceive(
159 byteArrayOf(
160 0x1B.toByte(),
161 (data1 and 0xFF).toByte(),
162 (data2 and 0xFF).toByte(),
163 (data3 and 0xFF).toByte(),
164 (data4 and 0xFF).toByte()
165 )
166 )
167 }
168}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt
new file mode 100644
index 000000000..87ee7f2e6
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt
@@ -0,0 +1,40 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.content.Intent
7import android.os.Build
8import android.os.Bundle
9import android.os.Parcelable
10import java.io.Serializable
11
12object SerializableHelper {
13 inline fun <reified T : Serializable> Bundle.serializable(key: String): T? {
14 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
15 getSerializable(key, T::class.java)
16 else
17 getSerializable(key) as? T
18 }
19
20 inline fun <reified T : Serializable> Intent.serializable(key: String): T? {
21 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
22 getSerializableExtra(key, T::class.java)
23 else
24 getSerializableExtra(key) as? T
25 }
26
27 inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? {
28 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
29 getParcelable(key, T::class.java)
30 else
31 getParcelable(key) as? T
32 }
33
34 inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? {
35 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
36 getParcelableExtra(key, T::class.java)
37 else
38 getParcelableExtra(key) as? T
39 }
40}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt
new file mode 100644
index 000000000..e55767c0f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt
@@ -0,0 +1,97 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.app.Activity
7import android.content.res.Configuration
8import android.graphics.Color
9import androidx.annotation.ColorInt
10import androidx.appcompat.app.AppCompatActivity
11import androidx.appcompat.app.AppCompatDelegate
12import androidx.core.content.ContextCompat
13import androidx.core.view.WindowCompat
14import androidx.core.view.WindowInsetsControllerCompat
15import androidx.preference.PreferenceManager
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.features.settings.model.Settings
19import org.yuzu.yuzu_emu.ui.main.ThemeProvider
20import kotlin.math.roundToInt
21
22object ThemeHelper {
23 const val SYSTEM_BAR_ALPHA = 0.9f
24
25 private const val DEFAULT = 0
26 private const val MATERIAL_YOU = 1
27
28 fun setTheme(activity: AppCompatActivity) {
29 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
30 setThemeMode(activity)
31 when (preferences.getInt(Settings.PREF_THEME, 0)) {
32 DEFAULT -> activity.setTheme(R.style.Theme_Yuzu_Main)
33 MATERIAL_YOU -> activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
34 }
35
36 // Using a specific night mode check because this could apply incorrectly when using the
37 // light app mode, dark system mode, and black backgrounds. Launching the settings activity
38 // will then show light mode colors/navigation bars but with black backgrounds.
39 if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
40 && isNightMode(activity)
41 ) {
42 activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark)
43 }
44 }
45
46 @ColorInt
47 fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
48 return Color.argb(
49 (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color),
50 Color.green(color), Color.blue(color)
51 )
52 }
53
54 fun setCorrectTheme(activity: AppCompatActivity) {
55 val currentTheme = (activity as ThemeProvider).themeId
56 setTheme(activity)
57 if (currentTheme != (activity as ThemeProvider).themeId) {
58 activity.recreate()
59 }
60 }
61
62 fun setThemeMode(activity: AppCompatActivity) {
63 val themeMode = PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
64 .getInt(Settings.PREF_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
65 activity.delegate.localNightMode = themeMode
66 val windowController = WindowCompat.getInsetsController(
67 activity.window,
68 activity.window.decorView
69 )
70 when (themeMode) {
71 AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> when (isNightMode(activity)) {
72 false -> setLightModeSystemBars(windowController)
73 true -> setDarkModeSystemBars(windowController)
74 }
75 AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
76 AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
77 }
78 }
79
80 private fun isNightMode(activity: AppCompatActivity): Boolean {
81 return when (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
82 Configuration.UI_MODE_NIGHT_NO -> false
83 Configuration.UI_MODE_NIGHT_YES -> true
84 else -> false
85 }
86 }
87
88 private fun setLightModeSystemBars(windowController: WindowInsetsControllerCompat) {
89 windowController.isAppearanceLightStatusBars = true
90 windowController.isAppearanceLightNavigationBars = true
91 }
92
93 private fun setDarkModeSystemBars(windowController: WindowInsetsControllerCompat) {
94 windowController.isAppearanceLightStatusBars = false
95 windowController.isAppearanceLightNavigationBars = false
96 }
97}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
new file mode 100644
index 000000000..c8ef8c1fd
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
@@ -0,0 +1,46 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.views
5
6import android.content.Context
7import android.util.AttributeSet
8import android.util.Rational
9import android.view.SurfaceView
10import kotlin.math.roundToInt
11
12class FixedRatioSurfaceView @JvmOverloads constructor(
13 context: Context,
14 attrs: AttributeSet? = null,
15 defStyleAttr: Int = 0
16) : SurfaceView(context, attrs, defStyleAttr) {
17 private var aspectRatio: Float = 0f // (width / height), 0f is a special value for stretch
18
19 /**
20 * Sets the desired aspect ratio for this view
21 * @param ratio the ratio to force the view to, or null to stretch to fit
22 */
23 fun setAspectRatio(ratio: Rational?) {
24 aspectRatio = ratio?.toFloat() ?: 0f
25 }
26
27 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
28 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
29 val width = MeasureSpec.getSize(widthMeasureSpec)
30 val height = MeasureSpec.getSize(heightMeasureSpec)
31 if (aspectRatio != 0f) {
32 val newWidth: Int
33 val newHeight: Int
34 if (height * aspectRatio < width) {
35 newWidth = (height * aspectRatio).roundToInt()
36 newHeight = height
37 } else {
38 newWidth = width
39 newHeight = (width / aspectRatio).roundToInt()
40 }
41 setMeasuredDimension(newWidth, newHeight)
42 } else {
43 setMeasuredDimension(width, height)
44 }
45 }
46}
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
new file mode 100644
index 000000000..041781577
--- /dev/null
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -0,0 +1,28 @@
1# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2# SPDX-License-Identifier: GPL-3.0-or-later
3
4add_library(yuzu-android SHARED
5 android_common/android_common.cpp
6 android_common/android_common.h
7 applets/software_keyboard.cpp
8 applets/software_keyboard.h
9 config.cpp
10 config.h
11 default_ini.h
12 emu_window/emu_window.cpp
13 emu_window/emu_window.h
14 id_cache.cpp
15 id_cache.h
16 native.cpp
17 native.h
18)
19
20set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
21
22target_link_libraries(yuzu-android PRIVATE audio_core common core input_common)
23target_link_libraries(yuzu-android PRIVATE android camera2ndk EGL glad inih jnigraphics log)
24if (ARCHITECTURE_arm64)
25 target_link_libraries(yuzu-android PRIVATE adrenotools)
26endif()
27
28set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} yuzu-android)
diff --git a/src/android/app/src/main/jni/android_common/android_common.cpp b/src/android/app/src/main/jni/android_common/android_common.cpp
new file mode 100644
index 000000000..52d8ecfeb
--- /dev/null
+++ b/src/android/app/src/main/jni/android_common/android_common.cpp
@@ -0,0 +1,35 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "jni/android_common/android_common.h"
5
6#include <string>
7#include <string_view>
8
9#include <jni.h>
10
11#include "common/string_util.h"
12
13std::string GetJString(JNIEnv* env, jstring jstr) {
14 if (!jstr) {
15 return {};
16 }
17
18 const jchar* jchars = env->GetStringChars(jstr, nullptr);
19 const jsize length = env->GetStringLength(jstr);
20 const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
21 const std::string converted_string = Common::UTF16ToUTF8(string_view);
22 env->ReleaseStringChars(jstr, jchars);
23
24 return converted_string;
25}
26
27jstring ToJString(JNIEnv* env, std::string_view str) {
28 const std::u16string converted_string = Common::UTF8ToUTF16(str);
29 return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
30 static_cast<jint>(converted_string.size()));
31}
32
33jstring ToJString(JNIEnv* env, std::u16string_view str) {
34 return ToJString(env, Common::UTF16ToUTF8(str));
35}
diff --git a/src/android/app/src/main/jni/android_common/android_common.h b/src/android/app/src/main/jni/android_common/android_common.h
new file mode 100644
index 000000000..ccb0c06f7
--- /dev/null
+++ b/src/android/app/src/main/jni/android_common/android_common.h
@@ -0,0 +1,12 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <string>
7
8#include <jni.h>
9
10std::string GetJString(JNIEnv* env, jstring jstr);
11jstring ToJString(JNIEnv* env, std::string_view str);
12jstring ToJString(JNIEnv* env, std::u16string_view str);
diff --git a/src/android/app/src/main/jni/applets/software_keyboard.cpp b/src/android/app/src/main/jni/applets/software_keyboard.cpp
new file mode 100644
index 000000000..74e040478
--- /dev/null
+++ b/src/android/app/src/main/jni/applets/software_keyboard.cpp
@@ -0,0 +1,277 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <map>
5#include <thread>
6
7#include <jni.h>
8
9#include "common/logging/log.h"
10#include "common/string_util.h"
11#include "core/core.h"
12#include "jni/android_common/android_common.h"
13#include "jni/applets/software_keyboard.h"
14#include "jni/id_cache.h"
15
16static jclass s_software_keyboard_class;
17static jclass s_keyboard_config_class;
18static jclass s_keyboard_data_class;
19static jmethodID s_swkbd_execute_normal;
20static jmethodID s_swkbd_execute_inline;
21
22namespace SoftwareKeyboard {
23
24static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
25 JNIEnv* env = IDCache::GetEnvForThread();
26 jobject object = env->AllocObject(s_keyboard_config_class);
27
28 env->SetObjectField(object,
29 env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"),
30 ToJString(env, config.ok_text));
31 env->SetObjectField(
32 object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"),
33 ToJString(env, config.header_text));
34 env->SetObjectField(object,
35 env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"),
36 ToJString(env, config.sub_text));
37 env->SetObjectField(
38 object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"),
39 ToJString(env, config.guide_text));
40 env->SetObjectField(
41 object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"),
42 ToJString(env, config.initial_text));
43 env->SetShortField(object,
44 env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"),
45 static_cast<jshort>(config.left_optional_symbol_key));
46 env->SetShortField(object,
47 env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"),
48 static_cast<jshort>(config.right_optional_symbol_key));
49 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
50 static_cast<jint>(config.max_text_length));
51 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"),
52 static_cast<jint>(config.min_text_length));
53 env->SetIntField(object,
54 env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"),
55 static_cast<jint>(config.initial_cursor_position));
56 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"),
57 static_cast<jint>(config.type));
58 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"),
59 static_cast<jint>(config.password_mode));
60 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"),
61 static_cast<jint>(config.text_draw_type));
62 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"),
63 static_cast<jint>(config.key_disable_flags.raw));
64 env->SetBooleanField(object,
65 env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"),
66 static_cast<jboolean>(config.use_blur_background));
67 env->SetBooleanField(object,
68 env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"),
69 static_cast<jboolean>(config.enable_backspace_button));
70 env->SetBooleanField(object,
71 env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"),
72 static_cast<jboolean>(config.enable_return_button));
73 env->SetBooleanField(object,
74 env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"),
75 static_cast<jboolean>(config.disable_cancel_button));
76
77 return object;
78}
79
80AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) {
81 JNIEnv* env = IDCache::GetEnvForThread();
82 const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
83 object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
84 return ResultData{GetJString(env, string),
85 static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
86 object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
87}
88
89AndroidKeyboard::~AndroidKeyboard() = default;
90
91void AndroidKeyboard::InitializeKeyboard(
92 bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters,
93 SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) {
94 if (is_inline) {
95 LOG_WARNING(
96 Frontend,
97 "(STUBBED) called, backend requested to initialize the inline software keyboard.");
98
99 submit_inline_callback = std::move(submit_inline_callback_);
100 } else {
101 LOG_WARNING(
102 Frontend,
103 "(STUBBED) called, backend requested to initialize the normal software keyboard.");
104
105 submit_normal_callback = std::move(submit_normal_callback_);
106 }
107
108 parameters = std::move(initialize_parameters);
109
110 LOG_INFO(Frontend,
111 "\nKeyboardInitializeParameters:"
112 "\nok_text={}"
113 "\nheader_text={}"
114 "\nsub_text={}"
115 "\nguide_text={}"
116 "\ninitial_text={}"
117 "\nmax_text_length={}"
118 "\nmin_text_length={}"
119 "\ninitial_cursor_position={}"
120 "\ntype={}"
121 "\npassword_mode={}"
122 "\ntext_draw_type={}"
123 "\nkey_disable_flags={}"
124 "\nuse_blur_background={}"
125 "\nenable_backspace_button={}"
126 "\nenable_return_button={}"
127 "\ndisable_cancel_button={}",
128 Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text),
129 Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text),
130 Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length,
131 parameters.min_text_length, parameters.initial_cursor_position, parameters.type,
132 parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw,
133 parameters.use_blur_background, parameters.enable_backspace_button,
134 parameters.enable_return_button, parameters.disable_cancel_button);
135}
136
137void AndroidKeyboard::ShowNormalKeyboard() const {
138 LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard.");
139
140 ResultData data{};
141
142 // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
143 std::thread([&] {
144 data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod(
145 s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters)));
146 }).join();
147
148 SubmitNormalText(data);
149}
150
151void AndroidKeyboard::ShowTextCheckDialog(
152 Service::AM::Applets::SwkbdTextCheckResult text_check_result,
153 std::u16string text_check_message) const {
154 LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
155}
156
157void AndroidKeyboard::ShowInlineKeyboard(
158 Core::Frontend::InlineAppearParameters appear_parameters) const {
159 LOG_WARNING(Frontend,
160 "(STUBBED) called, backend requested to show the inline software keyboard.");
161
162 LOG_INFO(Frontend,
163 "\nInlineAppearParameters:"
164 "\nmax_text_length={}"
165 "\nmin_text_length={}"
166 "\nkey_top_scale_x={}"
167 "\nkey_top_scale_y={}"
168 "\nkey_top_translate_x={}"
169 "\nkey_top_translate_y={}"
170 "\ntype={}"
171 "\nkey_disable_flags={}"
172 "\nkey_top_as_floating={}"
173 "\nenable_backspace_button={}"
174 "\nenable_return_button={}"
175 "\ndisable_cancel_button={}",
176 appear_parameters.max_text_length, appear_parameters.min_text_length,
177 appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y,
178 appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y,
179 appear_parameters.type, appear_parameters.key_disable_flags.raw,
180 appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button,
181 appear_parameters.enable_return_button, appear_parameters.disable_cancel_button);
182
183 // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
184 m_is_inline_active = true;
185 std::thread([&] {
186 IDCache::GetEnvForThread()->CallStaticVoidMethod(
187 s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters));
188 }).join();
189}
190
191void AndroidKeyboard::HideInlineKeyboard() const {
192 LOG_WARNING(Frontend,
193 "(STUBBED) called, backend requested to hide the inline software keyboard.");
194}
195
196void AndroidKeyboard::InlineTextChanged(
197 Core::Frontend::InlineTextParameters text_parameters) const {
198 LOG_WARNING(Frontend,
199 "(STUBBED) called, backend requested to change the inline keyboard text.");
200
201 LOG_INFO(Frontend,
202 "\nInlineTextParameters:"
203 "\ninput_text={}"
204 "\ncursor_position={}",
205 Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
206
207 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
208 text_parameters.input_text, text_parameters.cursor_position);
209}
210
211void AndroidKeyboard::ExitKeyboard() const {
212 LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard.");
213}
214
215void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
216 if (!m_is_inline_active) {
217 return;
218 }
219
220 m_current_text += submitted_text;
221
222 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
223 m_current_text.size());
224}
225
226void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
227 static constexpr int KEYCODE_BACK = 4;
228 static constexpr int KEYCODE_ENTER = 66;
229 static constexpr int KEYCODE_DEL = 67;
230
231 if (!m_is_inline_active) {
232 return;
233 }
234
235 switch (key_code) {
236 case KEYCODE_BACK:
237 case KEYCODE_ENTER:
238 m_is_inline_active = false;
239 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
240 static_cast<s32>(m_current_text.size()));
241 break;
242 case KEYCODE_DEL:
243 m_current_text.pop_back();
244 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
245 m_current_text.size());
246 break;
247 }
248}
249
250void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
251 submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true);
252}
253
254void InitJNI(JNIEnv* env) {
255 s_software_keyboard_class = reinterpret_cast<jclass>(
256 env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard")));
257 s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
258 env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig")));
259 s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
260 env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardData")));
261
262 s_swkbd_execute_normal = env->GetStaticMethodID(
263 s_software_keyboard_class, "executeNormal",
264 "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
265 "applets/keyboard/SoftwareKeyboard$KeyboardData;");
266 s_swkbd_execute_inline = env->GetStaticMethodID(
267 s_software_keyboard_class, "executeInline",
268 "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)V");
269}
270
271void CleanupJNI(JNIEnv* env) {
272 env->DeleteGlobalRef(s_software_keyboard_class);
273 env->DeleteGlobalRef(s_keyboard_config_class);
274 env->DeleteGlobalRef(s_keyboard_data_class);
275}
276
277} // namespace SoftwareKeyboard
diff --git a/src/android/app/src/main/jni/applets/software_keyboard.h b/src/android/app/src/main/jni/applets/software_keyboard.h
new file mode 100644
index 000000000..b2fb59b68
--- /dev/null
+++ b/src/android/app/src/main/jni/applets/software_keyboard.h
@@ -0,0 +1,78 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <jni.h>
7
8#include "core/frontend/applets/software_keyboard.h"
9
10namespace SoftwareKeyboard {
11
12class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet {
13public:
14 ~AndroidKeyboard() override;
15
16 void Close() const override {
17 ExitKeyboard();
18 }
19
20 void InitializeKeyboard(bool is_inline,
21 Core::Frontend::KeyboardInitializeParameters initialize_parameters,
22 SubmitNormalCallback submit_normal_callback_,
23 SubmitInlineCallback submit_inline_callback_) override;
24
25 void ShowNormalKeyboard() const override;
26
27 void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
28 std::u16string text_check_message) const override;
29
30 void ShowInlineKeyboard(
31 Core::Frontend::InlineAppearParameters appear_parameters) const override;
32
33 void HideInlineKeyboard() const override;
34
35 void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
36
37 void ExitKeyboard() const override;
38
39 void SubmitInlineKeyboardText(std::u16string submitted_text);
40
41 void SubmitInlineKeyboardInput(int key_code);
42
43private:
44 struct ResultData {
45 static ResultData CreateFromFrontend(jobject object);
46
47 std::string text;
48 Service::AM::Applets::SwkbdResult result{};
49 };
50
51 void SubmitNormalText(const ResultData& result) const;
52
53 Core::Frontend::KeyboardInitializeParameters parameters{};
54
55 mutable SubmitNormalCallback submit_normal_callback;
56 mutable SubmitInlineCallback submit_inline_callback;
57
58private:
59 mutable bool m_is_inline_active{};
60 std::u16string m_current_text;
61};
62
63// Should be called in JNI_Load
64void InitJNI(JNIEnv* env);
65
66// Should be called in JNI_Unload
67void CleanupJNI(JNIEnv* env);
68
69} // namespace SoftwareKeyboard
70
71// Native function calls
72extern "C" {
73JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(
74 JNIEnv* env, jclass clazz, jstring text);
75
76JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(
77 JNIEnv* env, jclass clazz, jstring text);
78}
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
new file mode 100644
index 000000000..2d622a048
--- /dev/null
+++ b/src/android/app/src/main/jni/config.cpp
@@ -0,0 +1,297 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <memory>
5#include <optional>
6#include <sstream>
7
8#include <INIReader.h>
9#include "common/fs/file.h"
10#include "common/fs/fs.h"
11#include "common/fs/path_util.h"
12#include "common/logging/log.h"
13#include "common/settings.h"
14#include "core/hle/service/acc/profile_manager.h"
15#include "input_common/main.h"
16#include "jni/config.h"
17#include "jni/default_ini.h"
18
19namespace FS = Common::FS;
20
21Config::Config(std::optional<std::filesystem::path> config_path)
22 : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")},
23 config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
24 Reload();
25}
26
27Config::~Config() = default;
28
29bool Config::LoadINI(const std::string& default_contents, bool retry) {
30 const auto config_loc_str = FS::PathToUTF8String(config_loc);
31 if (config->ParseError() < 0) {
32 if (retry) {
33 LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...",
34 config_loc_str);
35
36 void(FS::CreateParentDir(config_loc));
37 void(FS::WriteStringToFile(config_loc, FS::FileType::TextFile, default_contents));
38
39 config = std::make_unique<INIReader>(config_loc_str);
40
41 return LoadINI(default_contents, false);
42 }
43 LOG_ERROR(Config, "Failed.");
44 return false;
45 }
46 LOG_INFO(Config, "Successfully loaded {}", config_loc_str);
47 return true;
48}
49
50template <>
51void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
52 std::string setting_value = config->Get(group, setting.GetLabel(), setting.GetDefault());
53 if (setting_value.empty()) {
54 setting_value = setting.GetDefault();
55 }
56 setting = std::move(setting_value);
57}
58
59template <>
60void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) {
61 setting = config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
62}
63
64template <typename Type, bool ranged>
65void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
66 setting = static_cast<Type>(
67 config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
68}
69
70void Config::ReadValues() {
71 ReadSetting("ControlsGeneral", Settings::values.mouse_enabled);
72 ReadSetting("ControlsGeneral", Settings::values.touch_device);
73 ReadSetting("ControlsGeneral", Settings::values.keyboard_enabled);
74 ReadSetting("ControlsGeneral", Settings::values.debug_pad_enabled);
75 ReadSetting("ControlsGeneral", Settings::values.vibration_enabled);
76 ReadSetting("ControlsGeneral", Settings::values.enable_accurate_vibrations);
77 ReadSetting("ControlsGeneral", Settings::values.motion_enabled);
78 Settings::values.touchscreen.enabled =
79 config->GetBoolean("ControlsGeneral", "touch_enabled", true);
80 Settings::values.touchscreen.rotation_angle =
81 config->GetInteger("ControlsGeneral", "touch_angle", 0);
82 Settings::values.touchscreen.diameter_x =
83 config->GetInteger("ControlsGeneral", "touch_diameter_x", 15);
84 Settings::values.touchscreen.diameter_y =
85 config->GetInteger("ControlsGeneral", "touch_diameter_y", 15);
86
87 int num_touch_from_button_maps =
88 config->GetInteger("ControlsGeneral", "touch_from_button_map", 0);
89 if (num_touch_from_button_maps > 0) {
90 for (int i = 0; i < num_touch_from_button_maps; ++i) {
91 Settings::TouchFromButtonMap map;
92 map.name = config->Get("ControlsGeneral",
93 std::string("touch_from_button_maps_") + std::to_string(i) +
94 std::string("_name"),
95 "default");
96 const int num_touch_maps = config->GetInteger(
97 "ControlsGeneral",
98 std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
99 0);
100 map.buttons.reserve(num_touch_maps);
101
102 for (int j = 0; j < num_touch_maps; ++j) {
103 std::string touch_mapping =
104 config->Get("ControlsGeneral",
105 std::string("touch_from_button_maps_") + std::to_string(i) +
106 std::string("_bind_") + std::to_string(j),
107 "");
108 map.buttons.emplace_back(std::move(touch_mapping));
109 }
110
111 Settings::values.touch_from_button_maps.emplace_back(std::move(map));
112 }
113 } else {
114 Settings::values.touch_from_button_maps.emplace_back(
115 Settings::TouchFromButtonMap{"default", {}});
116 num_touch_from_button_maps = 1;
117 }
118 Settings::values.touch_from_button_map_index = std::clamp(
119 Settings::values.touch_from_button_map_index.GetValue(), 0, num_touch_from_button_maps - 1);
120
121 ReadSetting("ControlsGeneral", Settings::values.udp_input_servers);
122
123 // Data Storage
124 ReadSetting("Data Storage", Settings::values.use_virtual_sd);
125 FS::SetYuzuPath(FS::YuzuPath::NANDDir,
126 config->Get("Data Storage", "nand_directory",
127 FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
128 FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
129 config->Get("Data Storage", "sdmc_directory",
130 FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
131 FS::SetYuzuPath(FS::YuzuPath::LoadDir,
132 config->Get("Data Storage", "load_directory",
133 FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
134 FS::SetYuzuPath(FS::YuzuPath::DumpDir,
135 config->Get("Data Storage", "dump_directory",
136 FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
137 ReadSetting("Data Storage", Settings::values.gamecard_inserted);
138 ReadSetting("Data Storage", Settings::values.gamecard_current_game);
139 ReadSetting("Data Storage", Settings::values.gamecard_path);
140
141 // System
142 ReadSetting("System", Settings::values.current_user);
143 Settings::values.current_user = std::clamp<int>(Settings::values.current_user.GetValue(), 0,
144 Service::Account::MAX_USERS - 1);
145
146 // Disable docked mode by default on Android
147 Settings::values.use_docked_mode = config->GetBoolean("System", "use_docked_mode", false);
148
149 const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false);
150 if (rng_seed_enabled) {
151 Settings::values.rng_seed.SetValue(config->GetInteger("System", "rng_seed", 0));
152 } else {
153 Settings::values.rng_seed.SetValue(std::nullopt);
154 }
155
156 const auto custom_rtc_enabled = config->GetBoolean("System", "custom_rtc_enabled", false);
157 if (custom_rtc_enabled) {
158 Settings::values.custom_rtc = config->GetInteger("System", "custom_rtc", 0);
159 } else {
160 Settings::values.custom_rtc = std::nullopt;
161 }
162
163 ReadSetting("System", Settings::values.language_index);
164 ReadSetting("System", Settings::values.region_index);
165 ReadSetting("System", Settings::values.time_zone_index);
166 ReadSetting("System", Settings::values.sound_index);
167
168 // Core
169 ReadSetting("Core", Settings::values.use_multi_core);
170 ReadSetting("Core", Settings::values.use_unsafe_extended_memory_layout);
171
172 // Cpu
173 ReadSetting("Cpu", Settings::values.cpu_accuracy);
174 ReadSetting("Cpu", Settings::values.cpu_debug_mode);
175 ReadSetting("Cpu", Settings::values.cpuopt_page_tables);
176 ReadSetting("Cpu", Settings::values.cpuopt_block_linking);
177 ReadSetting("Cpu", Settings::values.cpuopt_return_stack_buffer);
178 ReadSetting("Cpu", Settings::values.cpuopt_fast_dispatcher);
179 ReadSetting("Cpu", Settings::values.cpuopt_context_elimination);
180 ReadSetting("Cpu", Settings::values.cpuopt_const_prop);
181 ReadSetting("Cpu", Settings::values.cpuopt_misc_ir);
182 ReadSetting("Cpu", Settings::values.cpuopt_reduce_misalign_checks);
183 ReadSetting("Cpu", Settings::values.cpuopt_fastmem);
184 ReadSetting("Cpu", Settings::values.cpuopt_fastmem_exclusives);
185 ReadSetting("Cpu", Settings::values.cpuopt_recompile_exclusives);
186 ReadSetting("Cpu", Settings::values.cpuopt_ignore_memory_aborts);
187 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_unfuse_fma);
188 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_reduce_fp_error);
189 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_standard_fpcr);
190 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_inaccurate_nan);
191 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_fastmem_check);
192 ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_global_monitor);
193
194 // Renderer
195 ReadSetting("Renderer", Settings::values.renderer_backend);
196 ReadSetting("Renderer", Settings::values.renderer_debug);
197 ReadSetting("Renderer", Settings::values.renderer_shader_feedback);
198 ReadSetting("Renderer", Settings::values.enable_nsight_aftermath);
199 ReadSetting("Renderer", Settings::values.disable_shader_loop_safety_checks);
200 ReadSetting("Renderer", Settings::values.vulkan_device);
201
202 ReadSetting("Renderer", Settings::values.resolution_setup);
203 ReadSetting("Renderer", Settings::values.scaling_filter);
204 ReadSetting("Renderer", Settings::values.fsr_sharpening_slider);
205 ReadSetting("Renderer", Settings::values.anti_aliasing);
206 ReadSetting("Renderer", Settings::values.fullscreen_mode);
207 ReadSetting("Renderer", Settings::values.aspect_ratio);
208 ReadSetting("Renderer", Settings::values.max_anisotropy);
209 ReadSetting("Renderer", Settings::values.use_speed_limit);
210 ReadSetting("Renderer", Settings::values.speed_limit);
211 ReadSetting("Renderer", Settings::values.use_disk_shader_cache);
212 ReadSetting("Renderer", Settings::values.use_asynchronous_gpu_emulation);
213 ReadSetting("Renderer", Settings::values.vsync_mode);
214 ReadSetting("Renderer", Settings::values.shader_backend);
215 ReadSetting("Renderer", Settings::values.use_asynchronous_shaders);
216 ReadSetting("Renderer", Settings::values.nvdec_emulation);
217 ReadSetting("Renderer", Settings::values.use_fast_gpu_time);
218 ReadSetting("Renderer", Settings::values.use_vulkan_driver_pipeline_cache);
219
220 ReadSetting("Renderer", Settings::values.bg_red);
221 ReadSetting("Renderer", Settings::values.bg_green);
222 ReadSetting("Renderer", Settings::values.bg_blue);
223
224 // Use GPU accuracy normal by default on Android
225 Settings::values.gpu_accuracy = static_cast<Settings::GPUAccuracy>(config->GetInteger(
226 "Renderer", "gpu_accuracy", static_cast<u32>(Settings::GPUAccuracy::Normal)));
227
228 // Use GPU default anisotropic filtering on Android
229 Settings::values.max_anisotropy = config->GetInteger("Renderer", "max_anisotropy", 1);
230
231 // Disable ASTC compute by default on Android
232 Settings::values.accelerate_astc = config->GetBoolean("Renderer", "accelerate_astc", false);
233
234 // Enable asynchronous presentation by default on Android
235 Settings::values.async_presentation =
236 config->GetBoolean("Renderer", "async_presentation", true);
237
238 // Enable force_max_clock by default on Android
239 Settings::values.renderer_force_max_clock =
240 config->GetBoolean("Renderer", "force_max_clock", true);
241
242 // Audio
243 ReadSetting("Audio", Settings::values.sink_id);
244 ReadSetting("Audio", Settings::values.audio_output_device_id);
245 ReadSetting("Audio", Settings::values.volume);
246
247 // Miscellaneous
248 // log_filter has a different default here than from common
249 Settings::values.log_filter = "*:Info";
250 ReadSetting("Miscellaneous", Settings::values.use_dev_keys);
251
252 // Debugging
253 Settings::values.record_frame_times =
254 config->GetBoolean("Debugging", "record_frame_times", false);
255 ReadSetting("Debugging", Settings::values.dump_exefs);
256 ReadSetting("Debugging", Settings::values.dump_nso);
257 ReadSetting("Debugging", Settings::values.enable_fs_access_log);
258 ReadSetting("Debugging", Settings::values.reporting_services);
259 ReadSetting("Debugging", Settings::values.quest_flag);
260 ReadSetting("Debugging", Settings::values.use_debug_asserts);
261 ReadSetting("Debugging", Settings::values.use_auto_stub);
262 ReadSetting("Debugging", Settings::values.disable_macro_jit);
263 ReadSetting("Debugging", Settings::values.disable_macro_hle);
264 ReadSetting("Debugging", Settings::values.use_gdbstub);
265 ReadSetting("Debugging", Settings::values.gdbstub_port);
266
267 const auto title_list = config->Get("AddOns", "title_ids", "");
268 std::stringstream ss(title_list);
269 std::string line;
270 while (std::getline(ss, line, '|')) {
271 const auto title_id = std::stoul(line, nullptr, 16);
272 const auto disabled_list = config->Get("AddOns", "disabled_" + line, "");
273
274 std::stringstream inner_ss(disabled_list);
275 std::string inner_line;
276 std::vector<std::string> out;
277 while (std::getline(inner_ss, inner_line, '|')) {
278 out.push_back(inner_line);
279 }
280
281 Settings::values.disabled_addons.insert_or_assign(title_id, out);
282 }
283
284 // Web Service
285 ReadSetting("WebService", Settings::values.enable_telemetry);
286 ReadSetting("WebService", Settings::values.web_api_url);
287 ReadSetting("WebService", Settings::values.yuzu_username);
288 ReadSetting("WebService", Settings::values.yuzu_token);
289
290 // Network
291 ReadSetting("Network", Settings::values.network_interface);
292}
293
294void Config::Reload() {
295 LoadINI(DefaultINI::android_config_file);
296 ReadValues();
297}
diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h
new file mode 100644
index 000000000..0d7d6e94d
--- /dev/null
+++ b/src/android/app/src/main/jni/config.h
@@ -0,0 +1,37 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <filesystem>
7#include <memory>
8#include <optional>
9#include <string>
10
11#include "common/settings.h"
12
13class INIReader;
14
15class Config {
16 std::filesystem::path config_loc;
17 std::unique_ptr<INIReader> config;
18
19 bool LoadINI(const std::string& default_contents = "", bool retry = true);
20 void ReadValues();
21
22public:
23 explicit Config(std::optional<std::filesystem::path> config_path = std::nullopt);
24 ~Config();
25
26 void Reload();
27
28private:
29 /**
30 * Applies a value read from the sdl2_config to a Setting.
31 *
32 * @param group The name of the INI group
33 * @param setting The yuzu setting to modify
34 */
35 template <typename Type, bool ranged>
36 void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
37};
diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h
new file mode 100644
index 000000000..c5dfaff54
--- /dev/null
+++ b/src/android/app/src/main/jni/default_ini.h
@@ -0,0 +1,507 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6namespace DefaultINI {
7
8const char* android_config_file = R"(
9
10[ControlsP0]
11# The input devices and parameters for each Switch native input
12# The config section determines the player number where the config will be applied on. For example "ControlsP0", "ControlsP1", ...
13# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..."
14# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values
15
16# Indicates if this player should be connected at boot
17connected=
18
19# for button input, the following devices are available:
20# - "keyboard" (default) for keyboard input. Required parameters:
21# - "code": the code of the key to bind
22# - "sdl" for joystick input using SDL. Required parameters:
23# - "guid": SDL identification GUID of the joystick
24# - "port": the index of the joystick to bind
25# - "button"(optional): the index of the button to bind
26# - "hat"(optional): the index of the hat to bind as direction buttons
27# - "axis"(optional): the index of the axis to bind
28# - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right"
29# - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is
30# triggered if the axis value crosses
31# - "direction"(only used for axis): "+" means the button is triggered when the axis value
32# is greater than the threshold; "-" means the button is triggered when the axis value
33# is smaller than the threshold
34button_a=
35button_b=
36button_x=
37button_y=
38button_lstick=
39button_rstick=
40button_l=
41button_r=
42button_zl=
43button_zr=
44button_plus=
45button_minus=
46button_dleft=
47button_dup=
48button_dright=
49button_ddown=
50button_lstick_left=
51button_lstick_up=
52button_lstick_right=
53button_lstick_down=
54button_sl=
55button_sr=
56button_home=
57button_screenshot=
58
59# for analog input, the following devices are available:
60# - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters:
61# - "up", "down", "left", "right": sub-devices for each direction.
62# Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00"
63# - "modifier": sub-devices as a modifier.
64# - "modifier_scale": a float number representing the applied modifier scale to the analog input.
65# Must be in range of 0.0-1.0. Defaults to 0.5
66# - "sdl" for joystick input using SDL. Required parameters:
67# - "guid": SDL identification GUID of the joystick
68# - "port": the index of the joystick to bind
69# - "axis_x": the index of the axis to bind as x-axis (default to 0)
70# - "axis_y": the index of the axis to bind as y-axis (default to 1)
71lstick=
72rstick=
73
74# for motion input, the following devices are available:
75# - "keyboard" (default) for emulating random motion input from buttons. Required parameters:
76# - "code": the code of the key to bind
77# - "sdl" for motion input using SDL. Required parameters:
78# - "guid": SDL identification GUID of the joystick
79# - "port": the index of the joystick to bind
80# - "motion": the index of the motion sensor to bind
81# - "cemuhookudp" for motion input using Cemu Hook protocol. Required parameters:
82# - "guid": the IP address of the cemu hook server encoded to a hex string. for example 192.168.0.1 = "c0a80001"
83# - "port": the port of the cemu hook server
84# - "pad": the index of the joystick
85# - "motion": the index of the motion sensor of the joystick to bind
86motionleft=
87motionright=
88
89[ControlsGeneral]
90# To use the debug_pad, prepend `debug_pad_` before each button setting above.
91# i.e. debug_pad_button_a=
92
93# Enable debug pad inputs to the guest
94# 0 (default): Disabled, 1: Enabled
95debug_pad_enabled =
96
97# Whether to enable or disable vibration
98# 0: Disabled, 1 (default): Enabled
99vibration_enabled=
100
101# Whether to enable or disable accurate vibrations
102# 0 (default): Disabled, 1: Enabled
103enable_accurate_vibrations=
104
105# Enables controller motion inputs
106# 0: Disabled, 1 (default): Enabled
107motion_enabled =
108
109# Defines the udp device's touch screen coordinate system for cemuhookudp devices
110# - "min_x", "min_y", "max_x", "max_y"
111touch_device=
112
113# for mapping buttons to touch inputs.
114#touch_from_button_map=1
115#touch_from_button_maps_0_name=default
116#touch_from_button_maps_0_count=2
117#touch_from_button_maps_0_bind_0=foo
118#touch_from_button_maps_0_bind_1=bar
119# etc.
120
121# List of Cemuhook UDP servers, delimited by ','.
122# Default: 127.0.0.1:26760
123# Example: 127.0.0.1:26760,123.4.5.67:26761
124udp_input_servers =
125
126# Enable controlling an axis via a mouse input.
127# 0 (default): Off, 1: On
128mouse_panning =
129
130# Set mouse sensitivity.
131# Default: 1.0
132mouse_panning_sensitivity =
133
134# Emulate an analog control stick from keyboard inputs.
135# 0 (default): Disabled, 1: Enabled
136emulate_analog_keyboard =
137
138# Enable mouse inputs to the guest
139# 0 (default): Disabled, 1: Enabled
140mouse_enabled =
141
142# Enable keyboard inputs to the guest
143# 0 (default): Disabled, 1: Enabled
144keyboard_enabled =
145
146[Core]
147# Whether to use multi-core for CPU emulation
148# 0: Disabled, 1 (default): Enabled
149use_multi_core =
150
151# Enable unsafe extended guest system memory layout (8GB DRAM)
152# 0 (default): Disabled, 1: Enabled
153use_unsafe_extended_memory_layout =
154
155[Cpu]
156# Adjusts various optimizations.
157# Auto-select mode enables choice unsafe optimizations.
158# Accurate enables only safe optimizations.
159# Unsafe allows any unsafe optimizations.
160# 0 (default): Auto-select, 1: Accurate, 2: Enable unsafe optimizations
161cpu_accuracy =
162
163# Allow disabling safe optimizations.
164# 0 (default): Disabled, 1: Enabled
165cpu_debug_mode =
166
167# Enable inline page tables optimization (faster guest memory access)
168# 0: Disabled, 1 (default): Enabled
169cpuopt_page_tables =
170
171# Enable block linking CPU optimization (reduce block dispatcher use during predictable jumps)
172# 0: Disabled, 1 (default): Enabled
173cpuopt_block_linking =
174
175# Enable return stack buffer CPU optimization (reduce block dispatcher use during predictable returns)
176# 0: Disabled, 1 (default): Enabled
177cpuopt_return_stack_buffer =
178
179# Enable fast dispatcher CPU optimization (use a two-tiered dispatcher architecture)
180# 0: Disabled, 1 (default): Enabled
181cpuopt_fast_dispatcher =
182
183# Enable context elimination CPU Optimization (reduce host memory use for guest context)
184# 0: Disabled, 1 (default): Enabled
185cpuopt_context_elimination =
186
187# Enable constant propagation CPU optimization (basic IR optimization)
188# 0: Disabled, 1 (default): Enabled
189cpuopt_const_prop =
190
191# Enable miscellaneous CPU optimizations (basic IR optimization)
192# 0: Disabled, 1 (default): Enabled
193cpuopt_misc_ir =
194
195# Enable reduction of memory misalignment checks (reduce memory fallbacks for misaligned access)
196# 0: Disabled, 1 (default): Enabled
197cpuopt_reduce_misalign_checks =
198
199# Enable Host MMU Emulation (faster guest memory access)
200# 0: Disabled, 1 (default): Enabled
201cpuopt_fastmem =
202
203# Enable Host MMU Emulation for exclusive memory instructions (faster guest memory access)
204# 0: Disabled, 1 (default): Enabled
205cpuopt_fastmem_exclusives =
206
207# Enable fallback on failure of fastmem of exclusive memory instructions (faster guest memory access)
208# 0: Disabled, 1 (default): Enabled
209cpuopt_recompile_exclusives =
210
211# Enable optimization to ignore invalid memory accesses (faster guest memory access)
212# 0: Disabled, 1 (default): Enabled
213cpuopt_ignore_memory_aborts =
214
215# Enable unfuse FMA (improve performance on CPUs without FMA)
216# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
217# 0: Disabled, 1 (default): Enabled
218cpuopt_unsafe_unfuse_fma =
219
220# Enable faster FRSQRTE and FRECPE
221# Only enabled if cpu_accuracy is set to Unsafe.
222# 0: Disabled, 1 (default): Enabled
223cpuopt_unsafe_reduce_fp_error =
224
225# Enable faster ASIMD instructions (32 bits only)
226# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
227# 0: Disabled, 1 (default): Enabled
228cpuopt_unsafe_ignore_standard_fpcr =
229
230# Enable inaccurate NaN handling
231# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
232# 0: Disabled, 1 (default): Enabled
233cpuopt_unsafe_inaccurate_nan =
234
235# Disable address space checks (64 bits only)
236# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
237# 0: Disabled, 1 (default): Enabled
238cpuopt_unsafe_fastmem_check =
239
240# Enable faster exclusive instructions
241# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
242# 0: Disabled, 1 (default): Enabled
243cpuopt_unsafe_ignore_global_monitor =
244
245[Renderer]
246# Which backend API to use.
247# 0: OpenGL (unsupported), 1 (default): Vulkan, 2: Null
248backend =
249
250# Whether to enable asynchronous presentation (Vulkan only)
251# 0: Off, 1 (default): On
252async_presentation =
253
254# Enable graphics API debugging mode.
255# 0 (default): Disabled, 1: Enabled
256force_max_clock =
257
258# Enable graphics API debugging mode.
259# 0 (default): Disabled, 1: Enabled
260debug =
261
262# Enable shader feedback.
263# 0 (default): Disabled, 1: Enabled
264renderer_shader_feedback =
265
266# Enable Nsight Aftermath crash dumps
267# 0 (default): Disabled, 1: Enabled
268nsight_aftermath =
269
270# Disable shader loop safety checks, executing the shader without loop logic changes
271# 0 (default): Disabled, 1: Enabled
272disable_shader_loop_safety_checks =
273
274# Which Vulkan physical device to use (defaults to 0)
275vulkan_device =
276
277# 0: 0.5x (360p/540p) [EXPERIMENTAL]
278# 1: 0.75x (540p/810p) [EXPERIMENTAL]
279# 2 (default): 1x (720p/1080p)
280# 3: 2x (1440p/2160p)
281# 4: 3x (2160p/3240p)
282# 5: 4x (2880p/4320p)
283# 6: 5x (3600p/5400p)
284# 7: 6x (4320p/6480p)
285resolution_setup =
286
287# Pixel filter to use when up- or down-sampling rendered frames.
288# 0: Nearest Neighbor
289# 1 (default): Bilinear
290# 2: Bicubic
291# 3: Gaussian
292# 4: ScaleForce
293# 5: AMD FidelityFX™️ Super Resolution [Vulkan Only]
294scaling_filter =
295
296# Anti-Aliasing (AA)
297# 0 (default): None, 1: FXAA
298anti_aliasing =
299
300# Whether to use fullscreen or borderless window mode
301# 0 (Windows default): Borderless window, 1 (All other default): Exclusive fullscreen
302fullscreen_mode =
303
304# Aspect ratio
305# 0: Default (16:9), 1: Force 4:3, 2: Force 21:9, 3: Force 16:10, 4: Stretch to Window
306aspect_ratio =
307
308# Anisotropic filtering
309# 0: Default, 1: 2x, 2: 4x, 3: 8x, 4: 16x
310max_anisotropy =
311
312# Whether to enable VSync or not.
313# OpenGL: Values other than 0 enable VSync
314# Vulkan: FIFO is selected if the requested mode is not supported by the driver.
315# FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen refresh rate.
316# FIFO Relaxed is similar to FIFO but allows tearing as it recovers from a slow down.
317# Mailbox can have lower latency than FIFO and does not tear but may drop frames.
318# Immediate (no synchronization) just presents whatever is available and can exhibit tearing.
319# 0: Immediate (Off), 1 (Default): Mailbox (On), 2: FIFO, 3: FIFO Relaxed
320use_vsync =
321
322# Selects the OpenGL shader backend. NV_gpu_program5 is required for GLASM. If NV_gpu_program5 is
323# not available and GLASM is selected, GLSL will be used.
324# 0: GLSL, 1 (default): GLASM, 2: SPIR-V
325shader_backend =
326
327# Whether to allow asynchronous shader building.
328# 0 (default): Off, 1: On
329use_asynchronous_shaders =
330
331# NVDEC emulation.
332# 0: Disabled, 1: CPU Decoding, 2 (default): GPU Decoding
333nvdec_emulation =
334
335# Accelerate ASTC texture decoding.
336# 0 (default): Off, 1: On
337accelerate_astc =
338
339# Turns on the speed limiter, which will limit the emulation speed to the desired speed limit value
340# 0: Off, 1: On (default)
341use_speed_limit =
342
343# Limits the speed of the game to run no faster than this value as a percentage of target speed
344# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
345speed_limit =
346
347# Whether to use disk based shader cache
348# 0: Off, 1 (default): On
349use_disk_shader_cache =
350
351# Which gpu accuracy level to use
352# 0 (default): Normal, 1: High, 2: Extreme (Very slow)
353gpu_accuracy =
354
355# Whether to use asynchronous GPU emulation
356# 0 : Off (slow), 1 (default): On (fast)
357use_asynchronous_gpu_emulation =
358
359# Inform the guest that GPU operations completed more quickly than they did.
360# 0: Off, 1 (default): On
361use_fast_gpu_time =
362
363# Force unmodified buffers to be flushed, which can cost performance.
364# 0: Off (default), 1: On
365use_pessimistic_flushes =
366
367# Whether to use garbage collection or not for GPU caches.
368# 0 (default): Off, 1: On
369use_caches_gc =
370
371# The clear color for the renderer. What shows up on the sides of the bottom screen.
372# Must be in range of 0-255. Defaults to 0 for all.
373bg_red =
374bg_blue =
375bg_green =
376
377[Audio]
378# Which audio output engine to use.
379# auto (default): Auto-select
380# cubeb: Cubeb audio engine (if available)
381# sdl2: SDL2 audio engine (if available)
382# null: No audio output
383output_engine =
384
385# Which audio device to use.
386# auto (default): Auto-select
387output_device =
388
389# Output volume.
390# 100 (default): 100%, 0; mute
391volume =
392
393[Data Storage]
394# Whether to create a virtual SD card.
395# 1: Yes, 0 (default): No
396use_virtual_sd =
397
398# Whether or not to enable gamecard emulation
399# 1: Yes, 0 (default): No
400gamecard_inserted =
401
402# Whether or not the gamecard should be emulated as the current game
403# If 'gamecard_inserted' is 0 this setting is irrelevant
404# 1: Yes, 0 (default): No
405gamecard_current_game =
406
407# Path to an XCI file to use as the gamecard
408# If 'gamecard_inserted' is 0 this setting is irrelevant
409# If 'gamecard_current_game' is 1 this setting is irrelevant
410gamecard_path =
411
412[System]
413# Whether the system is docked
414# 1 (default): Yes, 0: No
415use_docked_mode =
416
417# Sets the seed for the RNG generator built into the switch
418# rng_seed will be ignored and randomly generated if rng_seed_enabled is false
419rng_seed_enabled =
420rng_seed =
421
422# Sets the current time (in seconds since 12:00 AM Jan 1, 1970) that will be used by the time service
423# This will auto-increment, with the time set being the time the game is started
424# This override will only occur if custom_rtc_enabled is true, otherwise the current time is used
425custom_rtc_enabled =
426custom_rtc =
427
428# Sets the systems language index
429# 0: Japanese, 1: English (default), 2: French, 3: German, 4: Italian, 5: Spanish, 6: Chinese,
430# 7: Korean, 8: Dutch, 9: Portuguese, 10: Russian, 11: Taiwanese, 12: British English, 13: Canadian French,
431# 14: Latin American Spanish, 15: Simplified Chinese, 16: Traditional Chinese, 17: Brazilian Portuguese
432language_index =
433
434# The system region that yuzu will use during emulation
435# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan
436region_index =
437
438# The system time zone that yuzu will use during emulation
439# 0: Auto-select (default), 1: Default (system archive value), Others: Index for specified time zone
440time_zone_index =
441
442# Sets the sound output mode.
443# 0: Mono, 1 (default): Stereo, 2: Surround
444sound_index =
445
446[Miscellaneous]
447# A filter which removes logs below a certain logging level.
448# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical
449log_filter = *:Trace
450
451# Use developer keys
452# 0 (default): Disabled, 1: Enabled
453use_dev_keys =
454
455[Debugging]
456# Record frame time data, can be found in the log directory. Boolean value
457record_frame_times =
458# Determines whether or not yuzu will dump the ExeFS of all games it attempts to load while loading them
459dump_exefs=false
460# Determines whether or not yuzu will dump all NSOs it attempts to load while loading them
461dump_nso=false
462# Determines whether or not yuzu will save the filesystem access log.
463enable_fs_access_log=false
464# Enables verbose reporting services
465reporting_services =
466# Determines whether or not yuzu will report to the game that the emulated console is in Kiosk Mode
467# false: Retail/Normal Mode (default), true: Kiosk Mode
468quest_flag =
469# Determines whether debug asserts should be enabled, which will throw an exception on asserts.
470# false: Disabled (default), true: Enabled
471use_debug_asserts =
472# Determines whether unimplemented HLE service calls should be automatically stubbed.
473# false: Disabled (default), true: Enabled
474use_auto_stub =
475# Enables/Disables the macro JIT compiler
476disable_macro_jit=false
477# Determines whether to enable the GDB stub and wait for the debugger to attach before running.
478# false: Disabled (default), true: Enabled
479use_gdbstub=false
480# The port to use for the GDB server, if it is enabled.
481gdbstub_port=6543
482
483[WebService]
484# Whether or not to enable telemetry
485# 0: No, 1 (default): Yes
486enable_telemetry =
487# URL for Web API
488web_api_url = https://api.yuzu-emu.org
489# Username and token for yuzu Web Service
490# See https://profile.yuzu-emu.org/ for more info
491yuzu_username =
492yuzu_token =
493
494[Network]
495# Name of the network interface device to use with yuzu LAN play.
496# e.g. On *nix: 'enp7s0', 'wlp6s0u1u3u3', 'lo'
497# e.g. On Windows: 'Ethernet', 'Wi-Fi'
498network_interface =
499
500[AddOns]
501# Used to disable add-ons
502# List of title IDs of games that will have add-ons disabled (separated by '|'):
503title_ids =
504# For each title ID, have a key/value pair called `disabled_<title_id>` equal to the names of the add-ons to disable (sep. by '|')
505# e.x. disabled_0100000000010000 = Update|DLC <- disables Updates and DLC on Super Mario Odyssey
506)";
507} // namespace DefaultINI
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp
new file mode 100644
index 000000000..a890c6604
--- /dev/null
+++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp
@@ -0,0 +1,79 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#include <android/native_window_jni.h>
5
6#include "common/logging/log.h"
7#include "input_common/drivers/touch_screen.h"
8#include "input_common/drivers/virtual_amiibo.h"
9#include "input_common/drivers/virtual_gamepad.h"
10#include "input_common/main.h"
11#include "jni/emu_window/emu_window.h"
12
13void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
14 window_info.render_surface = reinterpret_cast<void*>(surface);
15}
16
17void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
18 const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
19 m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
20}
21
22void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
23 const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
24 m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
25}
26
27void EmuWindow_Android::OnTouchReleased(int id) {
28 m_input_subsystem->GetTouchScreen()->TouchReleased(id);
29}
30
31void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
32 m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
33}
34
35void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
36 m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
37}
38
39void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
40 float gyro_y, float gyro_z, float accel_x,
41 float accel_y, float accel_z) {
42 m_input_subsystem->GetVirtualGamepad()->SetMotionState(
43 player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
44}
45
46void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
47 m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
48}
49
50void EmuWindow_Android::OnRemoveNfcTag() {
51 m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
52}
53
54EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem,
55 ANativeWindow* surface,
56 std::shared_ptr<Common::DynamicLibrary> driver_library)
57 : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} {
58 LOG_INFO(Frontend, "initializing");
59
60 if (!surface) {
61 LOG_CRITICAL(Frontend, "surface is nullptr");
62 return;
63 }
64
65 m_window_width = ANativeWindow_getWidth(surface);
66 m_window_height = ANativeWindow_getHeight(surface);
67
68 // Ensures that we emulate with the correct aspect ratio.
69 UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
70
71 window_info.type = Core::Frontend::WindowSystemType::Android;
72 window_info.render_surface = reinterpret_cast<void*>(surface);
73
74 m_input_subsystem->Initialize();
75}
76
77EmuWindow_Android::~EmuWindow_Android() {
78 m_input_subsystem->Shutdown();
79}
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h
new file mode 100644
index 000000000..b38087f73
--- /dev/null
+++ b/src/android/app/src/main/jni/emu_window/emu_window.h
@@ -0,0 +1,64 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <span>
8
9#include "core/frontend/emu_window.h"
10#include "core/frontend/graphics_context.h"
11#include "input_common/main.h"
12
13struct ANativeWindow;
14
15class GraphicsContext_Android final : public Core::Frontend::GraphicsContext {
16public:
17 explicit GraphicsContext_Android(std::shared_ptr<Common::DynamicLibrary> driver_library)
18 : m_driver_library{driver_library} {}
19
20 ~GraphicsContext_Android() = default;
21
22 std::shared_ptr<Common::DynamicLibrary> GetDriverLibrary() override {
23 return m_driver_library;
24 }
25
26private:
27 std::shared_ptr<Common::DynamicLibrary> m_driver_library;
28};
29
30class EmuWindow_Android final : public Core::Frontend::EmuWindow {
31
32public:
33 EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface,
34 std::shared_ptr<Common::DynamicLibrary> driver_library);
35
36 ~EmuWindow_Android();
37
38 void OnSurfaceChanged(ANativeWindow* surface);
39 void OnTouchPressed(int id, float x, float y);
40 void OnTouchMoved(int id, float x, float y);
41 void OnTouchReleased(int id);
42 void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
43 void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
44 void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
45 float gyro_z, float accel_x, float accel_y, float accel_z);
46 void OnReadNfcTag(std::span<u8> data);
47 void OnRemoveNfcTag();
48 void OnFrameDisplayed() override {}
49
50 std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
51 return {std::make_unique<GraphicsContext_Android>(m_driver_library)};
52 }
53 bool IsShown() const override {
54 return true;
55 };
56
57private:
58 InputCommon::InputSubsystem* m_input_subsystem{};
59
60 float m_window_width{};
61 float m_window_height{};
62
63 std::shared_ptr<Common::DynamicLibrary> m_driver_library;
64};
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
new file mode 100644
index 000000000..9cbbf23a3
--- /dev/null
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -0,0 +1,116 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <jni.h>
5
6#include "common/assert.h"
7#include "common/fs/fs_android.h"
8#include "jni/applets/software_keyboard.h"
9#include "jni/id_cache.h"
10#include "video_core/rasterizer_interface.h"
11
12static JavaVM* s_java_vm;
13static jclass s_native_library_class;
14static jclass s_disk_cache_progress_class;
15static jclass s_load_callback_stage_class;
16static jmethodID s_exit_emulation_activity;
17static jmethodID s_disk_cache_load_progress;
18
19static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
20
21namespace IDCache {
22
23JNIEnv* GetEnvForThread() {
24 thread_local static struct OwnedEnv {
25 OwnedEnv() {
26 status = s_java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
27 if (status == JNI_EDETACHED)
28 s_java_vm->AttachCurrentThread(&env, nullptr);
29 }
30
31 ~OwnedEnv() {
32 if (status == JNI_EDETACHED)
33 s_java_vm->DetachCurrentThread();
34 }
35
36 int status;
37 JNIEnv* env = nullptr;
38 } owned;
39 return owned.env;
40}
41
42jclass GetNativeLibraryClass() {
43 return s_native_library_class;
44}
45
46jclass GetDiskCacheProgressClass() {
47 return s_disk_cache_progress_class;
48}
49
50jclass GetDiskCacheLoadCallbackStageClass() {
51 return s_load_callback_stage_class;
52}
53
54jmethodID GetExitEmulationActivity() {
55 return s_exit_emulation_activity;
56}
57
58jmethodID GetDiskCacheLoadProgress() {
59 return s_disk_cache_load_progress;
60}
61
62} // namespace IDCache
63
64#ifdef __cplusplus
65extern "C" {
66#endif
67
68jint JNI_OnLoad(JavaVM* vm, void* reserved) {
69 s_java_vm = vm;
70
71 JNIEnv* env;
72 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
73 return JNI_ERR;
74
75 // Initialize Java classes
76 const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
77 s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
78 s_disk_cache_progress_class = reinterpret_cast<jclass>(env->NewGlobalRef(
79 env->FindClass("org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress")));
80 s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
81 "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
82
83 // Initialize methods
84 s_exit_emulation_activity =
85 env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
86 s_disk_cache_load_progress =
87 env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
88
89 // Initialize Android Storage
90 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
91
92 // Initialize applets
93 SoftwareKeyboard::InitJNI(env);
94
95 return JNI_VERSION;
96}
97
98void JNI_OnUnload(JavaVM* vm, void* reserved) {
99 JNIEnv* env;
100 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
101 return;
102 }
103
104 // UnInitialize Android Storage
105 Common::FS::Android::UnRegisterCallbacks();
106 env->DeleteGlobalRef(s_native_library_class);
107 env->DeleteGlobalRef(s_disk_cache_progress_class);
108 env->DeleteGlobalRef(s_load_callback_stage_class);
109
110 // UnInitialize applets
111 SoftwareKeyboard::CleanupJNI(env);
112}
113
114#ifdef __cplusplus
115}
116#endif
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
new file mode 100644
index 000000000..be535fe1e
--- /dev/null
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -0,0 +1,19 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#pragma once
5
6#include <jni.h>
7
8#include "video_core/rasterizer_interface.h"
9
10namespace IDCache {
11
12JNIEnv* GetEnvForThread();
13jclass GetNativeLibraryClass();
14jclass GetDiskCacheProgressClass();
15jclass GetDiskCacheLoadCallbackStageClass();
16jmethodID GetExitEmulationActivity();
17jmethodID GetDiskCacheLoadProgress();
18
19} // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
new file mode 100644
index 000000000..b87e04b3d
--- /dev/null
+++ b/src/android/app/src/main/jni/native.cpp
@@ -0,0 +1,774 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <codecvt>
5#include <locale>
6#include <string>
7#include <string_view>
8#include <dlfcn.h>
9
10#ifdef ARCHITECTURE_arm64
11#include <adrenotools/driver.h>
12#endif
13
14#include <android/api-level.h>
15#include <android/native_window_jni.h>
16
17#include "common/detached_tasks.h"
18#include "common/dynamic_library.h"
19#include "common/fs/path_util.h"
20#include "common/logging/backend.h"
21#include "common/logging/log.h"
22#include "common/microprofile.h"
23#include "common/scm_rev.h"
24#include "common/scope_exit.h"
25#include "common/settings.h"
26#include "common/string_util.h"
27#include "core/core.h"
28#include "core/cpu_manager.h"
29#include "core/crypto/key_manager.h"
30#include "core/file_sys/registered_cache.h"
31#include "core/file_sys/vfs_real.h"
32#include "core/frontend/applets/cabinet.h"
33#include "core/frontend/applets/controller.h"
34#include "core/frontend/applets/error.h"
35#include "core/frontend/applets/general_frontend.h"
36#include "core/frontend/applets/mii_edit.h"
37#include "core/frontend/applets/profile_select.h"
38#include "core/frontend/applets/software_keyboard.h"
39#include "core/frontend/applets/web_browser.h"
40#include "core/hid/emulated_controller.h"
41#include "core/hid/hid_core.h"
42#include "core/hid/hid_types.h"
43#include "core/hle/service/acc/profile_manager.h"
44#include "core/hle/service/am/applet_ae.h"
45#include "core/hle/service/am/applet_oe.h"
46#include "core/hle/service/am/applets/applets.h"
47#include "core/hle/service/filesystem/filesystem.h"
48#include "core/loader/loader.h"
49#include "core/perf_stats.h"
50#include "jni/android_common/android_common.h"
51#include "jni/applets/software_keyboard.h"
52#include "jni/config.h"
53#include "jni/emu_window/emu_window.h"
54#include "jni/id_cache.h"
55#include "video_core/rasterizer_interface.h"
56#include "video_core/renderer_base.h"
57
58namespace {
59
60class EmulationSession final {
61public:
62 EmulationSession() {
63 m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
64 }
65
66 ~EmulationSession() = default;
67
68 static EmulationSession& GetInstance() {
69 return s_instance;
70 }
71
72 const Core::System& System() const {
73 return m_system;
74 }
75
76 Core::System& System() {
77 return m_system;
78 }
79
80 const EmuWindow_Android& Window() const {
81 return *m_window;
82 }
83
84 EmuWindow_Android& Window() {
85 return *m_window;
86 }
87
88 ANativeWindow* NativeWindow() const {
89 return m_native_window;
90 }
91
92 void SetNativeWindow(ANativeWindow* native_window) {
93 m_native_window = native_window;
94 }
95
96 u32 ScreenRotation() const {
97 return m_screen_rotation;
98 }
99
100 void SetScreenRotation(u32 screen_rotation) {
101 m_screen_rotation = screen_rotation;
102 }
103
104 void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
105 const std::string& custom_driver_name,
106 const std::string& file_redirect_dir) {
107#ifdef ARCHITECTURE_arm64
108 void* handle{};
109 const char* file_redirect_dir_{};
110 int featureFlags{};
111
112 // Enable driver file redirection when renderer debugging is enabled.
113 if (Settings::values.renderer_debug && file_redirect_dir.size()) {
114 featureFlags |= ADRENOTOOLS_DRIVER_FILE_REDIRECT;
115 file_redirect_dir_ = file_redirect_dir.c_str();
116 }
117
118 // Try to load a custom driver.
119 if (custom_driver_name.size()) {
120 handle = adrenotools_open_libvulkan(
121 RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
122 custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
123 }
124
125 // Try to load the system driver.
126 if (!handle) {
127 handle =
128 adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
129 nullptr, nullptr, file_redirect_dir_, nullptr);
130 }
131
132 m_vulkan_library = std::make_shared<Common::DynamicLibrary>(handle);
133#endif
134 }
135
136 bool IsRunning() const {
137 std::scoped_lock lock(m_mutex);
138 return m_is_running;
139 }
140
141 const Core::PerfStatsResults& PerfStats() const {
142 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
143 return m_perf_stats;
144 }
145
146 void SurfaceChanged() {
147 if (!IsRunning()) {
148 return;
149 }
150 m_window->OnSurfaceChanged(m_native_window);
151 m_system.Renderer().NotifySurfaceChanged();
152 }
153
154 Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
155 std::scoped_lock lock(m_mutex);
156
157 // Loads the configuration.
158 Config{};
159
160 // Create the render window.
161 m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window,
162 m_vulkan_library);
163
164 // Initialize system.
165 auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
166 m_software_keyboard = android_keyboard.get();
167 m_system.SetShuttingDown(false);
168 m_system.ApplySettings();
169 m_system.HIDCore().ReloadInputDevices();
170 m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
171 m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
172 m_system.SetAppletFrontendSet({
173 nullptr, // Amiibo Settings
174 nullptr, // Controller Selector
175 nullptr, // Error Display
176 nullptr, // Mii Editor
177 nullptr, // Parental Controls
178 nullptr, // Photo Viewer
179 nullptr, // Profile Selector
180 std::move(android_keyboard), // Software Keyboard
181 nullptr, // Web Browser
182 });
183 m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
184
185 // Initialize account manager
186 m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
187
188 // Load the ROM.
189 m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
190 if (m_load_result != Core::SystemResultStatus::Success) {
191 return m_load_result;
192 }
193
194 // Complete initialization.
195 m_system.GPU().Start();
196 m_system.GetCpuManager().OnGpuReady();
197 m_system.RegisterExitCallback([&] { HaltEmulation(); });
198
199 return Core::SystemResultStatus::Success;
200 }
201
202 void ShutdownEmulation() {
203 std::scoped_lock lock(m_mutex);
204
205 m_is_running = false;
206
207 // Unload user input.
208 m_system.HIDCore().UnloadInputDevices();
209
210 // Shutdown the main emulated process
211 if (m_load_result == Core::SystemResultStatus::Success) {
212 m_system.DetachDebugger();
213 m_system.ShutdownMainProcess();
214 m_detached_tasks.WaitForAllTasks();
215 m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
216 }
217
218 // Tear down the render window.
219 m_window.reset();
220 }
221
222 void PauseEmulation() {
223 std::scoped_lock lock(m_mutex);
224 m_system.Pause();
225 }
226
227 void UnPauseEmulation() {
228 std::scoped_lock lock(m_mutex);
229 m_system.Run();
230 }
231
232 void HaltEmulation() {
233 std::scoped_lock lock(m_mutex);
234 m_is_running = false;
235 m_cv.notify_one();
236 }
237
238 void RunEmulation() {
239 {
240 std::scoped_lock lock(m_mutex);
241 m_is_running = true;
242 }
243
244 // Load the disk shader cache.
245 if (Settings::values.use_disk_shader_cache.GetValue()) {
246 LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
247 m_system.Renderer().ReadRasterizer()->LoadDiskResources(
248 m_system.GetApplicationProcessProgramID(), std::stop_token{},
249 LoadDiskCacheProgress);
250 LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
251 }
252
253 void(m_system.Run());
254
255 if (m_system.DebuggerEnabled()) {
256 m_system.InitializeDebugger();
257 }
258
259 while (true) {
260 {
261 std::unique_lock lock(m_mutex);
262 if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
263 [&]() { return !m_is_running; })) {
264 // Emulation halted.
265 break;
266 }
267 }
268 {
269 // Refresh performance stats.
270 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
271 m_perf_stats = m_system.GetAndResetPerfStats();
272 }
273 }
274 }
275
276 std::string GetRomTitle(const std::string& path) {
277 return GetRomMetadata(path).title;
278 }
279
280 std::vector<u8> GetRomIcon(const std::string& path) {
281 return GetRomMetadata(path).icon;
282 }
283
284 void ResetRomMetadata() {
285 m_rom_metadata_cache.clear();
286 }
287
288 bool IsHandheldOnly() {
289 const auto npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
290
291 if (npad_style_set.fullkey == 1) {
292 return false;
293 }
294
295 if (npad_style_set.handheld == 0) {
296 return false;
297 }
298
299 return !Settings::values.use_docked_mode.GetValue();
300 }
301
302 void SetDeviceType(int index, int type) {
303 auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
304 controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
305 }
306
307 void OnGamepadConnectEvent(int index) {
308 auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
309
310 // Ensure that player1 is configured correctly and handheld disconnected
311 if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
312 auto handheld =
313 m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
314
315 if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
316 handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
317 controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
318 handheld->Disconnect();
319 }
320 }
321
322 // Ensure that handheld is configured correctly and player 1 disconnected
323 if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
324 auto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
325
326 if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
327 player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
328 controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
329 player1->Disconnect();
330 }
331 }
332
333 if (!controller->IsConnected()) {
334 controller->Connect();
335 }
336 }
337
338 void OnGamepadDisconnectEvent(int index) {
339 auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
340 controller->Disconnect();
341 }
342
343 SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() {
344 return m_software_keyboard;
345 }
346
347private:
348 struct RomMetadata {
349 std::string title;
350 std::vector<u8> icon;
351 };
352
353 RomMetadata GetRomMetadata(const std::string& path) {
354 if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
355 return search->second;
356 }
357
358 return CacheRomMetadata(path);
359 }
360
361 RomMetadata CacheRomMetadata(const std::string& path) {
362 const auto file = Core::GetGameFileFromPath(m_vfs, path);
363 const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
364
365 RomMetadata entry;
366 loader->ReadTitle(entry.title);
367 loader->ReadIcon(entry.icon);
368
369 m_rom_metadata_cache[path] = entry;
370
371 return entry;
372 }
373
374private:
375 static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
376 JNIEnv* env = IDCache::GetEnvForThread();
377 env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
378 IDCache::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
379 static_cast<jint>(progress), static_cast<jint>(max));
380 }
381
382private:
383 static EmulationSession s_instance;
384
385 // Frontend management
386 std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
387
388 // Window management
389 std::unique_ptr<EmuWindow_Android> m_window;
390 ANativeWindow* m_native_window{};
391 u32 m_screen_rotation{};
392
393 // Core emulation
394 Core::System m_system;
395 InputCommon::InputSubsystem m_input_subsystem;
396 Common::DetachedTasks m_detached_tasks;
397 Core::PerfStatsResults m_perf_stats{};
398 std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
399 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
400 bool m_is_running{};
401 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
402 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
403
404 // GPU driver parameters
405 std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
406
407 // Synchronization
408 std::condition_variable_any m_cv;
409 mutable std::mutex m_perf_stats_mutex;
410 mutable std::mutex m_mutex;
411};
412
413/*static*/ EmulationSession EmulationSession::s_instance;
414
415} // Anonymous namespace
416
417u32 GetAndroidScreenRotation() {
418 return EmulationSession::GetInstance().ScreenRotation();
419}
420
421static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
422 Common::Log::Initialize();
423 Common::Log::SetColorConsoleBackendEnabled(true);
424 Common::Log::Start();
425
426 MicroProfileOnThreadCreate("EmuThread");
427 SCOPE_EXIT({ MicroProfileShutdown(); });
428
429 LOG_INFO(Frontend, "starting");
430
431 if (filepath.empty()) {
432 LOG_CRITICAL(Frontend, "failed to load: filepath empty!");
433 return Core::SystemResultStatus::ErrorLoader;
434 }
435
436 SCOPE_EXIT({ EmulationSession::GetInstance().ShutdownEmulation(); });
437
438 const auto result = EmulationSession::GetInstance().InitializeEmulation(filepath);
439 if (result != Core::SystemResultStatus::Success) {
440 return result;
441 }
442
443 EmulationSession::GetInstance().RunEmulation();
444
445 return Core::SystemResultStatus::Success;
446}
447
448extern "C" {
449
450void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
451 [[maybe_unused]] jclass clazz,
452 jobject surf) {
453 EmulationSession::GetInstance().SetNativeWindow(ANativeWindow_fromSurface(env, surf));
454 EmulationSession::GetInstance().SurfaceChanged();
455}
456
457void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env,
458 [[maybe_unused]] jclass clazz) {
459 ANativeWindow_release(EmulationSession::GetInstance().NativeWindow());
460 EmulationSession::GetInstance().SetNativeWindow(nullptr);
461 EmulationSession::GetInstance().SurfaceChanged();
462}
463
464void Java_org_yuzu_yuzu_1emu_NativeLibrary_notifyOrientationChange(JNIEnv* env,
465 [[maybe_unused]] jclass clazz,
466 jint layout_option,
467 jint rotation) {
468 return EmulationSession::GetInstance().SetScreenRotation(static_cast<u32>(rotation));
469}
470
471void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env,
472 [[maybe_unused]] jclass clazz,
473 jstring j_directory) {
474 Common::FS::SetAppDirectory(GetJString(env, j_directory));
475}
476
477void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(
478 JNIEnv* env, [[maybe_unused]] jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir,
479 jstring custom_driver_name, jstring file_redirect_dir) {
480 EmulationSession::GetInstance().InitializeGpuDriver(
481 GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir),
482 GetJString(env, custom_driver_name), GetJString(env, file_redirect_dir));
483}
484
485jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env,
486 [[maybe_unused]] jclass clazz) {
487 Core::Crypto::KeyManager::Instance().ReloadKeys();
488 return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());
489}
490
491void Java_org_yuzu_yuzu_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv* env,
492 [[maybe_unused]] jclass clazz) {
493 EmulationSession::GetInstance().UnPauseEmulation();
494}
495
496void Java_org_yuzu_yuzu_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv* env,
497 [[maybe_unused]] jclass clazz) {
498 EmulationSession::GetInstance().PauseEmulation();
499}
500
501void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv* env,
502 [[maybe_unused]] jclass clazz) {
503 EmulationSession::GetInstance().HaltEmulation();
504}
505
506void Java_org_yuzu_yuzu_1emu_NativeLibrary_resetRomMetadata([[maybe_unused]] JNIEnv* env,
507 [[maybe_unused]] jclass clazz) {
508 EmulationSession::GetInstance().ResetRomMetadata();
509}
510
511jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv* env,
512 [[maybe_unused]] jclass clazz) {
513 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
514}
515
516jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
517 [[maybe_unused]] jclass clazz) {
518 return EmulationSession::GetInstance().IsHandheldOnly();
519}
520
521jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType([[maybe_unused]] JNIEnv* env,
522 [[maybe_unused]] jclass clazz,
523 jint j_device, jint j_type) {
524 if (EmulationSession::GetInstance().IsRunning()) {
525 EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
526 }
527 return static_cast<jboolean>(true);
528}
529
530jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent([[maybe_unused]] JNIEnv* env,
531 [[maybe_unused]] jclass clazz,
532 jint j_device) {
533 if (EmulationSession::GetInstance().IsRunning()) {
534 EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
535 }
536 return static_cast<jboolean>(true);
537}
538
539jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(
540 [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device) {
541 if (EmulationSession::GetInstance().IsRunning()) {
542 EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
543 }
544 return static_cast<jboolean>(true);
545}
546jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent([[maybe_unused]] JNIEnv* env,
547 [[maybe_unused]] jclass clazz,
548 [[maybe_unused]] jint j_device,
549 jint j_button, jint action) {
550 if (EmulationSession::GetInstance().IsRunning()) {
551 // Ensure gamepad is connected
552 EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
553 EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
554 action != 0);
555 }
556 return static_cast<jboolean>(true);
557}
558
559jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent([[maybe_unused]] JNIEnv* env,
560 [[maybe_unused]] jclass clazz,
561 jint j_device, jint stick_id,
562 jfloat x, jfloat y) {
563 if (EmulationSession::GetInstance().IsRunning()) {
564 EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
565 }
566 return static_cast<jboolean>(true);
567}
568
569jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
570 [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device,
571 jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, jfloat gyro_z, jfloat accel_x,
572 jfloat accel_y, jfloat accel_z) {
573 if (EmulationSession::GetInstance().IsRunning()) {
574 EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
575 j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
576 }
577 return static_cast<jboolean>(true);
578}
579
580jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag([[maybe_unused]] JNIEnv* env,
581 [[maybe_unused]] jclass clazz,
582 jbyteArray j_data) {
583 jboolean isCopy{false};
584 std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
585 static_cast<size_t>(env->GetArrayLength(j_data)));
586
587 if (EmulationSession::GetInstance().IsRunning()) {
588 EmulationSession::GetInstance().Window().OnReadNfcTag(data);
589 }
590 return static_cast<jboolean>(true);
591}
592
593jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag([[maybe_unused]] JNIEnv* env,
594 [[maybe_unused]] jclass clazz) {
595 if (EmulationSession::GetInstance().IsRunning()) {
596 EmulationSession::GetInstance().Window().OnRemoveNfcTag();
597 }
598 return static_cast<jboolean>(true);
599}
600
601void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env,
602 [[maybe_unused]] jclass clazz, jint id,
603 jfloat x, jfloat y) {
604 if (EmulationSession::GetInstance().IsRunning()) {
605 EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
606 }
607}
608
609void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env,
610 [[maybe_unused]] jclass clazz, jint id,
611 jfloat x, jfloat y) {
612 if (EmulationSession::GetInstance().IsRunning()) {
613 EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
614 }
615}
616
617void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased([[maybe_unused]] JNIEnv* env,
618 [[maybe_unused]] jclass clazz, jint id) {
619 if (EmulationSession::GetInstance().IsRunning()) {
620 EmulationSession::GetInstance().Window().OnTouchReleased(id);
621 }
622}
623
624jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon([[maybe_unused]] JNIEnv* env,
625 [[maybe_unused]] jclass clazz,
626 [[maybe_unused]] jstring j_filename) {
627 auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
628 jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
629 env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
630 reinterpret_cast<jbyte*>(icon_data.data()));
631 return icon;
632}
633
634jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle([[maybe_unused]] JNIEnv* env,
635 [[maybe_unused]] jclass clazz,
636 [[maybe_unused]] jstring j_filename) {
637 auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
638 return env->NewStringUTF(title.c_str());
639}
640
641jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDescription([[maybe_unused]] JNIEnv* env,
642 [[maybe_unused]] jclass clazz,
643 jstring j_filename) {
644 return j_filename;
645}
646
647jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId([[maybe_unused]] JNIEnv* env,
648 [[maybe_unused]] jclass clazz,
649 jstring j_filename) {
650 return j_filename;
651}
652
653jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions([[maybe_unused]] JNIEnv* env,
654 [[maybe_unused]] jclass clazz,
655 [[maybe_unused]] jstring j_filename) {
656 return env->NewStringUTF("");
657}
658
659jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany([[maybe_unused]] JNIEnv* env,
660 [[maybe_unused]] jclass clazz,
661 [[maybe_unused]] jstring j_filename) {
662 return env->NewStringUTF("");
663}
664
665void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation
666 [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) {
667 // Create the default config.ini.
668 Config{};
669 // Initialize the emulated system.
670 EmulationSession::GetInstance().System().Initialize();
671}
672
673jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore([[maybe_unused]] JNIEnv* env,
674 [[maybe_unused]] jclass clazz) {
675 return {};
676}
677
678void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
679 [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_file,
680 [[maybe_unused]] jstring j_savestate, [[maybe_unused]] jboolean j_delete_savestate) {}
681
682void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env,
683 [[maybe_unused]] jclass clazz) {
684 Config{};
685}
686
687jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserSetting([[maybe_unused]] JNIEnv* env,
688 [[maybe_unused]] jclass clazz,
689 jstring j_game_id, jstring j_section,
690 jstring j_key) {
691 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
692 std::string_view section = env->GetStringUTFChars(j_section, 0);
693 std::string_view key = env->GetStringUTFChars(j_key, 0);
694
695 env->ReleaseStringUTFChars(j_game_id, game_id.data());
696 env->ReleaseStringUTFChars(j_section, section.data());
697 env->ReleaseStringUTFChars(j_key, key.data());
698
699 return env->NewStringUTF("");
700}
701
702void Java_org_yuzu_yuzu_1emu_NativeLibrary_setUserSetting([[maybe_unused]] JNIEnv* env,
703 [[maybe_unused]] jclass clazz,
704 jstring j_game_id, jstring j_section,
705 jstring j_key, jstring j_value) {
706 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
707 std::string_view section = env->GetStringUTFChars(j_section, 0);
708 std::string_view key = env->GetStringUTFChars(j_key, 0);
709 std::string_view value = env->GetStringUTFChars(j_value, 0);
710
711 env->ReleaseStringUTFChars(j_game_id, game_id.data());
712 env->ReleaseStringUTFChars(j_section, section.data());
713 env->ReleaseStringUTFChars(j_key, key.data());
714 env->ReleaseStringUTFChars(j_value, value.data());
715}
716
717void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni([[maybe_unused]] JNIEnv* env,
718 [[maybe_unused]] jclass clazz,
719 jstring j_game_id) {
720 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
721
722 env->ReleaseStringUTFChars(j_game_id, game_id.data());
723}
724
725jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats([[maybe_unused]] JNIEnv* env,
726 [[maybe_unused]] jclass clazz) {
727 jdoubleArray j_stats = env->NewDoubleArray(4);
728
729 if (EmulationSession::GetInstance().IsRunning()) {
730 const auto results = EmulationSession::GetInstance().PerfStats();
731
732 // Converting the structure into an array makes it easier to pass it to the frontend
733 double stats[4] = {results.system_fps, results.average_game_fps, results.frametime,
734 results.emulation_speed};
735
736 env->SetDoubleArrayRegion(j_stats, 0, 4, stats);
737 }
738
739 return j_stats;
740}
741
742void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(
743 [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) {}
744
745void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2([[maybe_unused]] JNIEnv* env,
746 [[maybe_unused]] jclass clazz,
747 jstring j_path) {
748 const std::string path = GetJString(env, j_path);
749
750 const Core::SystemResultStatus result{RunEmulation(path)};
751 if (result != Core::SystemResultStatus::Success) {
752 env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
753 IDCache::GetExitEmulationActivity(), static_cast<int>(result));
754 }
755}
756
757void Java_org_yuzu_yuzu_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv* env,
758 [[maybe_unused]] jclass clazz) {
759 LOG_INFO(Frontend, "yuzu Version: {}-{}", Common::g_scm_branch, Common::g_scm_desc);
760 LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
761}
762
763void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardText(JNIEnv* env, jclass clazz,
764 jstring j_text) {
765 const std::u16string input = Common::UTF8ToUTF16(GetJString(env, j_text));
766 EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
767}
768
769void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env, jclass clazz,
770 jint j_key_code) {
771 EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
772}
773
774} // extern "C"
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
new file mode 100644
index 000000000..24dcbbcb8
--- /dev/null
+++ b/src/android/app/src/main/jni/native.h
@@ -0,0 +1,165 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <jni.h>
7
8// Function calls from the Java side
9#ifdef __cplusplus
10extern "C" {
11#endif
12
13JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
14 jclass clazz);
15
16JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
17 jclass clazz);
18
19JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
20 jclass clazz);
21
22JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata(JNIEnv* env,
23 jclass clazz);
24
25JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
26 jclass clazz);
27
28JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env,
29 jclass clazz);
30
31JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env,
32 jclass clazz,
33 jstring j_device,
34 jstring j_type);
35
36JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(
37 JNIEnv* env, jclass clazz, jstring j_device);
38
39JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(
40 JNIEnv* env, jclass clazz, jstring j_device);
41
42JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
43 JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
44
45JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEvent(
46 JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y);
47
48JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent(
49 JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
50
51JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env,
52 jclass clazz,
53 jbyteArray j_data);
54
55JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env,
56 jclass clazz);
57
58JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
59 jclass clazz,
60 jfloat x, jfloat y,
61 jboolean pressed);
62
63JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
64 jfloat x, jfloat y);
65
66JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
67 jclass clazz,
68 jstring j_file);
69
70JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
71 jstring j_filename);
72
73JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(JNIEnv* env,
74 jclass clazz,
75 jstring j_filename);
76
77JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz,
78 jstring j_filename);
79
80JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
81 jclass clazz,
82 jstring j_filename);
83
84JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
85 jclass clazz,
86 jstring j_filename);
87
88JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
89 jclass clazz);
90
91JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
92 jclass clazz,
93 jstring j_directory);
94
95JNIEXPORT void JNICALL
96Java_org_yuzu_yuzu_1emu_NativeLibrary_Java_org_yuzu_yuzu_1emu_NativeLibrary_InitializeGpuDriver(
97 JNIEnv* env, jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir,
98 jstring custom_driver_name, jstring file_redirect_dir);
99
100JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env,
101 jclass clazz);
102
103JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
104 JNIEnv* env, jclass clazz, jstring path_);
105
106JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
107 jclass clazz,
108 jstring path);
109
110JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitializeEmulation(JNIEnv* env,
111 jclass clazz);
112
113JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
114 jclass clazz);
115JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
116 jboolean enable);
117
118JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
119 jclass clazz);
120
121JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
122 JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
123
124JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2(
125 JNIEnv* env, jclass clazz, jstring j_path);
126
127JNIEXPORT void JNICALL
128Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z(
129 JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
130
131JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
132 jclass clazz,
133 jobject surf);
134
135JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
136 jclass clazz);
137
138JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
139 jstring j_game_id);
140
141JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
142 jclass clazz);
143
144JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
145 JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
146 jstring j_value);
147
148JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting(
149 JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
150
151JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
152 jclass clazz);
153
154JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
155 jclass clazz);
156
157JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(
158 JNIEnv* env, jclass clazz, jstring j_text);
159
160JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(
161 JNIEnv* env, jclass clazz, jint j_key_code);
162
163#ifdef __cplusplus
164}
165#endif
diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml
new file mode 100644
index 000000000..9f49c133a
--- /dev/null
+++ b/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="125"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10 <translate
11 android:duration="125"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="0"
14 android:toXDelta="-75" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml
new file mode 100644
index 000000000..82fd719db
--- /dev/null
+++ b/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="0"
8 android:toAlpha="1" />
9
10 <translate
11 android:duration="@android:integer/config_shortAnimTime"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="-200"
14 android:toXDelta="0" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml
new file mode 100644
index 000000000..5892128f1
--- /dev/null
+++ b/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="125"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10 <translate
11 android:duration="125"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="0"
14 android:toXDelta="75" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml
new file mode 100644
index 000000000..98e0cf8bd
--- /dev/null
+++ b/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="0"
8 android:toAlpha="1" />
9
10 <translate
11 android:duration="@android:integer/config_shortAnimTime"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="200"
14 android:toXDelta="0" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml
new file mode 100644
index 000000000..77a40a4d1
--- /dev/null
+++ b/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml
@@ -0,0 +1,10 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10</set>
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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:propertyName="translationX"
6 android:valueType="floatType"
7 android:valueFrom="-1280dp"
8 android:valueTo="0"
9 android:interpolator="@android:interpolator/decelerate_quad"
10 android:duration="300"/>
11
12 <objectAnimator
13 android:propertyName="alpha"
14 android:valueType="floatType"
15 android:valueFrom="0"
16 android:valueTo="1"
17 android:interpolator="@android:interpolator/accelerate_quad"
18 android:duration="300"/>
19
20</set> \ No newline at end of file
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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <!-- This animation is used ONLY when a submenu is replaced. -->
5 <objectAnimator
6 android:propertyName="translationX"
7 android:valueType="floatType"
8 android:valueFrom="0"
9 android:valueTo="-1280dp"
10 android:interpolator="@android:interpolator/decelerate_quad"
11 android:duration="200"/>
12
13 <objectAnimator
14 android:propertyName="alpha"
15 android:valueType="floatType"
16 android:valueFrom="1"
17 android:valueTo="0"
18 android:interpolator="@android:interpolator/decelerate_quad"
19 android:duration="200"/>
20
21</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..66ebfa85c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..71068f452
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png
new file mode 100644
index 000000000..d73fad15b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable/default_icon.jpg b/src/android/app/src/main/res/drawable/default_icon.jpg
new file mode 100644
index 000000000..859caf4af
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/default_icon.jpg
Binary files differ
diff --git a/src/android/app/src/main/res/drawable/dpad_standard.xml b/src/android/app/src/main/res/drawable/dpad_standard.xml
new file mode 100644
index 000000000..28aba657e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/dpad_standard.xml
@@ -0,0 +1,24 @@
1<vector android:alpha="0.6" android:height="221.78dp"
2 android:viewportHeight="221.78" android:viewportWidth="221.78"
3 android:width="221.78dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.75"
6 android:pathData="M221.78,87.07v47.64a11.53,11.53 0,0 1,-11.5 11.5H151.62a5.42,5.42 0,0 0,-5.41 5.41v58.66a11.53,11.53 0,0 1,-11.5 11.5H87.07a11.53,11.53 0,0 1,-11.5 -11.5V151.61a5.41,5.41 0,0 0,-5.41 -5.41H11.5A11.53,11.53 0,0 1,0 134.7V87.05a11.53,11.53 0,0 1,11.5 -11.5H70.16a5.41,5.41 0,0 0,5.41 -5.41V11.5A11.53,11.53 0,0 1,87.07 0h47.64a11.53,11.53 0,0 1,11.5 11.5V70.16a5.41,5.41 0,0 0,5.41 5.41h58.66A11.53,11.53 0,0 1,221.78 87.07Z" android:strokeAlpha="0.75">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="110.89" android:centerY="110.89"
9 android:gradientRadius="110.89" android:type="radial">
10 <item android:color="#FFC3C4C5" android:offset="0.58"/>
11 <item android:color="#FFC6C6C6" android:offset="0.84"/>
12 <item android:color="#FFC7C7C7" android:offset="0.88"/>
13 <item android:color="#FFC2C2C2" android:offset="0.91"/>
14 <item android:color="#FFB5B5B5" android:offset="0.94"/>
15 <item android:color="#FF9E9E9E" android:offset="0.98"/>
16 <item android:color="#FF8F8F8F" android:offset="1"/>
17 </gradient>
18 </aapt:attr>
19 </path>
20 <path android:fillColor="#FF000000" android:pathData="M195.47,110.32l-16.26,-9.38a0.65,0.65 0,0 0,-1 0.56v18.78a0.66,0.66 0,0 0,1 0.57l16.26,-9.39A0.66,0.66 0,0 0,195.47 110.32Z"/>
21 <path android:fillColor="#FF000000" android:pathData="M26.31,110.32l16.26,-9.38a0.65,0.65 0,0 1,1 0.56v18.78a0.66,0.66 0,0 1,-1 0.57l-16.26,-9.39A0.66,0.66 0,0 1,26.31 110.32Z"/>
22 <path android:fillColor="#FF000000" android:pathData="M110.32,26.31l-9.38,16.26a0.65,0.65 0,0 0,0.56 1h18.78a0.66,0.66 0,0 0,0.57 -1l-9.39,-16.26A0.66,0.66 0,0 0,110.32 26.31Z"/>
23 <path android:fillColor="#FF000000" android:pathData="M110.32,195.47l-9.38,-16.26a0.65,0.65 0,0 1,0.56 -1h18.78a0.66,0.66 0,0 1,0.57 1l-9.39,16.26A0.66,0.66 0,0 1,110.32 195.47Z"/>
24</vector>
diff --git a/src/android/app/src/main/res/drawable/dpad_standard_cardinal_depressed.xml b/src/android/app/src/main/res/drawable/dpad_standard_cardinal_depressed.xml
new file mode 100644
index 000000000..5eeb51dbe
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/dpad_standard_cardinal_depressed.xml
@@ -0,0 +1,24 @@
1<vector android:alpha="0.6" android:height="221.78dp"
2 android:viewportHeight="221.78" android:viewportWidth="221.78"
3 android:width="221.78dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.5"
6 android:pathData="M221.78,87.07v47.64a11.53,11.53 0,0 1,-11.5 11.5H151.62a5.42,5.42 0,0 0,-5.41 5.41v58.66a11.53,11.53 0,0 1,-11.5 11.5H87.07a11.53,11.53 0,0 1,-11.5 -11.5V151.61a5.41,5.41 0,0 0,-5.41 -5.41H11.5A11.53,11.53 0,0 1,0 134.7V87.05a11.53,11.53 0,0 1,11.5 -11.5H70.16a5.41,5.41 0,0 0,5.41 -5.41V11.5A11.53,11.53 0,0 1,87.07 0h47.64a11.53,11.53 0,0 1,11.5 11.5V70.16a5.41,5.41 0,0 0,5.41 5.41h58.66A11.53,11.53 0,0 1,221.78 87.07Z" android:strokeAlpha="0.5">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="110.89" android:endY="-38.27"
9 android:startX="110.89" android:startY="183.51" android:type="linear">
10 <item android:color="#7F000000" android:offset="0"/>
11 <item android:color="#BA000000" android:offset="0.43"/>
12 <item android:color="#FF000000" android:offset="0.5"/>
13 </gradient>
14 </aapt:attr>
15 </path>
16 <path android:fillAlpha="0.1" android:fillColor="#fff"
17 android:pathData="M195.47,110.32l-16.26,-9.38a0.65,0.65 0,0 0,-1 0.56v18.78a0.66,0.66 0,0 0,1 0.57l16.26,-9.39A0.66,0.66 0,0 0,195.47 110.32Z" android:strokeAlpha="0.1"/>
18 <path android:fillAlpha="0.1" android:fillColor="#fff"
19 android:pathData="M26.31,110.32l16.26,-9.38a0.65,0.65 0,0 1,1 0.56v18.78a0.66,0.66 0,0 1,-1 0.57l-16.26,-9.39A0.66,0.66 0,0 1,26.31 110.32Z" android:strokeAlpha="0.1"/>
20 <path android:fillAlpha="0.75" android:fillColor="#fff"
21 android:pathData="M110.32,26.31l-9.38,16.26a0.65,0.65 0,0 0,0.56 1h18.78a0.66,0.66 0,0 0,0.57 -1l-9.39,-16.26A0.66,0.66 0,0 0,110.32 26.31Z" android:strokeAlpha="0.75"/>
22 <path android:fillAlpha="0.1" android:fillColor="#fff"
23 android:pathData="M110.32,195.47l-9.38,-16.26a0.65,0.65 0,0 1,0.56 -1h18.78a0.66,0.66 0,0 1,0.57 1l-9.39,16.26A0.66,0.66 0,0 1,110.32 195.47Z" android:strokeAlpha="0.1"/>
24</vector>
diff --git a/src/android/app/src/main/res/drawable/dpad_standard_diagonal_depressed.xml b/src/android/app/src/main/res/drawable/dpad_standard_diagonal_depressed.xml
new file mode 100644
index 000000000..520fd447c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/dpad_standard_diagonal_depressed.xml
@@ -0,0 +1,24 @@
1<vector android:alpha="0.6" android:height="221.78dp"
2 android:viewportHeight="221.78" android:viewportWidth="221.78"
3 android:width="221.78dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.5"
6 android:pathData="M221.78,87.07v47.64a11.53,11.53 0,0 1,-11.5 11.5H151.62a5.42,5.42 0,0 0,-5.41 5.41v58.66a11.53,11.53 0,0 1,-11.5 11.5H87.07a11.53,11.53 0,0 1,-11.5 -11.5V151.61a5.41,5.41 0,0 0,-5.41 -5.41H11.5A11.53,11.53 0,0 1,0 134.7V87.05a11.53,11.53 0,0 1,11.5 -11.5H70.16a5.41,5.41 0,0 0,5.41 -5.41V11.5A11.53,11.53 0,0 1,87.07 0h47.64a11.53,11.53 0,0 1,11.5 11.5V70.16a5.41,5.41 0,0 0,5.41 5.41h58.66A11.53,11.53 0,0 1,221.78 87.07Z" android:strokeAlpha="0.5">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="31.24" android:endY="31.24"
9 android:startX="188.07" android:startY="188.07" android:type="linear">
10 <item android:color="#7F000000" android:offset="0"/>
11 <item android:color="#BA000000" android:offset="0.43"/>
12 <item android:color="#FF000000" android:offset="0.5"/>
13 </gradient>
14 </aapt:attr>
15 </path>
16 <path android:fillAlpha="0.1" android:fillColor="#fff"
17 android:pathData="M195.47,110.32l-16.26,-9.38a0.65,0.65 0,0 0,-1 0.56v18.78a0.66,0.66 0,0 0,1 0.57l16.26,-9.39A0.66,0.66 0,0 0,195.47 110.32Z" android:strokeAlpha="0.1"/>
18 <path android:fillAlpha="0.75" android:fillColor="#fff"
19 android:pathData="M26.31,110.32l16.26,-9.38a0.65,0.65 0,0 1,1 0.56v18.78a0.66,0.66 0,0 1,-1 0.57l-16.26,-9.39A0.66,0.66 0,0 1,26.31 110.32Z" android:strokeAlpha="0.75"/>
20 <path android:fillAlpha="0.75" android:fillColor="#fff"
21 android:pathData="M110.32,26.31l-9.38,16.26a0.65,0.65 0,0 0,0.56 1h18.78a0.66,0.66 0,0 0,0.57 -1l-9.39,-16.26A0.66,0.66 0,0 0,110.32 26.31Z" android:strokeAlpha="0.75"/>
22 <path android:fillAlpha="0.1" android:fillColor="#fff"
23 android:pathData="M110.32,195.47l-9.38,-16.26a0.65,0.65 0,0 1,0.56 -1h18.78a0.66,0.66 0,0 1,0.57 1l-9.39,16.26A0.66,0.66 0,0 1,110.32 195.47Z" android:strokeAlpha="0.1"/>
24</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_a.xml b/src/android/app/src/main/res/drawable/facebutton_a.xml
new file mode 100644
index 000000000..668652edb
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_a.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="51.8" android:centerY="51.8"
9 android:gradientRadius="51.8" android:type="radial">
10 <item android:color="#FFC3C4C5" android:offset="0.58"/>
11 <item android:color="#FFC6C6C6" android:offset="0.84"/>
12 <item android:color="#FFC7C7C7" android:offset="0.88"/>
13 <item android:color="#FFC2C2C2" android:offset="0.91"/>
14 <item android:color="#FFB5B5B5" android:offset="0.94"/>
15 <item android:color="#FF9E9E9E" android:offset="0.98"/>
16 <item android:color="#FF8F8F8F" android:offset="1"/>
17 </gradient>
18 </aapt:attr>
19 </path>
20 <path android:fillAlpha="0.6" android:fillColor="#FF000000"
21 android:pathData="M49.88,34.36h4.29L69.1,69.25L63.58,69.25l-3.5,-8.63L43.48,60.62L40,69.25L34.51,69.25ZM58.36,56.48 L51.85,40.48h-0.1l-6.6,16Z" android:strokeAlpha="0.6"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_a_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_a_depressed.xml
new file mode 100644
index 000000000..4fbe06962
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_a_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M49.88,34.36h4.29L69.1,69.25L63.58,69.25l-3.5,-8.63L43.48,60.62L40,69.25L34.51,69.25ZM58.36,56.48 L51.85,40.48h-0.1l-6.6,16Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_b.xml b/src/android/app/src/main/res/drawable/facebutton_b.xml
new file mode 100644
index 000000000..8912219ca
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_b.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="51.8" android:centerY="51.8"
9 android:gradientRadius="51.8" android:type="radial">
10 <item android:color="#FFC3C4C5" android:offset="0.58"/>
11 <item android:color="#FFC6C6C6" android:offset="0.84"/>
12 <item android:color="#FFC7C7C7" android:offset="0.88"/>
13 <item android:color="#FFC2C2C2" android:offset="0.91"/>
14 <item android:color="#FFB5B5B5" android:offset="0.94"/>
15 <item android:color="#FF9E9E9E" android:offset="0.98"/>
16 <item android:color="#FF8F8F8F" android:offset="1"/>
17 </gradient>
18 </aapt:attr>
19 </path>
20 <path android:fillAlpha="0.6" android:fillColor="#FF000000"
21 android:pathData="M41,35.67L53.15,35.67a15.78,15.78 0,0 1,4.22 0.54,10.07 10.07,0 0,1 3.36,1.6A7.49,7.49 0,0 1,63 40.53a8.73,8.73 0,0 1,0.81 3.88,7.13 7.13,0 0,1 -1.67,4.91 9.75,9.75 0,0 1,-4.35 2.79v0.1a7.4,7.4 0,0 1,3 0.82,8.1 8.1,0 0,1 2.4,1.87 9.14,9.14 0,0 1,2.2 6,8.73 8.73,0 0,1 -1,4.17 8.86,8.86 0,0 1,-2.64 3A12.39,12.39 0,0 1,57.79 70a17.12,17.12 0,0 1,-4.79 0.64L41,70.64ZM45.74,50.19h6.47a10.75,10.75 0,0 0,2.52 -0.28A5.56,5.56 0,0 0,56.8 49a4.73,4.73 0,0 0,1.41 -1.63A5.22,5.22 0,0 0,58.73 45a5,5 0,0 0,-5.53 -5.13L45.74,39.87ZM45.74,66.48h7a14.17,14.17 0,0 0,2.4 -0.22,7.23 7.23,0 0,0 2.44,-0.89 6,6 0,0 0,1.93 -1.8,5.15 5.15,0 0,0 0.79,-3 5.52,5.52 0,0 0,-2 -4.67,8.75 8.75,0 0,0 -5.48,-1.56h-7Z" android:strokeAlpha="0.6"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_b_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_b_depressed.xml
new file mode 100644
index 000000000..012abeaf1
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_b_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M41,35.67L53.15,35.67a15.78,15.78 0,0 1,4.22 0.54,10.07 10.07,0 0,1 3.36,1.6A7.49,7.49 0,0 1,63 40.53a8.73,8.73 0,0 1,0.81 3.88,7.13 7.13,0 0,1 -1.67,4.91 9.75,9.75 0,0 1,-4.35 2.79v0.1a7.4,7.4 0,0 1,3 0.82,8.1 8.1,0 0,1 2.4,1.87 9.14,9.14 0,0 1,2.2 6,8.73 8.73,0 0,1 -1,4.17 8.86,8.86 0,0 1,-2.64 3A12.39,12.39 0,0 1,57.79 70a17.12,17.12 0,0 1,-4.79 0.64L41,70.64ZM45.74,50.19h6.47a10.75,10.75 0,0 0,2.52 -0.28A5.56,5.56 0,0 0,56.8 49a4.73,4.73 0,0 0,1.41 -1.63A5.22,5.22 0,0 0,58.73 45a5,5 0,0 0,-5.53 -5.13L45.74,39.87ZM45.74,66.48h7a14.17,14.17 0,0 0,2.4 -0.22,7.23 7.23,0 0,0 2.44,-0.89 6,6 0,0 0,1.93 -1.8,5.15 5.15,0 0,0 0.79,-3 5.52,5.52 0,0 0,-2 -4.67,8.75 8.75,0 0,0 -5.48,-1.56h-7Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_home.xml b/src/android/app/src/main/res/drawable/facebutton_home.xml
new file mode 100644
index 000000000..03596ec2e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_home.xml
@@ -0,0 +1,21 @@
1<vector android:alpha="0.6" android:height="70.55dp"
2 android:viewportHeight="70.55" android:viewportWidth="70.55"
3 android:width="70.55dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.75"
5 android:pathData="M35.27,35.27m-35.27,0a35.27,35.27 0,1 1,70.54 0a35.27,35.27 0,1 1,-70.54 0" android:strokeAlpha="0.75">
6 <aapt:attr name="android:fillColor">
7 <gradient android:centerX="35.27" android:centerY="35.27"
8 android:gradientRadius="35.27" android:type="radial">
9 <item android:color="#FFC3C4C5" android:offset="0.58"/>
10 <item android:color="#FFC6C6C6" android:offset="0.84"/>
11 <item android:color="#FFC7C7C7" android:offset="0.88"/>
12 <item android:color="#FFC2C2C2" android:offset="0.91"/>
13 <item android:color="#FFB5B5B5" android:offset="0.94"/>
14 <item android:color="#FF9E9E9E" android:offset="0.98"/>
15 <item android:color="#FF8F8F8F" android:offset="1"/>
16 </gradient>
17 </aapt:attr>
18 </path>
19 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
20 android:pathData="M55.19,32.72 L36.06,15.21a1.14,1.14 0,0 0,-1.57 0L15.36,32.72a1.13,1.13 0,0 0,0.79 1.94H19.4a0.72,0.72 0,0 1,0.72 0.72V51.49a1.13,1.13 0,0 0,1.12 1.13H49.31a1.13,1.13 0,0 0,1.12 -1.13V35.38a0.72,0.72 0,0 1,0.72 -0.72H54.4A1.13,1.13 0,0 0,55.19 32.72ZM41.45,43.86a0.9,0.9 0,0 1,-0.9 0.9H30a0.9,0.9 0,0 1,-0.9 -0.9V35.55a0.89,0.89 0,0 1,0.9 -0.89H40.55a0.89,0.89 0,0 1,0.9 0.89Z" android:strokeAlpha="0.75"/>
21</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_home_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_home_depressed.xml
new file mode 100644
index 000000000..cde7c6a9e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_home_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="70.55dp"
2 android:viewportHeight="70.55" android:viewportWidth="70.55"
3 android:width="70.55dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M35.27,35.27m-35.27,0a35.27,35.27 0,1 1,70.54 0a35.27,35.27 0,1 1,-70.54 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M55.19,32.72 L36.06,15.21a1.14,1.14 0,0 0,-1.57 0L15.36,32.72a1.13,1.13 0,0 0,0.79 1.94H19.4a0.72,0.72 0,0 1,0.72 0.72V51.49a1.13,1.13 0,0 0,1.12 1.13H49.31a1.13,1.13 0,0 0,1.12 -1.13V35.38a0.72,0.72 0,0 1,0.72 -0.72H54.4A1.13,1.13 0,0 0,55.19 32.72ZM41.45,43.86a0.9,0.9 0,0 1,-0.9 0.9H30a0.9,0.9 0,0 1,-0.9 -0.9V35.55a0.89,0.89 0,0 1,0.9 -0.89H40.55a0.89,0.89 0,0 1,0.9 0.89Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_minus.xml b/src/android/app/src/main/res/drawable/facebutton_minus.xml
new file mode 100644
index 000000000..4296b4fcc
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_minus.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="69.95dp"
2 android:viewportHeight="69.95" android:viewportWidth="69.95"
3 android:width="69.95dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.75"
5 android:pathData="M34.97,34.97m-34.97,0a34.97,34.97 0,1 1,69.94 0a34.97,34.97 0,1 1,-69.94 0" android:strokeAlpha="0.75">
6 <aapt:attr name="android:fillColor">
7 <gradient android:centerX="34.97" android:centerY="34.97"
8 android:gradientRadius="34.97" android:type="radial">
9 <item android:color="#FFC3C4C5" android:offset="0.58"/>
10 <item android:color="#FFC6C6C6" android:offset="0.84"/>
11 <item android:color="#FFC7C7C7" android:offset="0.88"/>
12 <item android:color="#FFC2C2C2" android:offset="0.91"/>
13 <item android:color="#FFB5B5B5" android:offset="0.94"/>
14 <item android:color="#FF9E9E9E" android:offset="0.98"/>
15 <item android:color="#FF8F8F8F" android:offset="1"/>
16 </gradient>
17 </aapt:attr>
18 </path>
19 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
20 android:pathData="M52,38.28H17.91a0.52,0.52 0,0 1,-0.52 -0.52V32.19a0.52,0.52 0,0 1,0.52 -0.52H52a0.52,0.52 0,0 1,0.52 0.52v5.57A0.52,0.52 0,0 1,52 38.28Z"
21 android:strokeAlpha="0.75" android:strokeColor="#000" android:strokeWidth="2.5"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_minus_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_minus_depressed.xml
new file mode 100644
index 000000000..628027841
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_minus_depressed.xml
@@ -0,0 +1,9 @@
1<vector android:alpha="0.6" android:height="69.95dp"
2 android:viewportHeight="69.95" android:viewportWidth="69.95"
3 android:width="69.95dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M34.97,34.97m-34.97,0a34.97,34.97 0,1 1,69.94 0a34.97,34.97 0,1 1,-69.94 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M52,38.28H17.91a0.52,0.52 0,0 1,-0.52 -0.52V32.19a0.52,0.52 0,0 1,0.52 -0.52H52a0.52,0.52 0,0 1,0.52 0.52v5.57A0.52,0.52 0,0 1,52 38.28Z"
8 android:strokeAlpha="0.75" android:strokeColor="#fff" android:strokeWidth="2.5"/>
9</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_plus.xml b/src/android/app/src/main/res/drawable/facebutton_plus.xml
new file mode 100644
index 000000000..43ae14365
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_plus.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="69.95dp"
2 android:viewportHeight="69.95" android:viewportWidth="69.95"
3 android:width="69.95dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.75"
5 android:pathData="M34.97,34.97m-34.97,0a34.97,34.97 0,1 1,69.94 0a34.97,34.97 0,1 1,-69.94 0" android:strokeAlpha="0.75">
6 <aapt:attr name="android:fillColor">
7 <gradient android:centerX="34.97" android:centerY="34.97"
8 android:gradientRadius="34.97" android:type="radial">
9 <item android:color="#FFC3C4C5" android:offset="0.58"/>
10 <item android:color="#FFC6C6C6" android:offset="0.84"/>
11 <item android:color="#FFC7C7C7" android:offset="0.88"/>
12 <item android:color="#FFC2C2C2" android:offset="0.91"/>
13 <item android:color="#FFB5B5B5" android:offset="0.94"/>
14 <item android:color="#FF9E9E9E" android:offset="0.98"/>
15 <item android:color="#FF8F8F8F" android:offset="1"/>
16 </gradient>
17 </aapt:attr>
18 </path>
19 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
20 android:pathData="M13.94,31.9H31.16a0.65,0.65 0,0 0,0.65 -0.64V14.59a0.65,0.65 0,0 1,0.64 -0.65h5a0.65,0.65 0,0 1,0.65 0.65V31.26a0.65,0.65 0,0 0,0.65 0.64H56a0.65,0.65 0,0 1,0.65 0.65V37.4a0.65,0.65 0,0 1,-0.65 0.65H38.79a0.65,0.65 0,0 0,-0.65 0.64V55.36a0.65,0.65 0,0 1,-0.65 0.64h-5a0.64,0.64 0,0 1,-0.64 -0.64V38.69a0.65,0.65 0,0 0,-0.65 -0.64H13.94a0.65,0.65 0,0 1,-0.65 -0.65V32.55A0.65,0.65 0,0 1,13.94 31.9Z"
21 android:strokeAlpha="0.75" android:strokeColor="#000" android:strokeWidth="2.5"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_plus_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_plus_depressed.xml
new file mode 100644
index 000000000..c510e136e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_plus_depressed.xml
@@ -0,0 +1,9 @@
1<vector android:alpha="0.6" android:height="69.95dp"
2 android:viewportHeight="69.95" android:viewportWidth="69.95"
3 android:width="69.95dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M34.97,34.97m-34.97,0a34.97,34.97 0,1 1,69.94 0a34.97,34.97 0,1 1,-69.94 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M13.94,31.9H31.16a0.65,0.65 0,0 0,0.65 -0.64V14.59a0.65,0.65 0,0 1,0.64 -0.65h5a0.65,0.65 0,0 1,0.65 0.65V31.26a0.65,0.65 0,0 0,0.65 0.64H56a0.65,0.65 0,0 1,0.65 0.65V37.4a0.65,0.65 0,0 1,-0.65 0.65H38.79a0.65,0.65 0,0 0,-0.65 0.64V55.36a0.65,0.65 0,0 1,-0.65 0.64h-5a0.64,0.64 0,0 1,-0.64 -0.64V38.69a0.65,0.65 0,0 0,-0.65 -0.64H13.94a0.65,0.65 0,0 1,-0.65 -0.65V32.55A0.65,0.65 0,0 1,13.94 31.9Z"
8 android:strokeAlpha="0.75" android:strokeColor="#fff" android:strokeWidth="2.5"/>
9</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_screenshot.xml b/src/android/app/src/main/res/drawable/facebutton_screenshot.xml
new file mode 100644
index 000000000..984b4fd02
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_screenshot.xml
@@ -0,0 +1,21 @@
1<vector android:alpha="0.6" android:height="70dp"
2 android:viewportHeight="70" android:viewportWidth="70"
3 android:width="70dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.75"
5 android:pathData="M10.63,0L59.37,0A10.63,10.63 0,0 1,70 10.63L70,59.37A10.63,10.63 0,0 1,59.37 70L10.63,70A10.63,10.63 0,0 1,0 59.37L0,10.63A10.63,10.63 0,0 1,10.63 0z" android:strokeAlpha="0.75">
6 <aapt:attr name="android:fillColor">
7 <gradient android:centerX="35" android:centerY="35"
8 android:gradientRadius="42.51" android:type="radial">
9 <item android:color="#FFC3C4C5" android:offset="0.58"/>
10 <item android:color="#FFC6C6C6" android:offset="0.84"/>
11 <item android:color="#FFC7C7C7" android:offset="0.88"/>
12 <item android:color="#FFC2C2C2" android:offset="0.91"/>
13 <item android:color="#FFB5B5B5" android:offset="0.94"/>
14 <item android:color="#FF9E9E9E" android:offset="0.98"/>
15 <item android:color="#FF8F8F8F" android:offset="1"/>
16 </gradient>
17 </aapt:attr>
18 </path>
19 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
20 android:pathData="M35,35m-21.5,0a21.5,21.5 0,1 1,43 0a21.5,21.5 0,1 1,-43 0" android:strokeAlpha="0.75"/>
21</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_screenshot_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_screenshot_depressed.xml
new file mode 100644
index 000000000..fd2e44294
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_screenshot_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="70dp"
2 android:viewportHeight="70" android:viewportWidth="70"
3 android:width="70dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M10.63,0L59.37,0A10.63,10.63 0,0 1,70 10.63L70,59.37A10.63,10.63 0,0 1,59.37 70L10.63,70A10.63,10.63 0,0 1,0 59.37L0,10.63A10.63,10.63 0,0 1,10.63 0z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M35,35m-21.5,0a21.5,21.5 0,1 1,43 0a21.5,21.5 0,1 1,-43 0" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_x.xml b/src/android/app/src/main/res/drawable/facebutton_x.xml
new file mode 100644
index 000000000..43fdd14c4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_x.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="51.8" android:centerY="51.8"
9 android:gradientRadius="51.8" android:type="radial">
10 <item android:color="#FFC3C4C5" android:offset="0.58"/>
11 <item android:color="#FFC6C6C6" android:offset="0.84"/>
12 <item android:color="#FFC7C7C7" android:offset="0.88"/>
13 <item android:color="#FFC2C2C2" android:offset="0.91"/>
14 <item android:color="#FFB5B5B5" android:offset="0.94"/>
15 <item android:color="#FF9E9E9E" android:offset="0.98"/>
16 <item android:color="#FF8F8F8F" android:offset="1"/>
17 </gradient>
18 </aapt:attr>
19 </path>
20 <path android:fillAlpha="0.6" android:fillColor="#FF000000"
21 android:pathData="M48.39,50.91 L36.63,34.31h6.08L51.8,47.75l9,-13.44h5.93L55.07,50.86 67.92,69.3H61.69l-10,-15.17L41.57,69.3H35.69Z" android:strokeAlpha="0.6"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_x_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_x_depressed.xml
new file mode 100644
index 000000000..a9ba49169
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_x_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M48.39,50.91 L36.63,34.31h6.08L51.8,47.75l9,-13.44h5.93L55.07,50.86 67.92,69.3H61.69l-10,-15.17L41.57,69.3H35.69Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_y.xml b/src/android/app/src/main/res/drawable/facebutton_y.xml
new file mode 100644
index 000000000..980be3b2e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_y.xml
@@ -0,0 +1,22 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="51.8" android:centerY="51.8"
9 android:gradientRadius="51.8" android:type="radial">
10 <item android:color="#FFC3C4C5" android:offset="0.58"/>
11 <item android:color="#FFC6C6C6" android:offset="0.84"/>
12 <item android:color="#FFC7C7C7" android:offset="0.88"/>
13 <item android:color="#FFC2C2C2" android:offset="0.91"/>
14 <item android:color="#FFB5B5B5" android:offset="0.94"/>
15 <item android:color="#FF9E9E9E" android:offset="0.98"/>
16 <item android:color="#FF8F8F8F" android:offset="1"/>
17 </gradient>
18 </aapt:attr>
19 </path>
20 <path android:fillAlpha="0.6" android:fillColor="#FF000000"
21 android:pathData="M49.43,54.37l-13.23,-20h6.07L51.8,49.68l9.83,-15.36h5.78l-13.24,20V69.29H49.43Z" android:strokeAlpha="0.6"/>
22</vector>
diff --git a/src/android/app/src/main/res/drawable/facebutton_y_depressed.xml b/src/android/app/src/main/res/drawable/facebutton_y_depressed.xml
new file mode 100644
index 000000000..320d63897
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/facebutton_y_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="103.61dp"
2 android:viewportHeight="103.61" android:viewportWidth="103.61"
3 android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M51.8,51.8m-51.8,0a51.8,51.8 0,1 1,103.6 0a51.8,51.8 0,1 1,-103.6 0" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M49.43,54.37l-13.23,-20h6.07L51.8,49.68l9.83,-15.36h5.78l-13.24,20V69.29H49.43Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..f7deb2532
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_arrow_forward.xml b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml
new file mode 100644
index 000000000..3b85a3e2c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:autoMirrored="true"
5 android:viewportWidth="24"
6 android:viewportHeight="24">
7 <path
8 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_back.xml b/src/android/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 000000000..f99fea719
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:autoMirrored="true"
5 android:viewportWidth="24"
6 android:viewportHeight="24">
7 <path
8 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_cartridge.xml b/src/android/app/src/main/res/drawable/ic_cartridge.xml
new file mode 100644
index 000000000..b332d9c0a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_cartridge.xml
@@ -0,0 +1,12 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,7c-3.44,0 -4.16,0.35 -4.31,0.5s0,0 0,0.17v8.91a0.38,0.38 0,0 0,0.37 0.37h7.85a0.38,0.38 0,0 0,0.38 -0.37V7.53S15.41,7 12,7Z" />
9 <path
10 android:fillColor="?attr/colorControlNormal"
11 android:pathData="M22,6.51a23.12,23.12 0,0 0,-9.75 -2.1A26.09,26.09 0,0 0,2.05 6.5a1.43,1.43 0,0 0,-0.84 1.3L1.21,18.41A1.19,1.19 0,0 0,2.4 19.6L21.57,19.6a1.19,1.19 0,0 0,1.19 -1.19L22.76,7.81A1.43,1.43 0,0 0,22 6.51ZM5.56,18.59h-1v-12l1,-0.3ZM17.29,16.59A1.38,1.38 0,0 1,15.91 18L8,18a1.37,1.37 0,0 1,-1.37 -1.37L6.63,7.73A1.13,1.13 0,0 1,7 6.84c0.41,-0.41 1.3,-0.8 5,-0.8s4.57,0.38 5,0.79a1.12,1.12 0,0 1,0.31 0.87ZM18.39,18.59L18.39,6.26c0.33,0.09 0.66,0.19 1,0.31v12Z" />
12</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_cartridge_outline.xml b/src/android/app/src/main/res/drawable/ic_cartridge_outline.xml
new file mode 100644
index 000000000..cc35d7eff
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_cartridge_outline.xml
@@ -0,0 +1,12 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M22,6.5c-3.1,-1.4 -6.4,-2.1 -9.7,-2.1C8.7,4.4 5.3,5.1 2,6.5C1.5,6.7 1.2,7.2 1.2,7.8v10.6c0,0.7 0.5,1.2 1.2,1.2h19.2c0.7,0 1.2,-0.5 1.2,-1.2c0,0 0,0 0,0l0,0V7.8C22.8,7.3 22.5,6.7 22,6.5zM4.6,18.6H3.6v-3.2c0,-0.2 -0.1,-0.3 -0.2,-0.4c-0.4,-0.2 -0.8,-0.4 -1.2,-0.5V7.8c0,-0.2 0.1,-0.3 0.2,-0.4c0.7,-0.3 1.4,-0.6 2.1,-0.8V18.6zM18.4,18.6H5.6V6.3c2.2,-0.6 4.4,-0.9 6.7,-0.9c2.1,0 4.1,0.3 6.1,0.8L18.4,18.6zM21.8,14.5c-0.4,0.1 -0.8,0.3 -1.2,0.5c-0.1,0.1 -0.2,0.2 -0.2,0.4v3.2h-1v-12c0.7,0.2 1.5,0.5 2.2,0.8c0.1,0.1 0.2,0.2 0.2,0.4L21.8,14.5z" />
9 <path
10 android:fillColor="?attr/colorControlNormal"
11 android:pathData="M17,6.8C16.5,6.4 15.7,6 12,6S7.4,6.4 7,6.8C6.8,7.1 6.6,7.4 6.7,7.7v8.9C6.7,17.4 7.3,18 8,18h7.9c0.8,0 1.4,-0.6 1.4,-1.4l0,0V7.7C17.3,7.4 17.2,7.1 17,6.8zM16.2,15.9c0,0.6 -0.5,1.1 -1.1,1.1H8.9c-0.6,0 -1.1,-0.5 -1.1,-1.1V8.4c0,-0.3 0.1,-0.5 0.2,-0.8C8.4,7.3 9,7 12,7s3.6,0.3 4,0.7c0.2,0.2 0.2,0.5 0.2,0.8V15.9z" />
12</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_check.xml b/src/android/app/src/main/res/drawable/ic_check.xml
new file mode 100644
index 000000000..04b89abf2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_check_circle.xml b/src/android/app/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 000000000..49e6ecd71
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_clear.xml b/src/android/app/src/main/res/drawable/ic_clear.xml
new file mode 100644
index 000000000..b6edb1d32
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_clear.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_controller.xml b/src/android/app/src/main/res/drawable/ic_controller.xml
new file mode 100644
index 000000000..060cd9ae2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_controller.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_diamond.xml b/src/android/app/src/main/res/drawable/ic_diamond.xml
new file mode 100644
index 000000000..3896e12e4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_diamond.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19,3H5L2,9l10,12L22,9L19,3zM9.62,8l1.5,-3h1.76l1.5,3H9.62zM11,10v6.68L5.44,10H11zM13,10h5.56L13,16.68V10zM19.26,8h-2.65l-1.5,-3h2.65L19.26,8zM6.24,5h2.65l-1.5,3H4.74L6.24,5z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_discord.xml b/src/android/app/src/main/res/drawable/ic_discord.xml
new file mode 100644
index 000000000..7a9c6ba79
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_discord.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="200dp"
3 android:height="200dp"
4 android:viewportWidth="256"
5 android:viewportHeight="256">
6 <path
7 android:fillColor="#5865F2"
8 android:fillType="nonZero"
9 android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_exit.xml b/src/android/app/src/main/res/drawable/ic_exit.xml
new file mode 100644
index 000000000..a55a1d387
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_exit.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:autoMirrored="true"
5 android:viewportHeight="24"
6 android:viewportWidth="24">
7 <path
8 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_firmware.xml b/src/android/app/src/main/res/drawable/ic_firmware.xml
new file mode 100644
index 000000000..61f3485e4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_firmware.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="960"
5 android:viewportHeight="960"
6 android:tint="?attr/colorControlNormal">
7 <path
8 android:fillColor="@android:color/white"
9 android:pathData="M160,840Q127,840 103.5,816.5Q80,793 80,760L80,200Q80,167 103.5,143.5Q127,120 160,120L720,120Q753,120 776.5,143.5Q800,167 800,200L800,280L840,280Q857,280 868.5,291.5Q880,303 880,320Q880,337 868.5,348.5Q857,360 840,360L800,360L800,440L840,440Q857,440 868.5,451.5Q880,463 880,480Q880,497 868.5,508.5Q857,520 840,520L800,520L800,600L840,600Q857,600 868.5,611.5Q880,623 880,640Q880,657 868.5,668.5Q857,680 840,680L800,680L800,760Q800,793 776.5,816.5Q753,840 720,840L160,840ZM160,760L720,760Q720,760 720,760Q720,760 720,760L720,200Q720,200 720,200Q720,200 720,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760ZM280,680L400,680Q417,680 428.5,668.5Q440,657 440,640L440,560Q440,543 428.5,531.5Q417,520 400,520L280,520Q263,520 251.5,531.5Q240,543 240,560L240,640Q240,657 251.5,668.5Q263,680 280,680ZM520,400L600,400Q617,400 628.5,388.5Q640,377 640,360L640,320Q640,303 628.5,291.5Q617,280 600,280L520,280Q503,280 491.5,291.5Q480,303 480,320L480,360Q480,377 491.5,388.5Q503,400 520,400ZM280,480L400,480Q417,480 428.5,468.5Q440,457 440,440L440,320Q440,303 428.5,291.5Q417,280 400,280L280,280Q263,280 251.5,291.5Q240,303 240,320L240,440Q240,457 251.5,468.5Q263,480 280,480ZM520,680L600,680Q617,680 628.5,668.5Q640,657 640,640L640,480Q640,463 628.5,451.5Q617,440 600,440L520,440Q503,440 491.5,451.5Q480,463 480,480L480,640Q480,657 491.5,668.5Q503,680 520,680ZM160,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760L160,760Q160,760 160,760Q160,760 160,760L160,200Q160,200 160,200Q160,200 160,200Z"/>
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_folder_open.xml b/src/android/app/src/main/res/drawable/ic_folder_open.xml
new file mode 100644
index 000000000..7958fdaec
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_folder_open.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M4.17,20.48C3.431,20.48 2.805,20.223 2.291,19.709C1.777,19.195 1.52,18.569 1.52,17.83V5.958C1.52,5.21 1.777,4.581 2.291,4.072C2.805,3.562 3.431,3.308 4.17,3.308H9.788L12,5.52H19.83C20.578,5.52 21.207,5.777 21.716,6.291C22.226,6.805 22.48,7.431 22.48,8.17H4.17V17.901L6.57,10.17H23.938L21.448,18.194C21.201,18.957 20.817,19.528 20.294,19.909C19.77,20.29 19.118,20.48 18.336,20.48H4.17Z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_github.xml b/src/android/app/src/main/res/drawable/ic_github.xml
new file mode 100644
index 000000000..c2ee43803
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_github.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="98dp"
3 android:height="96dp"
4 android:viewportWidth="98"
5 android:viewportHeight="96">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:fillType="evenOdd"
9 android:pathData="M48.854,0C21.839,0 0,22 0,49.217c0,21.756 13.993,40.172 33.405,46.69 2.427,0.49 3.316,-1.059 3.316,-2.362 0,-1.141 -0.08,-5.052 -0.08,-9.127 -13.59,2.934 -16.42,-5.867 -16.42,-5.867 -2.184,-5.704 -5.42,-7.17 -5.42,-7.17 -4.448,-3.015 0.324,-3.015 0.324,-3.015 4.934,0.326 7.523,5.052 7.523,5.052 4.367,7.496 11.404,5.378 14.235,4.074 0.404,-3.178 1.699,-5.378 3.074,-6.6 -10.839,-1.141 -22.243,-5.378 -22.243,-24.283 0,-5.378 1.94,-9.778 5.014,-13.2 -0.485,-1.222 -2.184,-6.275 0.486,-13.038 0,0 4.125,-1.304 13.426,5.052a46.97,46.97 0,0 1,12.214 -1.63c4.125,0 8.33,0.571 12.213,1.63 9.302,-6.356 13.427,-5.052 13.427,-5.052 2.67,6.763 0.97,11.816 0.485,13.038 3.155,3.422 5.015,7.822 5.015,13.2 0,18.905 -11.404,23.06 -22.324,24.283 1.78,1.548 3.316,4.481 3.316,9.126 0,6.6 -0.08,11.897 -0.08,13.526 0,1.304 0.89,2.853 3.316,2.364 19.412,-6.52 33.405,-24.935 33.405,-46.691C97.707,22 75.788,0 48.854,0z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg.xml b/src/android/app/src/main/res/drawable/ic_icon_bg.xml
new file mode 100644
index 000000000..df62dde92
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_icon_bg.xml
@@ -0,0 +1,751 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="512dp"
3 android:height="512dp"
4 android:viewportWidth="512"
5 android:viewportHeight="512">
6 <group>
7 <clip-path
8 android:pathData="M0,0h512v512h-512z"/>
9 <path
10 android:pathData="M0,0h512v512h-512z"
11 android:fillColor="#ffffff"/>
12 <path
13 android:pathData="M0,0h512v512h-512z"
14 android:fillColor="#1C1C1C"/>
15 <path
16 android:pathData="M208.16,7H159.88C155.54,7 152,10.54 152,14.88V92.16C152,96.54 155.54,100.04 159.88,100.04H208.12C212.5,100.04 216,96.5 216,92.16V14.88C216.04,10.54 212.5,7 208.16,7Z"
17 android:strokeAlpha="0.7"
18 android:fillColor="#282828"
19 android:fillAlpha="0.7"/>
20 <path
21 android:pathData="M208.8,89.73H158.44C156.65,89.73 155.18,88.26 155.18,86.47V17.02C155.18,15.23 156.65,13.76 158.44,13.76H208.84C210.63,13.76 212.1,15.23 212.1,17.02V86.51C212.06,88.26 210.59,89.73 208.8,89.73Z"
22 android:strokeAlpha="0.7"
23 android:fillColor="#1C1C1C"
24 android:fillAlpha="0.7"/>
25 <path
26 android:pathData="M194.16,14.16H173.08V12.93C173.08,12.29 173.6,11.77 174.24,11.77H193.01C193.65,11.77 194.16,12.29 194.16,12.93V14.16Z"
27 android:strokeAlpha="0.7"
28 android:fillColor="#1C1C1C"
29 android:fillAlpha="0.7"/>
30 <path
31 android:pathData="M183.86,97.29L177.93,92.92H189.79L183.86,97.29Z"
32 android:strokeAlpha="0.7"
33 android:fillColor="#1C1C1C"
34 android:fillAlpha="0.7"/>
35 <path
36 android:pathData="M424.16,7H375.88C371.54,7 368,10.54 368,14.88V92.16C368,96.54 371.54,100.04 375.88,100.04H424.12C428.5,100.04 432,96.5 432,92.16V14.88C432.04,10.54 428.5,7 424.16,7Z"
37 android:strokeAlpha="0.7"
38 android:fillColor="#282828"
39 android:fillAlpha="0.7"/>
40 <path
41 android:pathData="M424.8,89.73H374.44C372.65,89.73 371.18,88.26 371.18,86.47V17.02C371.18,15.23 372.65,13.76 374.44,13.76H424.84C426.63,13.76 428.1,15.23 428.1,17.02V86.51C428.06,88.26 426.59,89.73 424.8,89.73Z"
42 android:strokeAlpha="0.7"
43 android:fillColor="#1C1C1C"
44 android:fillAlpha="0.7"/>
45 <path
46 android:pathData="M410.16,14.16H389.08V12.93C389.08,12.29 389.6,11.77 390.23,11.77H409.01C409.65,11.77 410.16,12.29 410.16,12.93V14.16Z"
47 android:strokeAlpha="0.7"
48 android:fillColor="#1C1C1C"
49 android:fillAlpha="0.7"/>
50 <path
51 android:pathData="M399.86,97.29L393.93,92.92H405.79L399.86,97.29Z"
52 android:strokeAlpha="0.7"
53 android:fillColor="#1C1C1C"
54 android:fillAlpha="0.7"/>
55 <path
56 android:pathData="M352.16,109H303.88C299.54,109 296,112.54 296,116.88V194.16C296,198.54 299.54,202.04 303.88,202.04H352.12C356.5,202.04 360,198.5 360,194.16V116.88C360.04,112.54 356.5,109 352.16,109Z"
57 android:strokeAlpha="0.7"
58 android:fillColor="#282828"
59 android:fillAlpha="0.7"/>
60 <path
61 android:pathData="M352.8,191.73H302.44C300.65,191.73 299.18,190.26 299.18,188.47V119.02C299.18,117.23 300.65,115.76 302.44,115.76H352.84C354.63,115.76 356.1,117.23 356.1,119.02V188.51C356.06,190.26 354.59,191.73 352.8,191.73Z"
62 android:strokeAlpha="0.7"
63 android:fillColor="#1C1C1C"
64 android:fillAlpha="0.7"/>
65 <path
66 android:pathData="M338.16,116.16H317.08V114.93C317.08,114.29 317.6,113.77 318.23,113.77H337.01C337.65,113.77 338.16,114.29 338.16,114.93V116.16Z"
67 android:strokeAlpha="0.7"
68 android:fillColor="#1C1C1C"
69 android:fillAlpha="0.7"/>
70 <path
71 android:pathData="M327.86,199.29L321.93,194.92H333.79L327.86,199.29Z"
72 android:strokeAlpha="0.7"
73 android:fillColor="#1C1C1C"
74 android:fillAlpha="0.7"/>
75 <path
76 android:pathData="M496.16,7H447.88C443.54,7 440,10.54 440,14.88V92.16C440,96.54 443.54,100.04 447.88,100.04H496.12C500.5,100.04 504,96.5 504,92.16V14.88C504.04,10.54 500.5,7 496.16,7Z"
77 android:strokeAlpha="0.7"
78 android:fillColor="#282828"
79 android:fillAlpha="0.7"/>
80 <path
81 android:pathData="M496.8,89.73H446.44C444.65,89.73 443.18,88.26 443.18,86.47V17.02C443.18,15.23 444.65,13.76 446.44,13.76H496.84C498.63,13.76 500.1,15.23 500.1,17.02V86.51C500.06,88.26 498.59,89.73 496.8,89.73Z"
82 android:strokeAlpha="0.7"
83 android:fillColor="#1C1C1C"
84 android:fillAlpha="0.7"/>
85 <path
86 android:pathData="M482.16,14.16H461.08V12.93C461.08,12.29 461.6,11.77 462.23,11.77H481.01C481.65,11.77 482.16,12.29 482.16,12.93V14.16Z"
87 android:strokeAlpha="0.7"
88 android:fillColor="#1C1C1C"
89 android:fillAlpha="0.7"/>
90 <path
91 android:pathData="M471.86,97.29L465.93,92.92H477.79L471.86,97.29Z"
92 android:strokeAlpha="0.7"
93 android:fillColor="#1C1C1C"
94 android:fillAlpha="0.7"/>
95 <path
96 android:pathData="M352.16,7H303.88C299.54,7 296,10.54 296,14.88V92.16C296,96.54 299.54,100.04 303.88,100.04H352.12C356.5,100.04 360,96.5 360,92.16V14.88C360.04,10.54 356.5,7 352.16,7Z"
97 android:strokeAlpha="0.7"
98 android:fillColor="#282828"
99 android:fillAlpha="0.7"/>
100 <path
101 android:pathData="M352.8,89.73H302.44C300.65,89.73 299.18,88.26 299.18,86.47V17.02C299.18,15.23 300.65,13.76 302.44,13.76H352.84C354.63,13.76 356.1,15.23 356.1,17.02V86.51C356.06,88.26 354.59,89.73 352.8,89.73Z"
102 android:strokeAlpha="0.7"
103 android:fillColor="#1C1C1C"
104 android:fillAlpha="0.7"/>
105 <path
106 android:pathData="M338.16,14.16H317.08V12.93C317.08,12.29 317.6,11.77 318.23,11.77H337.01C337.65,11.77 338.16,12.29 338.16,12.93V14.16Z"
107 android:strokeAlpha="0.7"
108 android:fillColor="#1C1C1C"
109 android:fillAlpha="0.7"/>
110 <path
111 android:pathData="M327.86,97.29L321.93,92.92H333.79L327.86,97.29Z"
112 android:strokeAlpha="0.7"
113 android:fillColor="#1C1C1C"
114 android:fillAlpha="0.7"/>
115 <path
116 android:pathData="M280.16,7H231.88C227.54,7 224,10.54 224,14.88V92.16C224,96.54 227.54,100.04 231.88,100.04H280.12C284.5,100.04 288,96.5 288,92.16V14.88C288.04,10.54 284.5,7 280.16,7Z"
117 android:strokeAlpha="0.7"
118 android:fillColor="#282828"
119 android:fillAlpha="0.7"/>
120 <path
121 android:pathData="M280.8,89.73H230.44C228.65,89.73 227.18,88.26 227.18,86.47V17.02C227.18,15.23 228.65,13.76 230.44,13.76H280.84C282.63,13.76 284.1,15.23 284.1,17.02V86.51C284.06,88.26 282.59,89.73 280.8,89.73Z"
122 android:strokeAlpha="0.7"
123 android:fillColor="#1C1C1C"
124 android:fillAlpha="0.7"/>
125 <path
126 android:pathData="M266.16,14.16H245.08V12.93C245.08,12.29 245.6,11.77 246.24,11.77H265.01C265.65,11.77 266.16,12.29 266.16,12.93V14.16Z"
127 android:strokeAlpha="0.7"
128 android:fillColor="#1C1C1C"
129 android:fillAlpha="0.7"/>
130 <path
131 android:pathData="M255.86,97.29L249.93,92.92H261.79L255.86,97.29Z"
132 android:strokeAlpha="0.7"
133 android:fillColor="#1C1C1C"
134 android:fillAlpha="0.7"/>
135 <path
136 android:pathData="M424.16,109H375.88C371.54,109 368,112.54 368,116.88V194.16C368,198.54 371.54,202.04 375.88,202.04H424.12C428.5,202.04 432,198.5 432,194.16V116.88C432.04,112.54 428.5,109 424.16,109Z"
137 android:strokeAlpha="0.7"
138 android:fillColor="#282828"
139 android:fillAlpha="0.7"/>
140 <path
141 android:pathData="M135.16,411H86.88C82.54,411 79,414.54 79,418.88V496.16C79,500.54 82.54,504.04 86.88,504.04H135.12C139.5,504.04 143,500.5 143,496.16V418.88C143.04,414.54 139.5,411 135.16,411Z"
142 android:strokeAlpha="0.7"
143 android:fillColor="#282828"
144 android:fillAlpha="0.7"/>
145 <path
146 android:pathData="M64.16,7H15.88C11.54,7 8,10.54 8,14.88V92.16C8,96.54 11.54,100.04 15.88,100.04H64.12C68.5,100.04 72,96.5 72,92.16V14.88C72.04,10.54 68.5,7 64.16,7Z"
147 android:strokeAlpha="0.7"
148 android:fillColor="#282828"
149 android:fillAlpha="0.7"/>
150 <path
151 android:pathData="M64.8,89.73H14.44C12.65,89.73 11.18,88.26 11.18,86.47V17.02C11.18,15.23 12.65,13.76 14.44,13.76H64.84C66.63,13.76 68.1,15.23 68.1,17.02V86.51C68.06,88.26 66.59,89.73 64.8,89.73Z"
152 android:strokeAlpha="0.7"
153 android:fillColor="#1C1C1C"
154 android:fillAlpha="0.7"/>
155 <path
156 android:pathData="M50.16,14.16H29.08V12.93C29.08,12.29 29.6,11.77 30.23,11.77H49.01C49.65,11.77 50.16,12.29 50.16,12.93V14.16Z"
157 android:strokeAlpha="0.7"
158 android:fillColor="#1C1C1C"
159 android:fillAlpha="0.7"/>
160 <path
161 android:pathData="M39.86,97.29L33.93,92.92H45.79L39.86,97.29Z"
162 android:strokeAlpha="0.7"
163 android:fillColor="#1C1C1C"
164 android:fillAlpha="0.7"/>
165 <path
166 android:pathData="M63.16,310H14.88C10.54,310 7,313.54 7,317.88V395.16C7,399.54 10.54,403.04 14.88,403.04H63.12C67.5,403.04 71,399.5 71,395.16V317.88C71.04,313.54 67.5,310 63.16,310Z"
167 android:strokeAlpha="0.7"
168 android:fillColor="#282828"
169 android:fillAlpha="0.7"/>
170 <path
171 android:pathData="M63.8,392.73H13.44C11.65,392.73 10.18,391.26 10.18,389.47V320.02C10.18,318.23 11.65,316.76 13.44,316.76H63.84C65.63,316.76 67.1,318.23 67.1,320.02V389.51C67.06,391.26 65.59,392.73 63.8,392.73Z"
172 android:strokeAlpha="0.7"
173 android:fillColor="#1C1C1C"
174 android:fillAlpha="0.7"/>
175 <path
176 android:pathData="M49.16,317.16H28.08V315.93C28.08,315.29 28.6,314.77 29.23,314.77H48.01C48.65,314.77 49.16,315.29 49.16,315.93V317.16Z"
177 android:strokeAlpha="0.7"
178 android:fillColor="#1C1C1C"
179 android:fillAlpha="0.7"/>
180 <path
181 android:pathData="M38.86,400.29L32.93,395.92H44.79L38.86,400.29Z"
182 android:strokeAlpha="0.7"
183 android:fillColor="#1C1C1C"
184 android:fillAlpha="0.7"/>
185 <path
186 android:pathData="M424.16,209H375.88C371.54,209 368,212.54 368,216.88V294.16C368,298.54 371.54,302.04 375.88,302.04H424.12C428.5,302.04 432,298.5 432,294.16V216.88C432.04,212.54 428.5,209 424.16,209Z"
187 android:strokeAlpha="0.7"
188 android:fillColor="#282828"
189 android:fillAlpha="0.7"/>
190 <path
191 android:pathData="M424.8,291.73H374.44C372.65,291.73 371.18,290.26 371.18,288.47V219.02C371.18,217.23 372.65,215.76 374.44,215.76H424.84C426.63,215.76 428.1,217.23 428.1,219.02V288.51C428.06,290.26 426.59,291.73 424.8,291.73Z"
192 android:strokeAlpha="0.7"
193 android:fillColor="#1C1C1C"
194 android:fillAlpha="0.7"/>
195 <path
196 android:pathData="M410.16,216.16H389.08V214.93C389.08,214.29 389.6,213.77 390.23,213.77H409.01C409.65,213.77 410.16,214.29 410.16,214.93V216.16Z"
197 android:strokeAlpha="0.7"
198 android:fillColor="#1C1C1C"
199 android:fillAlpha="0.7"/>
200 <path
201 android:pathData="M399.86,299.29L393.93,294.92H405.79L399.86,299.29Z"
202 android:strokeAlpha="0.7"
203 android:fillColor="#1C1C1C"
204 android:fillAlpha="0.7"/>
205 <path
206 android:pathData="M496.16,209H447.88C443.54,209 440,212.54 440,216.88V294.16C440,298.54 443.54,302.04 447.88,302.04H496.12C500.5,302.04 504,298.5 504,294.16V216.88C504.04,212.54 500.5,209 496.16,209Z"
207 android:strokeAlpha="0.7"
208 android:fillColor="#282828"
209 android:fillAlpha="0.7"/>
210 <path
211 android:pathData="M496.8,291.73H446.44C444.65,291.73 443.18,290.26 443.18,288.47V219.02C443.18,217.23 444.65,215.76 446.44,215.76H496.84C498.63,215.76 500.1,217.23 500.1,219.02V288.51C500.06,290.26 498.59,291.73 496.8,291.73Z"
212 android:strokeAlpha="0.7"
213 android:fillColor="#1C1C1C"
214 android:fillAlpha="0.7"/>
215 <path
216 android:pathData="M482.16,216.16H461.08V214.93C461.08,214.29 461.6,213.77 462.23,213.77H481.01C481.65,213.77 482.16,214.29 482.16,214.93V216.16Z"
217 android:strokeAlpha="0.7"
218 android:fillColor="#1C1C1C"
219 android:fillAlpha="0.7"/>
220 <path
221 android:pathData="M471.86,299.29L465.93,294.92H477.79L471.86,299.29Z"
222 android:strokeAlpha="0.7"
223 android:fillColor="#1C1C1C"
224 android:fillAlpha="0.7"/>
225 <path
226 android:pathData="M136.16,209H87.88C83.54,209 80,212.54 80,216.88V294.16C80,298.54 83.54,302.04 87.88,302.04H136.12C140.5,302.04 144,298.5 144,294.16V216.88C144.04,212.54 140.5,209 136.16,209Z"
227 android:strokeAlpha="0.7"
228 android:fillColor="#282828"
229 android:fillAlpha="0.7"/>
230 <path
231 android:pathData="M136.8,291.73H86.44C84.65,291.73 83.18,290.26 83.18,288.47V219.02C83.18,217.23 84.65,215.76 86.44,215.76H136.84C138.63,215.76 140.1,217.23 140.1,219.02V288.51C140.06,290.26 138.59,291.73 136.8,291.73Z"
232 android:strokeAlpha="0.7"
233 android:fillColor="#1C1C1C"
234 android:fillAlpha="0.7"/>
235 <path
236 android:pathData="M122.16,216.16H101.08V214.93C101.08,214.29 101.6,213.77 102.24,213.77H121.01C121.65,213.77 122.16,214.29 122.16,214.93V216.16Z"
237 android:strokeAlpha="0.7"
238 android:fillColor="#1C1C1C"
239 android:fillAlpha="0.7"/>
240 <path
241 android:pathData="M111.86,299.29L105.93,294.92H117.79L111.86,299.29Z"
242 android:strokeAlpha="0.7"
243 android:fillColor="#1C1C1C"
244 android:fillAlpha="0.7"/>
245 <path
246 android:pathData="M352.16,209H303.88C299.54,209 296,212.54 296,216.88V294.16C296,298.54 299.54,302.04 303.88,302.04H352.12C356.5,302.04 360,298.5 360,294.16V216.88C360.04,212.54 356.5,209 352.16,209Z"
247 android:strokeAlpha="0.7"
248 android:fillColor="#282828"
249 android:fillAlpha="0.7"/>
250 <path
251 android:pathData="M352.8,291.73H302.44C300.65,291.73 299.18,290.26 299.18,288.47V219.02C299.18,217.23 300.65,215.76 302.44,215.76H352.84C354.63,215.76 356.1,217.23 356.1,219.02V288.51C356.06,290.26 354.59,291.73 352.8,291.73Z"
252 android:strokeAlpha="0.7"
253 android:fillColor="#1C1C1C"
254 android:fillAlpha="0.7"/>
255 <path
256 android:pathData="M338.16,216.16H317.08V214.93C317.08,214.29 317.6,213.77 318.23,213.77H337.01C337.65,213.77 338.16,214.29 338.16,214.93V216.16Z"
257 android:strokeAlpha="0.7"
258 android:fillColor="#1C1C1C"
259 android:fillAlpha="0.7"/>
260 <path
261 android:pathData="M327.86,299.29L321.93,294.92H333.79L327.86,299.29Z"
262 android:strokeAlpha="0.7"
263 android:fillColor="#1C1C1C"
264 android:fillAlpha="0.7"/>
265 <path
266 android:pathData="M64.16,209H15.88C11.54,209 8,212.54 8,216.88V294.16C8,298.54 11.54,302.04 15.88,302.04H64.12C68.5,302.04 72,298.5 72,294.16V216.88C72.04,212.54 68.5,209 64.16,209Z"
267 android:strokeAlpha="0.7"
268 android:fillColor="#282828"
269 android:fillAlpha="0.7"/>
270 <path
271 android:pathData="M64.8,291.73H14.44C12.65,291.73 11.18,290.26 11.18,288.47V219.02C11.18,217.23 12.65,215.76 14.44,215.76H64.84C66.63,215.76 68.1,217.23 68.1,219.02V288.51C68.06,290.26 66.59,291.73 64.8,291.73Z"
272 android:strokeAlpha="0.7"
273 android:fillColor="#1C1C1C"
274 android:fillAlpha="0.7"/>
275 <path
276 android:pathData="M50.16,216.16H29.08V214.93C29.08,214.29 29.6,213.77 30.23,213.77H49.01C49.65,213.77 50.16,214.29 50.16,214.93V216.16Z"
277 android:strokeAlpha="0.7"
278 android:fillColor="#1C1C1C"
279 android:fillAlpha="0.7"/>
280 <path
281 android:pathData="M39.86,299.29L33.93,294.92H45.79L39.86,299.29Z"
282 android:strokeAlpha="0.7"
283 android:fillColor="#1C1C1C"
284 android:fillAlpha="0.7"/>
285 <path
286 android:pathData="M135.16,310H86.88C82.54,310 79,313.54 79,317.88V395.16C79,399.54 82.54,403.04 86.88,403.04H135.12C139.5,403.04 143,399.5 143,395.16V317.88C143.04,313.54 139.5,310 135.16,310Z"
287 android:strokeAlpha="0.7"
288 android:fillColor="#282828"
289 android:fillAlpha="0.7"/>
290 <path
291 android:pathData="M135.8,392.73H85.44C83.65,392.73 82.18,391.26 82.18,389.47V320.02C82.18,318.23 83.65,316.76 85.44,316.76H135.84C137.63,316.76 139.1,318.23 139.1,320.02V389.51C139.06,391.26 137.59,392.73 135.8,392.73Z"
292 android:strokeAlpha="0.7"
293 android:fillColor="#1C1C1C"
294 android:fillAlpha="0.7"/>
295 <path
296 android:pathData="M121.16,317.16H100.08V315.93C100.08,315.29 100.6,314.77 101.24,314.77H120.01C120.65,314.77 121.16,315.29 121.16,315.93V317.16Z"
297 android:strokeAlpha="0.7"
298 android:fillColor="#1C1C1C"
299 android:fillAlpha="0.7"/>
300 <path
301 android:pathData="M110.86,400.29L104.93,395.92H116.79L110.86,400.29Z"
302 android:strokeAlpha="0.7"
303 android:fillColor="#1C1C1C"
304 android:fillAlpha="0.7"/>
305 <path
306 android:pathData="M208.16,108H159.88C155.54,108 152,111.54 152,115.88V193.16C152,197.54 155.54,201.04 159.88,201.04H208.12C212.5,201.04 216,197.5 216,193.16V115.88C216.04,111.54 212.5,108 208.16,108Z"
307 android:strokeAlpha="0.7"
308 android:fillColor="#282828"
309 android:fillAlpha="0.7"/>
310 <path
311 android:pathData="M208.8,190.73H158.44C156.65,190.73 155.18,189.26 155.18,187.47V118.02C155.18,116.23 156.65,114.76 158.44,114.76H208.84C210.63,114.76 212.1,116.23 212.1,118.02V187.51C212.06,189.26 210.59,190.73 208.8,190.73Z"
312 android:strokeAlpha="0.7"
313 android:fillColor="#1C1C1C"
314 android:fillAlpha="0.7"/>
315 <path
316 android:pathData="M194.16,115.16H173.08V113.93C173.08,113.29 173.6,112.77 174.24,112.77H193.01C193.65,112.77 194.16,113.29 194.16,113.93V115.16Z"
317 android:strokeAlpha="0.7"
318 android:fillColor="#1C1C1C"
319 android:fillAlpha="0.7"/>
320 <path
321 android:pathData="M183.86,198.29L177.93,193.92H189.79L183.86,198.29Z"
322 android:strokeAlpha="0.7"
323 android:fillColor="#1C1C1C"
324 android:fillAlpha="0.7"/>
325 <path
326 android:pathData="M496.16,108H447.88C443.54,108 440,111.54 440,115.88V193.16C440,197.54 443.54,201.04 447.88,201.04H496.12C500.5,201.04 504,197.5 504,193.16V115.88C504.04,111.54 500.5,108 496.16,108Z"
327 android:strokeAlpha="0.7"
328 android:fillColor="#282828"
329 android:fillAlpha="0.7"/>
330 <path
331 android:pathData="M496.8,190.73H446.44C444.65,190.73 443.18,189.26 443.18,187.47V118.02C443.18,116.23 444.65,114.76 446.44,114.76H496.84C498.63,114.76 500.1,116.23 500.1,118.02V187.51C500.06,189.26 498.59,190.73 496.8,190.73Z"
332 android:strokeAlpha="0.7"
333 android:fillColor="#1C1C1C"
334 android:fillAlpha="0.7"/>
335 <path
336 android:pathData="M482.16,115.16H461.08V113.93C461.08,113.29 461.6,112.77 462.23,112.77H481.01C481.65,112.77 482.16,113.29 482.16,113.93V115.16Z"
337 android:strokeAlpha="0.7"
338 android:fillColor="#1C1C1C"
339 android:fillAlpha="0.7"/>
340 <path
341 android:pathData="M471.86,198.29L465.93,193.92H477.79L471.86,198.29Z"
342 android:strokeAlpha="0.7"
343 android:fillColor="#1C1C1C"
344 android:fillAlpha="0.7"/>
345 <path
346 android:pathData="M64.16,108H15.88C11.54,108 8,111.54 8,115.88V193.16C8,197.54 11.54,201.04 15.88,201.04H64.12C68.5,201.04 72,197.5 72,193.16V115.88C72.04,111.54 68.5,108 64.16,108Z"
347 android:strokeAlpha="0.7"
348 android:fillColor="#282828"
349 android:fillAlpha="0.7"/>
350 <path
351 android:pathData="M64.8,190.73H14.44C12.65,190.73 11.18,189.26 11.18,187.47V118.02C11.18,116.23 12.65,114.76 14.44,114.76H64.84C66.63,114.76 68.1,116.23 68.1,118.02V187.51C68.06,189.26 66.59,190.73 64.8,190.73Z"
352 android:strokeAlpha="0.7"
353 android:fillColor="#1C1C1C"
354 android:fillAlpha="0.7"/>
355 <path
356 android:pathData="M50.16,115.16H29.08V113.93C29.08,113.29 29.6,112.77 30.23,112.77H49.01C49.65,112.77 50.16,113.29 50.16,113.93V115.16Z"
357 android:strokeAlpha="0.7"
358 android:fillColor="#1C1C1C"
359 android:fillAlpha="0.7"/>
360 <path
361 android:pathData="M39.86,198.29L33.93,193.92H45.79L39.86,198.29Z"
362 android:strokeAlpha="0.7"
363 android:fillColor="#1C1C1C"
364 android:fillAlpha="0.7"/>
365 <path
366 android:pathData="M280.16,108H231.88C227.54,108 224,111.54 224,115.88V193.16C224,197.54 227.54,201.04 231.88,201.04H280.12C284.5,201.04 288,197.5 288,193.16V115.88C288.04,111.54 284.5,108 280.16,108Z"
367 android:strokeAlpha="0.7"
368 android:fillColor="#282828"
369 android:fillAlpha="0.7"/>
370 <path
371 android:pathData="M280.8,190.73H230.44C228.65,190.73 227.18,189.26 227.18,187.47V118.02C227.18,116.23 228.65,114.76 230.44,114.76H280.84C282.63,114.76 284.1,116.23 284.1,118.02V187.51C284.06,189.26 282.59,190.73 280.8,190.73Z"
372 android:strokeAlpha="0.7"
373 android:fillColor="#1C1C1C"
374 android:fillAlpha="0.7"/>
375 <path
376 android:pathData="M266.16,115.16H245.08V113.93C245.08,113.29 245.6,112.77 246.24,112.77H265.01C265.65,112.77 266.16,113.29 266.16,113.93V115.16Z"
377 android:strokeAlpha="0.7"
378 android:fillColor="#1C1C1C"
379 android:fillAlpha="0.7"/>
380 <path
381 android:pathData="M255.86,198.29L249.93,193.92H261.79L255.86,198.29Z"
382 android:strokeAlpha="0.7"
383 android:fillColor="#1C1C1C"
384 android:fillAlpha="0.7"/>
385 <path
386 android:pathData="M496.16,310H447.88C443.54,310 440,313.54 440,317.88V395.16C440,399.54 443.54,403.04 447.88,403.04H496.12C500.5,403.04 504,399.5 504,395.16V317.88C504.04,313.54 500.5,310 496.16,310Z"
387 android:strokeAlpha="0.7"
388 android:fillColor="#282828"
389 android:fillAlpha="0.7"/>
390 <path
391 android:pathData="M496.8,392.73H446.44C444.65,392.73 443.18,391.26 443.18,389.47V320.02C443.18,318.23 444.65,316.76 446.44,316.76H496.84C498.63,316.76 500.1,318.23 500.1,320.02V389.51C500.06,391.26 498.59,392.73 496.8,392.73Z"
392 android:strokeAlpha="0.7"
393 android:fillColor="#1C1C1C"
394 android:fillAlpha="0.7"/>
395 <path
396 android:pathData="M482.16,317.16H461.08V315.93C461.08,315.29 461.6,314.77 462.23,314.77H481.01C481.65,314.77 482.16,315.29 482.16,315.93V317.16Z"
397 android:strokeAlpha="0.7"
398 android:fillColor="#1C1C1C"
399 android:fillAlpha="0.7"/>
400 <path
401 android:pathData="M471.86,400.29L465.93,395.92H477.79L471.86,400.29Z"
402 android:strokeAlpha="0.7"
403 android:fillColor="#1C1C1C"
404 android:fillAlpha="0.7"/>
405 <path
406 android:pathData="M352.16,310H303.88C299.54,310 296,313.54 296,317.88V395.16C296,399.54 299.54,403.04 303.88,403.04H352.12C356.5,403.04 360,399.5 360,395.16V317.88C360.04,313.54 356.5,310 352.16,310Z"
407 android:strokeAlpha="0.7"
408 android:fillColor="#282828"
409 android:fillAlpha="0.7"/>
410 <path
411 android:pathData="M352.8,392.73H302.44C300.65,392.73 299.18,391.26 299.18,389.47V320.02C299.18,318.23 300.65,316.76 302.44,316.76H352.84C354.63,316.76 356.1,318.23 356.1,320.02V389.51C356.06,391.26 354.59,392.73 352.8,392.73Z"
412 android:strokeAlpha="0.7"
413 android:fillColor="#1C1C1C"
414 android:fillAlpha="0.7"/>
415 <path
416 android:pathData="M338.16,317.16H317.08V315.93C317.08,315.29 317.6,314.77 318.23,314.77H337.01C337.65,314.77 338.16,315.29 338.16,315.93V317.16Z"
417 android:strokeAlpha="0.7"
418 android:fillColor="#1C1C1C"
419 android:fillAlpha="0.7"/>
420 <path
421 android:pathData="M327.86,400.29L321.93,395.92H333.79L327.86,400.29Z"
422 android:strokeAlpha="0.7"
423 android:fillColor="#1C1C1C"
424 android:fillAlpha="0.7"/>
425 <path
426 android:pathData="M63.16,411H14.88C10.54,411 7,414.54 7,418.88V496.16C7,500.54 10.54,504.04 14.88,504.04H63.12C67.5,504.04 71,500.5 71,496.16V418.88C71.04,414.54 67.5,411 63.16,411Z"
427 android:strokeAlpha="0.7"
428 android:fillColor="#282828"
429 android:fillAlpha="0.7"/>
430 <path
431 android:pathData="M63.8,493.73H13.44C11.65,493.73 10.18,492.26 10.18,490.47V421.02C10.18,419.23 11.65,417.76 13.44,417.76H63.84C65.63,417.76 67.1,419.23 67.1,421.02V490.51C67.06,492.26 65.59,493.73 63.8,493.73Z"
432 android:strokeAlpha="0.7"
433 android:fillColor="#1C1C1C"
434 android:fillAlpha="0.7"/>
435 <path
436 android:pathData="M49.16,418.16H28.08V416.93C28.08,416.29 28.6,415.77 29.23,415.77H48.01C48.65,415.77 49.16,416.29 49.16,416.93V418.16Z"
437 android:strokeAlpha="0.7"
438 android:fillColor="#1C1C1C"
439 android:fillAlpha="0.7"/>
440 <path
441 android:pathData="M38.86,501.29L32.93,496.92H44.79L38.86,501.29Z"
442 android:strokeAlpha="0.7"
443 android:fillColor="#1C1C1C"
444 android:fillAlpha="0.7"/>
445 <path
446 android:pathData="M496.16,411H447.88C443.54,411 440,414.54 440,418.88V496.16C440,500.54 443.54,504.04 447.88,504.04H496.12C500.5,504.04 504,500.5 504,496.16V418.88C504.04,414.54 500.5,411 496.16,411Z"
447 android:strokeAlpha="0.7"
448 android:fillColor="#282828"
449 android:fillAlpha="0.7"/>
450 <path
451 android:pathData="M496.8,493.73H446.44C444.65,493.73 443.18,492.26 443.18,490.47V421.02C443.18,419.23 444.65,417.76 446.44,417.76H496.84C498.63,417.76 500.1,419.23 500.1,421.02V490.51C500.06,492.26 498.59,493.73 496.8,493.73Z"
452 android:strokeAlpha="0.7"
453 android:fillColor="#1C1C1C"
454 android:fillAlpha="0.7"/>
455 <path
456 android:pathData="M482.16,418.16H461.08V416.93C461.08,416.29 461.6,415.77 462.23,415.77H481.01C481.65,415.77 482.16,416.29 482.16,416.93V418.16Z"
457 android:strokeAlpha="0.7"
458 android:fillColor="#1C1C1C"
459 android:fillAlpha="0.7"/>
460 <path
461 android:pathData="M471.86,501.29L465.93,496.92H477.79L471.86,501.29Z"
462 android:strokeAlpha="0.7"
463 android:fillColor="#1C1C1C"
464 android:fillAlpha="0.7"/>
465 <path
466 android:pathData="M352.16,411H303.88C299.54,411 296,414.54 296,418.88V496.16C296,500.54 299.54,504.04 303.88,504.04H352.12C356.5,504.04 360,500.5 360,496.16V418.88C360.04,414.54 356.5,411 352.16,411Z"
467 android:strokeAlpha="0.7"
468 android:fillColor="#282828"
469 android:fillAlpha="0.7"/>
470 <path
471 android:pathData="M352.8,493.73H302.44C300.65,493.73 299.18,492.26 299.18,490.47V421.02C299.18,419.23 300.65,417.76 302.44,417.76H352.84C354.63,417.76 356.1,419.23 356.1,421.02V490.51C356.06,492.26 354.59,493.73 352.8,493.73Z"
472 android:strokeAlpha="0.7"
473 android:fillColor="#1C1C1C"
474 android:fillAlpha="0.7"/>
475 <path
476 android:pathData="M338.16,418.16H317.08V416.93C317.08,416.29 317.6,415.77 318.23,415.77H337.01C337.65,415.77 338.16,416.29 338.16,416.93V418.16Z"
477 android:strokeAlpha="0.7"
478 android:fillColor="#1C1C1C"
479 android:fillAlpha="0.7"/>
480 <path
481 android:pathData="M327.86,501.29L321.93,496.92H333.79L327.86,501.29Z"
482 android:strokeAlpha="0.7"
483 android:fillColor="#1C1C1C"
484 android:fillAlpha="0.7"/>
485 <path
486 android:pathData="M208.16,411H159.88C155.54,411 152,414.54 152,418.88V496.16C152,500.54 155.54,504.04 159.88,504.04H208.12C212.5,504.04 216,500.5 216,496.16V418.88C216.04,414.54 212.5,411 208.16,411Z"
487 android:strokeAlpha="0.7"
488 android:fillColor="#282828"
489 android:fillAlpha="0.7"/>
490 <path
491 android:pathData="M208.8,493.73H158.44C156.65,493.73 155.18,492.26 155.18,490.47V421.02C155.18,419.23 156.65,417.76 158.44,417.76H208.84C210.63,417.76 212.1,419.23 212.1,421.02V490.51C212.06,492.26 210.59,493.73 208.8,493.73Z"
492 android:strokeAlpha="0.7"
493 android:fillColor="#1C1C1C"
494 android:fillAlpha="0.7"/>
495 <path
496 android:pathData="M194.16,418.16H173.08V416.93C173.08,416.29 173.6,415.77 174.24,415.77H193.01C193.65,415.77 194.16,416.29 194.16,416.93V418.16Z"
497 android:strokeAlpha="0.7"
498 android:fillColor="#1C1C1C"
499 android:fillAlpha="0.7"/>
500 <path
501 android:pathData="M183.86,501.29L177.93,496.92H189.79L183.86,501.29Z"
502 android:strokeAlpha="0.7"
503 android:fillColor="#1C1C1C"
504 android:fillAlpha="0.7"/>
505 <path
506 android:pathData="M280.16,411H231.88C227.54,411 224,414.54 224,418.88V496.16C224,500.54 227.54,504.04 231.88,504.04H280.12C284.5,504.04 288,500.5 288,496.16V418.88C288.04,414.54 284.5,411 280.16,411Z"
507 android:strokeAlpha="0.7"
508 android:fillColor="#282828"
509 android:fillAlpha="0.7"/>
510 <path
511 android:pathData="M280.8,493.73H230.44C228.65,493.73 227.18,492.26 227.18,490.47V421.02C227.18,419.23 228.65,417.76 230.44,417.76H280.84C282.63,417.76 284.1,419.23 284.1,421.02V490.51C284.06,492.26 282.59,493.73 280.8,493.73Z"
512 android:strokeAlpha="0.7"
513 android:fillColor="#1C1C1C"
514 android:fillAlpha="0.7"/>
515 <path
516 android:pathData="M266.16,418.16H245.08V416.93C245.08,416.29 245.6,415.77 246.24,415.77H265.01C265.65,415.77 266.16,416.29 266.16,416.93V418.16Z"
517 android:strokeAlpha="0.7"
518 android:fillColor="#1C1C1C"
519 android:fillAlpha="0.7"/>
520 <path
521 android:pathData="M255.86,501.29L249.93,496.92H261.79L255.86,501.29Z"
522 android:strokeAlpha="0.7"
523 android:fillColor="#1C1C1C"
524 android:fillAlpha="0.7"/>
525 <group>
526 <clip-path
527 android:pathData="M80,8h64v192h-64z"/>
528 <path
529 android:pathData="M112.06,8H144.11V200H112.06C94.32,200 80,185.68 80,167.96V40.04C80,22.31 94.32,8 112.06,8Z"
530 android:strokeAlpha="0.7"
531 android:fillColor="#282828"
532 android:fillAlpha="0.7"/>
533 <path
534 android:pathData="M138.2,26.4H128.43C128.31,26.4 128.31,26.29 128.31,26.18V23.79C128.31,23.68 128.43,23.56 128.43,23.56H138.2C138.32,23.56 138.32,23.68 138.32,23.79V26.18C138.32,26.29 138.2,26.4 138.2,26.4Z"
535 android:strokeAlpha="0.7"
536 android:fillColor="#1C1C1C"
537 android:fillAlpha="0.7"/>
538 <path
539 android:pathData="M129.9,142.85V147.63C129.9,149.67 128.31,151.26 126.27,151.26H121.49C119.45,151.26 117.85,149.67 117.85,147.63V142.85C117.85,140.81 119.45,139.22 121.49,139.22H126.27C128.31,139.33 129.9,140.92 129.9,142.85Z"
540 android:strokeAlpha="0.7"
541 android:fillColor="#1C1C1C"
542 android:fillAlpha="0.7"/>
543 <path
544 android:pathData="M113.76,65.26C120.1,65.26 125.24,60.12 125.24,53.78C125.24,47.45 120.1,42.31 113.76,42.31C107.42,42.31 102.28,47.45 102.28,53.78C102.28,60.12 107.42,65.26 113.76,65.26Z"
545 android:strokeAlpha="0.7"
546 android:fillColor="#1C1C1C"
547 android:fillAlpha="0.7"/>
548 <path
549 android:pathData="M112.85,39.02V40.95C112.85,40.95 112.85,41.06 112.74,41.06C106.49,41.51 101.49,46.51 100.92,52.88C100.92,52.88 100.92,52.99 100.8,52.99H98.98C98.98,52.99 98.87,52.99 98.87,52.88C98.87,52.53 98.87,52.31 98.98,51.97C100.01,44.7 105.92,39.47 112.85,39.02Z"
550 android:strokeAlpha="0.7"
551 android:fillColor="#1C1C1C"
552 android:fillAlpha="0.7"/>
553 <path
554 android:pathData="M128.54,54.69C128.65,55.03 128.54,55.38 128.54,55.72C127.63,62.87 121.72,68.1 114.9,68.55C114.9,68.55 114.79,68.55 114.79,68.44V66.62C114.79,66.62 114.79,66.51 114.9,66.51C121.15,66.05 126.15,61.06 126.72,54.69C126.72,54.69 126.72,54.58 126.83,54.58H128.54V54.69Z"
555 android:strokeAlpha="0.7"
556 android:fillColor="#1C1C1C"
557 android:fillAlpha="0.7"/>
558 <path
559 android:pathData="M128.54,52.88H126.61C126.61,52.88 126.49,52.88 126.49,52.76C126.04,46.51 121.04,41.51 114.67,40.95C114.67,40.95 114.56,40.95 114.56,40.83V39.02C114.56,39.02 114.56,38.9 114.67,38.9C115.01,38.9 115.24,38.9 115.58,39.02C122.86,40.04 128.09,45.83 128.54,52.88Z"
560 android:strokeAlpha="0.7"
561 android:fillColor="#1C1C1C"
562 android:fillAlpha="0.7"/>
563 <path
564 android:pathData="M112.85,66.62V68.44C112.85,68.44 112.85,68.55 112.74,68.55C112.4,68.55 112.17,68.55 111.83,68.44C104.67,67.53 99.44,61.62 98.98,54.81C98.98,54.81 98.98,54.69 99.1,54.69H100.92C100.92,54.69 101.03,54.69 101.03,54.81C101.49,61.06 106.49,66.05 112.85,66.62C112.85,66.51 112.85,66.51 112.85,66.62Z"
565 android:strokeAlpha="0.7"
566 android:fillColor="#1C1C1C"
567 android:fillAlpha="0.7"/>
568 <path
569 android:pathData="M108.08,109.68C108.08,113.66 104.89,116.84 100.92,116.84C96.94,116.84 93.64,113.66 93.64,109.68C93.64,105.7 96.82,102.52 100.92,102.52C104.89,102.52 108.08,105.7 108.08,109.68Z"
570 android:strokeAlpha="0.7"
571 android:fillColor="#1C1C1C"
572 android:fillAlpha="0.7"/>
573 <path
574 android:pathData="M120.7,97.18C120.7,101.16 117.51,104.34 113.42,104.34C109.44,104.34 106.26,101.16 106.26,97.18C106.26,93.21 109.44,90.03 113.42,90.03C117.4,89.91 120.7,93.21 120.7,97.18Z"
575 android:strokeAlpha="0.7"
576 android:fillColor="#1C1C1C"
577 android:fillAlpha="0.7"/>
578 <path
579 android:pathData="M133.2,109.68C133.2,113.66 130.02,116.84 126.04,116.84C122.06,116.84 118.88,113.66 118.88,109.68C118.88,105.7 122.06,102.52 126.04,102.52C129.9,102.52 133.2,105.7 133.2,109.68Z"
580 android:strokeAlpha="0.7"
581 android:fillColor="#1C1C1C"
582 android:fillAlpha="0.7"/>
583 <path
584 android:pathData="M120.7,122.29C120.7,126.27 117.51,129.45 113.42,129.45C109.44,129.45 106.26,126.27 106.26,122.29C106.26,118.32 109.44,115.13 113.42,115.13C117.4,115.02 120.7,118.32 120.7,122.29Z"
585 android:strokeAlpha="0.7"
586 android:fillColor="#1C1C1C"
587 android:fillAlpha="0.7"/>
588 </group>
589 <path
590 android:pathData="M157.99,209.4C157.87,209.5 157.75,209.7 157.75,210C157.75,210.5 157.63,210.8 157.51,211.01C157.03,211.81 155.83,212.21 154.51,212.21L152.95,212.21C152.95,212.21 152.71,212.21 152.59,212.31C152.47,212.41 152.47,212.51 152.47,212.61L152.47,399.35C152.47,399.45 152.47,399.45 152.47,399.55C152.59,399.75 152.83,399.85 153.07,399.85L154.87,399.85C154.87,399.85 156.31,399.75 157.15,400.65C157.75,401.36 157.75,402.26 157.75,402.26C157.75,402.36 157.75,402.56 157.87,402.66C158.1,402.96 158.46,403.16 159.06,403.16L287.28,403.16C287.4,403.16 287.52,403.16 287.64,403.06C288,402.86 288,402.56 288,402.56L288,209.7C288,209.7 288,209.3 287.76,209.1C287.64,209 287.52,209 287.4,209L159.18,209C159.18,209 158.35,209 157.99,209.4ZM279.85,214.52C279.97,214.52 281.41,214.52 282.49,215.42C283.57,216.32 283.57,217.63 283.57,217.73L283.57,394.54C283.57,394.64 283.57,395.94 282.49,396.84C281.41,397.74 279.97,397.74 279.85,397.74L160.74,397.74C160.62,397.74 159.18,397.74 158.1,396.84C157.03,395.94 157.03,394.64 157.03,394.54L157.03,217.73C157.03,217.63 156.91,216.42 158.1,215.42C159.18,214.52 160.62,214.52 160.74,214.52L279.85,214.52Z"
591 android:strokeAlpha="0.7"
592 android:fillColor="#282828"
593 android:fillAlpha="0.7"/>
594 <path
595 android:pathData="M151.36,353.72L152.44,353.72L152.44,377.49L151.36,377.49C151.36,377.49 151,377.39 151,377.09L151,369.87C151,369.87 151,369.47 151.36,369.47L151.36,361.44C151.36,361.44 151,361.44 151,361.14L151,353.92C151.12,353.82 151.12,353.72 151.36,353.72Z"
596 android:strokeAlpha="0.7"
597 android:fillColor="#282828"
598 android:fillAlpha="0.7"/>
599 <path
600 android:pathData="M160.78,214.51L279.89,214.51C280.01,214.51 281.45,214.51 282.52,215.41C283.6,216.31 283.6,217.62 283.6,217.72L283.6,394.53C283.6,394.63 283.6,395.93 282.52,396.83C281.45,397.74 280.01,397.74 279.89,397.74L160.78,397.74C160.66,397.74 159.22,397.74 158.14,396.83C157.06,395.93 157.06,394.63 157.06,394.53L157.06,217.72C157.06,217.62 156.95,216.41 158.14,215.41C159.22,214.51 160.66,214.51 160.78,214.51Z"
601 android:strokeAlpha="0.7"
602 android:fillColor="#1C1C1C"
603 android:fillAlpha="0.7"/>
604 <group>
605 <clip-path
606 android:pathData="M368,311h64v192h-64z"/>
607 <path
608 android:pathData="M400.06,311H368V503H400.06C417.79,503 432.11,488.68 432.11,470.96V343.04C432,325.32 417.68,311 400.06,311Z"
609 android:strokeAlpha="0.7"
610 android:fillColor="#282828"
611 android:fillAlpha="0.7"/>
612 <path
613 android:strokeWidth="1"
614 android:pathData="M374.14,327.81H378.23C378.35,327.81 378.35,327.7 378.35,327.7V323.84C378.35,323.72 378.46,323.72 378.46,323.72H379.6C379.71,323.72 379.71,323.84 379.71,323.84V327.7C379.71,327.81 379.82,327.81 379.82,327.81H383.8C383.92,327.81 383.92,327.93 383.92,327.93V329.06C383.92,329.18 383.8,329.18 383.8,329.18H379.82C379.71,329.18 379.71,329.29 379.71,329.29V333.15C379.71,333.27 379.6,333.27 379.6,333.27H378.46C378.35,333.27 378.35,333.15 378.35,333.15V329.29C378.35,329.18 378.23,329.18 378.23,329.18H374.14C374.02,329.18 374.02,329.06 374.02,329.06V327.93C374.02,327.93 374.02,327.81 374.14,327.81Z"
615 android:strokeAlpha="0.7"
616 android:fillColor="#1C1C1C"
617 android:strokeColor="#1C1C1C"
618 android:fillAlpha="0.7"/>
619 <path
620 android:pathData="M399.49,423.81C405.83,423.81 410.97,418.68 410.97,412.34C410.97,406 405.83,400.86 399.49,400.86C393.15,400.86 388.01,406 388.01,412.34C388.01,418.68 393.15,423.81 399.49,423.81Z"
621 android:strokeAlpha="0.7"
622 android:fillColor="#1C1C1C"
623 android:fillAlpha="0.7"/>
624 <path
625 android:pathData="M398.58,397.68V399.5C398.58,399.5 398.58,399.61 398.46,399.61C392.21,400.07 387.21,405.07 386.64,411.43C386.64,411.43 386.64,411.54 386.53,411.54H384.71C384.71,411.54 384.6,411.54 384.6,411.43C384.6,411.09 384.6,410.86 384.71,410.52C385.73,403.25 391.64,398.02 398.58,397.68C398.58,397.57 398.58,397.57 398.58,397.68Z"
626 android:strokeAlpha="0.7"
627 android:fillColor="#1C1C1C"
628 android:fillAlpha="0.7"/>
629 <path
630 android:pathData="M414.27,413.25C414.38,413.59 414.27,413.93 414.27,414.27C413.36,421.43 407.45,426.65 400.63,427.11C400.63,427.11 400.51,427.11 400.51,426.99V425.18C400.51,425.18 400.51,425.06 400.63,425.06C406.88,424.61 411.88,419.61 412.45,413.25C412.45,413.25 412.45,413.14 412.56,413.14H414.27V413.25Z"
631 android:strokeAlpha="0.7"
632 android:fillColor="#1C1C1C"
633 android:fillAlpha="0.7"/>
634 <path
635 android:pathData="M414.27,411.43H412.33C412.33,411.43 412.22,411.43 412.22,411.32C411.77,405.07 406.76,400.07 400.4,399.5C400.4,399.5 400.28,399.5 400.28,399.39V397.57C400.28,397.57 400.28,397.46 400.4,397.46C400.74,397.46 400.97,397.46 401.31,397.57C408.58,398.59 413.81,404.39 414.27,411.43Z"
636 android:strokeAlpha="0.7"
637 android:fillColor="#1C1C1C"
638 android:fillAlpha="0.7"/>
639 <path
640 android:pathData="M398.58,425.18V426.99C398.58,426.99 398.58,427.11 398.46,427.11C398.12,427.11 397.9,427.11 397.56,426.99C390.39,426.09 385.17,420.18 384.71,413.36C384.71,413.36 384.71,413.25 384.82,413.25H386.64C386.64,413.25 386.76,413.25 386.76,413.36C387.21,419.61 392.21,424.61 398.58,425.18Z"
641 android:strokeAlpha="0.7"
642 android:fillColor="#1C1C1C"
643 android:fillAlpha="0.7"/>
644 <path
645 android:pathData="M392.67,358.15C392.67,362.12 389.48,365.3 385.51,365.3C381.53,365.42 378.23,362.12 378.23,358.15C378.23,354.17 381.41,350.99 385.51,350.99C389.48,350.99 392.67,354.17 392.67,358.15Z"
646 android:strokeAlpha="0.7"
647 android:fillColor="#1C1C1C"
648 android:fillAlpha="0.7"/>
649 <path
650 android:pathData="M405.29,345.65C405.29,349.63 402.1,352.81 398.01,352.81C394.03,352.81 390.85,349.63 390.85,345.65C390.85,341.67 394.03,338.49 398.01,338.49C401.99,338.38 405.29,341.67 405.29,345.65Z"
651 android:strokeAlpha="0.7"
652 android:fillColor="#1C1C1C"
653 android:fillAlpha="0.7"/>
654 <path
655 android:pathData="M417.79,358.15C417.79,362.12 414.61,365.3 410.63,365.3C406.65,365.3 403.47,362.12 403.47,358.15C403.47,354.17 406.65,350.99 410.63,350.99C414.49,350.99 417.79,354.17 417.79,358.15Z"
656 android:strokeAlpha="0.7"
657 android:fillColor="#1C1C1C"
658 android:fillAlpha="0.7"/>
659 <path
660 android:pathData="M405.29,370.76C405.29,374.73 402.1,377.92 398.01,377.92C394.03,377.92 390.85,374.73 390.85,370.76C390.85,366.78 394.03,363.6 398.01,363.6C401.99,363.49 405.29,366.78 405.29,370.76Z"
661 android:strokeAlpha="0.7"
662 android:fillColor="#1C1C1C"
663 android:fillAlpha="0.7"/>
664 <path
665 android:pathData="M394.15,448.81C394.15,452.33 391.3,455.17 387.78,455.17C384.26,455.17 381.41,452.33 381.41,448.81C381.41,445.29 384.26,442.45 387.78,442.45C391.3,442.56 394.15,445.4 394.15,448.81Z"
666 android:strokeAlpha="0.7"
667 android:fillColor="#1C1C1C"
668 android:fillAlpha="0.7"/>
669 </group>
670 <path
671 android:pathData="M91.95,468.95C97.99,468.95 102.9,464.05 102.9,458C102.9,451.95 97.99,447.05 91.95,447.05C85.9,447.05 81,451.95 81,458C81,464.05 85.9,468.95 91.95,468.95Z"
672 android:strokeAlpha="0.7"
673 android:fillColor="#1A1A1A"
674 android:fillAlpha="0.7"/>
675 <path
676 android:pathData="M88.14,457.83L93.88,454.5C94,454.42 94.17,454.53 94.17,454.67V461.3C94.17,461.44 94.02,461.53 93.88,461.47L88.14,458.14C88.02,458.08 88.02,457.92 88.14,457.83Z"
677 android:strokeAlpha="0.7"
678 android:fillColor="#282828"
679 android:fillAlpha="0.7"/>
680 <path
681 android:pathData="M111,449.9C117.05,449.9 121.95,444.99 121.95,438.95C121.95,432.9 117.05,428 111,428C104.95,428 100.05,432.9 100.05,438.95C100.05,444.99 104.95,449.9 111,449.9Z"
682 android:strokeAlpha="0.7"
683 android:fillColor="#1A1A1A"
684 android:fillAlpha="0.7"/>
685 <path
686 android:pathData="M111.17,435.14L114.5,440.88C114.58,440.99 114.47,441.17 114.33,441.17H107.7C107.56,441.17 107.47,441.02 107.53,440.88L110.86,435.14C110.92,435.02 111.08,435.02 111.17,435.14Z"
687 android:strokeAlpha="0.7"
688 android:fillColor="#282828"
689 android:fillAlpha="0.7"/>
690 <path
691 android:pathData="M130.05,468.95C136.1,468.95 141,464.05 141,458C141,451.95 136.1,447.05 130.05,447.05C124.01,447.05 119.1,451.95 119.1,458C119.1,464.05 124.01,468.95 130.05,468.95Z"
692 android:strokeAlpha="0.7"
693 android:fillColor="#1A1A1A"
694 android:fillAlpha="0.7"/>
695 <path
696 android:pathData="M133.86,458.17L128.12,461.5C128.01,461.58 127.83,461.47 127.83,461.33V454.7C127.83,454.56 127.98,454.47 128.12,454.53L133.86,457.86C134.01,457.92 134.01,458.08 133.86,458.17Z"
697 android:strokeAlpha="0.7"
698 android:fillColor="#282828"
699 android:fillAlpha="0.7"/>
700 <path
701 android:pathData="M111,488C117.05,488 121.95,483.1 121.95,477.05C121.95,471.01 117.05,466.1 111,466.1C104.95,466.1 100.05,471.01 100.05,477.05C100.05,483.1 104.95,488 111,488Z"
702 android:strokeAlpha="0.7"
703 android:fillColor="#1A1A1A"
704 android:fillAlpha="0.7"/>
705 <path
706 android:pathData="M110.83,480.86L107.5,475.12C107.42,475.01 107.53,474.83 107.67,474.83H114.3C114.44,474.83 114.53,474.98 114.47,475.12L111.14,480.86C111.08,481.01 110.92,481.01 110.83,480.86Z"
707 android:strokeAlpha="0.7"
708 android:fillColor="#282828"
709 android:fillAlpha="0.7"/>
710 <path
711 android:pathData="M380.95,165.95C386.99,165.95 391.9,161.05 391.9,155C391.9,148.95 386.99,144.05 380.95,144.05C374.9,144.05 370,148.95 370,155C370,161.05 374.9,165.95 380.95,165.95Z"
712 android:strokeAlpha="0.7"
713 android:fillColor="#1C1C1C"
714 android:fillAlpha="0.7"/>
715 <path
716 android:pathData="M380.46,155.54L377.68,151.3H378.96L380.98,154.54L383.05,151.3H384.27L381.49,155.54V158.7H380.49V155.54H380.46Z"
717 android:strokeAlpha="0.7"
718 android:fillColor="#282828"
719 android:fillAlpha="0.7"/>
720 <path
721 android:pathData="M399.72,185C405.76,185 410.66,180.1 410.66,174.05C410.66,168.01 405.76,163.1 399.72,163.1C393.67,163.1 388.77,168.01 388.77,174.05C388.77,180.1 393.67,185 399.72,185Z"
722 android:strokeAlpha="0.7"
723 android:fillColor="#1C1C1C"
724 android:fillAlpha="0.7"/>
725 <path
726 android:pathData="M397.44,170.64H400C400.31,170.64 400.63,170.67 400.88,170.75C401.17,170.84 401.39,170.95 401.59,171.1C401.79,171.24 401.93,171.44 402.08,171.66C402.19,171.89 402.25,172.18 402.25,172.49C402.25,172.91 402.13,173.26 401.9,173.54C401.68,173.8 401.36,173.99 400.99,174.14V174.17C401.22,174.17 401.42,174.25 401.62,174.34C401.82,174.42 401.99,174.56 402.13,174.74C402.27,174.9 402.39,175.08 402.47,175.3C402.56,175.53 402.59,175.76 402.59,176.01C402.59,176.35 402.53,176.64 402.39,176.9C402.25,177.15 402.08,177.35 401.82,177.55C401.59,177.72 401.31,177.86 400.99,177.95C400.68,178.03 400.34,178.09 399.97,178.09H397.44V170.64ZM398.44,173.71H399.8C400,173.71 400.17,173.68 400.34,173.65C400.51,173.63 400.65,173.54 400.77,173.46C400.88,173.37 400.99,173.26 401.05,173.11C401.14,172.97 401.17,172.8 401.17,172.6C401.17,172.32 401.08,172.06 400.88,171.83C400.68,171.61 400.4,171.52 400,171.52H398.44V173.71ZM398.44,177.15H399.92C400.06,177.15 400.23,177.12 400.43,177.1C400.6,177.07 400.8,177.01 400.94,176.9C401.11,176.81 401.22,176.67 401.34,176.53C401.45,176.35 401.51,176.16 401.51,175.9C401.51,175.47 401.36,175.13 401.08,174.9C400.8,174.68 400.4,174.56 399.92,174.56H398.44V177.15Z"
727 android:strokeAlpha="0.7"
728 android:fillColor="#282828"
729 android:fillAlpha="0.7"/>
730 <path
731 android:pathData="M419.05,165.95C425.1,165.95 430,161.05 430,155C430,148.95 425.1,144.05 419.05,144.05C413.01,144.05 408.1,148.95 408.1,155C408.1,161.05 413.01,165.95 419.05,165.95Z"
732 android:strokeAlpha="0.7"
733 android:fillColor="#1C1C1C"
734 android:fillAlpha="0.7"/>
735 <path
736 android:pathData="M418.63,151.3H419.54L422.69,158.67H421.53L420.79,156.85H417.29L416.55,158.67H415.38L418.63,151.3ZM420.42,155.99L419.05,152.61H419.02L417.63,155.99H420.42Z"
737 android:strokeAlpha="0.7"
738 android:fillColor="#282828"
739 android:fillAlpha="0.7"/>
740 <path
741 android:pathData="M400,146.9C406.05,146.9 410.95,141.99 410.95,135.95C410.95,129.9 406.05,125 400,125C393.95,125 389.05,129.9 389.05,135.95C389.05,141.99 393.95,146.9 400,146.9Z"
742 android:strokeAlpha="0.7"
743 android:fillColor="#1C1C1C"
744 android:fillAlpha="0.7"/>
745 <path
746 android:pathData="M399.26,135.78L396.79,132.28H398.07L400,135.12L401.9,132.28H403.16L400.68,135.78L403.41,139.67H402.1L399.97,136.46L397.84,139.67H396.59L399.26,135.78Z"
747 android:strokeAlpha="0.7"
748 android:fillColor="#282828"
749 android:fillAlpha="0.7"/>
750 </group>
751</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_info_outline.xml b/src/android/app/src/main/res/drawable/ic_info_outline.xml
new file mode 100644
index 000000000..92ae0eeaf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_info_outline.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_install.xml b/src/android/app/src/main/res/drawable/ic_install.xml
new file mode 100644
index 000000000..01f2de3da
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_install.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,16.5l4,-4h-3v-9h-2v9L8,12.5l4,4zM21,3.5h-6v1.99h6v14.03L3,19.52L3,5.49h6L9,3.5L3,3.5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2v-14c0,-1.1 -0.9,-2 -2,-2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_key.xml b/src/android/app/src/main/res/drawable/ic_key.xml
new file mode 100644
index 000000000..a3943634f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_key.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_launcher.xml b/src/android/app/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 000000000..3bb60fdfb
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,6 @@
1<?xml version="1.0" encoding="utf-8"?>
2<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3 <background android:drawable="@drawable/ic_icon_bg" />
4 <foreground android:drawable="@drawable/ic_yuzu" />
5 <monochrome android:drawable="@drawable/ic_yuzu" />
6</adaptive-icon>
diff --git a/src/android/app/src/main/res/drawable/ic_log.xml b/src/android/app/src/main/res/drawable/ic_log.xml
new file mode 100644
index 000000000..f55b9ad85
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_log.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="960"
5 android:viewportHeight="960"
6 android:tint="?attr/colorControlNormal">
7 <path
8 android:fillColor="@android:color/white"
9 android:pathData="M360,720L600,720Q617,720 628.5,708.5Q640,697 640,680Q640,663 628.5,651.5Q617,640 600,640L360,640Q343,640 331.5,651.5Q320,663 320,680Q320,697 331.5,708.5Q343,720 360,720ZM360,560L600,560Q617,560 628.5,548.5Q640,537 640,520Q640,503 628.5,491.5Q617,480 600,480L360,480Q343,480 331.5,491.5Q320,503 320,520Q320,537 331.5,548.5Q343,560 360,560ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L527,80Q543,80 557.5,86Q572,92 583,103L777,297Q788,308 794,322.5Q800,337 800,353L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,320L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L560,360Q543,360 531.5,348.5Q520,337 520,320ZM240,160L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/>
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_nfc.xml b/src/android/app/src/main/res/drawable/ic_nfc.xml
new file mode 100644
index 000000000..3dacf798b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_nfc.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_notification.xml b/src/android/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 000000000..b413f7585
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42zM18,11c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2v-5zM12,22c0.14,0 0.27,-0.01 0.4,-0.04 0.65,-0.14 1.18,-0.58 1.44,-1.18 0.1,-0.24 0.15,-0.5 0.15,-0.78h-4c0.01,1.1 0.9,2 2.01,2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_options.xml b/src/android/app/src/main/res/drawable/ic_options.xml
new file mode 100644
index 000000000..91d52f1b8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_options.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_palette.xml b/src/android/app/src/main/res/drawable/ic_palette.xml
new file mode 100644
index 000000000..43daec1ff
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_palette.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_pause.xml b/src/android/app/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 000000000..adb3ababc
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_play.xml b/src/android/app/src/main/res/drawable/ic_play.xml
new file mode 100644
index 000000000..7f01dc599
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_play.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M8,5v14l11,-7z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml
new file mode 100644
index 000000000..a9af3d9cf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_save.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="960"
5 android:viewportHeight="960"
6 android:tint="?attr/colorControlNormal">
7 <path
8 android:fillColor="@android:color/white"
9 android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_search.xml b/src/android/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 000000000..bb0726851
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_settings.xml b/src/android/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 000000000..e527f85fc
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_settings_outline.xml b/src/android/app/src/main/res/drawable/ic_settings_outline.xml
new file mode 100644
index 000000000..13b2745bf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_settings_outline.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_unlock.xml b/src/android/app/src/main/res/drawable/ic_unlock.xml
new file mode 100644
index 000000000..40952cbc5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_unlock.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_website.xml b/src/android/app/src/main/res/drawable/ic_website.xml
new file mode 100644
index 000000000..f35b84a7c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_website.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu.xml b/src/android/app/src/main/res/drawable/ic_yuzu.xml
new file mode 100644
index 000000000..5e2a8efd0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_yuzu.xml
@@ -0,0 +1,22 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="200dp"
3 android:height="200dp"
4 android:viewportWidth="500"
5 android:viewportHeight="500">
6 <path
7 android:fillColor="#FF3C28"
8 android:fillType="nonZero"
9 android:pathData="M262.66,175.11L262.66,375.05C318.54,375.05 363.85,330.29 363.85,275.08C363.85,219.87 318.54,175.11 262.66,175.11M282.43,197.01C318.67,206 344.09,238.19 344.09,275.11C344.09,312.03 318.67,344.22 282.43,353.2L282.43,197.01"
10 android:strokeWidth="1.46"
11 android:strokeColor="#00000000"
12 android:strokeLineCap="butt"
13 android:strokeLineJoin="miter" />
14 <path
15 android:fillColor="#0AB9E6"
16 android:fillType="nonZero"
17 android:pathData="M237.31,125.11C181.43,125.11 136.12,169.87 136.12,225.08C136.12,280.29 181.43,325.05 237.31,325.05ZM217.57,147.01L217.57,303.2C189.11,296.16 166.67,274.54 158.84,246.6C151.01,218.65 159,188.71 179.75,168.21C190.16,157.86 203.24,150.53 217.57,147.01"
18 android:strokeWidth="1.46"
19 android:strokeColor="#00000000"
20 android:strokeLineCap="butt"
21 android:strokeLineJoin="miter" />
22</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_full.xml b/src/android/app/src/main/res/drawable/ic_yuzu_full.xml
new file mode 100644
index 000000000..04e458400
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_yuzu_full.xml
@@ -0,0 +1,12 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="155.3dp"
3 android:height="172.55dp"
4 android:viewportWidth="155.3"
5 android:viewportHeight="172.55">
6 <path
7 android:fillColor="#FF3C28"
8 android:pathData="M86.28,34.51v138a69,69 0,0 0,0 -138M99.76,49.63a55.57,55.57 0,0 1,0 107.8V49.63" />
9 <path
10 android:fillColor="#0AB9E6"
11 android:pathData="M69,0a69,69 0,0 0,0 138ZM55.54,15.12v107.8A55.55,55.55 0,0 1,29.75 29.75,55.1 55.1,0 0,1 55.54,15.12" />
12</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_title.xml b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml
new file mode 100644
index 000000000..b733e5248
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml
@@ -0,0 +1,24 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="340.97dp"
3 android:height="389.85dp"
4 android:viewportWidth="340.97"
5 android:viewportHeight="389.85">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" />
9 <path
10 android:fillColor="?attr/colorOnSurface"
11 android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" />
12 <path
13 android:fillColor="?attr/colorOnSurface"
14 android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" />
15 <path
16 android:fillColor="?attr/colorOnSurface"
17 android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" />
18 <path
19 android:fillColor="#ff3c28"
20 android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" />
21 <path
22 android:fillColor="#0ab9e6"
23 android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" />
24</vector>
diff --git a/src/android/app/src/main/res/drawable/joystick.xml b/src/android/app/src/main/res/drawable/joystick.xml
new file mode 100644
index 000000000..bdd071212
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/joystick.xml
@@ -0,0 +1,45 @@
1<vector android:alpha="0.6" android:height="161.61dp"
2 android:viewportHeight="161.61" android:viewportWidth="161.61"
3 android:width="161.61dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.8"
6 android:pathData="M91.23,0.68a80.8,80.8 0,1 0,69.69 90.55A80.81,80.81 0,0 0,91.23 0.68ZM80.8,150.68A69.84,69.84 0,1 1,150.64 80.8,69.92 69.92,0 0,1 80.8,150.64Z" android:strokeAlpha="0.8">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="68.25" android:centerY="57.75"
9 android:gradientRadius="122.17" android:type="radial">
10 <item android:color="#FFD9D9D9" android:offset="0.44"/>
11 <item android:color="#FF141414" android:offset="1"/>
12 </gradient>
13 </aapt:attr>
14 </path>
15 <path android:fillAlpha="0.8"
16 android:pathData="M80.8,80.8m-67.05,0a67.05,67.05 0,1 1,134.1 0a67.05,67.05 0,1 1,-134.1 0" android:strokeAlpha="0.8">
17 <aapt:attr name="android:fillColor">
18 <gradient android:centerX="80.49" android:centerY="60.19"
19 android:gradientRadius="88.23" android:type="radial">
20 <item android:color="#FFBABABA" android:offset="0.15"/>
21 <item android:color="#FF9E9E9E" android:offset="0.46"/>
22 <item android:color="#FF868686" android:offset="0.63"/>
23 <item android:color="#FF575757" android:offset="1"/>
24 </gradient>
25 </aapt:attr>
26 </path>
27 <path android:fillAlpha="0.8"
28 android:pathData="M80.8,150.64A69.84,69.84 0,1 1,150.64 80.8,69.92 69.92,0 0,1 80.8,150.64ZM80.8,13.76a67,67 0,1 0,67.05 67A67.11,67.11 0,0 0,80.8 13.76Z" android:strokeAlpha="0.8">
29 <aapt:attr name="android:fillColor">
30 <gradient android:centerX="80.8" android:centerY="80.8"
31 android:gradientRadius="97.63" android:type="radial">
32 <item android:color="#FFC2C2C3" android:offset="0.04"/>
33 <item android:color="#FFC0C0C1" android:offset="0.35"/>
34 <item android:color="#FFB9B9BA" android:offset="0.47"/>
35 <item android:color="#FFADADAE" android:offset="0.56"/>
36 <item android:color="#FF9C9C9D" android:offset="0.63"/>
37 <item android:color="#FF868687" android:offset="0.69"/>
38 <item android:color="#FF6A6A6B" android:offset="0.74"/>
39 <item android:color="#FF4A4A4A" android:offset="0.79"/>
40 <item android:color="#FF252525" android:offset="0.83"/>
41 <item android:color="#FF000000" android:offset="0.87"/>
42 </gradient>
43 </aapt:attr>
44 </path>
45</vector>
diff --git a/src/android/app/src/main/res/drawable/joystick_depressed.xml b/src/android/app/src/main/res/drawable/joystick_depressed.xml
new file mode 100644
index 000000000..ad51d73ce
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/joystick_depressed.xml
@@ -0,0 +1,10 @@
1<vector android:alpha="0.6" android:height="161.73dp"
2 android:viewportHeight="161.73" android:viewportWidth="161.73"
3 android:width="161.73dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#FF000000"
5 android:pathData="M91.3,0.68A80.86,80.86 0,1 0,161.05 91.3,80.87 80.87,0 0,0 91.3,0.68ZM80.87,150.76a69.9,69.9 0,1 1,69.89 -69.89A70,70 0,0 1,80.87 150.76Z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.5" android:fillColor="#FF000000"
7 android:pathData="M80.87,80.87m-67.1,0a67.1,67.1 0,1 1,134.2 0a67.1,67.1 0,1 1,-134.2 0" android:strokeAlpha="0.5"/>
8 <path android:fillAlpha="0.75" android:fillColor="#fff"
9 android:pathData="M80.87,150.76a69.9,69.9 0,1 1,69.89 -69.89A70,70 0,0 1,80.87 150.76ZM80.87,13.76A67.1,67.1 0,1 0,148 80.87,67.17 67.17,0 0,0 80.87,13.77Z" android:strokeAlpha="0.75"/>
10</vector>
diff --git a/src/android/app/src/main/res/drawable/joystick_range.xml b/src/android/app/src/main/res/drawable/joystick_range.xml
new file mode 100644
index 000000000..f6282b5c8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/joystick_range.xml
@@ -0,0 +1,38 @@
1<vector android:alpha="0.6" android:height="265.64dp"
2 android:viewportHeight="265.64" android:viewportWidth="265.64"
3 android:width="265.64dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.8"
6 android:pathData="M132.82,132.82m-113.12,0a113.12,113.12 0,1 1,226.24 0a113.12,113.12 0,1 1,-226.24 0" android:strokeAlpha="0.8">
7 <aapt:attr name="android:fillColor">
8 <gradient android:centerX="132.82" android:centerY="132.82"
9 android:gradientRadius="195.71" android:type="radial">
10 <item android:color="#00000000" android:offset="0"/>
11 <item android:color="#14161515" android:offset="0.27"/>
12 <item android:color="#30333031" android:offset="0.42"/>
13 <item android:color="#42393738" android:offset="0.45"/>
14 <item android:color="#754B494A" android:offset="0.51"/>
15 <item android:color="#C6676666" android:offset="0.59"/>
16 <item android:color="#FF7A7A7A" android:offset="0.63"/>
17 <item android:color="#FF787878" android:offset="0.99"/>
18 <item android:color="#FF787878" android:offset="0.99"/>
19 </gradient>
20 </aapt:attr>
21 </path>
22 <path android:fillAlpha="0.6"
23 android:pathData="m18.72,64.82a132.8,132.8 0,1 0,182.06 -46.1,132.8 132.8,0 0,0 -182.06,46.1zM229.98,190.7a113.12,113.12 0,1 1,-39.28 -155.08,113.12 113.12,0 0,1 39.28,155.08z" android:strokeAlpha="0.6">
24 <aapt:attr name="android:fillColor">
25 <gradient android:centerX="132.82" android:centerY="132.7"
26 android:gradientRadius="141.24" android:type="radial">
27 <item android:color="#FF969696" android:offset="0"/>
28 <item android:color="#FF949494" android:offset="0.8"/>
29 <item android:color="#FF8D8D8D" android:offset="0.84"/>
30 <item android:color="#FF828282" android:offset="0.87"/>
31 <item android:color="#FF717171" android:offset="0.9"/>
32 <item android:color="#FF5B5B5B" android:offset="0.94"/>
33 <item android:color="#FF404040" android:offset="0.98"/>
34 <item android:color="#FF303030" android:offset="1"/>
35 </gradient>
36 </aapt:attr>
37 </path>
38</vector>
diff --git a/src/android/app/src/main/res/drawable/l_shoulder.xml b/src/android/app/src/main/res/drawable/l_shoulder.xml
new file mode 100644
index 000000000..28f9a9950
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/l_shoulder.xml
@@ -0,0 +1,23 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M33.05,0L211.86,0A33.05,33.05 0,0 1,244.91 33.05L244.91,65.53A33.05,33.05 0,0 1,211.86 98.58L33.05,98.58A33.05,33.05 0,0 1,0 65.53L0,33.05A33.05,33.05 0,0 1,33.05 0z" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="244.91" android:endY="49.29"
9 android:startX="0" android:startY="49.29" android:type="linear">
10 <item android:color="#FFC3C4C5" android:offset="0"/>
11 <item android:color="#FFC4C5C5" android:offset="0.05"/>
12 <item android:color="#FFC7C7C7" android:offset="0.47"/>
13 <item android:color="#F7C4C4C4" android:offset="0.54"/>
14 <item android:color="#E5BABABA" android:offset="0.65"/>
15 <item android:color="#C6ABABAB" android:offset="0.77"/>
16 <item android:color="#9E969696" android:offset="0.91"/>
17 <item android:color="#7F878787" android:offset="1"/>
18 </gradient>
19 </aapt:attr>
20 </path>
21 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
22 android:pathData="M106.15,20h7.57V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
23</vector>
diff --git a/src/android/app/src/main/res/drawable/l_shoulder_depressed.xml b/src/android/app/src/main/res/drawable/l_shoulder_depressed.xml
new file mode 100644
index 000000000..2f9a1fd7e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/l_shoulder_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M33.05,0L211.86,0A33.05,33.05 0,0 1,244.91 33.05L244.91,65.53A33.05,33.05 0,0 1,211.86 98.58L33.05,98.58A33.05,33.05 0,0 1,0 65.53L0,33.05A33.05,33.05 0,0 1,33.05 0z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M106.15,20h7.57V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/premium_background.xml b/src/android/app/src/main/res/drawable/premium_background.xml
new file mode 100644
index 000000000..c9c41ddbe
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/premium_background.xml
@@ -0,0 +1,9 @@
1<?xml version="1.0" encoding="utf-8"?>
2<shape xmlns:android="http://schemas.android.com/apk/res/android">
3 <gradient
4 android:type="linear"
5 android:angle="45"
6 android:startColor="@color/yuzu_ea_background_start"
7 android:endColor="@color/yuzu_ea_background_end" />
8 <corners android:radius="12dp" />
9</shape>
diff --git a/src/android/app/src/main/res/drawable/r_shoulder.xml b/src/android/app/src/main/res/drawable/r_shoulder.xml
new file mode 100644
index 000000000..97731cad2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/r_shoulder.xml
@@ -0,0 +1,23 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M211.86,98.58L33.05,98.58A33.05,33.05 0,0 1,0 65.53L0,33.05A33.05,33.05 0,0 1,33.05 0L211.86,0A33.05,33.05 0,0 1,244.91 33.05L244.91,65.53A33.05,33.05 0,0 1,211.86 98.58z" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="0" android:endY="49.29"
9 android:startX="244.91" android:startY="49.29" android:type="linear">
10 <item android:color="#FFC3C4C5" android:offset="0"/>
11 <item android:color="#FFC4C5C5" android:offset="0.05"/>
12 <item android:color="#FFC7C7C7" android:offset="0.47"/>
13 <item android:color="#F7C4C4C4" android:offset="0.54"/>
14 <item android:color="#E5BABABA" android:offset="0.65"/>
15 <item android:color="#C6ABABAB" android:offset="0.77"/>
16 <item android:color="#9E969696" android:offset="0.91"/>
17 <item android:color="#7F878787" android:offset="1"/>
18 </gradient>
19 </aapt:attr>
20 </path>
21 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
22 android:pathData="M103.37,21a78.13,78.13 0,0 1,14.52 -1.22c8.08,0 13.3,1.48 17,4.78a14.59,14.59 0,0 1,4.61 11.13c0,7.73 -4.87,12.86 -11,15v0.26c4.52,1.56 7.21,5.74 8.6,11.82 1.92,8.17 3.31,13.82 4.52,16.08h-7.82c-1,-1.65 -2.26,-6.69 -3.91,-14 -1.74,-8.09 -4.87,-11.13 -11.74,-11.39h-7.12L111.03,78.8h-7.57ZM110.94,47.68h7.73c8.09,0 13.22,-4.43 13.22,-11.12 0,-7.57 -5.48,-10.87 -13.48,-11a30.82,30.82 0,0 0,-7.47 0.7Z" android:strokeAlpha="0.75"/>
23</vector>
diff --git a/src/android/app/src/main/res/drawable/r_shoulder_depressed.xml b/src/android/app/src/main/res/drawable/r_shoulder_depressed.xml
new file mode 100644
index 000000000..e3aa46aa1
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/r_shoulder_depressed.xml
@@ -0,0 +1,8 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M211.86,98.58L33.05,98.58A33.05,33.05 0,0 1,0 65.53L0,33.05A33.05,33.05 0,0 1,33.05 0L211.86,0A33.05,33.05 0,0 1,244.91 33.05L244.91,65.53A33.05,33.05 0,0 1,211.86 98.58z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M103.37,21a78.13,78.13 0,0 1,14.52 -1.22c8.08,0 13.3,1.48 17,4.78a14.59,14.59 0,0 1,4.61 11.13c0,7.73 -4.87,12.86 -11,15v0.26c4.52,1.56 7.21,5.74 8.6,11.82 1.92,8.17 3.31,13.82 4.52,16.08h-7.82c-1,-1.65 -2.26,-6.69 -3.91,-14 -1.74,-8.09 -4.87,-11.13 -11.74,-11.39h-7.12L111.03,78.8h-7.57ZM110.94,47.68h7.73c8.09,0 13.22,-4.43 13.22,-11.12 0,-7.57 -5.48,-10.87 -13.48,-11a30.82,30.82 0,0 0,-7.47 0.7Z" android:strokeAlpha="0.75"/>
8</vector>
diff --git a/src/android/app/src/main/res/drawable/selector_cartridge.xml b/src/android/app/src/main/res/drawable/selector_cartridge.xml
new file mode 100644
index 000000000..85c918dae
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/selector_cartridge.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<selector xmlns:android="http://schemas.android.com/apk/res/android">
3 <item android:drawable="@drawable/ic_cartridge_outline" android:state_checked="false"/>
4 <item android:drawable="@drawable/ic_cartridge" android:state_checked="true"/>
5</selector>
diff --git a/src/android/app/src/main/res/drawable/selector_settings.xml b/src/android/app/src/main/res/drawable/selector_settings.xml
new file mode 100644
index 000000000..23748feb0
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/selector_settings.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<selector xmlns:android="http://schemas.android.com/apk/res/android">
3 <item android:drawable="@drawable/ic_settings_outline" android:state_checked="false"/>
4 <item android:drawable="@drawable/ic_settings" android:state_checked="true"/>
5</selector>
diff --git a/src/android/app/src/main/res/drawable/zl_trigger.xml b/src/android/app/src/main/res/drawable/zl_trigger.xml
new file mode 100644
index 000000000..436461c3b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/zl_trigger.xml
@@ -0,0 +1,25 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M84.62,0h145a15.32,15.32 0,0 1,15.32 15.32V67a31.54,31.54 0,0 1,-31.54 31.54H14a14,14 0,0 1,-14 -14v0A84.62,84.62 0,0 1,84.62 0Z" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="244.91" android:endY="49.29"
9 android:startX="0" android:startY="49.29" android:type="linear">
10 <item android:color="#FFC3C4C5" android:offset="0"/>
11 <item android:color="#FFC4C5C5" android:offset="0.05"/>
12 <item android:color="#FFC7C7C7" android:offset="0.47"/>
13 <item android:color="#F7C4C4C4" android:offset="0.54"/>
14 <item android:color="#E5BABABA" android:offset="0.65"/>
15 <item android:color="#C6ABABAB" android:offset="0.77"/>
16 <item android:color="#9E969696" android:offset="0.91"/>
17 <item android:color="#7F878787" android:offset="1"/>
18 </gradient>
19 </aapt:attr>
20 </path>
21 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
22 android:pathData="M80.12,74.15 L112.63,26.6v-0.26H82.9V20h39.56v4.6L90.12,72v0.26h32.77v6.35H80.12Z" android:strokeAlpha="0.75"/>
23 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
24 android:pathData="M132.19,20h7.56V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
25</vector>
diff --git a/src/android/app/src/main/res/drawable/zl_trigger_depressed.xml b/src/android/app/src/main/res/drawable/zl_trigger_depressed.xml
new file mode 100644
index 000000000..00393c04d
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/zl_trigger_depressed.xml
@@ -0,0 +1,10 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M84.62,0h145a15.32,15.32 0,0 1,15.32 15.32V67a31.54,31.54 0,0 1,-31.54 31.54H14a14,14 0,0 1,-14 -14v0A84.62,84.62 0,0 1,84.62 0Z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M80.12,74.15 L112.63,26.6v-0.26H82.9V20h39.56v4.6L90.12,72v0.26h32.77v6.35H80.12Z" android:strokeAlpha="0.75"/>
8 <path android:fillAlpha="0.75" android:fillColor="#fff"
9 android:pathData="M132.19,20h7.56V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
10</vector>
diff --git a/src/android/app/src/main/res/drawable/zr_trigger.xml b/src/android/app/src/main/res/drawable/zr_trigger.xml
new file mode 100644
index 000000000..2b3a92184
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/zr_trigger.xml
@@ -0,0 +1,25 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp"
4 xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
5 <path android:fillAlpha="0.6"
6 android:pathData="M230.91,98.58l-199.4,-0a31.54,31.54 135,0 1,-31.54 -31.54L-0.03,15.31a15.32,15.32 0,0 1,15.32 -15.32l145,-0A84.62,84.62 0,0 1,244.91 84.58l-0,-0A14,14 0,0 1,230.91 98.58Z" android:strokeAlpha="0.6">
7 <aapt:attr name="android:fillColor">
8 <gradient android:endX="0" android:endY="49.29"
9 android:startX="244.91" android:startY="49.29" android:type="linear">
10 <item android:color="#FFC3C4C5" android:offset="0"/>
11 <item android:color="#FFC4C5C5" android:offset="0.05"/>
12 <item android:color="#FFC7C7C7" android:offset="0.47"/>
13 <item android:color="#F7C4C4C4" android:offset="0.54"/>
14 <item android:color="#E5BABABA" android:offset="0.65"/>
15 <item android:color="#C6ABABAB" android:offset="0.77"/>
16 <item android:color="#9E969696" android:offset="0.91"/>
17 <item android:color="#7F878787" android:offset="1"/>
18 </gradient>
19 </aapt:attr>
20 </path>
21 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
22 android:pathData="M77.34,74.37l32.51,-47.55v-0.26H80.12V20.21h39.55v4.61L87.34,72.2v0.26h32.77V78.8H77.34Z" android:strokeAlpha="0.75"/>
23 <path android:fillAlpha="0.75" android:fillColor="#FF000000"
24 android:pathData="M129.41,21a78,78 0,0 1,14.51 -1.22c8.09,0 13.3,1.48 17,4.78a14.62,14.62 0,0 1,4.6 11.13c0,7.73 -4.87,12.86 -11,15v0.26c4.52,1.56 7.22,5.74 8.61,11.82 1.91,8.17 3.3,13.82 4.52,16.08h-7.82c-1,-1.65 -2.26,-6.69 -3.92,-14C154.1,56.72 151,53.68 144.1,53.42H137V78.8h-7.56ZM137,47.68h7.74c8.08,0 13.21,-4.43 13.21,-11.12 0,-7.57 -5.48,-10.87 -13.47,-11a30.92,30.92 0,0 0,-7.48 0.7Z" android:strokeAlpha="0.75"/>
25</vector>
diff --git a/src/android/app/src/main/res/drawable/zr_trigger_depressed.xml b/src/android/app/src/main/res/drawable/zr_trigger_depressed.xml
new file mode 100644
index 000000000..8a9ee5036
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/zr_trigger_depressed.xml
@@ -0,0 +1,10 @@
1<vector android:alpha="0.6" android:height="98.58dp"
2 android:viewportHeight="98.58" android:viewportWidth="244.91"
3 android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
4 <path android:fillAlpha="0.5" android:fillColor="#151515"
5 android:pathData="M230.91,98.58l-199.4,-0a31.54,31.54 135,0 1,-31.54 -31.54L-0.03,15.31a15.32,15.32 0,0 1,15.32 -15.32l145,-0A84.62,84.62 0,0 1,244.91 84.58l-0,-0A14,14 0,0 1,230.91 98.58Z" android:strokeAlpha="0.5"/>
6 <path android:fillAlpha="0.75" android:fillColor="#fff"
7 android:pathData="M77.34,74.37l32.51,-47.55v-0.26H80.12V20.21h39.55v4.61L87.34,72.2v0.26h32.77V78.8H77.34Z" android:strokeAlpha="0.75"/>
8 <path android:fillAlpha="0.75" android:fillColor="#fff"
9 android:pathData="M129.41,21a78,78 0,0 1,14.51 -1.22c8.09,0 13.3,1.48 17,4.78a14.62,14.62 0,0 1,4.6 11.13c0,7.73 -4.87,12.86 -11,15v0.26c4.52,1.56 7.22,5.74 8.61,11.82 1.91,8.17 3.3,13.82 4.52,16.08h-7.82c-1,-1.65 -2.26,-6.69 -3.92,-14C154.1,56.72 151,53.68 144.1,53.42H137V78.8h-7.56ZM137,47.68h7.74c8.08,0 13.21,-4.43 13.21,-11.12 0,-7.57 -5.48,-10.87 -13.47,-11a30.92,30.92 0,0 0,-7.48 0.7Z" android:strokeAlpha="0.75"/>
10</vector>
diff --git a/src/android/app/src/main/res/layout-w600dp/activity_main.xml b/src/android/app/src/main/res/layout-w600dp/activity_main.xml
new file mode 100644
index 000000000..74bee872e
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/activity_main.xml
@@ -0,0 +1,58 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/coordinator_main"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent"
9 android:background="?attr/colorSurface">
10
11 <androidx.fragment.app.FragmentContainerView
12 android:id="@+id/fragment_container"
13 android:name="androidx.navigation.fragment.NavHostFragment"
14 android:layout_width="0dp"
15 android:layout_height="0dp"
16 app:defaultNavHost="true"
17 app:layout_constraintBottom_toBottomOf="parent"
18 app:layout_constraintEnd_toEndOf="parent"
19 app:layout_constraintStart_toStartOf="parent"
20 app:layout_constraintTop_toTopOf="parent"
21 app:navGraph="@navigation/home_navigation"
22 tools:layout="@layout/fragment_games" />
23
24 <com.google.android.material.navigationrail.NavigationRailView
25 android:id="@+id/navigation_view"
26 android:layout_width="wrap_content"
27 android:layout_height="match_parent"
28 android:visibility="invisible"
29 app:layout_constraintBottom_toBottomOf="parent"
30 app:layout_constraintStart_toStartOf="parent"
31 app:layout_constraintTop_toTopOf="parent"
32 app:labelVisibilityMode="selected"
33 app:menu="@menu/menu_navigation"
34 tools:visibility="visible" />
35
36 <View
37 android:id="@+id/status_bar_shade"
38 android:layout_width="0dp"
39 android:layout_height="1px"
40 android:background="@android:color/transparent"
41 android:clickable="false"
42 android:focusable="false"
43 app:layout_constraintTop_toTopOf="parent"
44 app:layout_constraintEnd_toEndOf="parent"
45 app:layout_constraintStart_toStartOf="parent" />
46
47 <View
48 android:id="@+id/navigation_bar_shade"
49 android:layout_width="0dp"
50 android:layout_height="1px"
51 android:background="@android:color/transparent"
52 android:clickable="false"
53 android:focusable="false"
54 app:layout_constraintBottom_toBottomOf="parent"
55 app:layout_constraintEnd_toEndOf="parent"
56 app:layout_constraintStart_toStartOf="parent" />
57
58</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
new file mode 100644
index 000000000..cbe631d88
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/setup_root"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <androidx.viewpager2.widget.ViewPager2
10 android:id="@+id/viewPager2"
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 app:layout_constraintBottom_toBottomOf="parent"
14 app:layout_constraintEnd_toEndOf="parent"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent" />
17
18 <com.google.android.material.button.MaterialButton
19 style="@style/Widget.Material3.Button.TextButton"
20 android:id="@+id/button_next"
21 android:layout_width="wrap_content"
22 android:layout_height="wrap_content"
23 android:layout_margin="16dp"
24 android:text="@string/next"
25 android:visibility="invisible"
26 app:layout_constraintBottom_toBottomOf="parent"
27 app:layout_constraintEnd_toEndOf="parent" />
28
29 <com.google.android.material.button.MaterialButton
30 android:id="@+id/button_back"
31 style="@style/Widget.Material3.Button.TextButton"
32 android:layout_width="wrap_content"
33 android:layout_height="wrap_content"
34 android:layout_margin="16dp"
35 android:text="@string/back"
36 android:visibility="invisible"
37 app:layout_constraintBottom_toBottomOf="parent"
38 app:layout_constraintStart_toStartOf="parent" />
39
40</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
new file mode 100644
index 000000000..e1c26b2f8
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
@@ -0,0 +1,65 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <LinearLayout
10 android:layout_width="match_parent"
11 android:layout_height="match_parent"
12 android:orientation="vertical"
13 android:layout_weight="1"
14 android:gravity="center">
15
16 <ImageView
17 android:id="@+id/icon"
18 android:layout_width="260dp"
19 android:layout_height="260dp"
20 android:layout_gravity="center" />
21
22 </LinearLayout>
23
24 <LinearLayout
25 android:layout_width="match_parent"
26 android:layout_height="match_parent"
27 android:layout_weight="1"
28 android:orientation="vertical"
29 android:gravity="center">
30
31 <com.google.android.material.textview.MaterialTextView
32 style="@style/TextAppearance.Material3.DisplaySmall"
33 android:id="@+id/text_title"
34 android:layout_width="match_parent"
35 android:layout_height="wrap_content"
36 android:textAlignment="center"
37 android:textColor="?attr/colorOnSurface"
38 android:textStyle="bold"
39 tools:text="@string/welcome" />
40
41 <com.google.android.material.textview.MaterialTextView
42 style="@style/TextAppearance.Material3.TitleLarge"
43 android:id="@+id/text_description"
44 android:layout_width="match_parent"
45 android:layout_height="wrap_content"
46 android:layout_marginTop="16dp"
47 android:paddingHorizontal="32dp"
48 android:textAlignment="center"
49 android:textSize="26sp"
50 app:lineHeight="40sp"
51 tools:text="@string/welcome_description" />
52
53 <com.google.android.material.button.MaterialButton
54 android:id="@+id/button_action"
55 android:layout_width="wrap_content"
56 android:layout_height="56dp"
57 android:layout_marginTop="32dp"
58 android:textSize="20sp"
59 app:iconSize="24sp"
60 app:iconGravity="end"
61 tools:text="Get started" />
62
63 </LinearLayout>
64
65</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
new file mode 100644
index 000000000..f6360a65b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -0,0 +1,13 @@
1<FrameLayout
2 xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/frame_content"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:keepScreenOn="true">
7
8 <FrameLayout
9 android:id="@+id/frame_emulation_fragment"
10 android:layout_width="match_parent"
11 android:layout_height="match_parent" />
12
13</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..ad426457f
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,58 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/coordinator_main"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent"
9 android:background="?attr/colorSurface">
10
11 <androidx.fragment.app.FragmentContainerView
12 android:id="@+id/fragment_container"
13 android:name="androidx.navigation.fragment.NavHostFragment"
14 android:layout_width="0dp"
15 android:layout_height="0dp"
16 app:defaultNavHost="true"
17 app:layout_constraintBottom_toBottomOf="parent"
18 app:layout_constraintLeft_toLeftOf="parent"
19 app:layout_constraintRight_toRightOf="parent"
20 app:layout_constraintTop_toTopOf="parent"
21 app:navGraph="@navigation/home_navigation"
22 tools:layout="@layout/fragment_games" />
23
24 <com.google.android.material.bottomnavigation.BottomNavigationView
25 android:id="@+id/navigation_view"
26 android:layout_width="match_parent"
27 android:layout_height="wrap_content"
28 android:visibility="invisible"
29 app:layout_constraintBottom_toBottomOf="parent"
30 app:layout_constraintLeft_toLeftOf="parent"
31 app:layout_constraintRight_toRightOf="parent"
32 app:menu="@menu/menu_navigation"
33 app:labelVisibilityMode="selected"
34 tools:visibility="visible" />
35
36 <View
37 android:id="@+id/status_bar_shade"
38 android:layout_width="0dp"
39 android:layout_height="1px"
40 android:background="@android:color/transparent"
41 android:clickable="false"
42 android:focusable="false"
43 app:layout_constraintTop_toTopOf="parent"
44 app:layout_constraintEnd_toEndOf="parent"
45 app:layout_constraintStart_toStartOf="parent" />
46
47 <View
48 android:id="@+id/navigation_bar_shade"
49 android:layout_width="0dp"
50 android:layout_height="1px"
51 android:background="@android:color/transparent"
52 android:clickable="false"
53 android:focusable="false"
54 app:layout_constraintBottom_toBottomOf="parent"
55 app:layout_constraintEnd_toEndOf="parent"
56 app:layout_constraintStart_toStartOf="parent" />
57
58</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 000000000..14ae83b04
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,50 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout
3 android:id="@+id/coordinator_main"
4 xmlns:android="http://schemas.android.com/apk/res/android"
5 xmlns:app="http://schemas.android.com/apk/res-auto"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <com.google.android.material.appbar.AppBarLayout
11 android:id="@+id/appbar_settings"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:fitsSystemWindows="true"
15 app:elevation="0dp">
16
17 <com.google.android.material.appbar.CollapsingToolbarLayout
18 style="?attr/collapsingToolbarLayoutMediumStyle"
19 android:id="@+id/toolbar_settings_layout"
20 android:layout_width="match_parent"
21 android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
22 app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
23
24 <com.google.android.material.appbar.MaterialToolbar
25 android:id="@+id/toolbar_settings"
26 android:layout_width="match_parent"
27 android:layout_height="?attr/actionBarSize"
28 app:layout_collapseMode="pin" />
29
30 </com.google.android.material.appbar.CollapsingToolbarLayout>
31
32 </com.google.android.material.appbar.AppBarLayout>
33
34 <FrameLayout
35 android:id="@+id/frame_content"
36 android:layout_width="match_parent"
37 android:layout_height="match_parent"
38 android:layout_marginHorizontal="12dp"
39 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
40
41 <View
42 android:id="@+id/navigation_bar_shade"
43 android:layout_width="match_parent"
44 android:layout_height="1px"
45 android:background="@android:color/transparent"
46 android:clickable="false"
47 android:focusable="false"
48 android:layout_gravity="bottom|center_horizontal" />
49
50</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
new file mode 100644
index 000000000..1f5de219b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -0,0 +1,67 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content">
8
9 <com.google.android.material.card.MaterialCardView
10 style="?attr/materialCardViewElevatedStyle"
11 android:id="@+id/card_game"
12 android:layout_width="wrap_content"
13 android:layout_height="wrap_content"
14 android:background="?attr/selectableItemBackground"
15 android:clickable="true"
16 android:clipToPadding="false"
17 android:focusable="true"
18 android:transitionName="card_game"
19 android:layout_gravity="center"
20 app:cardElevation="0dp"
21 app:cardCornerRadius="12dp">
22
23 <androidx.constraintlayout.widget.ConstraintLayout
24 android:layout_width="wrap_content"
25 android:layout_height="wrap_content"
26 android:padding="6dp">
27
28 <com.google.android.material.card.MaterialCardView
29 style="?attr/materialCardViewElevatedStyle"
30 android:id="@+id/card_game_art"
31 android:layout_width="150dp"
32 android:layout_height="150dp"
33 app:cardCornerRadius="4dp"
34 app:layout_constraintEnd_toEndOf="parent"
35 app:layout_constraintStart_toStartOf="parent"
36 app:layout_constraintTop_toTopOf="parent">
37
38 <ImageView
39 android:id="@+id/image_game_screen"
40 android:layout_width="match_parent"
41 android:layout_height="match_parent"
42 tools:src="@drawable/default_icon" />
43
44 </com.google.android.material.card.MaterialCardView>
45
46 <com.google.android.material.textview.MaterialTextView
47 style="@style/TextAppearance.Material3.TitleMedium"
48 android:id="@+id/text_game_title"
49 android:layout_width="0dp"
50 android:layout_height="wrap_content"
51 android:layout_marginTop="8dp"
52 android:textAlignment="center"
53 android:textSize="14sp"
54 android:singleLine="true"
55 android:marqueeRepeatLimit="marquee_forever"
56 android:ellipsize="none"
57 android:requiresFadingEdge="horizontal"
58 app:layout_constraintEnd_toEndOf="@+id/card_game_art"
59 app:layout_constraintStart_toStartOf="@+id/card_game_art"
60 app:layout_constraintTop_toBottomOf="@+id/card_game_art"
61 tools:text="The Legend of Zelda: Skyward Sword" />
62
63 </androidx.constraintlayout.widget.ConstraintLayout>
64
65 </com.google.android.material.card.MaterialCardView>
66
67</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml
new file mode 100644
index 000000000..dc289db17
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_home_option.xml
@@ -0,0 +1,60 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewFilledStyle"
6 android:id="@+id/option_card"
7 android:layout_width="match_parent"
8 android:layout_height="wrap_content"
9 android:layout_marginVertical="12dp"
10 android:layout_marginHorizontal="16dp"
11 android:background="?attr/selectableItemBackground"
12 android:backgroundTint="?attr/colorSurfaceVariant"
13 android:clickable="true"
14 android:focusable="true">
15
16 <LinearLayout
17 android:id="@+id/option_layout"
18 android:layout_width="match_parent"
19 android:layout_height="wrap_content">
20
21 <ImageView
22 android:id="@+id/option_icon"
23 android:layout_width="24dp"
24 android:layout_height="24dp"
25 android:layout_marginStart="24dp"
26 android:layout_gravity="center_vertical"
27 app:tint="?attr/colorOnSurface" />
28
29 <LinearLayout
30 android:layout_width="match_parent"
31 android:layout_height="wrap_content"
32 android:layout_marginVertical="10dp"
33 android:layout_marginHorizontal="20dp"
34 android:orientation="vertical">
35
36 <com.google.android.material.textview.MaterialTextView
37 style="@style/TextAppearance.Material3.BodyMedium"
38 android:id="@+id/option_title"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:textAlignment="viewStart"
42 android:textStyle="bold"
43 android:textSize="16sp"
44 tools:text="@string/install_prod_keys" />
45
46 <com.google.android.material.textview.MaterialTextView
47 style="@style/TextAppearance.Material3.BodySmall"
48 android:id="@+id/option_description"
49 android:layout_width="match_parent"
50 android:layout_height="wrap_content"
51 android:textAlignment="viewStart"
52 android:textSize="14sp"
53 android:layout_marginTop="5dp"
54 tools:text="@string/install_prod_keys_description" />
55
56 </LinearLayout>
57
58 </LinearLayout>
59
60</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/dialog_edit_text.xml b/src/android/app/src/main/res/layout/dialog_edit_text.xml
new file mode 100644
index 000000000..58b905d71
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_edit_text.xml
@@ -0,0 +1,23 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <com.google.android.material.textfield.TextInputLayout
9 android:id="@+id/edit_text_layout"
10 android:layout_width="match_parent"
11 android:layout_height="wrap_content"
12 android:layout_margin="24dp"
13 app:layout_constraintTop_toTopOf="parent">
14
15 <com.google.android.material.textfield.TextInputEditText
16 android:id="@+id/edit_text"
17 android:layout_width="match_parent"
18 android:layout_height="wrap_content"
19 android:inputType="none" />
20
21 </com.google.android.material.textfield.TextInputLayout>
22
23</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_license.xml b/src/android/app/src/main/res/layout/dialog_license.xml
new file mode 100644
index 000000000..866857562
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_license.xml
@@ -0,0 +1,64 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 xmlns:tools="http://schemas.android.com/tools">
6
7 <androidx.core.widget.NestedScrollView
8 android:layout_width="match_parent"
9 android:layout_height="wrap_content">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:orientation="vertical"
15 android:layout_marginHorizontal="16dp">
16
17 <com.google.android.material.bottomsheet.BottomSheetDragHandleView
18 android:layout_width="wrap_content"
19 android:layout_height="wrap_content"
20 android:layout_gravity="center_horizontal"/>
21
22 <com.google.android.material.textview.MaterialTextView
23 style="@style/TextAppearance.Material3.HeadlineLarge"
24 android:id="@+id/text_title"
25 android:layout_width="match_parent"
26 android:layout_height="wrap_content"
27 android:gravity="center"
28 tools:text="@string/license_adreno_tools" />
29
30 <com.google.android.material.textview.MaterialTextView
31 style="@style/TextAppearance.Material3.BodyLarge"
32 android:id="@+id/text_link"
33 android:layout_width="match_parent"
34 android:layout_height="wrap_content"
35 android:gravity="center"
36 android:layout_marginTop="16dp"
37 android:autoLink="all"
38 tools:text="@string/license_adreno_tools_link" />
39
40 <com.google.android.material.textview.MaterialTextView
41 style="@style/TextAppearance.Material3.BodyLarge"
42 android:id="@+id/text_copyright"
43 android:layout_width="match_parent"
44 android:layout_height="wrap_content"
45 android:gravity="center"
46 android:layout_marginTop="16dp"
47 android:textStyle="bold"
48 tools:text="@string/license_adreno_tools_copyright" />
49
50 <com.google.android.material.textview.MaterialTextView
51 style="@style/TextAppearance.Material3.BodyMedium"
52 android:id="@+id/text_license"
53 android:layout_width="match_parent"
54 android:layout_height="wrap_content"
55 android:layout_gravity="center"
56 android:layout_marginVertical="16dp"
57 android:autoLink="all"
58 tools:text="@string/license_adreno_tools_text" />
59
60 </LinearLayout>
61
62 </androidx.core.widget.NestedScrollView>
63
64</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_overlay_adjust.xml b/src/android/app/src/main/res/layout/dialog_overlay_adjust.xml
new file mode 100644
index 000000000..59bb983e1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_overlay_adjust.xml
@@ -0,0 +1,67 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:tools="http://schemas.android.com/tools"
5 xmlns:app="http://schemas.android.com/apk/res-auto"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <TextView
10 android:id="@+id/input_scale_name"
11 android:layout_width="wrap_content"
12 android:layout_height="wrap_content"
13 android:layout_marginTop="16dp"
14 android:text="@string/emulation_control_scale"
15 android:textAlignment="viewStart"
16 android:textSize="16sp"
17 app:layout_constraintStart_toStartOf="@+id/input_scale_slider"
18 app:layout_constraintTop_toTopOf="parent" />
19
20 <com.google.android.material.slider.Slider
21 android:id="@+id/input_scale_slider"
22 android:layout_width="0dp"
23 android:layout_height="wrap_content"
24 android:layout_marginHorizontal="24dp"
25 app:layout_constraintEnd_toEndOf="parent"
26 app:layout_constraintStart_toStartOf="parent"
27 app:layout_constraintTop_toBottomOf="@+id/input_scale_name" />
28
29 <TextView
30 android:id="@+id/input_scale_value"
31 android:layout_width="wrap_content"
32 android:layout_height="wrap_content"
33 android:gravity="end"
34 app:layout_constraintBottom_toTopOf="@+id/input_scale_slider"
35 app:layout_constraintEnd_toEndOf="@+id/input_scale_slider"
36 tools:text="100%" />
37
38 <TextView
39 android:id="@+id/input_opacity_name"
40 android:layout_width="wrap_content"
41 android:layout_height="wrap_content"
42 android:layout_marginTop="16dp"
43 android:text="@string/emulation_control_opacity"
44 android:textAlignment="viewStart"
45 android:textSize="16sp"
46 app:layout_constraintStart_toStartOf="@+id/input_opacity_slider"
47 app:layout_constraintTop_toBottomOf="@+id/input_scale_slider" />
48
49 <com.google.android.material.slider.Slider
50 android:id="@+id/input_opacity_slider"
51 android:layout_width="0dp"
52 android:layout_height="wrap_content"
53 android:layout_marginHorizontal="24dp"
54 app:layout_constraintEnd_toEndOf="parent"
55 app:layout_constraintStart_toStartOf="parent"
56 app:layout_constraintTop_toBottomOf="@+id/input_opacity_name" />
57
58 <TextView
59 android:id="@+id/input_opacity_value"
60 android:layout_width="wrap_content"
61 android:layout_height="wrap_content"
62 android:gravity="end"
63 app:layout_constraintBottom_toTopOf="@+id/input_opacity_slider"
64 app:layout_constraintEnd_toEndOf="@+id/input_opacity_slider"
65 tools:text="100%" />
66
67</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
new file mode 100644
index 000000000..d17711a65
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -0,0 +1,24 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 xmlns:app="http://schemas.android.com/apk/res-auto"
6 android:orientation="vertical">
7
8 <com.google.android.material.progressindicator.LinearProgressIndicator
9 android:id="@+id/progress_bar"
10 android:layout_width="match_parent"
11 android:layout_height="wrap_content"
12 android:layout_margin="24dp"
13 app:trackCornerRadius="4dp" />
14
15 <TextView
16 android:id="@+id/progress_text"
17 android:layout_width="match_parent"
18 android:layout_height="wrap_content"
19 android:layout_marginLeft="24dp"
20 android:layout_marginRight="24dp"
21 android:layout_marginBottom="24dp"
22 android:gravity="end" />
23
24</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_slider.xml b/src/android/app/src/main/res/layout/dialog_slider.xml
new file mode 100644
index 000000000..8c84cb606
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_slider.xml
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:orientation="vertical">
7
8 <TextView
9 android:id="@+id/text_value"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_alignParentTop="true"
13 android:layout_centerHorizontal="true"
14 android:layout_marginBottom="@dimen/spacing_medlarge"
15 android:layout_marginTop="@dimen/spacing_medlarge"
16 tools:text="75" />
17
18 <TextView
19 android:id="@+id/text_units"
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 android:layout_alignTop="@+id/text_value"
23 android:layout_toEndOf="@+id/text_value"
24 tools:text="%" />
25
26 <com.google.android.material.slider.Slider
27 android:id="@+id/slider"
28 android:layout_width="match_parent"
29 android:layout_height="wrap_content"
30 android:layout_alignParentEnd="true"
31 android:layout_alignParentStart="true"
32 android:layout_below="@+id/text_value"
33 android:layout_marginBottom="@dimen/spacing_medlarge"
34 android:layout_marginLeft="@dimen/spacing_large"
35 android:layout_marginRight="@dimen/spacing_large" />
36
37</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml
new file mode 100644
index 000000000..3e1d98451
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_about.xml
@@ -0,0 +1,232 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/coordinator_about"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <com.google.android.material.appbar.AppBarLayout
11 android:id="@+id/appbar_about"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:fitsSystemWindows="true">
15
16 <com.google.android.material.appbar.MaterialToolbar
17 android:id="@+id/toolbar_about"
18 android:layout_width="match_parent"
19 android:layout_height="?attr/actionBarSize"
20 app:title="@string/about"
21 app:navigationIcon="@drawable/ic_back" />
22
23 </com.google.android.material.appbar.AppBarLayout>
24
25 <androidx.core.widget.NestedScrollView
26 android:id="@+id/scroll_about"
27 android:layout_width="match_parent"
28 android:layout_height="match_parent"
29 android:scrollbars="vertical"
30 android:fadeScrollbars="false"
31 app:layout_behavior="@string/appbar_scrolling_view_behavior">
32
33 <LinearLayout
34 android:id="@+id/content_about"
35 android:layout_width="match_parent"
36 android:layout_height="match_parent"
37 android:orientation="vertical">
38
39 <ImageView
40 android:id="@+id/image_logo"
41 android:layout_width="250dp"
42 android:layout_height="250dp"
43 android:layout_marginTop="20dp"
44 android:layout_gravity="center_horizontal"
45 android:src="@drawable/ic_yuzu_title" />
46
47 <com.google.android.material.divider.MaterialDivider
48 android:layout_width="match_parent"
49 android:layout_height="wrap_content"
50 android:layout_marginHorizontal="20dp"
51 android:layout_marginTop="28dp" />
52
53 <LinearLayout
54 android:layout_width="match_parent"
55 android:layout_height="wrap_content"
56 android:paddingVertical="16dp"
57 android:paddingHorizontal="16dp"
58 android:orientation="vertical">
59
60 <com.google.android.material.textview.MaterialTextView
61 style="@style/TextAppearance.Material3.TitleMedium"
62 android:layout_width="match_parent"
63 android:layout_height="wrap_content"
64 android:layout_marginHorizontal="24dp"
65 android:textAlignment="viewStart"
66 android:text="@string/about" />
67
68 <com.google.android.material.textview.MaterialTextView
69 style="@style/TextAppearance.Material3.BodyMedium"
70 android:layout_width="match_parent"
71 android:layout_height="wrap_content"
72 android:layout_marginHorizontal="24dp"
73 android:layout_marginTop="6dp"
74 android:textAlignment="viewStart"
75 android:text="@string/about_app_description" />
76
77 </LinearLayout>
78
79 <com.google.android.material.divider.MaterialDivider
80 android:layout_width="match_parent"
81 android:layout_height="wrap_content"
82 android:layout_marginHorizontal="20dp" />
83
84 <LinearLayout
85 android:id="@+id/button_contributors"
86 android:layout_width="match_parent"
87 android:layout_height="wrap_content"
88 android:paddingVertical="16dp"
89 android:paddingHorizontal="16dp"
90 android:background="?attr/selectableItemBackground"
91 android:orientation="vertical">
92
93 <com.google.android.material.textview.MaterialTextView
94 style="@style/TextAppearance.Material3.TitleMedium"
95 android:layout_width="match_parent"
96 android:layout_height="wrap_content"
97 android:layout_marginHorizontal="24dp"
98 android:textAlignment="viewStart"
99 android:text="@string/contributors" />
100
101 <com.google.android.material.textview.MaterialTextView
102 style="@style/TextAppearance.Material3.BodyMedium"
103 android:layout_width="match_parent"
104 android:layout_height="wrap_content"
105 android:layout_marginHorizontal="24dp"
106 android:layout_marginTop="6dp"
107 android:textAlignment="viewStart"
108 android:text="@string/contributors_description" />
109
110 </LinearLayout>
111
112 <com.google.android.material.divider.MaterialDivider
113 android:layout_width="match_parent"
114 android:layout_height="wrap_content"
115 android:layout_marginHorizontal="20dp" />
116
117 <LinearLayout
118 android:id="@+id/button_licenses"
119 android:layout_width="match_parent"
120 android:layout_height="wrap_content"
121 android:paddingVertical="16dp"
122 android:paddingHorizontal="16dp"
123 android:background="?attr/selectableItemBackground"
124 android:orientation="vertical">
125
126 <com.google.android.material.textview.MaterialTextView
127 style="@style/TextAppearance.Material3.TitleMedium"
128 android:layout_width="match_parent"
129 android:layout_height="wrap_content"
130 android:layout_marginHorizontal="24dp"
131 android:textAlignment="viewStart"
132 android:text="@string/licenses" />
133
134 <com.google.android.material.textview.MaterialTextView
135 style="@style/TextAppearance.Material3.BodyMedium"
136 android:layout_width="match_parent"
137 android:layout_height="wrap_content"
138 android:layout_marginHorizontal="24dp"
139 android:layout_marginTop="6dp"
140 android:textAlignment="viewStart"
141 android:text="@string/licenses_description" />
142
143 </LinearLayout>
144
145 <com.google.android.material.divider.MaterialDivider
146 android:layout_width="match_parent"
147 android:layout_height="wrap_content"
148 android:layout_marginHorizontal="20dp" />
149
150 <LinearLayout
151 android:id="@+id/button_build_hash"
152 android:layout_width="match_parent"
153 android:layout_height="wrap_content"
154 android:paddingVertical="16dp"
155 android:paddingHorizontal="16dp"
156 android:background="?attr/selectableItemBackground"
157 android:orientation="vertical">
158
159 <com.google.android.material.textview.MaterialTextView
160 style="@style/TextAppearance.Material3.TitleMedium"
161 android:layout_width="match_parent"
162 android:layout_height="wrap_content"
163 android:layout_marginHorizontal="24dp"
164 android:textAlignment="viewStart"
165 android:text="@string/build" />
166
167 <com.google.android.material.textview.MaterialTextView
168 android:id="@+id/text_build_hash"
169 style="@style/TextAppearance.Material3.BodyMedium"
170 android:layout_width="match_parent"
171 android:layout_height="wrap_content"
172 android:layout_marginHorizontal="24dp"
173 android:layout_marginTop="6dp"
174 android:textAlignment="viewStart"
175 tools:text="abc123" />
176
177 </LinearLayout>
178
179 <com.google.android.material.divider.MaterialDivider
180 android:layout_width="match_parent"
181 android:layout_height="wrap_content"
182 android:layout_marginHorizontal="20dp" />
183
184 <LinearLayout
185 android:layout_width="match_parent"
186 android:layout_height="wrap_content"
187 android:orientation="horizontal"
188 android:gravity="center_horizontal"
189 android:layout_marginTop="12dp"
190 android:layout_marginBottom="16dp"
191 android:layout_marginHorizontal="40dp">
192
193 <Button
194 style="?attr/materialIconButtonStyle"
195 android:id="@+id/button_discord"
196 android:layout_width="0dp"
197 android:layout_height="wrap_content"
198 android:layout_weight="1"
199 app:icon="@drawable/ic_discord"
200 app:iconTint="?attr/colorOnSurface"
201 app:iconSize="24dp"
202 app:iconGravity="textEnd" />
203
204 <Button
205 style="?attr/materialIconButtonStyle"
206 android:id="@+id/button_website"
207 android:layout_width="0dp"
208 android:layout_height="wrap_content"
209 android:layout_weight="1"
210 app:icon="@drawable/ic_website"
211 app:iconTint="?attr/colorOnSurface"
212 app:iconSize="24dp"
213 app:iconGravity="textEnd" />
214
215 <Button
216 android:id="@+id/button_github"
217 style="?attr/materialIconButtonStyle"
218 android:layout_width="0dp"
219 android:layout_height="wrap_content"
220 android:layout_weight="1"
221 app:icon="@drawable/ic_github"
222 app:iconTint="?attr/colorOnSurface"
223 app:iconSize="24dp"
224 app:iconGravity="textEnd" />
225
226 </LinearLayout>
227
228 </LinearLayout>
229
230 </androidx.core.widget.NestedScrollView>
231
232</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_early_access.xml b/src/android/app/src/main/res/layout/fragment_early_access.xml
new file mode 100644
index 000000000..644b4dd45
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_early_access.xml
@@ -0,0 +1,242 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/coordinator_about"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <com.google.android.material.appbar.AppBarLayout
11 android:id="@+id/appbar_ea"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:fitsSystemWindows="true">
15
16 <com.google.android.material.appbar.MaterialToolbar
17 android:id="@+id/toolbar_about"
18 android:layout_width="match_parent"
19 android:layout_height="?attr/actionBarSize"
20 app:navigationIcon="@drawable/ic_back"
21 app:title="@string/early_access" />
22
23 </com.google.android.material.appbar.AppBarLayout>
24
25 <androidx.core.widget.NestedScrollView
26 android:id="@+id/scroll_ea"
27 android:layout_width="match_parent"
28 android:layout_height="match_parent"
29 android:clipToPadding="false"
30 android:paddingBottom="20dp"
31 android:scrollbars="vertical"
32 android:fadeScrollbars="false"
33 app:layout_behavior="@string/appbar_scrolling_view_behavior">
34
35 <LinearLayout
36 android:id="@+id/card_ea"
37 android:layout_width="match_parent"
38 android:layout_height="match_parent"
39 android:layout_marginVertical="32dp"
40 android:layout_marginHorizontal="20dp"
41 android:background="@drawable/premium_background"
42 android:orientation="vertical">
43
44 <com.google.android.material.textview.MaterialTextView
45 style="@style/TextAppearance.Material3.TitleLarge"
46 android:layout_width="match_parent"
47 android:layout_height="wrap_content"
48 android:layout_marginTop="16dp"
49 android:layout_marginHorizontal="20dp"
50 android:text="@string/early_access_benefits"
51 android:textAlignment="center"
52 android:textStyle="bold" />
53
54 <LinearLayout
55 android:layout_width="match_parent"
56 android:layout_height="wrap_content"
57 android:layout_marginTop="32dp"
58 android:layout_marginHorizontal="20dp"
59 android:orientation="horizontal">
60
61 <ImageView
62 android:layout_width="24dp"
63 android:layout_height="24dp"
64 android:layout_gravity="center_vertical"
65 android:src="@drawable/ic_check_circle"
66 app:tint="?attr/colorOnSurface" />
67
68 <com.google.android.material.textview.MaterialTextView
69 style="@style/TextAppearance.Material3.BodyLarge"
70 android:layout_width="match_parent"
71 android:layout_height="wrap_content"
72 android:layout_marginStart="20dp"
73 android:text="@string/cutting_edge_features"
74 android:textAlignment="viewStart"
75 android:layout_gravity="start|center_vertical" />
76
77 </LinearLayout>
78
79 <LinearLayout
80 android:layout_width="match_parent"
81 android:layout_height="wrap_content"
82 android:layout_marginTop="32dp"
83 android:layout_marginHorizontal="20dp"
84 android:orientation="horizontal">
85
86 <ImageView
87 android:layout_width="24dp"
88 android:layout_height="24dp"
89 android:layout_gravity="center_vertical"
90 android:src="@drawable/ic_check_circle"
91 app:tint="?attr/colorOnSurface" />
92
93 <com.google.android.material.textview.MaterialTextView
94 style="@style/TextAppearance.Material3.BodyLarge"
95 android:layout_width="match_parent"
96 android:layout_height="wrap_content"
97 android:layout_marginStart="20dp"
98 android:text="@string/early_access_updates"
99 android:textAlignment="viewStart"
100 android:layout_gravity="start|center_vertical" />
101
102 </LinearLayout>
103
104 <LinearLayout
105 android:layout_width="match_parent"
106 android:layout_height="wrap_content"
107 android:layout_marginTop="32dp"
108 android:layout_marginHorizontal="20dp"
109 android:orientation="horizontal">
110
111 <ImageView
112 android:layout_width="24dp"
113 android:layout_height="24dp"
114 android:layout_gravity="center_vertical"
115 android:src="@drawable/ic_check_circle"
116 app:tint="?attr/colorOnSurface" />
117
118 <com.google.android.material.textview.MaterialTextView
119 style="@style/TextAppearance.Material3.BodyLarge"
120 android:layout_width="match_parent"
121 android:layout_height="wrap_content"
122 android:layout_marginStart="20dp"
123 android:text="@string/no_manual_installation"
124 android:textAlignment="viewStart"
125 android:layout_gravity="start|center_vertical" />
126
127 </LinearLayout>
128
129 <LinearLayout
130 android:layout_width="match_parent"
131 android:layout_height="wrap_content"
132 android:layout_marginTop="32dp"
133 android:layout_marginHorizontal="20dp"
134 android:orientation="horizontal">
135
136 <ImageView
137 android:layout_width="24dp"
138 android:layout_height="24dp"
139 android:layout_gravity="center_vertical"
140 android:src="@drawable/ic_check_circle"
141 app:tint="?attr/colorOnSurface" />
142
143 <com.google.android.material.textview.MaterialTextView
144 style="@style/TextAppearance.Material3.BodyLarge"
145 android:layout_width="match_parent"
146 android:layout_height="wrap_content"
147 android:layout_marginStart="20dp"
148 android:text="@string/prioritized_support"
149 android:textAlignment="viewStart"
150 android:layout_gravity="start|center_vertical" />
151
152 </LinearLayout>
153
154 <LinearLayout
155 android:layout_width="match_parent"
156 android:layout_height="wrap_content"
157 android:layout_marginTop="32dp"
158 android:layout_marginHorizontal="20dp"
159 android:orientation="horizontal">
160
161 <ImageView
162 android:layout_width="24dp"
163 android:layout_height="24dp"
164 android:layout_gravity="center_vertical"
165 android:src="@drawable/ic_check_circle"
166 app:tint="?attr/colorOnSurface" />
167
168 <com.google.android.material.textview.MaterialTextView
169 style="@style/TextAppearance.Material3.BodyLarge"
170 android:layout_width="match_parent"
171 android:layout_height="wrap_content"
172 android:layout_marginStart="20dp"
173 android:text="@string/helping_game_preservation"
174 android:textAlignment="viewStart"
175 android:layout_gravity="start|center_vertical" />
176
177 </LinearLayout>
178
179 <LinearLayout
180 android:layout_width="match_parent"
181 android:layout_height="wrap_content"
182 android:layout_marginTop="32dp"
183 android:layout_marginHorizontal="20dp"
184 android:orientation="horizontal">
185
186 <ImageView
187 android:layout_width="24dp"
188 android:layout_height="24dp"
189 android:layout_gravity="center_vertical"
190 android:src="@drawable/ic_check_circle"
191 app:tint="?attr/colorOnSurface" />
192
193 <com.google.android.material.textview.MaterialTextView
194 style="@style/TextAppearance.Material3.BodyLarge"
195 android:layout_width="match_parent"
196 android:layout_height="wrap_content"
197 android:layout_marginStart="20dp"
198 android:text="@string/our_eternal_gratitude"
199 android:textAlignment="viewStart"
200 android:layout_gravity="start|center_vertical" />
201
202 </LinearLayout>
203
204 <com.google.android.material.textview.MaterialTextView
205 style="@style/TextAppearance.Material3.TitleLarge"
206 android:layout_width="match_parent"
207 android:layout_height="wrap_content"
208 android:text="@string/are_you_interested"
209 android:layout_marginTop="80dp"
210 android:layout_marginHorizontal="20dp"
211 android:textStyle="bold"
212 android:textAlignment="center" />
213
214 <com.google.android.material.card.MaterialCardView
215 style="?attr/materialCardViewFilledStyle"
216 android:id="@+id/get_early_access_button"
217 android:layout_width="match_parent"
218 android:layout_height="wrap_content"
219 android:layout_marginTop="16dp"
220 android:layout_marginHorizontal="20dp"
221 android:layout_marginBottom="28dp"
222 android:background="?attr/selectableItemBackground"
223 android:backgroundTint="@android:color/black">
224
225 <com.google.android.material.textview.MaterialTextView
226 style="@style/TextAppearance.Material3.TitleLarge"
227 android:layout_width="match_parent"
228 android:layout_height="wrap_content"
229 android:text="@string/get_early_access"
230 android:layout_marginHorizontal="20dp"
231 android:layout_marginVertical="8dp"
232 android:textColor="@android:color/white"
233 android:textStyle="bold"
234 android:textAlignment="center" />
235
236 </com.google.android.material.card.MaterialCardView>
237
238 </LinearLayout>
239
240 </androidx.core.widget.NestedScrollView>
241
242</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
new file mode 100644
index 000000000..09b789b6b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -0,0 +1,70 @@
1<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:app="http://schemas.android.com/apk/res-auto"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:id="@+id/drawer_layout"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:keepScreenOn="true"
8 tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment"
9 tools:openDrawer="start">
10
11 <androidx.coordinatorlayout.widget.CoordinatorLayout
12 android:layout_width="match_parent"
13 android:layout_height="match_parent">
14
15 <!-- This is what everything is rendered to during emulation -->
16 <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
17 android:id="@+id/surface_emulation"
18 android:layout_width="match_parent"
19 android:layout_height="match_parent"
20 android:layout_gravity="center"
21 android:focusable="false"
22 android:focusableInTouchMode="false" />
23
24 <FrameLayout
25 android:id="@+id/overlay_container"
26 android:layout_width="match_parent"
27 android:layout_height="match_parent"
28 android:layout_gravity="bottom">
29
30 <!-- This is the onscreen input overlay -->
31 <org.yuzu.yuzu_emu.overlay.InputOverlay
32 android:id="@+id/surface_input_overlay"
33 android:layout_width="match_parent"
34 android:layout_height="match_parent"
35 android:focusable="true"
36 android:focusableInTouchMode="true" />
37
38 <TextView
39 android:id="@+id/show_fps_text"
40 android:layout_width="wrap_content"
41 android:layout_height="wrap_content"
42 android:layout_gravity="left"
43 android:clickable="false"
44 android:focusable="false"
45 android:shadowColor="@android:color/black"
46 android:textColor="@android:color/white"
47 android:textSize="12sp"
48 tools:ignore="RtlHardcoded" />
49
50 <Button
51 style="@style/Widget.Material3.Button.ElevatedButton"
52 android:id="@+id/done_control_config"
53 android:layout_width="wrap_content"
54 android:layout_height="wrap_content"
55 android:layout_gravity="center"
56 android:text="@string/emulation_done"
57 android:visibility="gone" />
58 </FrameLayout>
59
60 </androidx.coordinatorlayout.widget.CoordinatorLayout>
61
62 <com.google.android.material.navigation.NavigationView
63 android:id="@+id/in_game_menu"
64 android:layout_width="wrap_content"
65 android:layout_height="match_parent"
66 android:layout_gravity="start|bottom"
67 app:headerLayout="@layout/header_in_game"
68 app:menu="@menu/menu_in_game" />
69
70</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml
new file mode 100644
index 000000000..a0568668a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_games.xml
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/swipe_refresh"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface"
9 android:clipToPadding="false">
10
11 <RelativeLayout
12 android:layout_width="match_parent"
13 android:layout_height="match_parent">
14
15 <com.google.android.material.textview.MaterialTextView
16 android:id="@+id/notice_text"
17 style="@style/TextAppearance.Material3.BodyLarge"
18 android:layout_width="match_parent"
19 android:layout_height="match_parent"
20 android:gravity="center"
21 android:padding="@dimen/spacing_large"
22 android:text="@string/empty_gamelist"
23 android:visibility="gone" />
24
25 <androidx.recyclerview.widget.RecyclerView
26 android:id="@+id/grid_games"
27 android:layout_width="match_parent"
28 android:layout_height="match_parent"
29 android:clipToPadding="false"
30 tools:listitem="@layout/card_game" />
31
32 </RelativeLayout>
33
34</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_home_settings.xml b/src/android/app/src/main/res/layout/fragment_home_settings.xml
new file mode 100644
index 000000000..1cb421dcb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_home_settings.xml
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.core.widget.NestedScrollView
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:id="@+id/scroll_view_settings"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface"
8 android:scrollbars="vertical"
9 android:fadeScrollbars="false"
10 android:clipToPadding="false">
11
12 <androidx.appcompat.widget.LinearLayoutCompat
13 android:id="@+id/linear_layout_settings"
14 android:layout_width="match_parent"
15 android:layout_height="match_parent"
16 android:orientation="vertical"
17 android:background="?attr/colorSurface">
18
19 <ImageView
20 android:id="@+id/logo_image"
21 android:layout_width="128dp"
22 android:layout_height="128dp"
23 android:layout_margin="64dp"
24 android:layout_gravity="center_horizontal"
25 android:src="@drawable/ic_yuzu_full" />
26
27 <androidx.recyclerview.widget.RecyclerView
28 android:id="@+id/home_settings_list"
29 android:layout_width="match_parent"
30 android:layout_height="match_parent" />
31
32 </androidx.appcompat.widget.LinearLayoutCompat>
33
34</androidx.core.widget.NestedScrollView>
diff --git a/src/android/app/src/main/res/layout/fragment_licenses.xml b/src/android/app/src/main/res/layout/fragment_licenses.xml
new file mode 100644
index 000000000..6b31ff5b4
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_licenses.xml
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_licenses"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface">
8
9 <com.google.android.material.appbar.AppBarLayout
10 android:id="@+id/appbar_licenses"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:fitsSystemWindows="true">
14
15 <com.google.android.material.appbar.MaterialToolbar
16 android:id="@+id/toolbar_licenses"
17 android:layout_width="match_parent"
18 android:layout_height="?attr/actionBarSize"
19 app:title="@string/licenses"
20 app:navigationIcon="@drawable/ic_back" />
21
22 </com.google.android.material.appbar.AppBarLayout>
23
24 <androidx.recyclerview.widget.RecyclerView
25 android:id="@+id/list_licenses"
26 android:layout_width="match_parent"
27 android:layout_height="match_parent"
28 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
29
30</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_search.xml b/src/android/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 000000000..b8d54d947
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,183 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/constraint_search"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent"
9 android:background="?attr/colorSurface"
10 android:clipToPadding="false">
11
12 <RelativeLayout
13 android:layout_width="0dp"
14 android:layout_height="0dp"
15 app:layout_constraintBottom_toBottomOf="parent"
16 app:layout_constraintEnd_toEndOf="parent"
17 app:layout_constraintStart_toStartOf="parent"
18 app:layout_constraintTop_toBottomOf="@+id/divider">
19
20 <LinearLayout
21 android:id="@+id/no_results_view"
22 android:layout_width="match_parent"
23 android:layout_height="match_parent"
24 android:orientation="vertical"
25 android:gravity="center">
26
27 <ImageView
28 android:id="@+id/icon_no_results"
29 android:layout_width="match_parent"
30 android:layout_height="80dp"
31 android:src="@drawable/ic_search" />
32
33 <com.google.android.material.textview.MaterialTextView
34 android:id="@+id/notice_text"
35 style="@style/TextAppearance.Material3.TitleLarge"
36 android:layout_width="match_parent"
37 android:layout_height="wrap_content"
38 android:gravity="center"
39 android:paddingTop="8dp"
40 android:text="@string/search_and_filter_games"
41 tools:visibility="visible" />
42
43 </LinearLayout>
44
45 <androidx.recyclerview.widget.RecyclerView
46 android:id="@+id/grid_games_search"
47 android:layout_width="match_parent"
48 android:layout_height="match_parent"
49 android:clipToPadding="false" />
50
51 </RelativeLayout>
52
53 <FrameLayout
54 android:id="@+id/frame_search"
55 android:layout_width="match_parent"
56 android:layout_height="wrap_content"
57 android:layout_marginTop="12dp"
58 android:layout_marginHorizontal="20dp"
59 app:layout_constraintEnd_toEndOf="parent"
60 app:layout_constraintStart_toStartOf="parent"
61 app:layout_constraintTop_toTopOf="parent">
62
63 <com.google.android.material.card.MaterialCardView
64 android:id="@+id/search_background"
65 style="?attr/materialCardViewFilledStyle"
66 android:layout_width="match_parent"
67 android:layout_height="56dp"
68 app:cardCornerRadius="28dp">
69
70 <LinearLayout
71 android:id="@+id/search_container"
72 android:layout_width="match_parent"
73 android:layout_height="match_parent"
74 android:layout_marginStart="24dp"
75 android:layout_marginEnd="56dp"
76 android:orientation="horizontal">
77
78 <ImageView
79 android:layout_width="28dp"
80 android:layout_height="28dp"
81 android:layout_gravity="center_vertical"
82 android:layout_marginEnd="24dp"
83 android:src="@drawable/ic_search"
84 app:tint="?attr/colorOnSurfaceVariant" />
85
86 <EditText
87 android:id="@+id/search_text"
88 android:layout_width="match_parent"
89 android:layout_height="match_parent"
90 android:background="@android:color/transparent"
91 android:hint="@string/home_search_games"
92 android:inputType="text"
93 android:maxLines="1"
94 android:imeOptions="flagNoFullscreen" />
95
96 </LinearLayout>
97
98 <ImageView
99 android:id="@+id/clear_button"
100 android:layout_width="24dp"
101 android:layout_height="24dp"
102 android:layout_gravity="center_vertical|end"
103 android:layout_marginEnd="24dp"
104 android:background="?attr/selectableItemBackground"
105 android:src="@drawable/ic_clear"
106 android:visibility="invisible"
107 app:tint="?attr/colorOnSurfaceVariant"
108 tools:visibility="visible" />
109
110 </com.google.android.material.card.MaterialCardView>
111
112 </FrameLayout>
113
114 <HorizontalScrollView
115 android:id="@+id/horizontalScrollView"
116 android:layout_width="match_parent"
117 android:layout_height="wrap_content"
118 android:fadingEdge="horizontal"
119 android:scrollbars="none"
120 app:layout_constraintEnd_toEndOf="parent"
121 app:layout_constraintStart_toStartOf="parent"
122 app:layout_constraintTop_toBottomOf="@+id/frame_search">
123
124 <com.google.android.material.chip.ChipGroup
125 android:id="@+id/chip_group"
126 android:layout_width="wrap_content"
127 android:layout_height="wrap_content"
128 android:clipToPadding="false"
129 android:paddingVertical="4dp"
130 app:chipSpacingHorizontal="12dp"
131 app:singleLine="true"
132 app:singleSelection="true">
133
134 <com.google.android.material.chip.Chip
135 android:id="@+id/chip_recently_played"
136 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
137 android:layout_width="wrap_content"
138 android:layout_height="wrap_content"
139 android:checked="false"
140 android:text="@string/search_recently_played"
141 app:chipCornerRadius="28dp" />
142
143 <com.google.android.material.chip.Chip
144 android:id="@+id/chip_recently_added"
145 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
146 android:layout_width="wrap_content"
147 android:layout_height="wrap_content"
148 android:checked="false"
149 android:text="@string/search_recently_added"
150 app:chipCornerRadius="28dp" />
151
152 <com.google.android.material.chip.Chip
153 android:id="@+id/chip_retail"
154 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
155 android:layout_width="wrap_content"
156 android:layout_height="wrap_content"
157 android:checked="false"
158 android:text="@string/search_retail"
159 app:chipCornerRadius="28dp" />
160
161 <com.google.android.material.chip.Chip
162 android:id="@+id/chip_homebrew"
163 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
164 android:layout_width="wrap_content"
165 android:layout_height="wrap_content"
166 android:checked="false"
167 android:text="@string/search_homebrew"
168 app:chipCornerRadius="28dp" />
169
170 </com.google.android.material.chip.ChipGroup>
171
172 </HorizontalScrollView>
173
174 <com.google.android.material.divider.MaterialDivider
175 android:id="@+id/divider"
176 android:layout_width="match_parent"
177 android:layout_height="wrap_content"
178 android:layout_marginHorizontal="20dp"
179 app:layout_constraintEnd_toEndOf="parent"
180 app:layout_constraintStart_toStartOf="parent"
181 app:layout_constraintTop_toBottomOf="@+id/horizontalScrollView" />
182
183</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 000000000..167720347
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent">
6
7 <androidx.recyclerview.widget.RecyclerView
8 android:id="@+id/list_settings"
9 android:layout_width="match_parent"
10 android:layout_height="match_parent"
11 android:background="?attr/colorSurface"
12 android:clipToPadding="false" />
13
14</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml
new file mode 100644
index 000000000..d7bafaea2
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_setup.xml
@@ -0,0 +1,42 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/setup_root"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <androidx.viewpager2.widget.ViewPager2
10 android:id="@+id/viewPager2"
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 android:clipToPadding="false"
14 android:layout_marginBottom="16dp"
15 app:layout_constraintBottom_toTopOf="@+id/button_next"
16 app:layout_constraintEnd_toEndOf="parent"
17 app:layout_constraintStart_toStartOf="parent"
18 app:layout_constraintTop_toTopOf="parent" />
19
20 <com.google.android.material.button.MaterialButton
21 style="@style/Widget.Material3.Button.TextButton"
22 android:id="@+id/button_next"
23 android:layout_width="wrap_content"
24 android:layout_height="wrap_content"
25 android:layout_margin="12dp"
26 android:text="@string/next"
27 android:visibility="invisible"
28 app:layout_constraintBottom_toBottomOf="parent"
29 app:layout_constraintEnd_toEndOf="parent" />
30
31 <com.google.android.material.button.MaterialButton
32 style="@style/Widget.Material3.Button.TextButton"
33 android:id="@+id/button_back"
34 android:layout_width="wrap_content"
35 android:layout_height="wrap_content"
36 android:layout_margin="12dp"
37 android:text="@string/back"
38 android:visibility="invisible"
39 app:layout_constraintBottom_toBottomOf="parent"
40 app:layout_constraintStart_toStartOf="parent" />
41
42</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/header_in_game.xml b/src/android/app/src/main/res/layout/header_in_game.xml
new file mode 100644
index 000000000..958cfb7e3
--- /dev/null
+++ b/src/android/app/src/main/res/layout/header_in_game.xml
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.textview.MaterialTextView
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/text_game_title"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:layout_marginTop="24dp"
9 android:layout_marginStart="24dp"
10 android:layout_marginEnd="24dp"
11 android:textAppearance="?attr/textAppearanceHeadlineMedium"
12 android:textColor="?attr/colorOnSurface"
13 android:textAlignment="viewStart"
14 tools:text="Super Mario Odyssey" />
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
new file mode 100644
index 000000000..ec896342b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -0,0 +1,41 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 xmlns:app="http://schemas.android.com/apk/res-auto"
7 android:background="?android:attr/selectableItemBackground"
8 android:clickable="true"
9 android:focusable="true"
10 android:gravity="center_vertical"
11 android:minHeight="72dp"
12 android:padding="@dimen/spacing_large">
13
14 <com.google.android.material.textview.MaterialTextView
15 style="@style/TextAppearance.Material3.HeadlineMedium"
16 android:id="@+id/text_setting_name"
17 android:layout_width="0dp"
18 android:layout_height="wrap_content"
19 android:layout_alignParentEnd="true"
20 android:layout_alignParentStart="true"
21 android:layout_alignParentTop="true"
22 android:textSize="16sp"
23 android:textAlignment="viewStart"
24 app:lineHeight="28dp"
25 tools:text="Setting Name" />
26
27 <TextView
28 style="@style/TextAppearance.Material3.BodySmall"
29 android:id="@+id/text_setting_description"
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 android:layout_alignParentEnd="true"
33 android:layout_alignParentStart="true"
34 android:layout_alignStart="@+id/text_setting_name"
35 android:layout_below="@+id/text_setting_name"
36 android:layout_marginTop="@dimen/spacing_small"
37 android:visibility="visible"
38 android:textAlignment="viewStart"
39 tools:text="@string/app_disclaimer" />
40
41</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
new file mode 100644
index 000000000..599d845ad
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
@@ -0,0 +1,50 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 xmlns:app="http://schemas.android.com/apk/res-auto"
7 android:background="?android:attr/selectableItemBackground"
8 android:clickable="true"
9 android:focusable="true"
10 android:minHeight="72dp"
11 android:paddingStart="@dimen/spacing_large"
12 android:paddingEnd="24dp"
13 android:paddingVertical="@dimen/spacing_large">
14
15 <com.google.android.material.materialswitch.MaterialSwitch
16 android:id="@+id/switch_widget"
17 android:layout_width="wrap_content"
18 android:layout_height="wrap_content"
19 android:layout_alignParentEnd="true"
20 android:layout_centerVertical="true" />
21
22 <com.google.android.material.textview.MaterialTextView
23 style="@style/TextAppearance.Material3.BodySmall"
24 android:id="@+id/text_setting_description"
25 android:layout_width="wrap_content"
26 android:layout_height="wrap_content"
27 android:layout_alignParentStart="true"
28 android:layout_alignStart="@+id/text_setting_name"
29 android:layout_below="@+id/text_setting_name"
30 android:layout_marginEnd="@dimen/spacing_large"
31 android:layout_marginTop="@dimen/spacing_small"
32 android:layout_toStartOf="@+id/switch_widget"
33 android:textAlignment="viewStart"
34 tools:text="@string/frame_limit_enable_description" />
35
36 <com.google.android.material.textview.MaterialTextView
37 style="@style/TextAppearance.Material3.HeadlineMedium"
38 android:id="@+id/text_setting_name"
39 android:layout_width="0dp"
40 android:layout_height="wrap_content"
41 android:layout_alignParentStart="true"
42 android:layout_alignParentTop="true"
43 android:layout_marginEnd="@dimen/spacing_large"
44 android:layout_toStartOf="@+id/switch_widget"
45 android:textSize="16sp"
46 android:textAlignment="viewStart"
47 app:lineHeight="28dp"
48 tools:text="@string/frame_limit_enable" />
49
50</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml
new file mode 100644
index 000000000..abd24df6f
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml
@@ -0,0 +1,20 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="48dp"
6 android:paddingVertical="4dp"
7 android:paddingHorizontal="@dimen/spacing_large">
8
9 <com.google.android.material.textview.MaterialTextView
10 style="@style/TextAppearance.Material3.TitleSmall"
11 android:id="@+id/text_header_name"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:layout_gravity="start|center_vertical"
15 android:textColor="?attr/colorPrimary"
16 android:textAlignment="viewStart"
17 android:textStyle="bold"
18 tools:text="CPU Settings" />
19
20</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml
new file mode 100644
index 000000000..1436ef308
--- /dev/null
+++ b/src/android/app/src/main/res/layout/page_setup.xml
@@ -0,0 +1,72 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <ImageView
10 android:id="@+id/icon"
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 android:layout_marginTop="64dp"
14 android:layout_marginBottom="32dp"
15 app:layout_constraintBottom_toTopOf="@+id/text_title"
16 app:layout_constraintEnd_toEndOf="parent"
17 app:layout_constraintHeight_max="220dp"
18 app:layout_constraintHeight_min="110dp"
19 app:layout_constraintStart_toStartOf="parent"
20 app:layout_constraintTop_toTopOf="parent"
21 app:layout_constraintVertical_chainStyle="spread"
22 app:layout_constraintWidth_max="220dp"
23 app:layout_constraintWidth_min="110dp"
24 app:layout_constraintVertical_weight="3" />
25
26 <com.google.android.material.textview.MaterialTextView
27 android:id="@+id/text_title"
28 style="@style/TextAppearance.Material3.DisplayMedium"
29 android:layout_width="0dp"
30 android:layout_height="0dp"
31 android:textAlignment="center"
32 android:textColor="?attr/colorOnSurface"
33 android:textStyle="bold"
34 app:layout_constraintBottom_toTopOf="@+id/text_description"
35 app:layout_constraintEnd_toEndOf="parent"
36 app:layout_constraintStart_toStartOf="parent"
37 app:layout_constraintTop_toBottomOf="@+id/icon"
38 app:layout_constraintVertical_weight="1.3"
39 tools:text="@string/welcome" />
40
41 <com.google.android.material.textview.MaterialTextView
42 android:id="@+id/text_description"
43 style="@style/TextAppearance.Material3.TitleLarge"
44 android:layout_width="0dp"
45 android:layout_height="0dp"
46 android:textAlignment="center"
47 android:textSize="26sp"
48 android:paddingHorizontal="16dp"
49 app:layout_constraintBottom_toTopOf="@+id/button_action"
50 app:layout_constraintEnd_toEndOf="parent"
51 app:layout_constraintStart_toStartOf="parent"
52 app:layout_constraintTop_toBottomOf="@+id/text_title"
53 app:layout_constraintVertical_weight="2"
54 app:lineHeight="40sp"
55 tools:text="@string/welcome_description" />
56
57 <com.google.android.material.button.MaterialButton
58 android:id="@+id/button_action"
59 android:layout_width="wrap_content"
60 android:layout_height="56dp"
61 android:textSize="20sp"
62 android:layout_marginTop="16dp"
63 android:layout_marginBottom="48dp"
64 app:iconGravity="end"
65 app:iconSize="24sp"
66 app:layout_constraintBottom_toBottomOf="parent"
67 app:layout_constraintEnd_toEndOf="parent"
68 app:layout_constraintStart_toStartOf="parent"
69 app:layout_constraintTop_toBottomOf="@+id/text_description"
70 tools:text="Get started" />
71
72</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/menu-w600dp/menu_navigation.xml b/src/android/app/src/main/res/menu-w600dp/menu_navigation.xml
new file mode 100644
index 000000000..dd7698e78
--- /dev/null
+++ b/src/android/app/src/main/res/menu-w600dp/menu_navigation.xml
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/homeSettingsFragment"
6 android:icon="@drawable/selector_settings"
7 android:title="@string/home_settings" />
8
9 <item
10 android:id="@+id/searchFragment"
11 android:icon="@drawable/ic_search"
12 android:title="@string/home_search" />
13
14 <item
15 android:id="@+id/gamesFragment"
16 android:icon="@drawable/selector_cartridge"
17 android:title="@string/home_games" />
18
19</menu>
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml
new file mode 100644
index 000000000..f98f727b6
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_in_game.xml
@@ -0,0 +1,24 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/menu_pause_emulation"
6 android:icon="@drawable/ic_pause"
7 android:title="@string/emulation_pause" />
8
9 <item
10 android:id="@+id/menu_settings"
11 android:icon="@drawable/ic_settings"
12 android:title="@string/preferences_settings" />
13
14 <item
15 android:id="@+id/menu_overlay_controls"
16 android:icon="@drawable/ic_controller"
17 android:title="@string/emulation_input_overlay" />
18
19 <item
20 android:id="@+id/menu_exit"
21 android:icon="@drawable/ic_exit"
22 android:title="@string/emulation_exit" />
23
24</menu>
diff --git a/src/android/app/src/main/res/menu/menu_navigation.xml b/src/android/app/src/main/res/menu/menu_navigation.xml
new file mode 100644
index 000000000..da128c5a1
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_navigation.xml
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/gamesFragment"
6 android:icon="@drawable/selector_cartridge"
7 android:title="@string/home_games" />
8
9 <item
10 android:id="@+id/searchFragment"
11 android:icon="@drawable/ic_search"
12 android:title="@string/home_search" />
13
14 <item
15 android:id="@+id/homeSettingsFragment"
16 android:icon="@drawable/selector_settings"
17 android:title="@string/home_settings" />
18
19</menu>
diff --git a/src/android/app/src/main/res/menu/menu_overlay_options.xml b/src/android/app/src/main/res/menu/menu_overlay_options.xml
new file mode 100644
index 000000000..4885b4f6f
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_overlay_options.xml
@@ -0,0 +1,45 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/menu_toggle_fps"
6 android:title="@string/emulation_fps_counter"
7 android:checkable="true" />
8
9 <item
10 android:id="@+id/menu_edit_overlay"
11 android:title="@string/emulation_touch_overlay_edit" />
12
13 <item
14 android:id="@+id/menu_adjust_overlay"
15 android:title="@string/emulation_control_adjust" />
16
17 <item
18 android:id="@+id/menu_toggle_controls"
19 android:title="@string/emulation_toggle_controls" />
20
21 <item
22 android:id="@+id/menu_show_overlay"
23 android:title="@string/emulation_show_overlay"
24 android:checkable="true" />
25
26 <item
27 android:id="@+id/menu_rel_stick_center"
28 android:title="@string/emulation_rel_stick_center"
29 android:checkable="true" />
30
31 <item
32 android:id="@+id/menu_dpad_slide"
33 android:title="@string/emulation_dpad_slide"
34 android:checkable="true" />
35
36 <item
37 android:id="@+id/menu_haptics"
38 android:title="@string/emulation_haptics"
39 android:checkable="true" />
40
41 <item
42 android:id="@+id/menu_reset_overlay"
43 android:title="@string/emulation_touch_overlay_reset" />
44
45</menu>
diff --git a/src/android/app/src/main/res/menu/menu_settings.xml b/src/android/app/src/main/res/menu/menu_settings.xml
new file mode 100644
index 000000000..1fe7aa6d4
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_settings.xml
@@ -0,0 +1,2 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu /> \ No newline at end of file
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
new file mode 100644
index 000000000..48072683e
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -0,0 +1,59 @@
1<?xml version="1.0" encoding="utf-8"?>
2<navigation xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/home_navigation"
5 app:startDestination="@id/gamesFragment">
6
7 <fragment
8 android:id="@+id/gamesFragment"
9 android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
10 android:label="PlatformGamesFragment" />
11
12 <fragment
13 android:id="@+id/homeSettingsFragment"
14 android:name="org.yuzu.yuzu_emu.fragments.HomeSettingsFragment"
15 android:label="HomeSettingsFragment" >
16 <action
17 android:id="@+id/action_homeSettingsFragment_to_aboutFragment"
18 app:destination="@id/aboutFragment" />
19 <action
20 android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment"
21 app:destination="@id/earlyAccessFragment" />
22 </fragment>
23
24 <fragment
25 android:id="@+id/firstTimeSetupFragment"
26 android:name="org.yuzu.yuzu_emu.fragments.SetupFragment"
27 android:label="FirstTimeSetupFragment" >
28 <action
29 android:id="@+id/action_firstTimeSetupFragment_to_gamesFragment"
30 app:destination="@id/gamesFragment"
31 app:popUpTo="@id/firstTimeSetupFragment"
32 app:popUpToInclusive="true" />
33 </fragment>
34
35 <fragment
36 android:id="@+id/searchFragment"
37 android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
38 android:label="SearchFragment" />
39
40 <fragment
41 android:id="@+id/aboutFragment"
42 android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"
43 android:label="AboutFragment" >
44 <action
45 android:id="@+id/action_aboutFragment_to_licensesFragment"
46 app:destination="@id/licensesFragment" />
47 </fragment>
48
49 <fragment
50 android:id="@+id/earlyAccessFragment"
51 android:name="org.yuzu.yuzu_emu.fragments.EarlyAccessFragment"
52 android:label="EarlyAccessFragment" />
53
54 <fragment
55 android:id="@+id/licensesFragment"
56 android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment"
57 android:label="LicensesFragment" />
58
59</navigation>
diff --git a/src/android/app/src/main/res/values-night-v31/themes.xml b/src/android/app/src/main/res/values-night-v31/themes.xml
new file mode 100644
index 000000000..631d7fd1b
--- /dev/null
+++ b/src/android/app/src/main/res/values-night-v31/themes.xml
@@ -0,0 +1,31 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="Theme.Yuzu.Main.MaterialYou" parent="Theme.Yuzu.Main">
5 <item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
6 <item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item>
7 <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
8 <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item>
9 <item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item>
10 <item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item>
11 <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item>
12 <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item>
13 <item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item>
14 <item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item>
15 <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item>
16 <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item>
17 <item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_background</item>
18 <item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
19 <item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
20 <item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item>
21 <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
22 <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
23 <item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
24 <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
25 <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_surface_variant</item>
26 <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
27
28 <item name="materialAlertDialogTheme">@style/ThemeOverlay.Material3.MaterialAlertDialog</item>
29 </style>
30
31</resources>
diff --git a/src/android/app/src/main/res/values-night/themes.xml b/src/android/app/src/main/res/values-night/themes.xml
new file mode 100644
index 000000000..d7d24c24d
--- /dev/null
+++ b/src/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,9 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="ThemeOverlay.Yuzu.Dark" parent="">
5 <item name="colorSurface">@android:color/black</item>
6 <item name="android:colorBackground">@android:color/black</item>
7 </style>
8
9</resources>
diff --git a/src/android/app/src/main/res/values-night/yuzu_colors.xml b/src/android/app/src/main/res/values-night/yuzu_colors.xml
new file mode 100644
index 000000000..49d823324
--- /dev/null
+++ b/src/android/app/src/main/res/values-night/yuzu_colors.xml
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <color name="yuzu_primary">#A7DDEC</color>
5 <color name="yuzu_onPrimary">#003399</color>
6 <color name="yuzu_primaryContainer">#31323F</color>
7 <color name="yuzu_onPrimaryContainer">#D1E4FF</color>
8 <color name="yuzu_secondary">#BAC8DB</color>
9 <color name="yuzu_onSecondary">#253140</color>
10 <color name="yuzu_secondaryContainer">#3B4858</color>
11 <color name="yuzu_onSecondaryContainer">#D6E4F7</color>
12 <color name="yuzu_tertiary">#D6BEE5</color>
13 <color name="yuzu_onTertiary">#3A2948</color>
14 <color name="yuzu_tertiaryContainer">#524060</color>
15 <color name="yuzu_onTertiaryContainer">#F2DAFF</color>
16 <color name="yuzu_error">#FFB4AB</color>
17 <color name="yuzu_errorContainer">#93000A</color>
18 <color name="yuzu_onError">#690005</color>
19 <color name="yuzu_onErrorContainer">#FFDAD6</color>
20 <color name="yuzu_background">#1A1C1E</color>
21 <color name="yuzu_onBackground">#E2E2E6</color>
22 <color name="yuzu_surface">#1B1B1D</color>
23 <color name="yuzu_onSurface">#E2E2E6</color>
24 <color name="yuzu_surfaceVariant">#26282C</color>
25 <color name="yuzu_onSurfaceVariant">#C3C7CF</color>
26 <color name="yuzu_outline">#8C9199</color>
27 <color name="yuzu_inverseOnSurface">#1A1C1E</color>
28 <color name="yuzu_inverseSurface">#E2E2E6</color>
29 <color name="yuzu_inversePrimary">#0062A2</color>
30 <color name="yuzu_shadow">#000000</color>
31 <color name="yuzu_surfaceTint">#9DCAFF</color>
32 <color name="yuzu_outlineVariant">#42474E</color>
33
34 <color name="yuzu_ea_background_start">#840099</color>
35 <color name="yuzu_ea_background_end">#005AE1</color>
36
37</resources>
diff --git a/src/android/app/src/main/res/values-v31/themes.xml b/src/android/app/src/main/res/values-v31/themes.xml
new file mode 100644
index 000000000..5d3a86bf6
--- /dev/null
+++ b/src/android/app/src/main/res/values-v31/themes.xml
@@ -0,0 +1,31 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="Theme.Yuzu.Main.MaterialYou" parent="Theme.Yuzu.Main">
5 <item name="colorPrimary">@color/m3_sys_color_dynamic_light_primary</item>
6 <item name="colorOnPrimary">@color/m3_sys_color_dynamic_light_on_primary</item>
7 <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container</item>
8 <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_light_on_primary_container</item>
9 <item name="colorSecondary">@color/m3_sys_color_dynamic_light_secondary</item>
10 <item name="colorOnSecondary">@color/m3_sys_color_dynamic_light_on_secondary</item>
11 <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container</item>
12 <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_light_on_secondary_container</item>
13 <item name="colorTertiary">@color/m3_sys_color_dynamic_light_tertiary</item>
14 <item name="colorOnTertiary">@color/m3_sys_color_dynamic_light_on_tertiary</item>
15 <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container</item>
16 <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_light_on_tertiary_container</item>
17 <item name="android:colorBackground">@color/m3_sys_color_dynamic_light_background</item>
18 <item name="colorOnBackground">@color/m3_sys_color_dynamic_light_on_background</item>
19 <item name="colorSurface">@color/m3_sys_color_dynamic_light_surface</item>
20 <item name="colorOnSurface">@color/m3_sys_color_dynamic_light_on_surface</item>
21 <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_light_surface_variant</item>
22 <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
23 <item name="colorOutline">@color/m3_sys_color_dynamic_light_outline</item>
24 <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
25 <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_light_surface_variant</item>
26 <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_light_inverse_primary</item>
27
28 <item name="materialAlertDialogTheme">@style/ThemeOverlay.Material3.MaterialAlertDialog</item>
29 </style>
30
31</resources>
diff --git a/src/android/app/src/main/res/values-w600dp/bools.xml b/src/android/app/src/main/res/values-w600dp/bools.xml
new file mode 100644
index 000000000..b6833a702
--- /dev/null
+++ b/src/android/app/src/main/res/values-w600dp/bools.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <bool name="small_layout">false</bool>
4</resources>
diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml
new file mode 100644
index 000000000..128319e27
--- /dev/null
+++ b/src/android/app/src/main/res/values-w600dp/dimens.xml
@@ -0,0 +1,5 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <dimen name="spacing_navigation">0dp</dimen>
4 <dimen name="spacing_navigation_rail">80dp</dimen>
5</resources>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..ea20cb17c
--- /dev/null
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -0,0 +1,227 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <string-array name="regionNames">
5 <item>@string/auto</item>
6 <item>@string/region_australia</item>
7 <item>@string/region_china</item>
8 <item>@string/region_europe</item>
9 <item>@string/region_japan</item>
10 <item>@string/region_korea</item>
11 <item>@string/region_taiwan</item>
12 <item>@string/region_usa</item>
13 </string-array>
14
15 <integer-array name="regionValues">
16 <item>-1</item>
17 <item>3</item>
18 <item>4</item>
19 <item>2</item>
20 <item>0</item>
21 <item>5</item>
22 <item>6</item>
23 <item>1</item>
24 </integer-array>
25
26 <string-array name="languageNames">
27 <item>@string/language_brazilian_portuguese</item>
28 <item>@string/language_british_english</item>
29 <item>@string/language_canadian_french</item>
30 <item>@string/language_chinese</item>
31 <item>@string/language_dutch</item>
32 <item>@string/language_english</item>
33 <item>@string/language_french</item>
34 <item>@string/langauge_german</item>
35 <item>@string/language_italian</item>
36 <item>@string/language_japanese</item>
37 <item>@string/language_korean</item>
38 <item>@string/language_latin_american_spanish</item>
39 <item>@string/language_portuguese</item>
40 <item>@string/language_russian</item>
41 <item>@string/language_simplified_chinese</item>
42 <item>@string/language_spanish</item>
43 <item>@string/language_taiwanese</item>
44 <item>@string/language_traditional_chinese</item>
45 </string-array>
46
47 <integer-array name="languageValues">
48 <item>17</item>
49 <item>12</item>
50 <item>13</item>
51 <item>6</item>
52 <item>8</item>
53 <item>1</item>
54 <item>2</item>
55 <item>3</item>
56 <item>4</item>
57 <item>0</item>
58 <item>7</item>
59 <item>14</item>
60 <item>9</item>
61 <item>10</item>
62 <item>15</item>
63 <item>5</item>
64 <item>11</item>
65 <item>16</item>
66 </integer-array>
67
68 <string-array name="rendererApiNames">
69 <item>@string/renderer_vulkan</item>
70 <item>@string/renderer_none</item>
71 </string-array>
72
73 <integer-array name="rendererApiValues">
74 <item>1</item>
75 <item>2</item>
76 </integer-array>
77
78 <string-array name="rendererAccuracyNames">
79 <item>@string/renderer_accuracy_normal</item>
80 <item>@string/renderer_accuracy_high</item>
81 <item>@string/renderer_accuracy_extreme</item>
82 </string-array>
83
84 <integer-array name="rendererAccuracyValues">
85 <item>0</item>
86 <item>1</item>
87 <item>2</item>
88 </integer-array>
89
90 <string-array name="rendererResolutionNames">
91 <item>@string/resolution_half</item>
92 <item>@string/resolution_three_quarter</item>
93 <item>@string/resolution_one</item>
94 <item>@string/resolution_two</item>
95 <item>@string/resolution_three</item>
96 <item>@string/resolution_four</item>
97 </string-array>
98
99 <string-array name="rendererVSyncNames">
100 <item>@string/renderer_vsync_immediate</item>
101 <item>@string/renderer_vsync_mailbox</item>
102 <item>@string/renderer_vsync_fifo</item>
103 <item>@string/renderer_vsync_fifo_relaxed</item>
104 </string-array>
105
106 <integer-array name="rendererResolutionValues">
107 <item>0</item>
108 <item>1</item>
109 <item>2</item>
110 <item>3</item>
111 <item>4</item>
112 <item>5</item>
113 </integer-array>
114
115 <integer-array name="rendererVSyncValues">
116 <item>0</item>
117 <item>1</item>
118 <item>2</item>
119 <item>3</item>
120 </integer-array>
121
122 <string-array name="rendererAspectRatioNames">
123 <item>@string/ratio_default</item>
124 <item>@string/ratio_force_four_three</item>
125 <item>@string/ratio_force_twenty_one_nine</item>
126 <item>@string/ratio_force_sixteen_ten</item>
127 <item>@string/ratio_stretch</item>
128 </string-array>
129
130 <integer-array name="rendererAspectRatioValues">
131 <item>0</item>
132 <item>1</item>
133 <item>2</item>
134 <item>3</item>
135 <item>4</item>
136 </integer-array>
137
138 <string-array name="rendererScalingFilterNames">
139 <item>@string/scaling_filter_nearest_neighbor</item>
140 <item>@string/scaling_filter_bilinear</item>
141 <item>@string/scaling_filter_bicubic</item>
142 <item>@string/scaling_filter_gaussian</item>
143 <item>@string/scaling_filter_scale_force</item>
144 <item>@string/scaling_filter_fsr</item>
145 </string-array>
146
147 <integer-array name="rendererScalingFilterValues">
148 <item>0</item>
149 <item>1</item>
150 <item>2</item>
151 <item>3</item>
152 <item>4</item>
153 <item>5</item>
154 </integer-array>
155
156 <string-array name="rendererAntiAliasingNames">
157 <item>@string/anti_aliasing_none</item>
158 <item>@string/anti_aliasing_fxaa</item>
159 <item>@string/anti_aliasing_smaa</item>
160 </string-array>
161
162 <integer-array name="rendererAntiAliasingValues">
163 <item>0</item>
164 <item>1</item>
165 <item>2</item>
166 </integer-array>
167
168 <string-array name="cpuAccuracyNames">
169 <item>@string/auto</item>
170 <item>@string/cpu_accuracy_accurate</item>
171 <item>@string/cpu_accuracy_unsafe</item>
172 <item>@string/cpu_accuracy_paranoid</item>
173 </string-array>
174
175 <integer-array name="cpuAccuracyValues">
176 <item>0</item>
177 <item>1</item>
178 <item>2</item>
179 <item>3</item>
180 </integer-array>
181
182 <string-array name="gamepadButtons">
183 <item>A</item>
184 <item>B</item>
185 <item>X</item>
186 <item>Y</item>
187 <item>L</item>
188 <item>R</item>
189 <item>ZL</item>
190 <item>ZR</item>
191 <item>+</item>
192 <item>-</item>
193 <item>@string/gamepad_d_pad</item>
194 <item>@string/gamepad_left_stick</item>
195 <item>@string/gamepad_right_stick</item>
196 <item>@string/gamepad_home</item>
197 <item>@string/gamepad_screenshot</item>
198 </string-array>
199
200 <string-array name="themeEntries">
201 <item>@string/theme_default</item>
202 </string-array>
203 <integer-array name="themeValues">
204 <item>0</item>
205 </integer-array>
206
207 <string-array name="themeEntriesA12">
208 <item>@string/theme_default</item>
209 <item>@string/theme_material_you</item>
210 </string-array>
211 <integer-array name="themeValuesA12">
212 <item>0</item>
213 <item>1</item>
214 </integer-array>
215
216 <string-array name="themeModeEntries">
217 <item>@string/theme_mode_follow_system</item>
218 <item>@string/theme_mode_light</item>
219 <item>@string/theme_mode_dark</item>
220 </string-array>
221 <integer-array name="themeModeValues">
222 <item>-1</item>
223 <item>1</item>
224 <item>2</item>
225 </integer-array>
226
227</resources>
diff --git a/src/android/app/src/main/res/values/bools.xml b/src/android/app/src/main/res/values/bools.xml
new file mode 100644
index 000000000..e50f473fb
--- /dev/null
+++ b/src/android/app/src/main/res/values/bools.xml
@@ -0,0 +1,4 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <bool name="small_layout">true</bool>
4</resources>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..00757e5e8
--- /dev/null
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -0,0 +1,18 @@
1<resources>
2 <dimen name="spacing_small">4dp</dimen>
3 <dimen name="spacing_med">8dp</dimen>
4 <dimen name="spacing_medlarge">12dp</dimen>
5 <dimen name="spacing_large">16dp</dimen>
6 <dimen name="spacing_xtralarge">32dp</dimen>
7 <dimen name="spacing_list">64dp</dimen>
8 <dimen name="spacing_chip">20dp</dimen>
9 <dimen name="spacing_navigation">80dp</dimen>
10 <dimen name="spacing_navigation_rail">0dp</dimen>
11 <dimen name="spacing_search">128dp</dimen>
12 <dimen name="spacing_refresh_end">72dp</dimen>
13 <dimen name="menu_width">256dp</dimen>
14 <dimen name="card_width">165dp</dimen>
15
16 <dimen name="dialog_margin">20dp</dimen>
17 <dimen name="elevated_app_bar">3dp</dimen>
18</resources>
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
new file mode 100644
index 000000000..bc614b81d
--- /dev/null
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3 <integer name="game_title_lines">2</integer>
4
5 <!-- Default SWITCH landscape layout -->
6 <integer name="SWITCH_BUTTON_A_X">760</integer>
7 <integer name="SWITCH_BUTTON_A_Y">790</integer>
8 <integer name="SWITCH_BUTTON_B_X">710</integer>
9 <integer name="SWITCH_BUTTON_B_Y">900</integer>
10 <integer name="SWITCH_BUTTON_X_X">710</integer>
11 <integer name="SWITCH_BUTTON_X_Y">680</integer>
12 <integer name="SWITCH_BUTTON_Y_X">660</integer>
13 <integer name="SWITCH_BUTTON_Y_Y">790</integer>
14 <integer name="SWITCH_STICK_L_X">100</integer>
15 <integer name="SWITCH_STICK_L_Y">670</integer>
16 <integer name="SWITCH_STICK_R_X">900</integer>
17 <integer name="SWITCH_STICK_R_Y">670</integer>
18 <integer name="SWITCH_TRIGGER_L_X">70</integer>
19 <integer name="SWITCH_TRIGGER_L_Y">220</integer>
20 <integer name="SWITCH_TRIGGER_R_X">930</integer>
21 <integer name="SWITCH_TRIGGER_R_Y">220</integer>
22 <integer name="SWITCH_TRIGGER_ZL_X">70</integer>
23 <integer name="SWITCH_TRIGGER_ZL_Y">90</integer>
24 <integer name="SWITCH_TRIGGER_ZR_X">930</integer>
25 <integer name="SWITCH_TRIGGER_ZR_Y">90</integer>
26 <integer name="SWITCH_BUTTON_MINUS_X">460</integer>
27 <integer name="SWITCH_BUTTON_MINUS_Y">950</integer>
28 <integer name="SWITCH_BUTTON_PLUS_X">540</integer>
29 <integer name="SWITCH_BUTTON_PLUS_Y">950</integer>
30 <integer name="SWITCH_BUTTON_HOME_X">600</integer>
31 <integer name="SWITCH_BUTTON_HOME_Y">950</integer>
32 <integer name="SWITCH_BUTTON_CAPTURE_X">400</integer>
33 <integer name="SWITCH_BUTTON_CAPTURE_Y">950</integer>
34 <integer name="SWITCH_BUTTON_DPAD_X">260</integer>
35 <integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
36
37</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..0ae69afb4
--- /dev/null
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,866 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <!-- General application strings -->
5 <string name="app_name" translatable="false">yuzu</string>
6 <string name="app_disclaimer">This software will run games for the Nintendo Switch game console. No game titles or keys are included.&lt;br /&gt;&lt;br /&gt;Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href="https://yuzu-emu.org/help/quickstart">Learn more</a>]]></string>
7 <string name="emulation_notification_channel_name">Emulation is Active</string>
8 <string name="emulation_notification_channel_id" translatable="false">emulationIsActive</string>
9 <string name="emulation_notification_channel_description">Shows a persistent notification when emulation is running.</string>
10 <string name="emulation_notification_running">yuzu is running</string>
11 <string name="notice_notification_channel_name">Notices and errors</string>
12 <string name="notice_notification_channel_id" translatable="false">noticesAndErrors</string>
13 <string name="notice_notification_channel_description">Shows notifications when something goes wrong.</string>
14 <string name="notification_permission_not_granted">Notification permission not granted!</string>
15
16 <!-- Setup strings -->
17 <string name="welcome">Welcome!</string>
18 <string name="welcome_description">Learn how to setup &lt;b>yuzu&lt;/b> and jump into emulation.</string>
19 <string name="get_started">Get started</string>
20 <string name="keys">Keys</string>
21 <string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
22 <string name="select_keys">Select Keys</string>
23 <string name="games">Games</string>
24 <string name="games_description">Select your &lt;b>Games&lt;/b> folder with the button below.</string>
25 <string name="done">Done</string>
26 <string name="done_description">You\'re all set.\nEnjoy your games!</string>
27 <string name="text_continue">Continue</string>
28 <string name="next">Next</string>
29 <string name="back">Back</string>
30 <string name="add_games">Add Games</string>
31 <string name="add_games_description">Select your games folder</string>
32
33 <!-- Home strings -->
34 <string name="home_games">Games</string>
35 <string name="home_search">Search</string>
36 <string name="home_settings">Settings</string>
37 <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
38 <string name="search_and_filter_games">Search and filter games</string>
39 <string name="select_games_folder">Select games folder</string>
40 <string name="select_games_folder_description">Allows yuzu to populate the games list</string>
41 <string name="add_games_warning">Skip selecting games folder?</string>
42 <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
43 <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
44 <string name="home_search_games">Search games</string>
45 <string name="games_dir_selected">Games directory selected</string>
46 <string name="install_prod_keys">Install prod.keys</string>
47 <string name="install_prod_keys_description">Required to decrypt retail games</string>
48 <string name="install_prod_keys_warning">Skip adding keys?</string>
49 <string name="install_prod_keys_warning_description">Valid keys are required to emulate retail games. Only homebrew apps will function if you continue.</string>
50 <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
51 <string name="notifications">Notifications</string>
52 <string name="notifications_description">Grant the notification permission with the button below.</string>
53 <string name="give_permission">Grant permission</string>
54 <string name="notification_warning">Skip granting the notification permission?</string>
55 <string name="notification_warning_description">yuzu won\'t be able to notify you of important information.</string>
56 <string name="permission_denied">Permission denied</string>
57 <string name="permission_denied_description">You denied this permission too many times and now you have to manually grant it in system settings.</string>
58 <string name="about">About</string>
59 <string name="about_description">Build version, credits, and more</string>
60 <string name="warning_help">Help</string>
61 <string name="warning_skip">Skip</string>
62 <string name="warning_cancel">Cancel</string>
63 <string name="install_amiibo_keys">Install Amiibo keys</string>
64 <string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
65 <string name="invalid_keys_file">Invalid keys file selected</string>
66 <string name="install_keys_success">Keys successfully installed</string>
67 <string name="reading_keys_failure">Error reading encryption keys</string>
68 <string name="install_keys_failure_extension_description">
69 1. Verify your keys have the .keys extension.\n\n
70 2. Keys must not be stored in the Downloads folder.\n\n
71 Resolve the issue(s) and try again.
72 </string>
73 <string name="invalid_keys_error">Invalid encryption keys</string>
74 <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
75 <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
76 <string name="install_gpu_driver">Install GPU driver</string>
77 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
78 <string name="advanced_settings">Advanced settings</string>
79 <string name="settings_description">Configure emulator settings</string>
80 <string name="search_recently_played">Recently played</string>
81 <string name="search_recently_added">Recently added</string>
82 <string name="search_retail">Retail</string>
83 <string name="search_homebrew">Homebrew</string>
84 <string name="open_user_folder">Open yuzu folder</string>
85 <string name="open_user_folder_description">Manage yuzu\'s internal files</string>
86 <string name="theme_and_color_description">Modify the look of the app</string>
87 <string name="no_file_manager">No file manager found</string>
88 <string name="notification_no_directory_link">Could not open yuzu directory</string>
89 <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
90 <string name="manage_save_data">Manage save data</string>
91 <string name="manage_save_data_description">Save data found. Please select an option below.</string>
92 <string name="import_export_saves_description">Import or export save files</string>
93 <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>
94 <string name="save_file_imported_success">Imported successfully</string>
95 <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
96 <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
97 <string name="import_saves">Import</string>
98 <string name="export_saves">Export</string>
99 <string name="install_firmware">Install firmware</string>
100 <string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string>
101 <string name="firmware_installing">Installing firmware</string>
102 <string name="firmware_installed_success">Firmware installed successfully</string>
103 <string name="firmware_installed_failure">Firmware installation failed</string>
104 <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string>
105 <string name="share_log">Share debug logs</string>
106 <string name="share_log_description">Share yuzu\'s log file to debug issues</string>
107 <string name="share_log_missing">No log file found</string>
108
109 <!-- About screen strings -->
110 <string name="gaia_is_not_real">Gaia isn\'t real</string>
111 <string name="copied_to_clipboard">Copied to clipboard</string>
112 <string name="about_app_description">An open-source Switch emulator</string>
113 <string name="contributors">Contributors</string>
114 <string name="contributors_description">Made with \u2764 from the yuzu team</string>
115 <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
116 <string name="licenses_description">Projects that make yuzu for Android possible</string>
117 <string name="build">Build</string>
118 <string name="support_link">https://discord.gg/u77vRWY</string>
119 <string name="website_link">https://yuzu-emu.org/</string>
120 <string name="github_link">https://github.com/yuzu-emu</string>
121
122 <!-- Early access upgrade strings -->
123 <string name="early_access">Early Access</string>
124 <string name="get_early_access">Get Early Access</string>
125 <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
126 <string name="get_early_access_description">Cutting-edge features, early access to updates, and more</string>
127 <string name="early_access_benefits">Early Access Benefits</string>
128 <string name="cutting_edge_features">Cutting-edge features</string>
129 <string name="early_access_updates">Early access to updates</string>
130 <string name="no_manual_installation">No manual installation</string>
131 <string name="prioritized_support">Prioritized support</string>
132 <string name="helping_game_preservation">Helping game preservation</string>
133 <string name="our_eternal_gratitude">Our eternal gratitude</string>
134 <string name="are_you_interested">Are you interested?</string>
135
136 <!-- General settings strings -->
137 <string name="frame_limit_enable">Limit speed</string>
138 <string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
139 <string name="frame_limit_slider">Limit speed percent</string>
140 <string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
141 <string name="cpu_accuracy">CPU accuracy</string>
142
143 <!-- System settings strings -->
144 <string name="use_docked_mode">Docked Mode</string>
145 <string name="use_docked_mode_description">Increases resolution, decreasing performance. Handheld Mode is used when disabled, lowering resolution and increasing performance.</string>
146 <string name="emulated_region">Emulated region</string>
147 <string name="emulated_language">Emulated language</string>
148 <string name="select_rtc_date">Select RTC date</string>
149 <string name="select_rtc_time">Select RTC time</string>
150 <string name="use_custom_rtc">Custom RTC</string>
151 <string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string>
152 <string name="set_custom_rtc">Set custom RTC</string>
153
154 <!-- Graphics settings strings -->
155 <string name="renderer_api">API</string>
156 <string name="renderer_accuracy">Accuracy level</string>
157 <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
158 <string name="renderer_vsync">VSync mode</string>
159 <string name="renderer_aspect_ratio">Aspect ratio</string>
160 <string name="renderer_scaling_filter">Window adapting filter</string>
161 <string name="renderer_anti_aliasing">Anti-aliasing method</string>
162 <string name="renderer_force_max_clock">Force maximum clocks (Adreno only)</string>
163 <string name="renderer_force_max_clock_description">Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).</string>
164 <string name="renderer_asynchronous_shaders">Use asynchronous shaders</string>
165 <string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously, reducing stutter but may introduce glitches.</string>
166 <string name="renderer_debug">Graphics debugging</string>
167 <string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string>
168 <string name="use_disk_shader_cache">Disk shader cache</string>
169 <string name="use_disk_shader_cache_description">Reduces stuttering by locally storing and loading generated shaders.</string>
170
171 <!-- Audio settings strings -->
172 <string name="audio_volume">Volume</string>
173 <string name="audio_volume_description">Specifies the volume of audio output.</string>
174
175 <!-- Miscellaneous -->
176 <string name="slider_default">Default</string>
177 <string name="ini_saved">Saved settings</string>
178 <string name="gameid_saved">Saved settings for %1$s</string>
179 <string name="error_saving">Error saving %1$s.ini: %2$s</string>
180 <string name="loading">Loading…</string>
181 <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
182 <string name="reset_to_default">Reset to default</string>
183 <string name="reset_all_settings">Reset all settings?</string>
184 <string name="reset_all_settings_description">All advanced settings will be reset to their default configuration. This can not be undone.</string>
185 <string name="settings_reset">Settings reset</string>
186 <string name="close">Close</string>
187 <string name="learn_more">Learn more</string>
188 <string name="auto">Auto</string>
189 <string name="submit">Submit</string>
190
191 <!-- GPU driver installation -->
192 <string name="select_gpu_driver">Select GPU driver</string>
193 <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
194 <string name="select_gpu_driver_install">Install</string>
195 <string name="select_gpu_driver_default">Default</string>
196 <string name="select_gpu_driver_install_success">Installed %s</string>
197 <string name="select_gpu_driver_use_default">Using default GPU driver</string>
198 <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string>
199 <string name="system_gpu_driver">System GPU driver</string>
200 <string name="installing_driver">Installing driver…</string>
201
202 <!-- Preferences Screen -->
203 <string name="preferences_settings">Settings</string>
204 <string name="preferences_general">General</string>
205 <string name="preferences_system">System</string>
206 <string name="preferences_graphics">Graphics</string>
207 <string name="preferences_audio">Audio</string>
208 <string name="preferences_theme">Theme and color</string>
209 <string name="preferences_debug">Debug</string>
210
211 <!-- ROM loading errors -->
212 <string name="loader_error_encrypted">Your ROM is encrypted</string>
213 <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop">installed titles</a>.]]></string>
214 <string name="loader_error_encrypted_keys_description"><![CDATA[Please ensure your <a href="https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys">prod.keys</a> file is installed so that games can be decrypted.]]></string>
215 <string name="loader_error_video_core">An error occurred initializing the video core</string>
216 <string name="loader_error_video_core_description">This is usually caused by an incompatible GPU driver. Installing a custom GPU driver may resolve this problem.</string>
217 <string name="loader_error_invalid_format">Unable to load ROM</string>
218 <string name="loader_error_file_not_found">ROM file does not exist</string>
219
220 <!-- Emulation Menu -->
221 <string name="emulation_exit">Exit emulation</string>
222 <string name="emulation_done">Done</string>
223 <string name="emulation_fps_counter">FPS counter</string>
224 <string name="emulation_toggle_controls">Toggle controls</string>
225 <string name="emulation_rel_stick_center">Relative stick center</string>
226 <string name="emulation_dpad_slide">D-pad slide</string>
227 <string name="emulation_haptics">Touch haptics</string>
228 <string name="emulation_show_overlay">Show overlay</string>
229 <string name="emulation_toggle_all">Toggle all</string>
230 <string name="emulation_control_adjust">Adjust overlay</string>
231 <string name="emulation_control_scale">Scale</string>
232 <string name="emulation_control_opacity">Opacity</string>
233 <string name="emulation_touch_overlay_reset">Reset overlay</string>
234 <string name="emulation_touch_overlay_edit">Edit overlay</string>
235 <string name="emulation_pause">Pause emulation</string>
236 <string name="emulation_unpause">Unpause emulation</string>
237 <string name="emulation_input_overlay">Overlay options</string>
238 <string name="emulation_game_loading">Game loading…</string>
239
240 <string name="load_settings">Loading settings…</string>
241
242 <!-- Software keyboard -->
243 <string name="software_keyboard">Software keyboard</string>
244
245 <!-- Errors and warnings -->
246 <string name="abort_button">Abort</string>
247 <string name="continue_button">Continue</string>
248 <string name="system_archive_not_found">System Archive Not Found</string>
249 <string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string>
250 <string name="system_archive_general">A system archive</string>
251 <string name="save_load_error">Save/Load Error</string>
252 <string name="fatal_error">Fatal Error</string>
253 <string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string>
254 <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string>
255
256 <!-- Region Names -->
257 <string name="region_japan">Japan</string>
258 <string name="region_usa">USA</string>
259 <string name="region_europe">Europe</string>
260 <string name="region_australia">Australia</string>
261 <string name="region_china">China</string>
262 <string name="region_korea">Korea</string>
263 <string name="region_taiwan">Taiwan</string>
264
265 <!-- Language Names -->
266 <string name="language_japanese">Japanese (日本語)</string>
267 <string name="language_english">English</string>
268 <string name="language_french">French (Français)</string>
269 <string name="langauge_german">German (Deutsch)</string>
270 <string name="language_italian">Italian (Italiano)</string>
271 <string name="language_spanish">Spanish (Español)</string>
272 <string name="language_chinese">Chinese (简体中文)</string>
273 <string name="language_korean">Korean (한국어)</string>
274 <string name="language_dutch">Dutch (Nederlands)</string>
275 <string name="language_portuguese">Portuguese (Português)</string>
276 <string name="language_russian">Russian (Русский)</string>
277 <string name="language_taiwanese">Taiwanese (台湾)</string>
278 <string name="language_british_english">British English</string>
279 <string name="language_canadian_french">Canadian French (Français canadien)</string>
280 <string name="language_latin_american_spanish">Latin American Spanish (Español latinoamericano)</string>
281 <string name="language_simplified_chinese">Simplified Chinese (简体中文)</string>
282 <string name="language_traditional_chinese">Traditional Chinese (正體中文)</string>
283 <string name="language_brazilian_portuguese">Brazilian Portuguese (Português do Brasil)</string>
284
285 <!-- Renderer APIs -->
286 <string name="renderer_vulkan">Vulkan</string>
287 <string name="renderer_none">None</string>
288
289 <!-- Renderer Accuracy -->
290 <string name="renderer_accuracy_normal">Normal</string>
291 <string name="renderer_accuracy_high">High</string>
292 <string name="renderer_accuracy_extreme">Extreme (Slow)</string>
293
294 <!-- Resolutions -->
295 <string name="resolution_half">0.5X (360p/540p)</string>
296 <string name="resolution_three_quarter">0.75X (540p/810p)</string>
297 <string name="resolution_one">1X (720p/1080p)</string>
298 <string name="resolution_two">2X (1440p/2160p) (Slow)</string>
299 <string name="resolution_three">3X (2160p/3240p) (Slow)</string>
300 <string name="resolution_four">4X (2880p/4320p) (Slow)</string>
301
302 <!-- Renderer VSync -->
303 <string name="renderer_vsync_immediate">Immediate (Off)</string>
304 <string name="renderer_vsync_mailbox">Mailbox</string>
305 <string name="renderer_vsync_fifo">FIFO (On)</string>
306 <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
307
308 <!-- Scaling Filters -->
309 <string name="scaling_filter_nearest_neighbor">Nearest Neighbor</string>
310 <string name="scaling_filter_bilinear">Bilinear</string>
311 <string name="scaling_filter_bicubic">Bicubic</string>
312 <string name="scaling_filter_gaussian">Gaussian</string>
313 <string name="scaling_filter_scale_force">ScaleForce</string>
314 <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
315
316 <!-- Anti-Aliasing -->
317 <string name="anti_aliasing_none">None</string>
318 <string name="anti_aliasing_fxaa">FXAA</string>
319 <string name="anti_aliasing_smaa">SMAA</string>
320
321 <!-- Aspect Ratios -->
322 <string name="ratio_default">Default (16:9)</string>
323 <string name="ratio_force_four_three">Force 4:3</string>
324 <string name="ratio_force_twenty_one_nine">Force 21:9</string>
325 <string name="ratio_force_sixteen_ten">Force 16:10</string>
326 <string name="ratio_stretch">Stretch to window</string>
327
328 <!-- CPU Accuracy -->
329 <string name="cpu_accuracy_accurate">Accurate</string>
330 <string name="cpu_accuracy_unsafe">Unsafe</string>
331 <string name="cpu_accuracy_paranoid">Paranoid (Slow)</string>
332
333 <!-- Gamepad Buttons -->
334 <string name="gamepad_d_pad">D-pad</string>
335 <string name="gamepad_left_stick">Left stick</string>
336 <string name="gamepad_right_stick">Right stick</string>
337 <string name="gamepad_home">Home</string>
338 <string name="gamepad_screenshot">Screenshot</string>
339
340 <!-- Disk shader cache -->
341 <string name="preparing_shaders">Preparing shaders</string>
342 <string name="building_shaders">Building shaders</string>
343
344 <!-- Theme options -->
345 <string name="change_app_theme">Change app theme</string>
346 <string name="theme_default">Default</string>
347 <string name="theme_material_you">Material You</string>
348
349 <!-- Theme Modes -->
350 <string name="change_theme_mode">Change theme mode</string>
351 <string name="theme_mode_follow_system">Follow System</string>
352 <string name="theme_mode_light">Light</string>
353 <string name="theme_mode_dark">Dark</string>
354
355 <!-- Black backgrounds theme -->
356 <string name="use_black_backgrounds">Black backgrounds</string>
357 <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
358
359 <!-- Licenses screen strings -->
360 <string name="licenses">Licenses</string>
361 <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
362 <string name="license_fidelityfx_fsr_description">High-quality upscaling from AMD</string>
363 <string name="license_fidelityfx_fsr_link" translatable="false">https://github.com/GPUOpen-Effects/FidelityFX-FSR</string>
364 <string name="license_fidelityfx_fsr_copyright" translatable="false">Copyright © 2021 Advanced Micro Devices, Inc.</string>
365 <string name="license_fidelityfx_fsr_text" translatable="false">
366Permission is hereby granted, free of charge, to any person obtaining a copy
367of this software and associated documentation files (the \"Software"), to deal
368in the Software without restriction, including without limitation the rights
369to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
370copies of the Software, and to permit persons to whom the Software is
371furnished to do so, subject to the following conditions:\n\n
372
373The above copyright notice and this permission notice shall be included in
374all copies or substantial portions of the Software.\n\n
375
376THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
377IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
378FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
379AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
380LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
381OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
382THE SOFTWARE.
383 </string>
384 <string name="license_cubeb" translatable="false">cubeb</string>
385 <string name="license_cubeb_description" translatable="false">Cross platform audio library</string>
386 <string name="license_cubeb_link" translatable="false">https://github.com/mozilla/cubeb</string>
387 <string name="license_cubeb_copyright" translatable="false">Copyright © 2011 Mozilla Foundation</string>
388 <string name="license_cubeb_text" translatable="false">
389Permission to use, copy, modify, and distribute this software for any
390purpose with or without fee is hereby granted, provided that the above
391copyright notice and this permission notice appear in all copies.\n\n
392
393THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
394WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
395MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
396ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
397WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
398ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
399OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
400 </string>
401 <string name="license_dynarmic" translatable="false">Dynarmic</string>
402 <string name="license_dynarmic_description" translatable="false">An ARM dynamic recompiler</string>
403 <string name="license_dynarmic_link" translatable="false">https://github.com/merryhime/dynarmic</string>
404 <string name="license_dynarmic_copyright" translatable="false">Copyright © 2017 merryhime</string>
405 <string name="license_dynarmic_text" translatable="false">
406Permission to use, copy, modify, and/or distribute this software for
407any purpose with or without fee is hereby granted.\n\n
408
409THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
410WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
411MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
412ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
413WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
414AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
415OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
416 </string>
417 <string name="license_ffmpeg" translatable="false">FFmpeg</string>
418 <string name="license_ffmpeg_description" translatable="false">FFmpeg is a collection of libraries and tools to process multimedia content such as audio, video, subtitles and related metadata.</string>
419 <string name="license_ffmpeg_link" translatable="false">https://github.com/FFmpeg/FFmpeg</string>
420 <string name="license_ffmpeg_copyright" translatable="false">Copyright © 1991, 1999 Free Software Foundation, Inc.</string>
421 <string name="license_ffmpeg_text" translatable="false">
422GNU LESSER GENERAL PUBLIC LICENSE\n
423TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n
424
425 0. This License Agreement applies to any software library or other
426program which contains a notice placed by the copyright holder or
427other authorized party saying it may be distributed under the terms of
428this Lesser General Public License (also called \"this License\").
429Each licensee is addressed as \"you\".\n\n
430
431 A \"library\" means a collection of software functions and/or data
432prepared so as to be conveniently linked with application programs
433(which use some of those functions and data) to form executables.\n\n
434
435 The \"Library\", below, refers to any such software library or work
436which has been distributed under these terms. A \"work based on the
437Library\" means either the Library or any derivative work under
438copyright law: that is to say, a work containing the Library or a
439portion of it, either verbatim or with modifications and/or translated
440straightforwardly into another language. (Hereinafter, translation is
441included without limitation in the term \"modification\".)\n\n
442
443 \"Source code\" for a work means the preferred form of the work for
444making modifications to it. For a library, complete source code means
445all the source code for all modules it contains, plus any associated
446interface definition files, plus the scripts used to control compilation
447and installation of the library.\n\n
448
449 Activities other than copying, distribution and modification are not
450covered by this License; they are outside its scope. The act of
451running a program using the Library is not restricted, and output from
452such a program is covered only if its contents constitute a work based
453on the Library (independent of the use of the Library in a tool for
454writing it). Whether that is true depends on what the Library does
455and what the program that uses the Library does.\n\n
456
457 1. You may copy and distribute verbatim copies of the Library\'s
458complete source code as you receive it, in any medium, provided that
459you conspicuously and appropriately publish on each copy an
460appropriate copyright notice and disclaimer of warranty; keep intact
461all the notices that refer to this License and to the absence of any
462warranty; and distribute a copy of this License along with the
463Library.\n\n
464
465 You may charge a fee for the physical act of transferring a copy,
466and you may at your option offer warranty protection in exchange for a
467fee.\n\n
468
469 2. You may modify your copy or copies of the Library or any portion
470of it, thus forming a work based on the Library, and copy and
471distribute such modifications or work under the terms of Section 1
472above, provided that you also meet all of these conditions:\n\n
473
474a) The modified work must itself be a software library.\n\n
475
476b) You must cause the files modified to carry prominent notices
477stating that you changed the files and the date of any change.\n\n
478
479c) You must cause the whole of the work to be licensed at no
480charge to all third parties under the terms of this License.\n\n
481
482d) If a facility in the modified Library refers to a function or a
483table of data to be supplied by an application program that uses
484the facility, other than as an argument passed when the facility
485is invoked, then you must make a good faith effort to ensure that,
486in the event an application does not supply such function or
487table, the facility still operates, and performs whatever part of
488its purpose remains meaningful.\n\n
489
490(For example, a function in a library to compute square roots has
491a purpose that is entirely well-defined independent of the
492application. Therefore, Subsection 2d requires that any
493application-supplied function or table used by this function must
494be optional: if the application does not supply it, the square
495root function must still compute square roots.)\n\n
496
497These requirements apply to the modified work as a whole. If
498identifiable sections of that work are not derived from the Library,
499and can be reasonably considered independent and separate works in
500themselves, then this License, and its terms, do not apply to those
501sections when you distribute them as separate works. But when you
502distribute the same sections as part of a whole which is a work based
503on the Library, the distribution of the whole must be on the terms of
504this License, whose permissions for other licensees extend to the
505entire whole, and thus to each and every part regardless of who wrote
506it.\n\n
507
508Thus, it is not the intent of this section to claim rights or contest
509your rights to work written entirely by you; rather, the intent is to
510exercise the right to control the distribution of derivative or
511collective works based on the Library.\n\n
512
513In addition, mere aggregation of another work not based on the Library
514with the Library (or with a work based on the Library) on a volume of
515a storage or distribution medium does not bring the other work under
516the scope of this License.\n\n
517
518 3. You may opt to apply the terms of the ordinary GNU General Public
519License instead of this License to a given copy of the Library. To do
520this, you must alter all the notices that refer to this License, so
521that they refer to the ordinary GNU General Public License, version 2,
522instead of to this License. (If a newer version than version 2 of the
523ordinary GNU General Public License has appeared, then you can specify
524that version instead if you wish.) Do not make any other change in
525these notices.\n\n
526
527 Once this change is made in a given copy, it is irreversible for
528that copy, so the ordinary GNU General Public License applies to all
529subsequent copies and derivative works made from that copy.\n\n
530
531 This option is useful when you wish to copy part of the code of
532the Library into a program that is not a library.\n\n
533
534 4. You may copy and distribute the Library (or a portion or
535derivative of it, under Section 2) in object code or executable form
536under the terms of Sections 1 and 2 above provided that you accompany
537it with the complete corresponding machine-readable source code, which
538must be distributed under the terms of Sections 1 and 2 above on a
539medium customarily used for software interchange.\n\n
540
541 If distribution of object code is made by offering access to copy
542from a designated place, then offering equivalent access to copy the
543source code from the same place satisfies the requirement to
544distribute the source code, even though third parties are not
545compelled to copy the source along with the object code.\n\n
546
547 5. A program that contains no derivative of any portion of the
548Library, but is designed to work with the Library by being compiled or
549linked with it, is called a \"work that uses the Library\". Such a
550work, in isolation, is not a derivative work of the Library, and
551therefore falls outside the scope of this License.\n\n
552
553 However, linking a \"work that uses the Library\" with the Library
554creates an executable that is a derivative of the Library (because it
555contains portions of the Library), rather than a \"work that uses the
556library\". The executable is therefore covered by this License.
557Section 6 states terms for distribution of such executables.\n\n
558
559 When a \"work that uses the Library\" uses material from a header file
560that is part of the Library, the object code for the work may be a
561derivative work of the Library even though the source code is not.
562Whether this is true is especially significant if the work can be
563linked without the Library, or if the work is itself a library. The
564threshold for this to be true is not precisely defined by law.\n\n
565
566 If such an object file uses only numerical parameters, data
567structure layouts and accessors, and small macros and small inline
568functions (ten lines or less in length), then the use of the object
569file is unrestricted, regardless of whether it is legally a derivative
570work. (Executables containing this object code plus portions of the
571Library will still fall under Section 6.)\n\n
572
573 Otherwise, if the work is a derivative of the Library, you may
574distribute the object code for the work under the terms of Section 6.
575Any executables containing that work also fall under Section 6,
576whether or not they are linked directly with the Library itself.\n\n
577
578 6. As an exception to the Sections above, you may also combine or
579link a \"work that uses the Library\" with the Library to produce a
580work containing portions of the Library, and distribute that work
581under terms of your choice, provided that the terms permit
582modification of the work for the customer\'s own use and reverse
583engineering for debugging such modifications.\n\n
584
585 You must give prominent notice with each copy of the work that the
586Library is used in it and that the Library and its use are covered by
587this License. You must supply a copy of this License. If the work
588during execution displays copyright notices, you must include the
589copyright notice for the Library among them, as well as a reference
590directing the user to the copy of this License. Also, you must do one
591of these things:\n\n
592
593a) Accompany the work with the complete corresponding
594machine-readable source code for the Library including whatever
595changes were used in the work (which must be distributed under
596Sections 1 and 2 above); and, if the work is an executable linked
597with the Library, with the complete machine-readable \"work that
598uses the Library\", as object code and/or source code, so that the
599user can modify the Library and then relink to produce a modified
600executable containing the modified Library. (It is understood
601that the user who changes the contents of definitions files in the
602Library will not necessarily be able to recompile the application
603to use the modified definitions.)\n\n
604
605b) Use a suitable shared library mechanism for linking with the
606Library. A suitable mechanism is one that (1) uses at run time a
607copy of the library already present on the user\'s computer system,
608rather than copying library functions into the executable, and (2)
609will operate properly with a modified version of the library, if
610the user installs one, as long as the modified version is
611interface-compatible with the version that the work was made with.\n\n
612
613c) Accompany the work with a written offer, valid for at
614least three years, to give the same user the materials
615specified in Subsection 6a, above, for a charge no more
616than the cost of performing this distribution.\n\n
617
618d) If distribution of the work is made by offering access to copy
619from a designated place, offer equivalent access to copy the above
620specified materials from the same place.\n\n
621
622e) Verify that the user has already received a copy of these
623materials or that you have already sent this user a copy.\n\n
624
625 For an executable, the required form of the \"work that uses the
626Library\" must include any data and utility programs needed for
627reproducing the executable from it. However, as a special exception,
628the materials to be distributed need not include anything that is
629normally distributed (in either source or binary form) with the major
630components (compiler, kernel, and so on) of the operating system on
631which the executable runs, unless that component itself accompanies
632the executable.\n\n
633
634 It may happen that this requirement contradicts the license
635restrictions of other proprietary libraries that do not normally
636accompany the operating system. Such a contradiction means you cannot
637use both them and the Library together in an executable that you
638distribute.\n\n
639
640 7. You may place library facilities that are a work based on the
641Library side-by-side in a single library together with other library
642facilities not covered by this License, and distribute such a combined
643library, provided that the separate distribution of the work based on
644the Library and of the other library facilities is otherwise
645permitted, and provided that you do these two things:\n\n
646
647a) Accompany the combined library with a copy of the same work
648based on the Library, uncombined with any other library
649facilities. This must be distributed under the terms of the
650Sections above.\n\n
651
652b) Give prominent notice with the combined library of the fact
653that part of it is a work based on the Library, and explaining
654where to find the accompanying uncombined form of the same work.\n\n
655
656 8. You may not copy, modify, sublicense, link with, or distribute
657the Library except as expressly provided under this License. Any
658attempt otherwise to copy, modify, sublicense, link with, or
659distribute the Library is void, and will automatically terminate your
660rights under this License. However, parties who have received copies,
661or rights, from you under this License will not have their licenses
662terminated so long as such parties remain in full compliance.\n\n
663
664 9. You are not required to accept this License, since you have not
665signed it. However, nothing else grants you permission to modify or
666distribute the Library or its derivative works. These actions are
667prohibited by law if you do not accept this License. Therefore, by
668modifying or distributing the Library (or any work based on the
669Library), you indicate your acceptance of this License to do so, and
670all its terms and conditions for copying, distributing or modifying
671the Library or works based on it.\n\n
672
673 10. Each time you redistribute the Library (or any work based on the
674Library), the recipient automatically receives a license from the
675original licensor to copy, distribute, link with or modify the Library
676subject to these terms and conditions. You may not impose any further
677restrictions on the recipients\' exercise of the rights granted herein.
678You are not responsible for enforcing compliance by third parties with
679this License.\n\n
680
681 11. If, as a consequence of a court judgment or allegation of patent
682infringement or for any other reason (not limited to patent issues),
683conditions are imposed on you (whether by court order, agreement or
684otherwise) that contradict the conditions of this License, they do not
685excuse you from the conditions of this License. If you cannot
686distribute so as to satisfy simultaneously your obligations under this
687License and any other pertinent obligations, then as a consequence you
688may not distribute the Library at all. For example, if a patent
689license would not permit royalty-free redistribution of the Library by
690all those who receive copies directly or indirectly through you, then
691the only way you could satisfy both it and this License would be to
692refrain entirely from distribution of the Library.\n\n
693
694If any portion of this section is held invalid or unenforceable under any
695particular circumstance, the balance of the section is intended to apply,
696and the section as a whole is intended to apply in other circumstances.\n\n
697
698It is not the purpose of this section to induce you to infringe any
699patents or other property right claims or to contest validity of any
700such claims; this section has the sole purpose of protecting the
701integrity of the free software distribution system which is
702implemented by public license practices. Many people have made
703generous contributions to the wide range of software distributed
704through that system in reliance on consistent application of that
705system; it is up to the author/donor to decide if he or she is willing
706to distribute software through any other system and a licensee cannot
707impose that choice.\n\n
708
709This section is intended to make thoroughly clear what is believed to
710be a consequence of the rest of this License.\n\n
711
712 12. If the distribution and/or use of the Library is restricted in
713certain countries either by patents or by copyrighted interfaces, the
714original copyright holder who places the Library under this License may add
715an explicit geographical distribution limitation excluding those countries,
716so that distribution is permitted only in or among countries not thus
717excluded. In such case, this License incorporates the limitation as if
718written in the body of this License.\n\n
719
720 13. The Free Software Foundation may publish revised and/or new
721versions of the Lesser General Public License from time to time.
722Such new versions will be similar in spirit to the present version,
723but may differ in detail to address new problems or concerns.\n\n
724
725Each version is given a distinguishing version number. If the Library
726specifies a version number of this License which applies to it and
727"any later version\", you have the option of following the terms and
728conditions either of that version or of any later version published by
729the Free Software Foundation. If the Library does not specify a
730license version number, you may choose any version ever published by
731the Free Software Foundation.\n\n
732
733 14. If you wish to incorporate parts of the Library into other free
734programs whose distribution conditions are incompatible with these,
735write to the author to ask for permission. For software which is
736copyrighted by the Free Software Foundation, write to the Free
737Software Foundation; we sometimes make exceptions for this. Our
738decision will be guided by the two goals of preserving the free status
739of all derivatives of our free software and of promoting the sharing
740and reuse of software generally.\n\n
741
742NO WARRANTY\n\n
743
744 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
745WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
746EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
747OTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY
748KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
749IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
750PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
751LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
752THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n
753
754 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
755WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
756AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
757FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
758CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
759LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
760RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
761FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
762SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
763DAMAGES.
764 </string>
765 <string name="license_opus" translatable="false">Opus</string>
766 <string name="license_opus_description" translatable="false">Modern audio compression for the internet</string>
767 <string name="license_opus_link" translatable="false">https://github.com/xiph/opus</string>
768 <string name="license_opus_copyright" translatable="false">Copyright 2001–2011 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo</string>
769 <string name="license_opus_text" translatable="false">
770Redistribution and use in source and binary forms, with or without
771modification, are permitted provided that the following conditions
772are met:\n\n
773
774- Redistributions of source code must retain the above copyright
775notice, this list of conditions and the following disclaimer.\n\n
776
777- Redistributions in binary form must reproduce the above copyright
778notice, this list of conditions and the following disclaimer in the
779documentation and/or other materials provided with the distribution.\n\n
780
781- Neither the name of Internet Society, IETF or IETF Trust, nor the
782names of specific contributors, may be used to endorse or promote
783products derived from this software without specific prior written
784permission.\n\n
785
786THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
787``AS IS\'\' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
788LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
789A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
790OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
791EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
792PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
793PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
794LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
795NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
796SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n
797
798Opus is subject to the royalty-free patent licenses which are
799specified at:\n\n
800
801Xiph.Org Foundation:
802https://datatracker.ietf.org/ipr/1524/ \n\n
803
804Microsoft Corporation:
805https://datatracker.ietf.org/ipr/1914/ \n\n
806
807Broadcom Corporation:
808https://datatracker.ietf.org/ipr/1526/
809 </string>
810 <string name="license_sirit" translatable="false">Sirit</string>
811 <string name="license_sirit_description" translatable="false">A runtime SPIR-V assembler</string>
812 <string name="license_sirit_link" translatable="false">https://github.com/ReinUsesLisp/sirit</string>
813 <string name="license_sirit_copyright" translatable="false">Copyright © 2019, sirit All rights reserved.</string>
814 <string name="license_sirit_text" translatable="false">
815Redistribution and use in source and binary forms, with or without
816modification, are permitted provided that the following conditions are met:\n
817* Redistributions of source code must retain the above copyright
818 notice, this list of conditions and the following disclaimer.\n
819* Redistributions in binary form must reproduce the above copyright
820 notice, this list of conditions and the following disclaimer in the
821 documentation and/or other materials provided with the distribution.\n
822* Neither the name of the organization nor the
823 names of its contributors may be used to endorse or promote products
824 derived from this software without specific prior written permission.\n\n
825
826THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
827ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
828WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
829DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY
830DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
831(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
832LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
833ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
834(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
835SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
836 </string>
837 <string name="license_adreno_tools" translatable="false">Adreno Tools</string>
838 <string name="license_adreno_tools_description" translatable="false">A library for applying rootless Adreno GPU driver modifications/replacements</string>
839 <string name="license_adreno_tools_link" translatable="false">https://github.com/bylaws/libadrenotools</string>
840 <string name="license_adreno_tools_copyright" translatable="false">Copyright © 2021, Billy Laws</string>
841 <string name="license_adreno_tools_text" translatable="false">
842BSD 2-Clause License\n\n
843
844Redistribution and use in source and binary forms, with or without
845modification, are permitted provided that the following conditions are met:\n\n
846
8471. Redistributions of source code must retain the above copyright notice, this
848 list of conditions and the following disclaimer.\n\n
849
8502. Redistributions in binary form must reproduce the above copyright notice,
851 this list of conditions and the following disclaimer in the documentation
852 and/or other materials provided with the distribution.\n\n
853
854THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
855AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
856IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
857DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
858FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
859DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
860SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
861CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
862OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
863OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
864 </string>
865
866</resources>
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..4f5de7360
--- /dev/null
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,36 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <!-- Custom button styles -->
5 <style name="InGameMenuOption" parent="Widget.Material3.Button.TextButton">
6 <item name="android:layout_width">match_parent</item>
7 <item name="android:layout_height">48dp</item>
8 <item name="android:textColor">@android:color/black</item>
9 <item name="android:textSize">16sp</item>
10 <item name="android:fontFamily">sans-serif-condensed</item>
11 <item name="android:gravity">center_vertical|left</item>
12 <item name="android:paddingLeft">32dp</item>
13 <item name="android:paddingRight">32dp</item>
14 </style>
15
16 <style name="YuzuSlider" parent="Widget.Material3.Slider">
17 <item name="tickVisible">false</item>
18 <item name="labelBehavior">gone</item>
19 </style>
20
21 <style name="YuzuMaterialDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
22 <item name="colorPrimary">@color/yuzu_primaryContainer</item>
23 <item name="colorSurface">@color/yuzu_primaryContainer</item>
24 <item name="colorSecondary">@color/yuzu_primary</item>
25 <item name="android:textColorLink">@color/yuzu_primary</item>
26 <item name="buttonBarPositiveButtonStyle">@style/YuzuButton</item>
27 <item name="buttonBarNegativeButtonStyle">@style/YuzuButton</item>
28 <item name="buttonBarNeutralButtonStyle">@style/YuzuButton</item>
29 </style>
30
31 <style name="YuzuButton" parent="Widget.Material3.Button.TextButton.Dialog">
32 <item name="android:textColor">@color/yuzu_primary</item>
33 <item name="rippleColor">@color/yuzu_inversePrimary</item>
34 </style>
35
36</resources>
diff --git a/src/android/app/src/main/res/values/themes.xml b/src/android/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..60388b71e
--- /dev/null
+++ b/src/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,51 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="Theme.Yuzu.Splash.Main" parent="Theme.SplashScreen">
5 <item name="windowSplashScreenBackground">@color/yuzu_surface</item>
6 <item name="windowSplashScreenAnimatedIcon">@drawable/ic_yuzu</item>
7 <item name="postSplashScreenTheme">@style/Theme.Yuzu.Main</item>
8 </style>
9
10 <style name="Theme.Yuzu.Main" parent="Theme.Material3.DayNight.NoActionBar">
11 <item name="colorPrimary">@color/yuzu_primary</item>
12 <item name="colorOnPrimary">@color/yuzu_onPrimary</item>
13 <item name="colorPrimaryContainer">@color/yuzu_primaryContainer</item>
14 <item name="colorOnPrimaryContainer">@color/yuzu_onPrimaryContainer</item>
15 <item name="colorSecondary">@color/yuzu_secondary</item>
16 <item name="colorOnSecondary">@color/yuzu_onSecondary</item>
17 <item name="colorSecondaryContainer">@color/yuzu_secondaryContainer</item>
18 <item name="colorOnSecondaryContainer">@color/yuzu_onSecondaryContainer</item>
19 <item name="colorTertiary">@color/yuzu_tertiary</item>
20 <item name="colorOnTertiary">@color/yuzu_onTertiary</item>
21 <item name="colorTertiaryContainer">@color/yuzu_tertiaryContainer</item>
22 <item name="colorOnTertiaryContainer">@color/yuzu_onTertiaryContainer</item>
23 <item name="colorError">@color/yuzu_error</item>
24 <item name="colorErrorContainer">@color/yuzu_errorContainer</item>
25 <item name="colorOnError">@color/yuzu_onError</item>
26 <item name="colorOnErrorContainer">@color/yuzu_onErrorContainer</item>
27 <item name="android:colorBackground">@color/yuzu_background</item>
28 <item name="colorOnBackground">@color/yuzu_onBackground</item>
29 <item name="colorSurface">@color/yuzu_surface</item>
30 <item name="colorOnSurface">@color/yuzu_onSurface</item>
31 <item name="colorSurfaceVariant">@color/yuzu_surfaceVariant</item>
32 <item name="colorOnSurfaceVariant">@color/yuzu_onSurfaceVariant</item>
33 <item name="colorOutline">@color/yuzu_outline</item>
34 <item name="colorOnSurfaceInverse">@color/yuzu_inverseOnSurface</item>
35 <item name="colorSurfaceInverse">@color/yuzu_inverseSurface</item>
36 <item name="colorPrimaryInverse">@color/yuzu_inversePrimary</item>
37 <item name="android:shadowColor">@color/yuzu_shadow</item>
38
39 <item name="android:statusBarColor">@android:color/transparent</item>
40 <item name="android:navigationBarColor">@android:color/transparent</item>
41
42 <item name="sliderStyle">@style/YuzuSlider</item>
43 <item name="materialAlertDialogTheme">@style/YuzuMaterialDialog</item>
44
45 <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
46
47 <item name="android:enforceStatusBarContrast">false</item>
48 <item name="android:enforceNavigationBarContrast">false</item>
49 </style>
50
51</resources>
diff --git a/src/android/app/src/main/res/values/yuzu_colors.xml b/src/android/app/src/main/res/values/yuzu_colors.xml
new file mode 100644
index 000000000..5b7d189dc
--- /dev/null
+++ b/src/android/app/src/main/res/values/yuzu_colors.xml
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <color name="yuzu_primary">#990E00</color>
5 <color name="yuzu_onPrimary">#FFFFFF</color>
6 <color name="yuzu_primaryContainer">#EEDEDD</color>
7 <color name="yuzu_onPrimaryContainer">#400200</color>
8 <color name="yuzu_secondary">#775650</color>
9 <color name="yuzu_onSecondary">#FFFFFF</color>
10 <color name="yuzu_secondaryContainer">#FFDAD4</color>
11 <color name="yuzu_onSecondaryContainer">#2C1511</color>
12 <color name="yuzu_tertiary">#6F5C2E</color>
13 <color name="yuzu_onTertiary">#FFFFFF</color>
14 <color name="yuzu_tertiaryContainer">#FAE0A6</color>
15 <color name="yuzu_onTertiaryContainer">#251A00</color>
16 <color name="yuzu_error">#BA1A1A</color>
17 <color name="yuzu_errorContainer">#FFDAD6</color>
18 <color name="yuzu_onError">#FFFFFF</color>
19 <color name="yuzu_onErrorContainer">#410002</color>
20 <color name="yuzu_background">#FFFBFF</color>
21 <color name="yuzu_onBackground">#201A19</color>
22 <color name="yuzu_surface">#FFFBFF</color>
23 <color name="yuzu_onSurface">#201A19</color>
24 <color name="yuzu_surfaceVariant">#F5DDD9</color>
25 <color name="yuzu_onSurfaceVariant">#534340</color>
26 <color name="yuzu_outline">#857370</color>
27 <color name="yuzu_inverseOnSurface">#FBEEEB</color>
28 <color name="yuzu_inverseSurface">#362F2D</color>
29 <color name="yuzu_inversePrimary">#FFB4A6</color>
30 <color name="yuzu_shadow">#000000</color>
31 <color name="yuzu_surfaceTint">#B52612</color>
32 <color name="yuzu_outlineVariant">#D8C2BE</color>
33
34 <color name="yuzu_ea_background_start">#99FFE1</color>
35 <color name="yuzu_ea_background_end">#76C5FF</color>
36
37</resources>
diff --git a/src/android/app/src/main/res/xml/data_extraction_rules.xml b/src/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..c10efcf56
--- /dev/null
+++ b/src/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,20 @@
1<?xml version="1.0" encoding="utf-8"?>
2<full-backup-content>
3
4 <exclude
5 domain="external"
6 path="./load/" />
7
8 <exclude
9 domain="external"
10 path="./log/" />
11
12 <include
13 domain="external"
14 path="." />
15
16 <include
17 domain="sharedpref"
18 path="." />
19
20</full-backup-content>
diff --git a/src/android/app/src/main/res/xml/data_extraction_rules_api_31.xml b/src/android/app/src/main/res/xml/data_extraction_rules_api_31.xml
new file mode 100644
index 000000000..3ff6cc170
--- /dev/null
+++ b/src/android/app/src/main/res/xml/data_extraction_rules_api_31.xml
@@ -0,0 +1,43 @@
1<?xml version="1.0" encoding="utf-8"?>
2<data-extraction-rules>
3 <cloud-backup disableIfNoEncryptionCapabilities="false">
4
5 <exclude
6 domain="external"
7 path="./load/" />
8
9 <exclude
10 domain="external"
11 path="./log/" />
12
13 <include
14 domain="external"
15 path="." />
16
17 <include
18 domain="sharedpref"
19 path="." />
20
21 </cloud-backup>
22
23 <device-transfer>
24
25 <exclude
26 domain="external"
27 path="./load/" />
28
29 <exclude
30 domain="external"
31 path="./log/" />
32
33 <include
34 domain="external"
35 path="." />
36
37 <include
38 domain="sharedpref"
39 path="." />
40
41 </device-transfer>
42
43</data-extraction-rules>
diff --git a/src/android/app/src/main/res/xml/nfc_tech_filter.xml b/src/android/app/src/main/res/xml/nfc_tech_filter.xml
new file mode 100644
index 000000000..eb4497446
--- /dev/null
+++ b/src/android/app/src/main/res/xml/nfc_tech_filter.xml
@@ -0,0 +1,6 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
3 <tech-list>
4 <tech>android.nfc.tech.NfcA</tech>
5 </tech-list>
6</resources>
diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts
new file mode 100644
index 000000000..e19e8ce58
--- /dev/null
+++ b/src/android/build.gradle.kts
@@ -0,0 +1,13 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4// Top-level build file where you can add configuration options common to all sub-projects/modules.
5plugins {
6 id("com.android.application") version "8.0.2" apply false
7 id("com.android.library") version "8.0.2" apply false
8 id("org.jetbrains.kotlin.android") version "1.8.21" apply false
9}
10
11tasks.register("clean").configure {
12 delete(rootProject.buildDir)
13}
diff --git a/src/android/gradle.properties b/src/android/gradle.properties
new file mode 100644
index 000000000..653a35ce8
--- /dev/null
+++ b/src/android/gradle.properties
@@ -0,0 +1,16 @@
1# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2# SPDX-License-Identifier: GPL-3.0-or-later
3
4# Project-wide Gradle settings.
5# IDE (e.g. Android Studio) users:
6# Gradle settings configured through the IDE *will override*
7# any settings specified in this file.
8# For more details on how to configure your build environment visit
9# http://www.gradle.org/docs/current/userguide/build_environment.html
10# Specifies the JVM arguments used for the daemon process.
11# The setting is particularly useful for tweaking memory settings.
12org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
13android.useAndroidX=true
14# Kotlin code style for this project: "official" or "obsolete":
15kotlin.code.style=official
16android.defaults.buildfeatures.buildconfig=true
diff --git a/src/android/gradle/wrapper/gradle-wrapper.jar b/src/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..7a3265ee9
--- /dev/null
+++ b/src/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..578c71b94
--- /dev/null
+++ b/src/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
1#Sun Feb 21 18:16:59 EST 2021
2distributionBase=GRADLE_USER_HOME
3distributionPath=wrapper/dists
4zipStoreBase=GRADLE_USER_HOME
5zipStorePath=wrapper/dists
6distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
diff --git a/src/android/gradlew b/src/android/gradlew
new file mode 100755
index 000000000..afa127966
--- /dev/null
+++ b/src/android/gradlew
@@ -0,0 +1,175 @@
1#!/usr/bin/env sh
2
3# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
4# SPDX-License-Identifier: GPL-3.0-or-later
5
6##############################################################################
7##
8## Gradle start up script for UN*X
9##
10##############################################################################
11
12# Attempt to set APP_HOME
13# Resolve links: $0 may be a link
14PRG="$0"
15# Need this for relative symlinks.
16while [ -h "$PRG" ] ; do
17 ls=`ls -ld "$PRG"`
18 link=`expr "$ls" : '.*-> \(.*\)$'`
19 if expr "$link" : '/.*' > /dev/null; then
20 PRG="$link"
21 else
22 PRG=`dirname "$PRG"`"/$link"
23 fi
24done
25SAVED="`pwd`"
26cd "`dirname \"$PRG\"`/" >/dev/null
27APP_HOME="`pwd -P`"
28cd "$SAVED" >/dev/null
29
30APP_NAME="Gradle"
31APP_BASE_NAME=`basename "$0"`
32
33# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
34DEFAULT_JVM_OPTS=""
35
36# Use the maximum available, or set MAX_FD != -1 to use that value.
37MAX_FD="maximum"
38
39warn () {
40 echo "$*"
41}
42
43die () {
44 echo
45 echo "$*"
46 echo
47 exit 1
48}
49
50# OS specific support (must be 'true' or 'false').
51cygwin=false
52msys=false
53darwin=false
54nonstop=false
55case "`uname`" in
56 CYGWIN* )
57 cygwin=true
58 ;;
59 Darwin* )
60 darwin=true
61 ;;
62 MINGW* )
63 msys=true
64 ;;
65 NONSTOP* )
66 nonstop=true
67 ;;
68esac
69
70CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
71
72# Determine the Java command to use to start the JVM.
73if [ -n "$JAVA_HOME" ] ; then
74 if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
75 # IBM's JDK on AIX uses strange locations for the executables
76 JAVACMD="$JAVA_HOME/jre/sh/java"
77 else
78 JAVACMD="$JAVA_HOME/bin/java"
79 fi
80 if [ ! -x "$JAVACMD" ] ; then
81 die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
82
83Please set the JAVA_HOME variable in your environment to match the
84location of your Java installation."
85 fi
86else
87 JAVACMD="java"
88 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
89
90Please set the JAVA_HOME variable in your environment to match the
91location of your Java installation."
92fi
93
94# Increase the maximum file descriptors if we can.
95if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
96 MAX_FD_LIMIT=`ulimit -H -n`
97 if [ $? -eq 0 ] ; then
98 if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
99 MAX_FD="$MAX_FD_LIMIT"
100 fi
101 ulimit -n $MAX_FD
102 if [ $? -ne 0 ] ; then
103 warn "Could not set maximum file descriptor limit: $MAX_FD"
104 fi
105 else
106 warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
107 fi
108fi
109
110# For Darwin, add options to specify how the application appears in the dock
111if $darwin; then
112 GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
113fi
114
115# For Cygwin, switch paths to Windows format before running java
116if $cygwin ; then
117 APP_HOME=`cygpath --path --mixed "$APP_HOME"`
118 CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
119 JAVACMD=`cygpath --unix "$JAVACMD"`
120
121 # We build the pattern for arguments to be converted via cygpath
122 ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
123 SEP=""
124 for dir in $ROOTDIRSRAW ; do
125 ROOTDIRS="$ROOTDIRS$SEP$dir"
126 SEP="|"
127 done
128 OURCYGPATTERN="(^($ROOTDIRS))"
129 # Add a user-defined pattern to the cygpath arguments
130 if [ "$GRADLE_CYGPATTERN" != "" ] ; then
131 OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
132 fi
133 # Now convert the arguments - kludge to limit ourselves to /bin/sh
134 i=0
135 for arg in "$@" ; do
136 CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
137 CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
138
139 if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
140 eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
141 else
142 eval `echo args$i`="\"$arg\""
143 fi
144 i=$((i+1))
145 done
146 case $i in
147 (0) set -- ;;
148 (1) set -- "$args0" ;;
149 (2) set -- "$args0" "$args1" ;;
150 (3) set -- "$args0" "$args1" "$args2" ;;
151 (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
152 (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
153 (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
154 (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
155 (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
156 (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
157 esac
158fi
159
160# Escape application args
161save () {
162 for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
163 echo " "
164}
165APP_ARGS=$(save "$@")
166
167# Collect all arguments for the java command, following the shell quoting and substitution rules
168eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
169
170# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
171if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
172 cd "$(dirname "$0")"
173fi
174
175exec "$JAVACMD" "$@"
diff --git a/src/android/gradlew.bat b/src/android/gradlew.bat
new file mode 100644
index 000000000..be152d108
--- /dev/null
+++ b/src/android/gradlew.bat
@@ -0,0 +1,87 @@
1@rem SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2@rem SPDX-License-Identifier: GPL-3.0-or-later
3
4@if "%DEBUG%" == "" @echo off
5@rem ##########################################################################
6@rem
7@rem Gradle startup script for Windows
8@rem
9@rem ##########################################################################
10
11@rem Set local scope for the variables with windows NT shell
12if "%OS%"=="Windows_NT" setlocal
13
14set DIRNAME=%~dp0
15if "%DIRNAME%" == "" set DIRNAME=.
16set APP_BASE_NAME=%~n0
17set APP_HOME=%DIRNAME%
18
19@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
20set DEFAULT_JVM_OPTS=
21
22@rem Find java.exe
23if defined JAVA_HOME goto findJavaFromJavaHome
24
25set JAVA_EXE=java.exe
26%JAVA_EXE% -version >NUL 2>&1
27if "%ERRORLEVEL%" == "0" goto init
28
29echo.
30echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
31echo.
32echo Please set the JAVA_HOME variable in your environment to match the
33echo location of your Java installation.
34
35goto fail
36
37:findJavaFromJavaHome
38set JAVA_HOME=%JAVA_HOME:"=%
39set JAVA_EXE=%JAVA_HOME%/bin/java.exe
40
41if exist "%JAVA_EXE%" goto init
42
43echo.
44echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
45echo.
46echo Please set the JAVA_HOME variable in your environment to match the
47echo location of your Java installation.
48
49goto fail
50
51:init
52@rem Get command-line arguments, handling Windows variants
53
54if not "%OS%" == "Windows_NT" goto win9xME_args
55
56:win9xME_args
57@rem Slurp the command line arguments.
58set CMD_LINE_ARGS=
59set _SKIP=2
60
61:win9xME_args_slurp
62if "x%~1" == "x" goto execute
63
64set CMD_LINE_ARGS=%*
65
66:execute
67@rem Setup the command line
68
69set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
70
71@rem Execute Gradle
72"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
73
74:end
75@rem End local scope for the variables with windows NT shell
76if "%ERRORLEVEL%"=="0" goto mainEnd
77
78:fail
79rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
80rem the _cmd.exe /c_ return code!
81if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
82exit /b 1
83
84:mainEnd
85if "%OS%"=="Windows_NT" endlocal
86
87:omega
diff --git a/src/android/settings.gradle.kts b/src/android/settings.gradle.kts
new file mode 100644
index 000000000..af910b906
--- /dev/null
+++ b/src/android/settings.gradle.kts
@@ -0,0 +1,21 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4pluginManagement {
5 repositories {
6 gradlePluginPortal()
7 google()
8 mavenCentral()
9 }
10}
11
12@Suppress("UnstableApiUsage")
13dependencyResolutionManagement {
14 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
15 repositories {
16 google()
17 mavenCentral()
18 }
19}
20
21include(":app")
diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp
index f44fedfd5..5d58b950c 100644
--- a/src/audio_core/sink/sink_stream.cpp
+++ b/src/audio_core/sink/sink_stream.cpp
@@ -273,10 +273,13 @@ void SinkStream::WaitFreeSpace(std::stop_token stop_token) {
273 std::unique_lock lk{release_mutex}; 273 std::unique_lock lk{release_mutex};
274 release_cv.wait_for(lk, std::chrono::milliseconds(5), 274 release_cv.wait_for(lk, std::chrono::milliseconds(5),
275 [this]() { return queued_buffers < max_queue_size; }); 275 [this]() { return queued_buffers < max_queue_size; });
276#ifndef ANDROID
277 // This wait can cause a problematic shutdown hang on Android.
276 if (queued_buffers > max_queue_size + 3) { 278 if (queued_buffers > max_queue_size + 3) {
277 Common::CondvarWait(release_cv, lk, stop_token, 279 Common::CondvarWait(release_cv, lk, stop_token,
278 [this] { return queued_buffers < max_queue_size; }); 280 [this] { return queued_buffers < max_queue_size; });
279 } 281 }
282#endif
280} 283}
281 284
282} // namespace AudioCore::Sink 285} // namespace AudioCore::Sink
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 13ed68b3f..efc4a9fe9 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -155,6 +155,14 @@ if (WIN32)
155 target_link_libraries(common PRIVATE ntdll) 155 target_link_libraries(common PRIVATE ntdll)
156endif() 156endif()
157 157
158if(ANDROID)
159 target_sources(common
160 PRIVATE
161 fs/fs_android.cpp
162 fs/fs_android.h
163 )
164endif()
165
158if(ARCHITECTURE_x86_64) 166if(ARCHITECTURE_x86_64)
159 target_sources(common 167 target_sources(common
160 PRIVATE 168 PRIVATE
@@ -194,6 +202,11 @@ create_target_directory_groups(common)
194target_link_libraries(common PUBLIC Boost::context Boost::headers fmt::fmt microprofile Threads::Threads) 202target_link_libraries(common PUBLIC Boost::context Boost::headers fmt::fmt microprofile Threads::Threads)
195target_link_libraries(common PRIVATE lz4::lz4 zstd::zstd LLVM::Demangle) 203target_link_libraries(common PRIVATE lz4::lz4 zstd::zstd LLVM::Demangle)
196 204
205if (ANDROID)
206 # For ASharedMemory_create
207 target_link_libraries(common PRIVATE android)
208endif()
209
197if (YUZU_USE_PRECOMPILED_HEADERS) 210if (YUZU_USE_PRECOMPILED_HEADERS)
198 target_precompile_headers(common PRIVATE precompiled_headers.h) 211 target_precompile_headers(common PRIVATE precompiled_headers.h)
199endif() 212endif()
diff --git a/src/common/dynamic_library.cpp b/src/common/dynamic_library.cpp
index 054277a2b..4fabe7e52 100644
--- a/src/common/dynamic_library.cpp
+++ b/src/common/dynamic_library.cpp
@@ -22,6 +22,8 @@ DynamicLibrary::DynamicLibrary(const char* filename) {
22 void(Open(filename)); 22 void(Open(filename));
23} 23}
24 24
25DynamicLibrary::DynamicLibrary(void* handle_) : handle{handle_} {}
26
25DynamicLibrary::DynamicLibrary(DynamicLibrary&& rhs) noexcept 27DynamicLibrary::DynamicLibrary(DynamicLibrary&& rhs) noexcept
26 : handle{std::exchange(rhs.handle, nullptr)} {} 28 : handle{std::exchange(rhs.handle, nullptr)} {}
27 29
diff --git a/src/common/dynamic_library.h b/src/common/dynamic_library.h
index f42bdf441..662d454d4 100644
--- a/src/common/dynamic_library.h
+++ b/src/common/dynamic_library.h
@@ -20,6 +20,9 @@ public:
20 /// Automatically loads the specified library. Call IsOpen() to check validity before use. 20 /// Automatically loads the specified library. Call IsOpen() to check validity before use.
21 explicit DynamicLibrary(const char* filename); 21 explicit DynamicLibrary(const char* filename);
22 22
23 /// Initializes the dynamic library with an already opened handle.
24 explicit DynamicLibrary(void* handle_);
25
23 /// Moves the library. 26 /// Moves the library.
24 DynamicLibrary(DynamicLibrary&&) noexcept; 27 DynamicLibrary(DynamicLibrary&&) noexcept;
25 DynamicLibrary& operator=(DynamicLibrary&&) noexcept; 28 DynamicLibrary& operator=(DynamicLibrary&&) noexcept;
diff --git a/src/common/error.cpp b/src/common/error.cpp
index ddb03bd45..1b2009db7 100644
--- a/src/common/error.cpp
+++ b/src/common/error.cpp
@@ -30,7 +30,8 @@ std::string NativeErrorToString(int e) {
30 return ret; 30 return ret;
31#else 31#else
32 char err_str[255]; 32 char err_str[255];
33#if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) 33#if defined(ANDROID) || \
34 (defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)))
34 // Thread safe (GNU-specific) 35 // Thread safe (GNU-specific)
35 const char* str = strerror_r(e, err_str, sizeof(err_str)); 36 const char* str = strerror_r(e, err_str, sizeof(err_str));
36 return std::string(str); 37 return std::string(str);
diff --git a/src/common/fs/file.cpp b/src/common/fs/file.cpp
index 656b03cc5..b0b25eb43 100644
--- a/src/common/fs/file.cpp
+++ b/src/common/fs/file.cpp
@@ -5,6 +5,9 @@
5 5
6#include "common/fs/file.h" 6#include "common/fs/file.h"
7#include "common/fs/fs.h" 7#include "common/fs/fs.h"
8#ifdef ANDROID
9#include "common/fs/fs_android.h"
10#endif
8#include "common/logging/log.h" 11#include "common/logging/log.h"
9 12
10#ifdef _WIN32 13#ifdef _WIN32
@@ -252,6 +255,23 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
252 } else { 255 } else {
253 _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type)); 256 _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
254 } 257 }
258#elif ANDROID
259 if (Android::IsContentUri(path)) {
260 ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!");
261 const auto fd = Android::OpenContentUri(path, Android::OpenMode::Read);
262 if (fd != -1) {
263 file = fdopen(fd, "r");
264 const auto error_num = errno;
265 if (error_num != 0 && file == nullptr) {
266 LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(),
267 strerror(error_num));
268 }
269 } else {
270 LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
271 }
272 } else {
273 file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
274 }
255#else 275#else
256 file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); 276 file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
257#endif 277#endif
@@ -372,6 +392,23 @@ u64 IOFile::GetSize() const {
372 // Flush any unwritten buffered data into the file prior to retrieving the file size. 392 // Flush any unwritten buffered data into the file prior to retrieving the file size.
373 std::fflush(file); 393 std::fflush(file);
374 394
395#if ANDROID
396 u64 file_size = 0;
397 if (Android::IsContentUri(file_path)) {
398 file_size = Android::GetSize(file_path);
399 } else {
400 std::error_code ec;
401
402 file_size = fs::file_size(file_path, ec);
403
404 if (ec) {
405 LOG_ERROR(Common_Filesystem,
406 "Failed to retrieve the file size of path={}, ec_message={}",
407 PathToUTF8String(file_path), ec.message());
408 return 0;
409 }
410 }
411#else
375 std::error_code ec; 412 std::error_code ec;
376 413
377 const auto file_size = fs::file_size(file_path, ec); 414 const auto file_size = fs::file_size(file_path, ec);
@@ -381,6 +418,7 @@ u64 IOFile::GetSize() const {
381 PathToUTF8String(file_path), ec.message()); 418 PathToUTF8String(file_path), ec.message());
382 return 0; 419 return 0;
383 } 420 }
421#endif
384 422
385 return file_size; 423 return file_size;
386} 424}
diff --git a/src/common/fs/fs_android.cpp b/src/common/fs/fs_android.cpp
new file mode 100644
index 000000000..298a79bac
--- /dev/null
+++ b/src/common/fs/fs_android.cpp
@@ -0,0 +1,98 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "common/fs/fs_android.h"
5
6namespace Common::FS::Android {
7
8JNIEnv* GetEnvForThread() {
9 thread_local static struct OwnedEnv {
10 OwnedEnv() {
11 status = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
12 if (status == JNI_EDETACHED)
13 g_jvm->AttachCurrentThread(&env, nullptr);
14 }
15
16 ~OwnedEnv() {
17 if (status == JNI_EDETACHED)
18 g_jvm->DetachCurrentThread();
19 }
20
21 int status;
22 JNIEnv* env = nullptr;
23 } owned;
24 return owned.env;
25}
26
27void RegisterCallbacks(JNIEnv* env, jclass clazz) {
28 env->GetJavaVM(&g_jvm);
29 native_library = clazz;
30
31#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
32 F(JMethodID, JMethodName, Signature)
33#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
34 F(JMethodID, JMethodName, Signature)
35#define F(JMethodID, JMethodName, Signature) \
36 JMethodID = env->GetStaticMethodID(native_library, JMethodName, Signature);
37 ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
38 ANDROID_STORAGE_FUNCTIONS(FS)
39#undef F
40#undef FS
41#undef FR
42}
43
44void UnRegisterCallbacks() {
45#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
46#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
47#define F(JMethodID) JMethodID = nullptr;
48 ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
49 ANDROID_STORAGE_FUNCTIONS(FS)
50#undef F
51#undef FS
52#undef FR
53}
54
55bool IsContentUri(const std::string& path) {
56 constexpr std::string_view prefix = "content://";
57 if (path.size() < prefix.size()) [[unlikely]] {
58 return false;
59 }
60
61 return path.find(prefix) == 0;
62}
63
64int OpenContentUri(const std::string& filepath, OpenMode openmode) {
65 if (open_content_uri == nullptr)
66 return -1;
67
68 const char* mode = "";
69 switch (openmode) {
70 case OpenMode::Read:
71 mode = "r";
72 break;
73 default:
74 UNIMPLEMENTED();
75 return -1;
76 }
77 auto env = GetEnvForThread();
78 jstring j_filepath = env->NewStringUTF(filepath.c_str());
79 jstring j_mode = env->NewStringUTF(mode);
80 return env->CallStaticIntMethod(native_library, open_content_uri, j_filepath, j_mode);
81}
82
83#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
84 F(FunctionName, ReturnValue, JMethodID, Caller)
85#define F(FunctionName, ReturnValue, JMethodID, Caller) \
86 ReturnValue FunctionName(const std::string& filepath) { \
87 if (JMethodID == nullptr) { \
88 return 0; \
89 } \
90 auto env = GetEnvForThread(); \
91 jstring j_filepath = env->NewStringUTF(filepath.c_str()); \
92 return env->Caller(native_library, JMethodID, j_filepath); \
93 }
94ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
95#undef F
96#undef FR
97
98} // namespace Common::FS::Android
diff --git a/src/common/fs/fs_android.h b/src/common/fs/fs_android.h
new file mode 100644
index 000000000..bb8a52648
--- /dev/null
+++ b/src/common/fs/fs_android.h
@@ -0,0 +1,62 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <string>
7#include <vector>
8#include <jni.h>
9
10#define ANDROID_STORAGE_FUNCTIONS(V) \
11 V(OpenContentUri, int, (const std::string& filepath, OpenMode openmode), open_content_uri, \
12 "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I")
13
14#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
15 V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J")
16
17namespace Common::FS::Android {
18
19static JavaVM* g_jvm = nullptr;
20static jclass native_library = nullptr;
21
22#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
23#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
24#define F(JMethodID) static jmethodID JMethodID = nullptr;
25ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
26ANDROID_STORAGE_FUNCTIONS(FS)
27#undef F
28#undef FS
29#undef FR
30
31enum class OpenMode {
32 Read,
33 Write,
34 ReadWrite,
35 WriteAppend,
36 WriteTruncate,
37 ReadWriteAppend,
38 ReadWriteTruncate,
39 Never
40};
41
42void RegisterCallbacks(JNIEnv* env, jclass clazz);
43
44void UnRegisterCallbacks();
45
46bool IsContentUri(const std::string& path);
47
48#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
49 F(FunctionName, Parameters, ReturnValue)
50#define F(FunctionName, Parameters, ReturnValue) ReturnValue FunctionName Parameters;
51ANDROID_STORAGE_FUNCTIONS(FS)
52#undef F
53#undef FS
54
55#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
56 F(FunctionName, ReturnValue)
57#define F(FunctionName, ReturnValue) ReturnValue FunctionName(const std::string& filepath);
58ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
59#undef F
60#undef FR
61
62} // namespace Common::FS::Android
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index defa3e918..e026a13d9 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -6,6 +6,9 @@
6#include <unordered_map> 6#include <unordered_map>
7 7
8#include "common/fs/fs.h" 8#include "common/fs/fs.h"
9#ifdef ANDROID
10#include "common/fs/fs_android.h"
11#endif
9#include "common/fs/fs_paths.h" 12#include "common/fs/fs_paths.h"
10#include "common/fs/path_util.h" 13#include "common/fs/path_util.h"
11#include "common/logging/log.h" 14#include "common/logging/log.h"
@@ -80,9 +83,7 @@ public:
80 yuzu_paths.insert_or_assign(yuzu_path, new_path); 83 yuzu_paths.insert_or_assign(yuzu_path, new_path);
81 } 84 }
82 85
83private: 86 void Reinitialize(fs::path yuzu_path = {}) {
84 PathManagerImpl() {
85 fs::path yuzu_path;
86 fs::path yuzu_path_cache; 87 fs::path yuzu_path_cache;
87 fs::path yuzu_path_config; 88 fs::path yuzu_path_config;
88 89
@@ -95,6 +96,10 @@ private:
95 96
96 yuzu_path_cache = yuzu_path / CACHE_DIR; 97 yuzu_path_cache = yuzu_path / CACHE_DIR;
97 yuzu_path_config = yuzu_path / CONFIG_DIR; 98 yuzu_path_config = yuzu_path / CONFIG_DIR;
99#elif ANDROID
100 ASSERT(!yuzu_path.empty());
101 yuzu_path_cache = yuzu_path / CACHE_DIR;
102 yuzu_path_config = yuzu_path / CONFIG_DIR;
98#else 103#else
99 yuzu_path = GetCurrentDir() / PORTABLE_DIR; 104 yuzu_path = GetCurrentDir() / PORTABLE_DIR;
100 105
@@ -122,6 +127,11 @@ private:
122 GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); 127 GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
123 } 128 }
124 129
130private:
131 PathManagerImpl() {
132 Reinitialize();
133 }
134
125 ~PathManagerImpl() = default; 135 ~PathManagerImpl() = default;
126 136
127 void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) { 137 void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
@@ -210,6 +220,10 @@ fs::path RemoveTrailingSeparators(const fs::path& path) {
210 return fs::path{string_path}; 220 return fs::path{string_path};
211} 221}
212 222
223void SetAppDirectory(const std::string& app_directory) {
224 PathManagerImpl::GetInstance().Reinitialize(app_directory);
225}
226
213const fs::path& GetYuzuPath(YuzuPath yuzu_path) { 227const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
214 return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path); 228 return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
215} 229}
@@ -350,6 +364,12 @@ std::vector<std::string> SplitPathComponents(std::string_view filename) {
350 364
351std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { 365std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
352 std::string path(path_); 366 std::string path(path_);
367#ifdef ANDROID
368 if (Android::IsContentUri(path)) {
369 return path;
370 }
371#endif // ANDROID
372
353 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; 373 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
354 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/'; 374 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
355 375
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index 13d713f1e..7cfe85b70 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -181,6 +181,14 @@ template <typename Path>
181#endif 181#endif
182 182
183/** 183/**
184 * Sets the directory used for application storage. Used on Android where we do not know internal
185 * storage until informed by the frontend.
186 *
187 * @param app_directory Directory to use for application storage.
188 */
189void SetAppDirectory(const std::string& app_directory);
190
191/**
184 * Gets the filesystem path associated with the YuzuPath enum. 192 * Gets the filesystem path associated with the YuzuPath enum.
185 * 193 *
186 * @param yuzu_path YuzuPath enum 194 * @param yuzu_path YuzuPath enum
diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp
index 01457d8c6..ba22595e0 100644
--- a/src/common/host_memory.cpp
+++ b/src/common/host_memory.cpp
@@ -11,6 +11,10 @@
11 11
12#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv 12#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv
13 13
14#ifdef ANDROID
15#include <android/sharedmem.h>
16#endif
17
14#ifndef _GNU_SOURCE 18#ifndef _GNU_SOURCE
15#define _GNU_SOURCE 19#define _GNU_SOURCE
16#endif 20#endif
@@ -367,17 +371,20 @@ public:
367 } 371 }
368 372
369 // Backing memory initialization 373 // Backing memory initialization
370#if defined(__FreeBSD__) && __FreeBSD__ < 13 374#ifdef ANDROID
375 fd = ASharedMemory_create("HostMemory", backing_size);
376#elif defined(__FreeBSD__) && __FreeBSD__ < 13
371 // XXX Drop after FreeBSD 12.* reaches EOL on 2024-06-30 377 // XXX Drop after FreeBSD 12.* reaches EOL on 2024-06-30
372 fd = shm_open(SHM_ANON, O_RDWR, 0600); 378 fd = shm_open(SHM_ANON, O_RDWR, 0600);
373#else 379#else
374 fd = memfd_create("HostMemory", 0); 380 fd = memfd_create("HostMemory", 0);
375#endif 381#endif
376 if (fd == -1) { 382 if (fd < 0) {
377 LOG_CRITICAL(HW_Memory, "memfd_create failed: {}", strerror(errno)); 383 LOG_CRITICAL(HW_Memory, "memfd_create failed: {}", strerror(errno));
378 throw std::bad_alloc{}; 384 throw std::bad_alloc{};
379 } 385 }
380 386
387#ifndef ANDROID
381 // Defined to extend the file with zeros 388 // Defined to extend the file with zeros
382 int ret = ftruncate(fd, backing_size); 389 int ret = ftruncate(fd, backing_size);
383 if (ret != 0) { 390 if (ret != 0) {
@@ -385,6 +392,7 @@ public:
385 strerror(errno)); 392 strerror(errno));
386 throw std::bad_alloc{}; 393 throw std::bad_alloc{};
387 } 394 }
395#endif
388 396
389 backing_base = static_cast<u8*>( 397 backing_base = static_cast<u8*>(
390 mmap(nullptr, backing_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); 398 mmap(nullptr, backing_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index f96c7c222..6e8e8eb36 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -155,6 +155,26 @@ public:
155 void EnableForStacktrace() override {} 155 void EnableForStacktrace() override {}
156}; 156};
157 157
158#ifdef ANDROID
159/**
160 * Backend that writes to the Android logcat
161 */
162class LogcatBackend : public Backend {
163public:
164 explicit LogcatBackend() = default;
165
166 ~LogcatBackend() override = default;
167
168 void Write(const Entry& entry) override {
169 PrintMessageToLogcat(entry);
170 }
171
172 void Flush() override {}
173
174 void EnableForStacktrace() override {}
175};
176#endif
177
158bool initialization_in_progress_suppress_logging = true; 178bool initialization_in_progress_suppress_logging = true;
159 179
160/** 180/**
@@ -260,6 +280,9 @@ private:
260 lambda(static_cast<Backend&>(debugger_backend)); 280 lambda(static_cast<Backend&>(debugger_backend));
261 lambda(static_cast<Backend&>(color_console_backend)); 281 lambda(static_cast<Backend&>(color_console_backend));
262 lambda(static_cast<Backend&>(file_backend)); 282 lambda(static_cast<Backend&>(file_backend));
283#ifdef ANDROID
284 lambda(static_cast<Backend&>(lc_backend));
285#endif
263 } 286 }
264 287
265 static void Deleter(Impl* ptr) { 288 static void Deleter(Impl* ptr) {
@@ -272,6 +295,9 @@ private:
272 DebuggerBackend debugger_backend{}; 295 DebuggerBackend debugger_backend{};
273 ColorConsoleBackend color_console_backend{}; 296 ColorConsoleBackend color_console_backend{};
274 FileBackend file_backend; 297 FileBackend file_backend;
298#ifdef ANDROID
299 LogcatBackend lc_backend{};
300#endif
275 301
276 MPSCQueue<Entry> message_queue{}; 302 MPSCQueue<Entry> message_queue{};
277 std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; 303 std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()};
diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp
index 09398ea64..2c453177b 100644
--- a/src/common/logging/text_formatter.cpp
+++ b/src/common/logging/text_formatter.cpp
@@ -8,6 +8,10 @@
8#include <windows.h> 8#include <windows.h>
9#endif 9#endif
10 10
11#ifdef ANDROID
12#include <android/log.h>
13#endif
14
11#include "common/assert.h" 15#include "common/assert.h"
12#include "common/logging/filter.h" 16#include "common/logging/filter.h"
13#include "common/logging/log.h" 17#include "common/logging/log.h"
@@ -106,4 +110,35 @@ void PrintColoredMessage(const Entry& entry) {
106#undef ESC 110#undef ESC
107#endif 111#endif
108} 112}
113
114void PrintMessageToLogcat(const Entry& entry) {
115#ifdef ANDROID
116 const auto str = FormatLogMessage(entry);
117
118 android_LogPriority android_log_priority;
119 switch (entry.log_level) {
120 case Level::Trace:
121 android_log_priority = ANDROID_LOG_VERBOSE;
122 break;
123 case Level::Debug:
124 android_log_priority = ANDROID_LOG_DEBUG;
125 break;
126 case Level::Info:
127 android_log_priority = ANDROID_LOG_INFO;
128 break;
129 case Level::Warning:
130 android_log_priority = ANDROID_LOG_WARN;
131 break;
132 case Level::Error:
133 android_log_priority = ANDROID_LOG_ERROR;
134 break;
135 case Level::Critical:
136 android_log_priority = ANDROID_LOG_FATAL;
137 break;
138 case Level::Count:
139 UNREACHABLE();
140 }
141 __android_log_print(android_log_priority, "YuzuNative", "%s", str.c_str());
142#endif
143}
109} // namespace Common::Log 144} // namespace Common::Log
diff --git a/src/common/logging/text_formatter.h b/src/common/logging/text_formatter.h
index 0d0ec4370..68417420b 100644
--- a/src/common/logging/text_formatter.h
+++ b/src/common/logging/text_formatter.h
@@ -15,4 +15,6 @@ std::string FormatLogMessage(const Entry& entry);
15void PrintMessage(const Entry& entry); 15void PrintMessage(const Entry& entry);
16/// Prints the same message as `PrintMessage`, but colored according to the severity level. 16/// Prints the same message as `PrintMessage`, but colored according to the severity level.
17void PrintColoredMessage(const Entry& entry); 17void PrintColoredMessage(const Entry& entry);
18/// Formats and prints a log entry to the android logcat.
19void PrintMessageToLogcat(const Entry& entry);
18} // namespace Common::Log 20} // namespace Common::Log
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index e8bf68866..99602699a 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -142,6 +142,7 @@ add_library(core STATIC
142 frontend/emu_window.h 142 frontend/emu_window.h
143 frontend/framebuffer_layout.cpp 143 frontend/framebuffer_layout.cpp
144 frontend/framebuffer_layout.h 144 frontend/framebuffer_layout.h
145 frontend/graphics_context.h
145 hid/emulated_console.cpp 146 hid/emulated_console.cpp
146 hid/emulated_console.h 147 hid/emulated_console.h
147 hid/emulated_controller.cpp 148 hid/emulated_controller.cpp
diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp
index 65a9fe802..4ff2c50e5 100644
--- a/src/core/crypto/key_manager.cpp
+++ b/src/core/crypto/key_manager.cpp
@@ -569,6 +569,10 @@ std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket,
569} 569}
570 570
571KeyManager::KeyManager() { 571KeyManager::KeyManager() {
572 ReloadKeys();
573}
574
575void KeyManager::ReloadKeys() {
572 // Initialize keys 576 // Initialize keys
573 const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir); 577 const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir);
574 578
@@ -702,6 +706,10 @@ void KeyManager::LoadFromFile(const std::filesystem::path& file_path, bool is_ti
702 } 706 }
703} 707}
704 708
709bool KeyManager::AreKeysLoaded() const {
710 return !s128_keys.empty() && !s256_keys.empty();
711}
712
705bool KeyManager::BaseDeriveNecessary() const { 713bool KeyManager::BaseDeriveNecessary() const {
706 const auto check_key_existence = [this](auto key_type, u64 index1 = 0, u64 index2 = 0) { 714 const auto check_key_existence = [this](auto key_type, u64 index1 = 0, u64 index2 = 0) {
707 return !HasKey(key_type, index1, index2); 715 return !HasKey(key_type, index1, index2);
diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h
index 673cec463..8c864503b 100644
--- a/src/core/crypto/key_manager.h
+++ b/src/core/crypto/key_manager.h
@@ -267,6 +267,9 @@ public:
267 bool AddTicketCommon(Ticket raw); 267 bool AddTicketCommon(Ticket raw);
268 bool AddTicketPersonalized(Ticket raw); 268 bool AddTicketPersonalized(Ticket raw);
269 269
270 void ReloadKeys();
271 bool AreKeysLoaded() const;
272
270private: 273private:
271 KeyManager(); 274 KeyManager();
272 275
diff --git a/src/core/device_memory.cpp b/src/core/device_memory.cpp
index f8b5be2b4..de3f8ef8f 100644
--- a/src/core/device_memory.cpp
+++ b/src/core/device_memory.cpp
@@ -6,9 +6,15 @@
6 6
7namespace Core { 7namespace Core {
8 8
9#ifdef ANDROID
10constexpr size_t VirtualReserveSize = 1ULL << 38;
11#else
12constexpr size_t VirtualReserveSize = 1ULL << 39;
13#endif
14
9DeviceMemory::DeviceMemory() 15DeviceMemory::DeviceMemory()
10 : buffer{Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize(), 16 : buffer{Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize(),
11 1ULL << 39} {} 17 VirtualReserveSize} {}
12DeviceMemory::~DeviceMemory() = default; 18DeviceMemory::~DeviceMemory() = default;
13 19
14} // namespace Core 20} // namespace Core
diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp
index 1be2dccb0..d1f1ca8c9 100644
--- a/src/core/frontend/emu_window.cpp
+++ b/src/core/frontend/emu_window.cpp
@@ -6,8 +6,6 @@
6 6
7namespace Core::Frontend { 7namespace Core::Frontend {
8 8
9GraphicsContext::~GraphicsContext() = default;
10
11EmuWindow::EmuWindow() { 9EmuWindow::EmuWindow() {
12 // TODO: Find a better place to set this. 10 // TODO: Find a better place to set this.
13 config.min_client_area_size = 11 config.min_client_area_size =
diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h
index 1093800f6..a72df034e 100644
--- a/src/core/frontend/emu_window.h
+++ b/src/core/frontend/emu_window.h
@@ -5,11 +5,14 @@
5 5
6#include <memory> 6#include <memory>
7#include <utility> 7#include <utility>
8
8#include "common/common_types.h" 9#include "common/common_types.h"
9#include "core/frontend/framebuffer_layout.h" 10#include "core/frontend/framebuffer_layout.h"
10 11
11namespace Core::Frontend { 12namespace Core::Frontend {
12 13
14class GraphicsContext;
15
13/// Information for the Graphics Backends signifying what type of screen pointer is in 16/// Information for the Graphics Backends signifying what type of screen pointer is in
14/// WindowInformation 17/// WindowInformation
15enum class WindowSystemType { 18enum class WindowSystemType {
@@ -22,51 +25,6 @@ enum class WindowSystemType {
22}; 25};
23 26
24/** 27/**
25 * Represents a drawing context that supports graphics operations.
26 */
27class GraphicsContext {
28public:
29 virtual ~GraphicsContext();
30
31 /// Inform the driver to swap the front/back buffers and present the current image
32 virtual void SwapBuffers() {}
33
34 /// Makes the graphics context current for the caller thread
35 virtual void MakeCurrent() {}
36
37 /// Releases (dunno if this is the "right" word) the context from the caller thread
38 virtual void DoneCurrent() {}
39
40 class Scoped {
41 public:
42 [[nodiscard]] explicit Scoped(GraphicsContext& context_) : context(context_) {
43 context.MakeCurrent();
44 }
45 ~Scoped() {
46 if (active) {
47 context.DoneCurrent();
48 }
49 }
50
51 /// In the event that context was destroyed before the Scoped is destroyed, this provides a
52 /// mechanism to prevent calling a destroyed object's method during the deconstructor
53 void Cancel() {
54 active = false;
55 }
56
57 private:
58 GraphicsContext& context;
59 bool active{true};
60 };
61
62 /// Calls MakeCurrent on the context and calls DoneCurrent when the scope for the returned value
63 /// ends
64 [[nodiscard]] Scoped Acquire() {
65 return Scoped{*this};
66 }
67};
68
69/**
70 * Abstraction class used to provide an interface between emulation code and the frontend 28 * Abstraction class used to provide an interface between emulation code and the frontend
71 * (e.g. SDL, QGLWidget, GLFW, etc...). 29 * (e.g. SDL, QGLWidget, GLFW, etc...).
72 * 30 *
diff --git a/src/core/frontend/graphics_context.h b/src/core/frontend/graphics_context.h
new file mode 100644
index 000000000..7554c1583
--- /dev/null
+++ b/src/core/frontend/graphics_context.h
@@ -0,0 +1,62 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7
8#include "common/dynamic_library.h"
9
10namespace Core::Frontend {
11
12/**
13 * Represents a drawing context that supports graphics operations.
14 */
15class GraphicsContext {
16public:
17 virtual ~GraphicsContext() = default;
18
19 /// Inform the driver to swap the front/back buffers and present the current image
20 virtual void SwapBuffers() {}
21
22 /// Makes the graphics context current for the caller thread
23 virtual void MakeCurrent() {}
24
25 /// Releases (dunno if this is the "right" word) the context from the caller thread
26 virtual void DoneCurrent() {}
27
28 /// Gets the GPU driver library (used by Android only)
29 virtual std::shared_ptr<Common::DynamicLibrary> GetDriverLibrary() {
30 return {};
31 }
32
33 class Scoped {
34 public:
35 [[nodiscard]] explicit Scoped(GraphicsContext& context_) : context(context_) {
36 context.MakeCurrent();
37 }
38 ~Scoped() {
39 if (active) {
40 context.DoneCurrent();
41 }
42 }
43
44 /// In the event that context was destroyed before the Scoped is destroyed, this provides a
45 /// mechanism to prevent calling a destroyed object's method during the deconstructor
46 void Cancel() {
47 active = false;
48 }
49
50 private:
51 GraphicsContext& context;
52 bool active{true};
53 };
54
55 /// Calls MakeCurrent on the context and calls DoneCurrent when the scope for the returned value
56 /// ends
57 [[nodiscard]] Scoped Acquire() {
58 return Scoped{*this};
59 }
60};
61
62} // namespace Core::Frontend
diff --git a/src/core/hid/emulated_console.cpp b/src/core/hid/emulated_console.cpp
index 17d663379..b4afd930e 100644
--- a/src/core/hid/emulated_console.cpp
+++ b/src/core/hid/emulated_console.cpp
@@ -13,7 +13,7 @@ EmulatedConsole::~EmulatedConsole() = default;
13void EmulatedConsole::ReloadFromSettings() { 13void EmulatedConsole::ReloadFromSettings() {
14 // Using first motion device from player 1. No need to assign any unique config at the moment 14 // Using first motion device from player 1. No need to assign any unique config at the moment
15 const auto& player = Settings::values.players.GetValue()[0]; 15 const auto& player = Settings::values.players.GetValue()[0];
16 motion_params = Common::ParamPackage(player.motions[0]); 16 motion_params[0] = Common::ParamPackage(player.motions[0]);
17 17
18 ReloadInput(); 18 ReloadInput();
19} 19}
@@ -74,14 +74,30 @@ void EmulatedConsole::ReloadInput() {
74 // If you load any device here add the equivalent to the UnloadInput() function 74 // If you load any device here add the equivalent to the UnloadInput() function
75 SetTouchParams(); 75 SetTouchParams();
76 76
77 motion_devices = Common::Input::CreateInputDevice(motion_params); 77 motion_params[1] = Common::ParamPackage{"engine:virtual_gamepad,port:8,motion:0"};
78 if (motion_devices) { 78
79 motion_devices->SetCallback({ 79 for (std::size_t index = 0; index < motion_devices.size(); ++index) {
80 motion_devices[index] = Common::Input::CreateInputDevice(motion_params[index]);
81 if (!motion_devices[index]) {
82 continue;
83 }
84 motion_devices[index]->SetCallback({
80 .on_change = 85 .on_change =
81 [this](const Common::Input::CallbackStatus& callback) { SetMotion(callback); }, 86 [this](const Common::Input::CallbackStatus& callback) { SetMotion(callback); },
82 }); 87 });
83 } 88 }
84 89
90 // Restore motion state
91 auto& emulated_motion = console.motion_values.emulated;
92 auto& motion = console.motion_state;
93 emulated_motion.ResetRotations();
94 emulated_motion.ResetQuaternion();
95 motion.accel = emulated_motion.GetAcceleration();
96 motion.gyro = emulated_motion.GetGyroscope();
97 motion.rotation = emulated_motion.GetRotations();
98 motion.orientation = emulated_motion.GetOrientation();
99 motion.is_at_rest = !emulated_motion.IsMoving(motion_sensitivity);
100
85 // Unique index for identifying touch device source 101 // Unique index for identifying touch device source
86 std::size_t index = 0; 102 std::size_t index = 0;
87 for (auto& touch_device : touch_devices) { 103 for (auto& touch_device : touch_devices) {
@@ -100,7 +116,9 @@ void EmulatedConsole::ReloadInput() {
100} 116}
101 117
102void EmulatedConsole::UnloadInput() { 118void EmulatedConsole::UnloadInput() {
103 motion_devices.reset(); 119 for (auto& motion : motion_devices) {
120 motion.reset();
121 }
104 for (auto& touch : touch_devices) { 122 for (auto& touch : touch_devices) {
105 touch.reset(); 123 touch.reset();
106 } 124 }
@@ -133,11 +151,11 @@ void EmulatedConsole::RestoreConfig() {
133} 151}
134 152
135Common::ParamPackage EmulatedConsole::GetMotionParam() const { 153Common::ParamPackage EmulatedConsole::GetMotionParam() const {
136 return motion_params; 154 return motion_params[0];
137} 155}
138 156
139void EmulatedConsole::SetMotionParam(Common::ParamPackage param) { 157void EmulatedConsole::SetMotionParam(Common::ParamPackage param) {
140 motion_params = std::move(param); 158 motion_params[0] = std::move(param);
141 ReloadInput(); 159 ReloadInput();
142} 160}
143 161
diff --git a/src/core/hid/emulated_console.h b/src/core/hid/emulated_console.h
index 697ecd2d6..79114bb6d 100644
--- a/src/core/hid/emulated_console.h
+++ b/src/core/hid/emulated_console.h
@@ -29,10 +29,10 @@ struct ConsoleMotionInfo {
29 MotionInput emulated{}; 29 MotionInput emulated{};
30}; 30};
31 31
32using ConsoleMotionDevices = std::unique_ptr<Common::Input::InputDevice>; 32using ConsoleMotionDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, 2>;
33using TouchDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, MaxTouchDevices>; 33using TouchDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, MaxTouchDevices>;
34 34
35using ConsoleMotionParams = Common::ParamPackage; 35using ConsoleMotionParams = std::array<Common::ParamPackage, 2>;
36using TouchParams = std::array<Common::ParamPackage, MaxTouchDevices>; 36using TouchParams = std::array<Common::ParamPackage, MaxTouchDevices>;
37 37
38using ConsoleMotionValues = ConsoleMotionInfo; 38using ConsoleMotionValues = ConsoleMotionInfo;
diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index bbfea7117..0a7777732 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -193,6 +193,8 @@ void EmulatedController::LoadDevices() {
193 Common::Input::CreateInputDevice); 193 Common::Input::CreateInputDevice);
194 std::ranges::transform(virtual_stick_params, virtual_stick_devices.begin(), 194 std::ranges::transform(virtual_stick_params, virtual_stick_devices.begin(),
195 Common::Input::CreateInputDevice); 195 Common::Input::CreateInputDevice);
196 std::ranges::transform(virtual_motion_params, virtual_motion_devices.begin(),
197 Common::Input::CreateInputDevice);
196} 198}
197 199
198void EmulatedController::LoadTASParams() { 200void EmulatedController::LoadTASParams() {
@@ -253,6 +255,12 @@ void EmulatedController::LoadVirtualGamepadParams() {
253 for (auto& param : virtual_stick_params) { 255 for (auto& param : virtual_stick_params) {
254 param = common_params; 256 param = common_params;
255 } 257 }
258 for (auto& param : virtual_stick_params) {
259 param = common_params;
260 }
261 for (auto& param : virtual_motion_params) {
262 param = common_params;
263 }
256 264
257 // TODO(german77): Replace this with an input profile or something better 265 // TODO(german77): Replace this with an input profile or something better
258 virtual_button_params[Settings::NativeButton::A].Set("button", 0); 266 virtual_button_params[Settings::NativeButton::A].Set("button", 0);
@@ -284,6 +292,9 @@ void EmulatedController::LoadVirtualGamepadParams() {
284 virtual_stick_params[Settings::NativeAnalog::LStick].Set("range", 1.0f); 292 virtual_stick_params[Settings::NativeAnalog::LStick].Set("range", 1.0f);
285 virtual_stick_params[Settings::NativeAnalog::RStick].Set("deadzone", 0.0f); 293 virtual_stick_params[Settings::NativeAnalog::RStick].Set("deadzone", 0.0f);
286 virtual_stick_params[Settings::NativeAnalog::RStick].Set("range", 1.0f); 294 virtual_stick_params[Settings::NativeAnalog::RStick].Set("range", 1.0f);
295
296 virtual_motion_params[Settings::NativeMotion::MotionLeft].Set("motion", 0);
297 virtual_motion_params[Settings::NativeMotion::MotionRight].Set("motion", 0);
287} 298}
288 299
289void EmulatedController::ReloadInput() { 300void EmulatedController::ReloadInput() {
@@ -463,6 +474,18 @@ void EmulatedController::ReloadInput() {
463 }, 474 },
464 }); 475 });
465 } 476 }
477
478 for (std::size_t index = 0; index < virtual_motion_devices.size(); ++index) {
479 if (!virtual_motion_devices[index]) {
480 continue;
481 }
482 virtual_motion_devices[index]->SetCallback({
483 .on_change =
484 [this, index](const Common::Input::CallbackStatus& callback) {
485 SetMotion(callback, index);
486 },
487 });
488 }
466 turbo_button_state = 0; 489 turbo_button_state = 0;
467} 490}
468 491
@@ -500,6 +523,9 @@ void EmulatedController::UnloadInput() {
500 for (auto& stick : virtual_stick_devices) { 523 for (auto& stick : virtual_stick_devices) {
501 stick.reset(); 524 stick.reset();
502 } 525 }
526 for (auto& motion : virtual_motion_devices) {
527 motion.reset();
528 }
503 for (auto& camera : camera_devices) { 529 for (auto& camera : camera_devices) {
504 camera.reset(); 530 camera.reset();
505 } 531 }
diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h
index 88fad2f56..09fe1a0ab 100644
--- a/src/core/hid/emulated_controller.h
+++ b/src/core/hid/emulated_controller.h
@@ -568,8 +568,10 @@ private:
568 // Virtual gamepad related variables 568 // Virtual gamepad related variables
569 ButtonParams virtual_button_params; 569 ButtonParams virtual_button_params;
570 StickParams virtual_stick_params; 570 StickParams virtual_stick_params;
571 ControllerMotionParams virtual_motion_params;
571 ButtonDevices virtual_button_devices; 572 ButtonDevices virtual_button_devices;
572 StickDevices virtual_stick_devices; 573 StickDevices virtual_stick_devices;
574 ControllerMotionDevices virtual_motion_devices;
573 575
574 mutable std::mutex mutex; 576 mutable std::mutex mutex;
575 mutable std::mutex callback_mutex; 577 mutable std::mutex callback_mutex;
diff --git a/src/core/hle/kernel/k_address_space_info.cpp b/src/core/hle/kernel/k_address_space_info.cpp
index c36eb5dc4..32173e52b 100644
--- a/src/core/hle/kernel/k_address_space_info.cpp
+++ b/src/core/hle/kernel/k_address_space_info.cpp
@@ -25,7 +25,12 @@ constexpr std::array<KAddressSpaceInfo, 13> AddressSpaceInfos{{
25 { .bit_width = 36, .address = 2_GiB , .size = 64_GiB - 2_GiB , .type = KAddressSpaceInfo::Type::MapLarge, }, 25 { .bit_width = 36, .address = 2_GiB , .size = 64_GiB - 2_GiB , .type = KAddressSpaceInfo::Type::MapLarge, },
26 { .bit_width = 36, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, }, 26 { .bit_width = 36, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, },
27 { .bit_width = 36, .address = Size_Invalid, .size = 6_GiB , .type = KAddressSpaceInfo::Type::Alias, }, 27 { .bit_width = 36, .address = Size_Invalid, .size = 6_GiB , .type = KAddressSpaceInfo::Type::Alias, },
28#ifdef ANDROID
29 // With Android, we use a 38-bit address space due to memory limitations. This should (safely) truncate ASLR region.
30 { .bit_width = 39, .address = 128_MiB , .size = 256_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
31#else
28 { .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, }, 32 { .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
33#endif
29 { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::MapSmall }, 34 { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::MapSmall },
30 { .bit_width = 39, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, }, 35 { .bit_width = 39, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, },
31 { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::Alias, }, 36 { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::Alias, },
diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp
index 63fd5bfd6..5542d6cbc 100644
--- a/src/core/hle/service/acc/profile_manager.cpp
+++ b/src/core/hle/service/acc/profile_manager.cpp
@@ -46,6 +46,7 @@ ProfileManager::ProfileManager() {
46 // Create an user if none are present 46 // Create an user if none are present
47 if (user_count == 0) { 47 if (user_count == 0) {
48 CreateNewUser(UUID::MakeRandom(), "yuzu"); 48 CreateNewUser(UUID::MakeRandom(), "yuzu");
49 WriteUserSaveFile();
49 } 50 }
50 51
51 auto current = 52 auto current =
diff --git a/src/input_common/drivers/virtual_amiibo.cpp b/src/input_common/drivers/virtual_amiibo.cpp
index 304f4c70b..f8bafe553 100644
--- a/src/input_common/drivers/virtual_amiibo.cpp
+++ b/src/input_common/drivers/virtual_amiibo.cpp
@@ -73,10 +73,7 @@ VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
73VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) { 73VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
74 const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read, 74 const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
75 Common::FS::FileType::BinaryFile}; 75 Common::FS::FileType::BinaryFile};
76 76 std::vector<u8> data{};
77 if (state != State::WaitingForAmiibo) {
78 return Info::WrongDeviceState;
79 }
80 77
81 if (!nfc_file.IsOpen()) { 78 if (!nfc_file.IsOpen()) {
82 return Info::UnableToLoad; 79 return Info::UnableToLoad;
@@ -85,14 +82,14 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
85 switch (nfc_file.GetSize()) { 82 switch (nfc_file.GetSize()) {
86 case AmiiboSize: 83 case AmiiboSize:
87 case AmiiboSizeWithoutPassword: 84 case AmiiboSizeWithoutPassword:
88 nfc_data.resize(AmiiboSize); 85 data.resize(AmiiboSize);
89 if (nfc_file.Read(nfc_data) < AmiiboSizeWithoutPassword) { 86 if (nfc_file.Read(data) < AmiiboSizeWithoutPassword) {
90 return Info::NotAnAmiibo; 87 return Info::NotAnAmiibo;
91 } 88 }
92 break; 89 break;
93 case MifareSize: 90 case MifareSize:
94 nfc_data.resize(MifareSize); 91 data.resize(MifareSize);
95 if (nfc_file.Read(nfc_data) < MifareSize) { 92 if (nfc_file.Read(data) < MifareSize) {
96 return Info::NotAnAmiibo; 93 return Info::NotAnAmiibo;
97 } 94 }
98 break; 95 break;
@@ -101,7 +98,28 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
101 } 98 }
102 99
103 file_path = filename; 100 file_path = filename;
101 return LoadAmiibo(data);
102}
103
104VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> data) {
105 if (state != State::WaitingForAmiibo) {
106 return Info::WrongDeviceState;
107 }
108
109 switch (data.size_bytes()) {
110 case AmiiboSize:
111 case AmiiboSizeWithoutPassword:
112 nfc_data.resize(AmiiboSize);
113 break;
114 case MifareSize:
115 nfc_data.resize(MifareSize);
116 break;
117 default:
118 return Info::NotAnAmiibo;
119 }
120
104 state = State::AmiiboIsOpen; 121 state = State::AmiiboIsOpen;
122 memcpy(nfc_data.data(), data.data(), data.size_bytes());
105 SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data}); 123 SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data});
106 return Info::Success; 124 return Info::Success;
107} 125}
diff --git a/src/input_common/drivers/virtual_amiibo.h b/src/input_common/drivers/virtual_amiibo.h
index 488d00b31..34e97cd91 100644
--- a/src/input_common/drivers/virtual_amiibo.h
+++ b/src/input_common/drivers/virtual_amiibo.h
@@ -4,6 +4,7 @@
4#pragma once 4#pragma once
5 5
6#include <array> 6#include <array>
7#include <span>
7#include <string> 8#include <string>
8#include <vector> 9#include <vector>
9 10
@@ -47,6 +48,7 @@ public:
47 State GetCurrentState() const; 48 State GetCurrentState() const;
48 49
49 Info LoadAmiibo(const std::string& amiibo_file); 50 Info LoadAmiibo(const std::string& amiibo_file);
51 Info LoadAmiibo(std::span<u8> data);
50 Info ReloadAmiibo(); 52 Info ReloadAmiibo();
51 Info CloseAmiibo(); 53 Info CloseAmiibo();
52 54
diff --git a/src/input_common/drivers/virtual_gamepad.cpp b/src/input_common/drivers/virtual_gamepad.cpp
index 7db945aa6..c15cbbe58 100644
--- a/src/input_common/drivers/virtual_gamepad.cpp
+++ b/src/input_common/drivers/virtual_gamepad.cpp
@@ -39,6 +39,22 @@ void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axi
39 SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value); 39 SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
40} 40}
41 41
42void VirtualGamepad::SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x,
43 float gyro_y, float gyro_z, float accel_x, float accel_y,
44 float accel_z) {
45 const auto identifier = GetIdentifier(player_index);
46 const BasicMotion motion_data{
47 .gyro_x = gyro_x,
48 .gyro_y = gyro_y,
49 .gyro_z = gyro_z,
50 .accel_x = accel_x,
51 .accel_y = accel_y,
52 .accel_z = accel_z,
53 .delta_timestamp = delta_timestamp,
54 };
55 SetMotion(identifier, 0, motion_data);
56}
57
42void VirtualGamepad::ResetControllers() { 58void VirtualGamepad::ResetControllers() {
43 for (std::size_t i = 0; i < PlayerIndexCount; i++) { 59 for (std::size_t i = 0; i < PlayerIndexCount; i++) {
44 SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f); 60 SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
diff --git a/src/input_common/drivers/virtual_gamepad.h b/src/input_common/drivers/virtual_gamepad.h
index 3df91cc6f..dfbc45a28 100644
--- a/src/input_common/drivers/virtual_gamepad.h
+++ b/src/input_common/drivers/virtual_gamepad.h
@@ -52,7 +52,7 @@ public:
52 void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value); 52 void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
53 53
54 /** 54 /**
55 * Sets the status of all buttons bound with the key to released 55 * Sets the status of a stick to a specific player index
56 * @param player_index the player number that will take this action 56 * @param player_index the player number that will take this action
57 * @param axis_id the id of the axis to move 57 * @param axis_id the id of the axis to move
58 * @param x_value the position of the stick in the x axis 58 * @param x_value the position of the stick in the x axis
@@ -62,6 +62,16 @@ public:
62 void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value, 62 void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
63 float y_value); 63 float y_value);
64 64
65 /**
66 * Sets the status of the motion sensor to a specific player index
67 * @param player_index the player number that will take this action
68 * @param delta_timestamp time passed since last reading
69 * @param gyro_x,gyro_y,gyro_z the gyro sensor readings
70 * @param accel_x,accel_y,accel_z the acelerometer reading
71 */
72 void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
73 float gyro_z, float accel_x, float accel_y, float accel_z);
74
65 /// Restores all inputs into the neutral position 75 /// Restores all inputs into the neutral position
66 void ResetControllers(); 76 void ResetControllers();
67 77
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp
index 4b3043b65..0ce73f289 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp
@@ -69,6 +69,11 @@ Id StorageAtomicU32(EmitContext& ctx, const IR::Value& binding, const IR::Value&
69Id StorageAtomicU64(EmitContext& ctx, const IR::Value& binding, const IR::Value& offset, Id value, 69Id StorageAtomicU64(EmitContext& ctx, const IR::Value& binding, const IR::Value& offset, Id value,
70 Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id), 70 Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id),
71 Id (Sirit::Module::*non_atomic_func)(Id, Id, Id)) { 71 Id (Sirit::Module::*non_atomic_func)(Id, Id, Id)) {
72 if (!ctx.profile.support_descriptor_aliasing) {
73 LOG_WARNING(Shader_SPIRV, "Descriptor aliasing not supported, this cannot be atomic.");
74 return ctx.ConstantNull(ctx.U64);
75 }
76
72 if (ctx.profile.support_int64_atomics) { 77 if (ctx.profile.support_int64_atomics) {
73 const Id pointer{StoragePointer(ctx, ctx.storage_types.U64, &StorageDefinitions::U64, 78 const Id pointer{StoragePointer(ctx, ctx.storage_types.U64, &StorageDefinitions::U64,
74 binding, offset, sizeof(u64))}; 79 binding, offset, sizeof(u64))};
@@ -86,6 +91,11 @@ Id StorageAtomicU64(EmitContext& ctx, const IR::Value& binding, const IR::Value&
86 91
87Id StorageAtomicU32x2(EmitContext& ctx, const IR::Value& binding, const IR::Value& offset, Id value, 92Id StorageAtomicU32x2(EmitContext& ctx, const IR::Value& binding, const IR::Value& offset, Id value,
88 Id (Sirit::Module::*non_atomic_func)(Id, Id, Id)) { 93 Id (Sirit::Module::*non_atomic_func)(Id, Id, Id)) {
94 if (!ctx.profile.support_descriptor_aliasing) {
95 LOG_WARNING(Shader_SPIRV, "Descriptor aliasing not supported, this cannot be atomic.");
96 return ctx.ConstantNull(ctx.U32[2]);
97 }
98
89 LOG_WARNING(Shader_SPIRV, "Int64 atomics not supported, fallback to non-atomic"); 99 LOG_WARNING(Shader_SPIRV, "Int64 atomics not supported, fallback to non-atomic");
90 const Id pointer{StoragePointer(ctx, ctx.storage_types.U32x2, &StorageDefinitions::U32x2, 100 const Id pointer{StoragePointer(ctx, ctx.storage_types.U32x2, &StorageDefinitions::U32x2,
91 binding, offset, sizeof(u32[2]))}; 101 binding, offset, sizeof(u32[2]))};
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp
index 07c2b7b8a..2868fc57d 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp
@@ -10,27 +10,6 @@
10 10
11namespace Shader::Backend::SPIRV { 11namespace Shader::Backend::SPIRV {
12namespace { 12namespace {
13struct AttrInfo {
14 Id pointer;
15 Id id;
16 bool needs_cast;
17};
18
19std::optional<AttrInfo> AttrTypes(EmitContext& ctx, u32 index) {
20 const AttributeType type{ctx.runtime_info.generic_input_types.at(index)};
21 switch (type) {
22 case AttributeType::Float:
23 return AttrInfo{ctx.input_f32, ctx.F32[1], false};
24 case AttributeType::UnsignedInt:
25 return AttrInfo{ctx.input_u32, ctx.U32[1], true};
26 case AttributeType::SignedInt:
27 return AttrInfo{ctx.input_s32, ctx.TypeInt(32, true), true};
28 case AttributeType::Disabled:
29 return std::nullopt;
30 }
31 throw InvalidArgument("Invalid attribute type {}", type);
32}
33
34template <typename... Args> 13template <typename... Args>
35Id AttrPointer(EmitContext& ctx, Id pointer_type, Id vertex, Id base, Args&&... args) { 14Id AttrPointer(EmitContext& ctx, Id pointer_type, Id vertex, Id base, Args&&... args) {
36 switch (ctx.stage) { 15 switch (ctx.stage) {
@@ -302,15 +281,26 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, Id vertex) {
302 const u32 element{static_cast<u32>(attr) % 4}; 281 const u32 element{static_cast<u32>(attr) % 4};
303 if (IR::IsGeneric(attr)) { 282 if (IR::IsGeneric(attr)) {
304 const u32 index{IR::GenericAttributeIndex(attr)}; 283 const u32 index{IR::GenericAttributeIndex(attr)};
305 const std::optional<AttrInfo> type{AttrTypes(ctx, index)}; 284 const auto& generic{ctx.input_generics.at(index)};
306 if (!type || !ctx.runtime_info.previous_stage_stores.Generic(index, element)) { 285 if (!ValidId(generic.id)) {
307 // Attribute is disabled or varying component is not written 286 // Attribute is disabled or varying component is not written
308 return ctx.Const(element == 3 ? 1.0f : 0.0f); 287 return ctx.Const(element == 3 ? 1.0f : 0.0f);
309 } 288 }
310 const Id generic_id{ctx.input_generics.at(index)}; 289 const Id pointer{
311 const Id pointer{AttrPointer(ctx, type->pointer, vertex, generic_id, ctx.Const(element))}; 290 AttrPointer(ctx, generic.pointer_type, vertex, generic.id, ctx.Const(element))};
312 const Id value{ctx.OpLoad(type->id, pointer)}; 291 const Id value{ctx.OpLoad(generic.component_type, pointer)};
313 return type->needs_cast ? ctx.OpBitcast(ctx.F32[1], value) : value; 292 return [&ctx, generic, value]() {
293 switch (generic.load_op) {
294 case InputGenericLoadOp::Bitcast:
295 return ctx.OpBitcast(ctx.F32[1], value);
296 case InputGenericLoadOp::SToF:
297 return ctx.OpConvertSToF(ctx.F32[1], value);
298 case InputGenericLoadOp::UToF:
299 return ctx.OpConvertUToF(ctx.F32[1], value);
300 default:
301 return value;
302 };
303 }();
314 } 304 }
315 switch (attr) { 305 switch (attr) {
316 case IR::Attribute::PrimitiveId: 306 case IR::Attribute::PrimitiveId:
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp
index c5db19d09..77ff8c573 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp
@@ -17,7 +17,22 @@ Id GetThreadId(EmitContext& ctx) {
17Id WarpExtract(EmitContext& ctx, Id value) { 17Id WarpExtract(EmitContext& ctx, Id value) {
18 const Id thread_id{GetThreadId(ctx)}; 18 const Id thread_id{GetThreadId(ctx)};
19 const Id local_index{ctx.OpShiftRightArithmetic(ctx.U32[1], thread_id, ctx.Const(5U))}; 19 const Id local_index{ctx.OpShiftRightArithmetic(ctx.U32[1], thread_id, ctx.Const(5U))};
20 return ctx.OpVectorExtractDynamic(ctx.U32[1], value, local_index); 20 if (ctx.profile.has_broken_spirv_subgroup_mask_vector_extract_dynamic) {
21 const Id c0_sel{ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, local_index, ctx.Const(0U)),
22 ctx.OpCompositeExtract(ctx.U32[1], value, 0U), ctx.Const(0U))};
23 const Id c1_sel{ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, local_index, ctx.Const(1U)),
24 ctx.OpCompositeExtract(ctx.U32[1], value, 1U), ctx.Const(0U))};
25 const Id c2_sel{ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, local_index, ctx.Const(2U)),
26 ctx.OpCompositeExtract(ctx.U32[1], value, 2U), ctx.Const(0U))};
27 const Id c3_sel{ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, local_index, ctx.Const(3U)),
28 ctx.OpCompositeExtract(ctx.U32[1], value, 3U), ctx.Const(0U))};
29 const Id c0_or_c1{ctx.OpBitwiseOr(ctx.U32[1], c0_sel, c1_sel)};
30 const Id c2_or_c3{ctx.OpBitwiseOr(ctx.U32[1], c2_sel, c3_sel)};
31 const Id c0_or_c1_or_c2_or_c3{ctx.OpBitwiseOr(ctx.U32[1], c0_or_c1, c2_or_c3)};
32 return c0_or_c1_or_c2_or_c3;
33 } else {
34 return ctx.OpVectorExtractDynamic(ctx.U32[1], value, local_index);
35 }
21} 36}
22 37
23Id LoadMask(EmitContext& ctx, Id mask) { 38Id LoadMask(EmitContext& ctx, Id mask) {
diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
index 47739794f..fd15f47ea 100644
--- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
+++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
@@ -25,12 +25,6 @@ enum class Operation {
25 FPMax, 25 FPMax,
26}; 26};
27 27
28struct AttrInfo {
29 Id pointer;
30 Id id;
31 bool needs_cast;
32};
33
34Id ImageType(EmitContext& ctx, const TextureDescriptor& desc) { 28Id ImageType(EmitContext& ctx, const TextureDescriptor& desc) {
35 const spv::ImageFormat format{spv::ImageFormat::Unknown}; 29 const spv::ImageFormat format{spv::ImageFormat::Unknown};
36 const Id type{ctx.F32[1]}; 30 const Id type{ctx.F32[1]};
@@ -206,23 +200,37 @@ Id GetAttributeType(EmitContext& ctx, AttributeType type) {
206 return ctx.TypeVector(ctx.TypeInt(32, true), 4); 200 return ctx.TypeVector(ctx.TypeInt(32, true), 4);
207 case AttributeType::UnsignedInt: 201 case AttributeType::UnsignedInt:
208 return ctx.U32[4]; 202 return ctx.U32[4];
203 case AttributeType::SignedScaled:
204 return ctx.profile.support_scaled_attributes ? ctx.F32[4]
205 : ctx.TypeVector(ctx.TypeInt(32, true), 4);
206 case AttributeType::UnsignedScaled:
207 return ctx.profile.support_scaled_attributes ? ctx.F32[4] : ctx.U32[4];
209 case AttributeType::Disabled: 208 case AttributeType::Disabled:
210 break; 209 break;
211 } 210 }
212 throw InvalidArgument("Invalid attribute type {}", type); 211 throw InvalidArgument("Invalid attribute type {}", type);
213} 212}
214 213
215std::optional<AttrInfo> AttrTypes(EmitContext& ctx, u32 index) { 214InputGenericInfo GetAttributeInfo(EmitContext& ctx, AttributeType type, Id id) {
216 const AttributeType type{ctx.runtime_info.generic_input_types.at(index)};
217 switch (type) { 215 switch (type) {
218 case AttributeType::Float: 216 case AttributeType::Float:
219 return AttrInfo{ctx.input_f32, ctx.F32[1], false}; 217 return InputGenericInfo{id, ctx.input_f32, ctx.F32[1], InputGenericLoadOp::None};
220 case AttributeType::UnsignedInt: 218 case AttributeType::UnsignedInt:
221 return AttrInfo{ctx.input_u32, ctx.U32[1], true}; 219 return InputGenericInfo{id, ctx.input_u32, ctx.U32[1], InputGenericLoadOp::Bitcast};
222 case AttributeType::SignedInt: 220 case AttributeType::SignedInt:
223 return AttrInfo{ctx.input_s32, ctx.TypeInt(32, true), true}; 221 return InputGenericInfo{id, ctx.input_s32, ctx.TypeInt(32, true),
222 InputGenericLoadOp::Bitcast};
223 case AttributeType::SignedScaled:
224 return ctx.profile.support_scaled_attributes
225 ? InputGenericInfo{id, ctx.input_f32, ctx.F32[1], InputGenericLoadOp::None}
226 : InputGenericInfo{id, ctx.input_s32, ctx.TypeInt(32, true),
227 InputGenericLoadOp::SToF};
228 case AttributeType::UnsignedScaled:
229 return ctx.profile.support_scaled_attributes
230 ? InputGenericInfo{id, ctx.input_f32, ctx.F32[1], InputGenericLoadOp::None}
231 : InputGenericInfo{id, ctx.input_u32, ctx.U32[1], InputGenericLoadOp::UToF};
224 case AttributeType::Disabled: 232 case AttributeType::Disabled:
225 return std::nullopt; 233 return InputGenericInfo{};
226 } 234 }
227 throw InvalidArgument("Invalid attribute type {}", type); 235 throw InvalidArgument("Invalid attribute type {}", type);
228} 236}
@@ -746,18 +754,29 @@ void EmitContext::DefineAttributeMemAccess(const Info& info) {
746 continue; 754 continue;
747 } 755 }
748 AddLabel(labels[label_index]); 756 AddLabel(labels[label_index]);
749 const auto type{AttrTypes(*this, static_cast<u32>(index))}; 757 const auto& generic{input_generics.at(index)};
750 if (!type) { 758 const Id generic_id{generic.id};
759 if (!ValidId(generic_id)) {
751 OpReturnValue(Const(0.0f)); 760 OpReturnValue(Const(0.0f));
752 ++label_index; 761 ++label_index;
753 continue; 762 continue;
754 } 763 }
755 const Id generic_id{input_generics.at(index)}; 764 const Id pointer{
756 const Id pointer{is_array 765 is_array ? OpAccessChain(generic.pointer_type, generic_id, vertex, masked_index)
757 ? OpAccessChain(type->pointer, generic_id, vertex, masked_index) 766 : OpAccessChain(generic.pointer_type, generic_id, masked_index)};
758 : OpAccessChain(type->pointer, generic_id, masked_index)}; 767 const Id value{OpLoad(generic.component_type, pointer)};
759 const Id value{OpLoad(type->id, pointer)}; 768 const Id result{[this, generic, value]() {
760 const Id result{type->needs_cast ? OpBitcast(F32[1], value) : value}; 769 switch (generic.load_op) {
770 case InputGenericLoadOp::Bitcast:
771 return OpBitcast(F32[1], value);
772 case InputGenericLoadOp::SToF:
773 return OpConvertSToF(F32[1], value);
774 case InputGenericLoadOp::UToF:
775 return OpConvertUToF(F32[1], value);
776 default:
777 return value;
778 };
779 }()};
761 OpReturnValue(result); 780 OpReturnValue(result);
762 ++label_index; 781 ++label_index;
763 } 782 }
@@ -1457,7 +1476,7 @@ void EmitContext::DefineInputs(const IR::Program& program) {
1457 const Id id{DefineInput(*this, type, true)}; 1476 const Id id{DefineInput(*this, type, true)};
1458 Decorate(id, spv::Decoration::Location, static_cast<u32>(index)); 1477 Decorate(id, spv::Decoration::Location, static_cast<u32>(index));
1459 Name(id, fmt::format("in_attr{}", index)); 1478 Name(id, fmt::format("in_attr{}", index));
1460 input_generics[index] = id; 1479 input_generics[index] = GetAttributeInfo(*this, input_type, id);
1461 1480
1462 if (info.passthrough.Generic(index) && profile.support_geometry_shader_passthrough) { 1481 if (info.passthrough.Generic(index) && profile.support_geometry_shader_passthrough) {
1463 Decorate(id, spv::Decoration::PassthroughNV); 1482 Decorate(id, spv::Decoration::PassthroughNV);
diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h
index 768a4fbb5..e63330f11 100644
--- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h
+++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h
@@ -95,6 +95,20 @@ struct StorageDefinitions {
95 Id U32x4{}; 95 Id U32x4{};
96}; 96};
97 97
98enum class InputGenericLoadOp {
99 None,
100 Bitcast,
101 SToF,
102 UToF,
103};
104
105struct InputGenericInfo {
106 Id id;
107 Id pointer_type;
108 Id component_type;
109 InputGenericLoadOp load_op;
110};
111
98struct GenericElementInfo { 112struct GenericElementInfo {
99 Id id{}; 113 Id id{};
100 u32 first_element{}; 114 u32 first_element{};
@@ -283,7 +297,7 @@ public:
283 297
284 bool need_input_position_indirect{}; 298 bool need_input_position_indirect{};
285 Id input_position{}; 299 Id input_position{};
286 std::array<Id, 32> input_generics{}; 300 std::array<InputGenericInfo, 32> input_generics{};
287 301
288 Id output_point_size{}; 302 Id output_point_size{};
289 Id output_position{}; 303 Id output_position{};
diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h
index 9f88fb440..9ca97f6a4 100644
--- a/src/shader_recompiler/profile.h
+++ b/src/shader_recompiler/profile.h
@@ -43,6 +43,7 @@ struct Profile {
43 bool support_gl_variable_aoffi{}; 43 bool support_gl_variable_aoffi{};
44 bool support_gl_sparse_textures{}; 44 bool support_gl_sparse_textures{};
45 bool support_gl_derivative_control{}; 45 bool support_gl_derivative_control{};
46 bool support_scaled_attributes{};
46 47
47 bool warp_size_potentially_larger_than_guest{}; 48 bool warp_size_potentially_larger_than_guest{};
48 49
@@ -77,6 +78,8 @@ struct Profile {
77 bool has_gl_bool_ref_bug{}; 78 bool has_gl_bool_ref_bug{};
78 /// Ignores SPIR-V ordered vs unordered using GLSL semantics 79 /// Ignores SPIR-V ordered vs unordered using GLSL semantics
79 bool ignore_nan_fp_comparisons{}; 80 bool ignore_nan_fp_comparisons{};
81 /// Some drivers have broken support for OpVectorExtractDynamic on subgroup mask inputs
82 bool has_broken_spirv_subgroup_mask_vector_extract_dynamic{};
80 83
81 u32 gl_max_compute_smem_size{}; 84 u32 gl_max_compute_smem_size{};
82}; 85};
diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h
index 549b81ef7..3b63c249f 100644
--- a/src/shader_recompiler/runtime_info.h
+++ b/src/shader_recompiler/runtime_info.h
@@ -17,6 +17,8 @@ enum class AttributeType : u8 {
17 Float, 17 Float,
18 SignedInt, 18 SignedInt,
19 UnsignedInt, 19 UnsignedInt,
20 SignedScaled,
21 UnsignedScaled,
20 Disabled, 22 Disabled,
21}; 23};
22 24
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 308d013d6..94e3000ba 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -281,7 +281,7 @@ create_target_directory_groups(video_core)
281target_link_libraries(video_core PUBLIC common core) 281target_link_libraries(video_core PUBLIC common core)
282target_link_libraries(video_core PUBLIC glad shader_recompiler stb) 282target_link_libraries(video_core PUBLIC glad shader_recompiler stb)
283 283
284if (YUZU_USE_BUNDLED_FFMPEG AND NOT WIN32) 284if (YUZU_USE_BUNDLED_FFMPEG AND NOT (WIN32 OR ANDROID))
285 add_dependencies(video_core ffmpeg-build) 285 add_dependencies(video_core ffmpeg-build)
286endif() 286endif()
287 287
@@ -345,3 +345,7 @@ endif()
345if (YUZU_ENABLE_LTO) 345if (YUZU_ENABLE_LTO)
346 set_property(TARGET video_core PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) 346 set_property(TARGET video_core PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
347endif() 347endif()
348
349if (ANDROID AND ARCHITECTURE_arm64)
350 target_link_libraries(video_core PRIVATE adrenotools)
351endif()
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 2f986097f..62d70e9f3 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -593,6 +593,12 @@ void Maxwell3D::ProcessQueryCondition() {
593} 593}
594 594
595void Maxwell3D::ProcessCounterReset() { 595void Maxwell3D::ProcessCounterReset() {
596#if ANDROID
597 if (!Settings::IsGPULevelHigh()) {
598 // This is problematic on Android, disable on GPU Normal.
599 return;
600 }
601#endif
596 switch (regs.clear_report_value) { 602 switch (regs.clear_report_value) {
597 case Regs::ClearReport::ZPassPixelCount: 603 case Regs::ClearReport::ZPassPixelCount:
598 rasterizer->ResetCounter(QueryType::SamplesPassed); 604 rasterizer->ResetCounter(QueryType::SamplesPassed);
@@ -614,6 +620,12 @@ std::optional<u64> Maxwell3D::GetQueryResult() {
614 case Regs::ReportSemaphore::Report::Payload: 620 case Regs::ReportSemaphore::Report::Payload:
615 return regs.report_semaphore.payload; 621 return regs.report_semaphore.payload;
616 case Regs::ReportSemaphore::Report::ZPassPixelCount64: 622 case Regs::ReportSemaphore::Report::ZPassPixelCount64:
623#if ANDROID
624 if (!Settings::IsGPULevelHigh()) {
625 // This is problematic on Android, disable on GPU Normal.
626 return 120;
627 }
628#endif
617 // Deferred. 629 // Deferred.
618 rasterizer->Query(regs.report_semaphore.Address(), QueryType::SamplesPassed, 630 rasterizer->Query(regs.report_semaphore.Address(), QueryType::SamplesPassed,
619 system.GPU().GetTicks()); 631 system.GPU().GetTicks());
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 295a416a8..456f733cf 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -14,6 +14,7 @@
14#include "core/core.h" 14#include "core/core.h"
15#include "core/core_timing.h" 15#include "core/core_timing.h"
16#include "core/frontend/emu_window.h" 16#include "core/frontend/emu_window.h"
17#include "core/frontend/graphics_context.h"
17#include "core/hle/service/nvdrv/nvdata.h" 18#include "core/hle/service/nvdrv/nvdata.h"
18#include "core/perf_stats.h" 19#include "core/perf_stats.h"
19#include "video_core/cdma_pusher.h" 20#include "video_core/cdma_pusher.h"
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 3c5317777..889144f38 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -7,7 +7,7 @@
7#include "common/settings.h" 7#include "common/settings.h"
8#include "common/thread.h" 8#include "common/thread.h"
9#include "core/core.h" 9#include "core/core.h"
10#include "core/frontend/emu_window.h" 10#include "core/frontend/graphics_context.h"
11#include "video_core/control/scheduler.h" 11#include "video_core/control/scheduler.h"
12#include "video_core/dma_pusher.h" 12#include "video_core/dma_pusher.h"
13#include "video_core/gpu.h" 13#include "video_core/gpu.h"
diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp
index e8761a747..2d3f58201 100644
--- a/src/video_core/renderer_base.cpp
+++ b/src/video_core/renderer_base.cpp
@@ -5,6 +5,7 @@
5 5
6#include "common/logging/log.h" 6#include "common/logging/log.h"
7#include "core/frontend/emu_window.h" 7#include "core/frontend/emu_window.h"
8#include "core/frontend/graphics_context.h"
8#include "video_core/renderer_base.h" 9#include "video_core/renderer_base.h"
9 10
10namespace VideoCore { 11namespace VideoCore {
diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h
index 8d20cbece..3e12a8813 100644
--- a/src/video_core/renderer_base.h
+++ b/src/video_core/renderer_base.h
@@ -9,7 +9,7 @@
9 9
10#include "common/common_funcs.h" 10#include "common/common_funcs.h"
11#include "common/common_types.h" 11#include "common/common_types.h"
12#include "core/frontend/emu_window.h" 12#include "core/frontend/framebuffer_layout.h"
13#include "video_core/gpu.h" 13#include "video_core/gpu.h"
14#include "video_core/rasterizer_interface.h" 14#include "video_core/rasterizer_interface.h"
15 15
@@ -89,6 +89,9 @@ public:
89 void RequestScreenshot(void* data, std::function<void(bool)> callback, 89 void RequestScreenshot(void* data, std::function<void(bool)> callback,
90 const Layout::FramebufferLayout& layout); 90 const Layout::FramebufferLayout& layout);
91 91
92 /// This is called to notify the rendering backend of a surface change
93 virtual void NotifySurfaceChanged() {}
94
92protected: 95protected:
93 Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle. 96 Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle.
94 std::unique_ptr<Core::Frontend::GraphicsContext> context; 97 std::unique_ptr<Core::Frontend::GraphicsContext> context;
diff --git a/src/video_core/renderer_null/renderer_null.cpp b/src/video_core/renderer_null/renderer_null.cpp
index e2a189b63..be92cc2f4 100644
--- a/src/video_core/renderer_null/renderer_null.cpp
+++ b/src/video_core/renderer_null/renderer_null.cpp
@@ -1,6 +1,8 @@
1// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project 1// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4#include "core/frontend/emu_window.h"
5#include "core/frontend/graphics_context.h"
4#include "video_core/renderer_null/renderer_null.h" 6#include "video_core/renderer_null/renderer_null.h"
5 7
6namespace Null { 8namespace Null {
diff --git a/src/video_core/renderer_opengl/gl_shader_context.h b/src/video_core/renderer_opengl/gl_shader_context.h
index ca2bd8e8e..207a75d42 100644
--- a/src/video_core/renderer_opengl/gl_shader_context.h
+++ b/src/video_core/renderer_opengl/gl_shader_context.h
@@ -4,6 +4,7 @@
4#pragma once 4#pragma once
5 5
6#include "core/frontend/emu_window.h" 6#include "core/frontend/emu_window.h"
7#include "core/frontend/graphics_context.h"
7#include "shader_recompiler/frontend/ir/basic_block.h" 8#include "shader_recompiler/frontend/ir/basic_block.h"
8#include "shader_recompiler/frontend/maxwell/control_flow.h" 9#include "shader_recompiler/frontend/maxwell/control_flow.h"
9 10
diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
index b75d7220d..9a0b10568 100644
--- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
+++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
@@ -347,6 +347,14 @@ VkPrimitiveTopology PrimitiveTopology([[maybe_unused]] const Device& device,
347 347
348VkFormat VertexFormat(const Device& device, Maxwell::VertexAttribute::Type type, 348VkFormat VertexFormat(const Device& device, Maxwell::VertexAttribute::Type type,
349 Maxwell::VertexAttribute::Size size) { 349 Maxwell::VertexAttribute::Size size) {
350 if (device.MustEmulateScaledFormats()) {
351 if (type == Maxwell::VertexAttribute::Type::SScaled) {
352 type = Maxwell::VertexAttribute::Type::SInt;
353 } else if (type == Maxwell::VertexAttribute::Type::UScaled) {
354 type = Maxwell::VertexAttribute::Type::UInt;
355 }
356 }
357
350 const VkFormat format{([&]() { 358 const VkFormat format{([&]() {
351 switch (type) { 359 switch (type) {
352 case Maxwell::VertexAttribute::Type::UnusedEnumDoNotUseBecauseItWillGoAway: 360 case Maxwell::VertexAttribute::Type::UnusedEnumDoNotUseBecauseItWillGoAway:
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
index 8e31eba34..77128c6e2 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
@@ -16,7 +16,7 @@
16#include "common/settings.h" 16#include "common/settings.h"
17#include "common/telemetry.h" 17#include "common/telemetry.h"
18#include "core/core_timing.h" 18#include "core/core_timing.h"
19#include "core/frontend/emu_window.h" 19#include "core/frontend/graphics_context.h"
20#include "core/telemetry_session.h" 20#include "core/telemetry_session.h"
21#include "video_core/gpu.h" 21#include "video_core/gpu.h"
22#include "video_core/renderer_vulkan/renderer_vulkan.h" 22#include "video_core/renderer_vulkan/renderer_vulkan.h"
@@ -84,8 +84,8 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
84 Core::Memory::Memory& cpu_memory_, Tegra::GPU& gpu_, 84 Core::Memory::Memory& cpu_memory_, Tegra::GPU& gpu_,
85 std::unique_ptr<Core::Frontend::GraphicsContext> context_) try 85 std::unique_ptr<Core::Frontend::GraphicsContext> context_) try
86 : RendererBase(emu_window, std::move(context_)), telemetry_session(telemetry_session_), 86 : RendererBase(emu_window, std::move(context_)), telemetry_session(telemetry_session_),
87 cpu_memory(cpu_memory_), gpu(gpu_), library(OpenLibrary()), 87 cpu_memory(cpu_memory_), gpu(gpu_), library(OpenLibrary(context.get())),
88 instance(CreateInstance(library, dld, VK_API_VERSION_1_1, render_window.GetWindowInfo().type, 88 instance(CreateInstance(*library, dld, VK_API_VERSION_1_1, render_window.GetWindowInfo().type,
89 Settings::values.renderer_debug.GetValue())), 89 Settings::values.renderer_debug.GetValue())),
90 debug_callback(Settings::values.renderer_debug ? CreateDebugCallback(instance) : nullptr), 90 debug_callback(Settings::values.renderer_debug ? CreateDebugCallback(instance) : nullptr),
91 surface(CreateSurface(instance, render_window.GetWindowInfo())), 91 surface(CreateSurface(instance, render_window.GetWindowInfo())),
@@ -93,7 +93,8 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
93 state_tracker(), scheduler(device, state_tracker), 93 state_tracker(), scheduler(device, state_tracker),
94 swapchain(*surface, device, scheduler, render_window.GetFramebufferLayout().width, 94 swapchain(*surface, device, scheduler, render_window.GetFramebufferLayout().width,
95 render_window.GetFramebufferLayout().height, false), 95 render_window.GetFramebufferLayout().height, false),
96 present_manager(render_window, device, memory_allocator, scheduler, swapchain), 96 present_manager(instance, render_window, device, memory_allocator, scheduler, swapchain,
97 surface),
97 blit_screen(cpu_memory, render_window, device, memory_allocator, swapchain, present_manager, 98 blit_screen(cpu_memory, render_window, device, memory_allocator, swapchain, present_manager,
98 scheduler, screen_info), 99 scheduler, screen_info),
99 rasterizer(render_window, gpu, cpu_memory, screen_info, device, memory_allocator, 100 rasterizer(render_window, gpu, cpu_memory, screen_info, device, memory_allocator,
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h
index f44367cb2..b2e8cbd1b 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.h
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.h
@@ -54,6 +54,10 @@ public:
54 return device.GetDriverName(); 54 return device.GetDriverName();
55 } 55 }
56 56
57 void NotifySurfaceChanged() override {
58 present_manager.NotifySurfaceChanged();
59 }
60
57private: 61private:
58 void Report() const; 62 void Report() const;
59 63
@@ -63,7 +67,7 @@ private:
63 Core::Memory::Memory& cpu_memory; 67 Core::Memory::Memory& cpu_memory;
64 Tegra::GPU& gpu; 68 Tegra::GPU& gpu;
65 69
66 Common::DynamicLibrary library; 70 std::shared_ptr<Common::DynamicLibrary> library;
67 vk::InstanceDispatch dld; 71 vk::InstanceDispatch dld;
68 72
69 vk::Instance instance; 73 vk::Instance instance;
diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp
index 1e0fdd3d9..7cdde992b 100644
--- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp
+++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp
@@ -37,6 +37,10 @@
37#include "video_core/vulkan_common/vulkan_memory_allocator.h" 37#include "video_core/vulkan_common/vulkan_memory_allocator.h"
38#include "video_core/vulkan_common/vulkan_wrapper.h" 38#include "video_core/vulkan_common/vulkan_wrapper.h"
39 39
40#ifdef ANDROID
41extern u32 GetAndroidScreenRotation();
42#endif
43
40namespace Vulkan { 44namespace Vulkan {
41 45
42namespace { 46namespace {
@@ -74,7 +78,48 @@ struct ScreenRectVertex {
74 } 78 }
75}; 79};
76 80
77constexpr std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) { 81#ifdef ANDROID
82
83std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
84 constexpr u32 ROTATION_0 = 0;
85 constexpr u32 ROTATION_90 = 1;
86 constexpr u32 ROTATION_180 = 2;
87 constexpr u32 ROTATION_270 = 3;
88
89 // clang-format off
90 switch (GetAndroidScreenRotation()) {
91 case ROTATION_0:
92 // Desktop
93 return { 2.f / width, 0.f, 0.f, 0.f,
94 0.f, 2.f / height, 0.f, 0.f,
95 0.f, 0.f, 1.f, 0.f,
96 -1.f, -1.f, 0.f, 1.f};
97 case ROTATION_180:
98 // Reverse desktop
99 return {-2.f / width, 0.f, 0.f, 0.f,
100 0.f, -2.f / height, 0.f, 0.f,
101 0.f, 0.f, 1.f, 0.f,
102 1.f, 1.f, 0.f, 1.f};
103 case ROTATION_270:
104 // Reverse landscape
105 return { 0.f, -2.f / width, 0.f, 0.f,
106 2.f / height, 0.f, 0.f, 0.f,
107 0.f, 0.f, 1.f, 0.f,
108 -1.f, 1.f, 0.f, 1.f};
109 case ROTATION_90:
110 default:
111 // Landscape
112 return { 0.f, 2.f / width, 0.f, 0.f,
113 -2.f / height, 0.f, 0.f, 0.f,
114 0.f, 0.f, 1.f, 0.f,
115 1.f, -1.f, 0.f, 1.f};
116 }
117 // clang-format on
118}
119
120#else
121
122std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
78 // clang-format off 123 // clang-format off
79 return { 2.f / width, 0.f, 0.f, 0.f, 124 return { 2.f / width, 0.f, 0.f, 0.f,
80 0.f, 2.f / height, 0.f, 0.f, 125 0.f, 2.f / height, 0.f, 0.f,
@@ -83,6 +128,8 @@ constexpr std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
83 // clang-format on 128 // clang-format on
84} 129}
85 130
131#endif
132
86u32 GetBytesPerPixel(const Tegra::FramebufferConfig& framebuffer) { 133u32 GetBytesPerPixel(const Tegra::FramebufferConfig& framebuffer) {
87 using namespace VideoCore::Surface; 134 using namespace VideoCore::Surface;
88 return BytesPerBlock(PixelFormatFromGPUPixelFormat(framebuffer.pixel_format)); 135 return BytesPerBlock(PixelFormatFromGPUPixelFormat(framebuffer.pixel_format));
@@ -441,7 +488,12 @@ void BlitScreen::DrawToSwapchain(Frame* frame, const Tegra::FramebufferConfig& f
441 if (const std::size_t swapchain_images = swapchain.GetImageCount(); 488 if (const std::size_t swapchain_images = swapchain.GetImageCount();
442 swapchain_images != image_count || current_srgb != is_srgb) { 489 swapchain_images != image_count || current_srgb != is_srgb) {
443 current_srgb = is_srgb; 490 current_srgb = is_srgb;
491#ifdef ANDROID
492 // Android is already ordered the same as Switch.
493 image_view_format = current_srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
494#else
444 image_view_format = current_srgb ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM; 495 image_view_format = current_srgb ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM;
496#endif
445 image_count = swapchain_images; 497 image_count = swapchain_images;
446 Recreate(); 498 Recreate();
447 } 499 }
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index 9627eb129..daa128399 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -303,9 +303,13 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
303 DescriptorPool& descriptor_pool) 303 DescriptorPool& descriptor_pool)
304 : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_}, 304 : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_},
305 staging_pool{staging_pool_}, guest_descriptor_queue{guest_descriptor_queue_}, 305 staging_pool{staging_pool_}, guest_descriptor_queue{guest_descriptor_queue_},
306 uint8_pass(device, scheduler, descriptor_pool, staging_pool, compute_pass_descriptor_queue),
307 quad_index_pass(device, scheduler, descriptor_pool, staging_pool, 306 quad_index_pass(device, scheduler, descriptor_pool, staging_pool,
308 compute_pass_descriptor_queue) { 307 compute_pass_descriptor_queue) {
308 if (device.GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {
309 // TODO: FixMe: Uint8Pass compute shader does not build on some Qualcomm drivers.
310 uint8_pass = std::make_unique<Uint8Pass>(device, scheduler, descriptor_pool, staging_pool,
311 compute_pass_descriptor_queue);
312 }
309 quad_array_index_buffer = std::make_shared<QuadArrayIndexBuffer>(device_, memory_allocator_, 313 quad_array_index_buffer = std::make_shared<QuadArrayIndexBuffer>(device_, memory_allocator_,
310 scheduler_, staging_pool_); 314 scheduler_, staging_pool_);
311 quad_strip_index_buffer = std::make_shared<QuadStripIndexBuffer>(device_, memory_allocator_, 315 quad_strip_index_buffer = std::make_shared<QuadStripIndexBuffer>(device_, memory_allocator_,
@@ -442,7 +446,9 @@ void BufferCacheRuntime::BindIndexBuffer(PrimitiveTopology topology, IndexFormat
442 topology == PrimitiveTopology::QuadStrip); 446 topology == PrimitiveTopology::QuadStrip);
443 } else if (vk_index_type == VK_INDEX_TYPE_UINT8_EXT && !device.IsExtIndexTypeUint8Supported()) { 447 } else if (vk_index_type == VK_INDEX_TYPE_UINT8_EXT && !device.IsExtIndexTypeUint8Supported()) {
444 vk_index_type = VK_INDEX_TYPE_UINT16; 448 vk_index_type = VK_INDEX_TYPE_UINT16;
445 std::tie(vk_buffer, vk_offset) = uint8_pass.Assemble(num_indices, buffer, offset); 449 if (uint8_pass) {
450 std::tie(vk_buffer, vk_offset) = uint8_pass->Assemble(num_indices, buffer, offset);
451 }
446 } 452 }
447 if (vk_buffer == VK_NULL_HANDLE) { 453 if (vk_buffer == VK_NULL_HANDLE) {
448 // Vulkan doesn't support null index buffers. Replace it with our own null buffer. 454 // Vulkan doesn't support null index buffers. Replace it with our own null buffer.
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index 5e9602905..794dd0758 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -139,7 +139,7 @@ private:
139 vk::Buffer null_buffer; 139 vk::Buffer null_buffer;
140 MemoryCommit null_buffer_commit; 140 MemoryCommit null_buffer_commit;
141 141
142 Uint8Pass uint8_pass; 142 std::unique_ptr<Uint8Pass> uint8_pass;
143 QuadIndexedPass quad_index_pass; 143 QuadIndexedPass quad_index_pass;
144}; 144};
145 145
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 66dfe5733..9482e91b0 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -114,14 +114,16 @@ Shader::AttributeType CastAttributeType(const FixedPipelineState::VertexAttribut
114 return Shader::AttributeType::Disabled; 114 return Shader::AttributeType::Disabled;
115 case Maxwell::VertexAttribute::Type::SNorm: 115 case Maxwell::VertexAttribute::Type::SNorm:
116 case Maxwell::VertexAttribute::Type::UNorm: 116 case Maxwell::VertexAttribute::Type::UNorm:
117 case Maxwell::VertexAttribute::Type::UScaled:
118 case Maxwell::VertexAttribute::Type::SScaled:
119 case Maxwell::VertexAttribute::Type::Float: 117 case Maxwell::VertexAttribute::Type::Float:
120 return Shader::AttributeType::Float; 118 return Shader::AttributeType::Float;
121 case Maxwell::VertexAttribute::Type::SInt: 119 case Maxwell::VertexAttribute::Type::SInt:
122 return Shader::AttributeType::SignedInt; 120 return Shader::AttributeType::SignedInt;
123 case Maxwell::VertexAttribute::Type::UInt: 121 case Maxwell::VertexAttribute::Type::UInt:
124 return Shader::AttributeType::UnsignedInt; 122 return Shader::AttributeType::UnsignedInt;
123 case Maxwell::VertexAttribute::Type::UScaled:
124 return Shader::AttributeType::UnsignedScaled;
125 case Maxwell::VertexAttribute::Type::SScaled:
126 return Shader::AttributeType::SignedScaled;
125 } 127 }
126 return Shader::AttributeType::Float; 128 return Shader::AttributeType::Float;
127} 129}
@@ -286,14 +288,17 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device
286 texture_cache{texture_cache_}, shader_notify{shader_notify_}, 288 texture_cache{texture_cache_}, shader_notify{shader_notify_},
287 use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()}, 289 use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()},
288 use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue()}, 290 use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue()},
289 workers(std::max(std::thread::hardware_concurrency(), 2U) - 1, "VkPipelineBuilder"), 291 workers(device.GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY
292 ? 1
293 : (std::max(std::thread::hardware_concurrency(), 2U) - 1),
294 "VkPipelineBuilder"),
290 serialization_thread(1, "VkPipelineSerialization") { 295 serialization_thread(1, "VkPipelineSerialization") {
291 const auto& float_control{device.FloatControlProperties()}; 296 const auto& float_control{device.FloatControlProperties()};
292 const VkDriverId driver_id{device.GetDriverID()}; 297 const VkDriverId driver_id{device.GetDriverID()};
293 profile = Shader::Profile{ 298 profile = Shader::Profile{
294 .supported_spirv = device.SupportedSpirvVersion(), 299 .supported_spirv = device.SupportedSpirvVersion(),
295 .unified_descriptor_binding = true, 300 .unified_descriptor_binding = true,
296 .support_descriptor_aliasing = true, 301 .support_descriptor_aliasing = device.IsDescriptorAliasingSupported(),
297 .support_int8 = device.IsInt8Supported(), 302 .support_int8 = device.IsInt8Supported(),
298 .support_int16 = device.IsShaderInt16Supported(), 303 .support_int16 = device.IsShaderInt16Supported(),
299 .support_int64 = device.IsShaderInt64Supported(), 304 .support_int64 = device.IsShaderInt64Supported(),
@@ -324,6 +329,7 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device
324 .support_derivative_control = true, 329 .support_derivative_control = true,
325 .support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(), 330 .support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
326 .support_native_ndc = device.IsExtDepthClipControlSupported(), 331 .support_native_ndc = device.IsExtDepthClipControlSupported(),
332 .support_scaled_attributes = !device.MustEmulateScaledFormats(),
327 333
328 .warp_size_potentially_larger_than_guest = device.IsWarpSizePotentiallyBiggerThanGuest(), 334 .warp_size_potentially_larger_than_guest = device.IsWarpSizePotentiallyBiggerThanGuest(),
329 335
@@ -341,7 +347,8 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device
341 .has_broken_signed_operations = false, 347 .has_broken_signed_operations = false,
342 .has_broken_fp16_float_controls = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY, 348 .has_broken_fp16_float_controls = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY,
343 .ignore_nan_fp_comparisons = false, 349 .ignore_nan_fp_comparisons = false,
344 }; 350 .has_broken_spirv_subgroup_mask_vector_extract_dynamic =
351 driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY};
345 host_info = Shader::HostTranslateInfo{ 352 host_info = Shader::HostTranslateInfo{
346 .support_float16 = device.IsFloat16Supported(), 353 .support_float16 = device.IsFloat16Supported(),
347 .support_int64 = device.IsShaderInt64Supported(), 354 .support_int64 = device.IsShaderInt64Supported(),
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp
index c49583013..10ace0420 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.cpp
+++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp
@@ -4,10 +4,12 @@
4#include "common/microprofile.h" 4#include "common/microprofile.h"
5#include "common/settings.h" 5#include "common/settings.h"
6#include "common/thread.h" 6#include "common/thread.h"
7#include "core/frontend/emu_window.h"
7#include "video_core/renderer_vulkan/vk_present_manager.h" 8#include "video_core/renderer_vulkan/vk_present_manager.h"
8#include "video_core/renderer_vulkan/vk_scheduler.h" 9#include "video_core/renderer_vulkan/vk_scheduler.h"
9#include "video_core/renderer_vulkan/vk_swapchain.h" 10#include "video_core/renderer_vulkan/vk_swapchain.h"
10#include "video_core/vulkan_common/vulkan_device.h" 11#include "video_core/vulkan_common/vulkan_device.h"
12#include "video_core/vulkan_common/vulkan_surface.h"
11 13
12namespace Vulkan { 14namespace Vulkan {
13 15
@@ -92,14 +94,17 @@ bool CanBlitToSwapchain(const vk::PhysicalDevice& physical_device, VkFormat form
92 94
93} // Anonymous namespace 95} // Anonymous namespace
94 96
95PresentManager::PresentManager(Core::Frontend::EmuWindow& render_window_, const Device& device_, 97PresentManager::PresentManager(const vk::Instance& instance_,
98 Core::Frontend::EmuWindow& render_window_, const Device& device_,
96 MemoryAllocator& memory_allocator_, Scheduler& scheduler_, 99 MemoryAllocator& memory_allocator_, Scheduler& scheduler_,
97 Swapchain& swapchain_) 100 Swapchain& swapchain_, vk::SurfaceKHR& surface_)
98 : render_window{render_window_}, device{device_}, 101 : instance{instance_}, render_window{render_window_}, device{device_},
99 memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, 102 memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_},
100 blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())}, 103 surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(),
104 swapchain.GetImageViewFormat())},
101 use_present_thread{Settings::values.async_presentation.GetValue()}, 105 use_present_thread{Settings::values.async_presentation.GetValue()},
102 image_count{swapchain.GetImageCount()} { 106 image_count{swapchain.GetImageCount()}, last_render_surface{
107 render_window_.GetWindowInfo().render_surface} {
103 108
104 auto& dld = device.GetLogical(); 109 auto& dld = device.GetLogical();
105 cmdpool = dld.CreateCommandPool({ 110 cmdpool = dld.CreateCommandPool({
@@ -286,14 +291,45 @@ void PresentManager::PresentThread(std::stop_token token) {
286 } 291 }
287} 292}
288 293
294void PresentManager::NotifySurfaceChanged() {
295#ifdef ANDROID
296 std::scoped_lock lock{recreate_surface_mutex};
297 recreate_surface_cv.notify_one();
298#endif
299}
300
289void PresentManager::CopyToSwapchain(Frame* frame) { 301void PresentManager::CopyToSwapchain(Frame* frame) {
290 MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain); 302 MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain);
291 303
292 const auto recreate_swapchain = [&] { 304 const auto recreate_swapchain = [&] {
293 swapchain.Create(frame->width, frame->height, frame->is_srgb); 305 swapchain.Create(*surface, frame->width, frame->height, frame->is_srgb);
294 image_count = swapchain.GetImageCount(); 306 image_count = swapchain.GetImageCount();
295 }; 307 };
296 308
309#ifdef ANDROID
310 std::unique_lock lock{recreate_surface_mutex};
311
312 const auto needs_recreation = [&] {
313 if (last_render_surface != render_window.GetWindowInfo().render_surface) {
314 return true;
315 }
316 if (swapchain.NeedsRecreation(frame->is_srgb)) {
317 return true;
318 }
319 return false;
320 };
321
322 recreate_surface_cv.wait_for(lock, std::chrono::milliseconds(400),
323 [&]() { return !needs_recreation(); });
324
325 // If the frontend recreated the surface, recreate the renderer surface and swapchain.
326 if (last_render_surface != render_window.GetWindowInfo().render_surface) {
327 last_render_surface = render_window.GetWindowInfo().render_surface;
328 surface = CreateSurface(instance, render_window.GetWindowInfo());
329 recreate_swapchain();
330 }
331#endif
332
297 // If the size or colorspace of the incoming frames has changed, recreate the swapchain 333 // If the size or colorspace of the incoming frames has changed, recreate the swapchain
298 // to account for that. 334 // to account for that.
299 const bool srgb_changed = swapchain.NeedsRecreation(frame->is_srgb); 335 const bool srgb_changed = swapchain.NeedsRecreation(frame->is_srgb);
@@ -436,7 +472,7 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
436 472
437 // Submit the image copy/blit to the swapchain 473 // Submit the image copy/blit to the swapchain
438 { 474 {
439 std::scoped_lock lock{scheduler.submit_mutex}; 475 std::scoped_lock submit_lock{scheduler.submit_mutex};
440 switch (const VkResult result = 476 switch (const VkResult result =
441 device.GetGraphicsQueue().Submit(submit_info, *frame->present_done)) { 477 device.GetGraphicsQueue().Submit(submit_info, *frame->present_done)) {
442 case VK_SUCCESS: 478 case VK_SUCCESS:
@@ -454,4 +490,4 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
454 swapchain.Present(render_semaphore); 490 swapchain.Present(render_semaphore);
455} 491}
456 492
457} // namespace Vulkan 493} // namespace Vulkan \ No newline at end of file
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h
index 420a775e2..4ac2e2395 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.h
+++ b/src/video_core/renderer_vulkan/vk_present_manager.h
@@ -37,8 +37,9 @@ struct Frame {
37 37
38class PresentManager { 38class PresentManager {
39public: 39public:
40 PresentManager(Core::Frontend::EmuWindow& render_window, const Device& device, 40 PresentManager(const vk::Instance& instance, Core::Frontend::EmuWindow& render_window,
41 MemoryAllocator& memory_allocator, Scheduler& scheduler, Swapchain& swapchain); 41 const Device& device, MemoryAllocator& memory_allocator, Scheduler& scheduler,
42 Swapchain& swapchain, vk::SurfaceKHR& surface);
42 ~PresentManager(); 43 ~PresentManager();
43 44
44 /// Returns the last used presentation frame 45 /// Returns the last used presentation frame
@@ -54,30 +55,38 @@ public:
54 /// Waits for the present thread to finish presenting all queued frames. 55 /// Waits for the present thread to finish presenting all queued frames.
55 void WaitPresent(); 56 void WaitPresent();
56 57
58 /// This is called to notify the rendering backend of a surface change
59 void NotifySurfaceChanged();
60
57private: 61private:
58 void PresentThread(std::stop_token token); 62 void PresentThread(std::stop_token token);
59 63
60 void CopyToSwapchain(Frame* frame); 64 void CopyToSwapchain(Frame* frame);
61 65
62private: 66private:
67 const vk::Instance& instance;
63 Core::Frontend::EmuWindow& render_window; 68 Core::Frontend::EmuWindow& render_window;
64 const Device& device; 69 const Device& device;
65 MemoryAllocator& memory_allocator; 70 MemoryAllocator& memory_allocator;
66 Scheduler& scheduler; 71 Scheduler& scheduler;
67 Swapchain& swapchain; 72 Swapchain& swapchain;
73 vk::SurfaceKHR& surface;
68 vk::CommandPool cmdpool; 74 vk::CommandPool cmdpool;
69 std::vector<Frame> frames; 75 std::vector<Frame> frames;
70 std::queue<Frame*> present_queue; 76 std::queue<Frame*> present_queue;
71 std::queue<Frame*> free_queue; 77 std::queue<Frame*> free_queue;
72 std::condition_variable_any frame_cv; 78 std::condition_variable_any frame_cv;
73 std::condition_variable free_cv; 79 std::condition_variable free_cv;
80 std::condition_variable recreate_surface_cv;
74 std::mutex swapchain_mutex; 81 std::mutex swapchain_mutex;
82 std::mutex recreate_surface_mutex;
75 std::mutex queue_mutex; 83 std::mutex queue_mutex;
76 std::mutex free_mutex; 84 std::mutex free_mutex;
77 std::jthread present_thread; 85 std::jthread present_thread;
78 bool blit_supported; 86 bool blit_supported;
79 bool use_present_thread; 87 bool use_present_thread;
80 std::size_t image_count; 88 std::size_t image_count{};
89 void* last_render_surface{};
81}; 90};
82 91
83} // namespace Vulkan 92} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 8d3a9736b..84e3a30cc 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -188,7 +188,14 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
188 FlushWork(); 188 FlushWork();
189 gpu_memory->FlushCaching(); 189 gpu_memory->FlushCaching();
190 190
191#if ANDROID
192 if (Settings::IsGPULevelHigh()) {
193 // This is problematic on Android, disable on GPU Normal.
194 query_cache.UpdateCounters();
195 }
196#else
191 query_cache.UpdateCounters(); 197 query_cache.UpdateCounters();
198#endif
192 199
193 GraphicsPipeline* const pipeline{pipeline_cache.CurrentGraphicsPipeline()}; 200 GraphicsPipeline* const pipeline{pipeline_cache.CurrentGraphicsPipeline()};
194 if (!pipeline) { 201 if (!pipeline) {
@@ -272,7 +279,14 @@ void RasterizerVulkan::DrawTexture() {
272 SCOPE_EXIT({ gpu.TickWork(); }); 279 SCOPE_EXIT({ gpu.TickWork(); });
273 FlushWork(); 280 FlushWork();
274 281
282#if ANDROID
283 if (Settings::IsGPULevelHigh()) {
284 // This is problematic on Android, disable on GPU Normal.
285 query_cache.UpdateCounters();
286 }
287#else
275 query_cache.UpdateCounters(); 288 query_cache.UpdateCounters();
289#endif
276 290
277 texture_cache.SynchronizeGraphicsDescriptors(); 291 texture_cache.SynchronizeGraphicsDescriptors();
278 texture_cache.UpdateRenderTargets(false); 292 texture_cache.UpdateRenderTargets(false);
@@ -743,7 +757,11 @@ void RasterizerVulkan::LoadDiskResources(u64 title_id, std::stop_token stop_load
743} 757}
744 758
745void RasterizerVulkan::FlushWork() { 759void RasterizerVulkan::FlushWork() {
760#ifdef ANDROID
761 static constexpr u32 DRAWS_TO_DISPATCH = 1024;
762#else
746 static constexpr u32 DRAWS_TO_DISPATCH = 4096; 763 static constexpr u32 DRAWS_TO_DISPATCH = 4096;
764#endif // ANDROID
747 765
748 // Only check multiples of 8 draws 766 // Only check multiples of 8 draws
749 static_assert(DRAWS_TO_DISPATCH % 8 == 0); 767 static_assert(DRAWS_TO_DISPATCH % 8 == 0);
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index 80455ec08..17ef61147 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -239,7 +239,14 @@ u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_se
239void Scheduler::AllocateNewContext() { 239void Scheduler::AllocateNewContext() {
240 // Enable counters once again. These are disabled when a command buffer is finished. 240 // Enable counters once again. These are disabled when a command buffer is finished.
241 if (query_cache) { 241 if (query_cache) {
242#if ANDROID
243 if (Settings::IsGPULevelHigh()) {
244 // This is problematic on Android, disable on GPU Normal.
245 query_cache->UpdateCounters();
246 }
247#else
242 query_cache->UpdateCounters(); 248 query_cache->UpdateCounters();
249#endif
243 } 250 }
244} 251}
245 252
@@ -250,7 +257,14 @@ void Scheduler::InvalidateState() {
250} 257}
251 258
252void Scheduler::EndPendingOperations() { 259void Scheduler::EndPendingOperations() {
260#if ANDROID
261 if (Settings::IsGPULevelHigh()) {
262 // This is problematic on Android, disable on GPU Normal.
263 query_cache->DisableStreams();
264 }
265#else
253 query_cache->DisableStreams(); 266 query_cache->DisableStreams();
267#endif
254 EndRenderPass(); 268 EndRenderPass();
255} 269}
256 270
diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp
index 8c0dec590..afcf34fba 100644
--- a/src/video_core/renderer_vulkan/vk_swapchain.cpp
+++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp
@@ -107,16 +107,17 @@ VkCompositeAlphaFlagBitsKHR ChooseAlphaFlags(const VkSurfaceCapabilitiesKHR& cap
107Swapchain::Swapchain(VkSurfaceKHR surface_, const Device& device_, Scheduler& scheduler_, 107Swapchain::Swapchain(VkSurfaceKHR surface_, const Device& device_, Scheduler& scheduler_,
108 u32 width_, u32 height_, bool srgb) 108 u32 width_, u32 height_, bool srgb)
109 : surface{surface_}, device{device_}, scheduler{scheduler_} { 109 : surface{surface_}, device{device_}, scheduler{scheduler_} {
110 Create(width_, height_, srgb); 110 Create(surface_, width_, height_, srgb);
111} 111}
112 112
113Swapchain::~Swapchain() = default; 113Swapchain::~Swapchain() = default;
114 114
115void Swapchain::Create(u32 width_, u32 height_, bool srgb) { 115void Swapchain::Create(VkSurfaceKHR surface_, u32 width_, u32 height_, bool srgb) {
116 is_outdated = false; 116 is_outdated = false;
117 is_suboptimal = false; 117 is_suboptimal = false;
118 width = width_; 118 width = width_;
119 height = height_; 119 height = height_;
120 surface = surface_;
120 121
121 const auto physical_device = device.GetPhysical(); 122 const auto physical_device = device.GetPhysical();
122 const auto capabilities{physical_device.GetSurfaceCapabilitiesKHR(surface)}; 123 const auto capabilities{physical_device.GetSurfaceCapabilitiesKHR(surface)};
@@ -266,7 +267,12 @@ void Swapchain::CreateSwapchain(const VkSurfaceCapabilitiesKHR& capabilities, bo
266 267
267 images = swapchain.GetImages(); 268 images = swapchain.GetImages();
268 image_count = static_cast<u32>(images.size()); 269 image_count = static_cast<u32>(images.size());
270#ifdef ANDROID
271 // Android is already ordered the same as Switch.
272 image_view_format = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
273#else
269 image_view_format = srgb ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM; 274 image_view_format = srgb ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM;
275#endif
270} 276}
271 277
272void Swapchain::CreateSemaphores() { 278void Swapchain::CreateSemaphores() {
diff --git a/src/video_core/renderer_vulkan/vk_swapchain.h b/src/video_core/renderer_vulkan/vk_swapchain.h
index bf1ea7254..b8a1465a6 100644
--- a/src/video_core/renderer_vulkan/vk_swapchain.h
+++ b/src/video_core/renderer_vulkan/vk_swapchain.h
@@ -24,7 +24,7 @@ public:
24 ~Swapchain(); 24 ~Swapchain();
25 25
26 /// Creates (or recreates) the swapchain with a given size. 26 /// Creates (or recreates) the swapchain with a given size.
27 void Create(u32 width, u32 height, bool srgb); 27 void Create(VkSurfaceKHR surface, u32 width, u32 height, bool srgb);
28 28
29 /// Acquires the next image in the swapchain, waits as needed. 29 /// Acquires the next image in the swapchain, waits as needed.
30 bool AcquireNextImage(); 30 bool AcquireNextImage();
@@ -118,7 +118,7 @@ private:
118 118
119 bool NeedsPresentModeUpdate() const; 119 bool NeedsPresentModeUpdate() const;
120 120
121 const VkSurfaceKHR surface; 121 VkSurfaceKHR surface;
122 const Device& device; 122 const Device& device;
123 Scheduler& scheduler; 123 Scheduler& scheduler;
124 124
diff --git a/src/video_core/renderer_vulkan/vk_turbo_mode.cpp b/src/video_core/renderer_vulkan/vk_turbo_mode.cpp
index db04943eb..a802d3c49 100644
--- a/src/video_core/renderer_vulkan/vk_turbo_mode.cpp
+++ b/src/video_core/renderer_vulkan/vk_turbo_mode.cpp
@@ -1,6 +1,10 @@
1// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project 1// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
5#include <adrenotools/driver.h>
6#endif
7
4#include "common/literals.h" 8#include "common/literals.h"
5#include "video_core/host_shaders/vulkan_turbo_mode_comp_spv.h" 9#include "video_core/host_shaders/vulkan_turbo_mode_comp_spv.h"
6#include "video_core/renderer_vulkan/renderer_vulkan.h" 10#include "video_core/renderer_vulkan/renderer_vulkan.h"
@@ -13,7 +17,10 @@ namespace Vulkan {
13using namespace Common::Literals; 17using namespace Common::Literals;
14 18
15TurboMode::TurboMode(const vk::Instance& instance, const vk::InstanceDispatch& dld) 19TurboMode::TurboMode(const vk::Instance& instance, const vk::InstanceDispatch& dld)
16 : m_device{CreateDevice(instance, dld, VK_NULL_HANDLE)}, m_allocator{m_device, false} { 20#ifndef ANDROID
21 : m_device{CreateDevice(instance, dld, VK_NULL_HANDLE)}, m_allocator{m_device, false}
22#endif
23{
17 { 24 {
18 std::scoped_lock lk{m_submission_lock}; 25 std::scoped_lock lk{m_submission_lock};
19 m_submission_time = std::chrono::steady_clock::now(); 26 m_submission_time = std::chrono::steady_clock::now();
@@ -30,6 +37,7 @@ void TurboMode::QueueSubmitted() {
30} 37}
31 38
32void TurboMode::Run(std::stop_token stop_token) { 39void TurboMode::Run(std::stop_token stop_token) {
40#ifndef ANDROID
33 auto& dld = m_device.GetLogical(); 41 auto& dld = m_device.GetLogical();
34 42
35 // Allocate buffer. 2MiB should be sufficient. 43 // Allocate buffer. 2MiB should be sufficient.
@@ -142,8 +150,14 @@ void TurboMode::Run(std::stop_token stop_token) {
142 // Create a single command buffer. 150 // Create a single command buffer.
143 auto cmdbufs = command_pool.Allocate(1, VK_COMMAND_BUFFER_LEVEL_PRIMARY); 151 auto cmdbufs = command_pool.Allocate(1, VK_COMMAND_BUFFER_LEVEL_PRIMARY);
144 auto cmdbuf = vk::CommandBuffer{cmdbufs[0], m_device.GetDispatchLoader()}; 152 auto cmdbuf = vk::CommandBuffer{cmdbufs[0], m_device.GetDispatchLoader()};
153#endif
145 154
146 while (!stop_token.stop_requested()) { 155 while (!stop_token.stop_requested()) {
156#ifdef ANDROID
157#ifdef ARCHITECTURE_arm64
158 adrenotools_set_turbo(true);
159#endif
160#else
147 // Reset the fence. 161 // Reset the fence.
148 fence.Reset(); 162 fence.Reset();
149 163
@@ -209,7 +223,7 @@ void TurboMode::Run(std::stop_token stop_token) {
209 223
210 // Wait for completion. 224 // Wait for completion.
211 fence.Wait(); 225 fence.Wait();
212 226#endif
213 // Wait for the next graphics queue submission if necessary. 227 // Wait for the next graphics queue submission if necessary.
214 std::unique_lock lk{m_submission_lock}; 228 std::unique_lock lk{m_submission_lock};
215 Common::CondvarWait(m_submission_cv, lk, stop_token, [this] { 229 Common::CondvarWait(m_submission_cv, lk, stop_token, [this] {
@@ -217,6 +231,9 @@ void TurboMode::Run(std::stop_token stop_token) {
217 std::chrono::milliseconds{100}; 231 std::chrono::milliseconds{100};
218 }); 232 });
219 } 233 }
234#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
235 adrenotools_set_turbo(false);
236#endif
220} 237}
221 238
222} // namespace Vulkan 239} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_turbo_mode.h b/src/video_core/renderer_vulkan/vk_turbo_mode.h
index 99b5ac50b..9341c9867 100644
--- a/src/video_core/renderer_vulkan/vk_turbo_mode.h
+++ b/src/video_core/renderer_vulkan/vk_turbo_mode.h
@@ -23,8 +23,10 @@ public:
23private: 23private:
24 void Run(std::stop_token stop_token); 24 void Run(std::stop_token stop_token);
25 25
26#ifndef ANDROID
26 Device m_device; 27 Device m_device;
27 MemoryAllocator m_allocator; 28 MemoryAllocator m_allocator;
29#endif
28 std::mutex m_submission_lock; 30 std::mutex m_submission_lock;
29 std::condition_variable_any m_submission_cv; 31 std::condition_variable_any m_submission_cv;
30 std::chrono::time_point<std::chrono::steady_clock> m_submission_time{}; 32 std::chrono::time_point<std::chrono::steady_clock> m_submission_time{};
diff --git a/src/video_core/renderer_vulkan/vk_update_descriptor.h b/src/video_core/renderer_vulkan/vk_update_descriptor.h
index 310fb551a..e77b576ec 100644
--- a/src/video_core/renderer_vulkan/vk_update_descriptor.h
+++ b/src/video_core/renderer_vulkan/vk_update_descriptor.h
@@ -31,7 +31,7 @@ struct DescriptorUpdateEntry {
31class UpdateDescriptorQueue final { 31class UpdateDescriptorQueue final {
32 // This should be plenty for the vast majority of cases. Most desktop platforms only 32 // This should be plenty for the vast majority of cases. Most desktop platforms only
33 // provide up to 3 swapchain images. 33 // provide up to 3 swapchain images.
34 static constexpr size_t FRAMES_IN_FLIGHT = 5; 34 static constexpr size_t FRAMES_IN_FLIGHT = 7;
35 static constexpr size_t FRAME_PAYLOAD_SIZE = 0x20000; 35 static constexpr size_t FRAME_PAYLOAD_SIZE = 0x20000;
36 static constexpr size_t PAYLOAD_SIZE = FRAME_PAYLOAD_SIZE * FRAMES_IN_FLIGHT; 36 static constexpr size_t PAYLOAD_SIZE = FRAME_PAYLOAD_SIZE * FRAMES_IN_FLIGHT;
37 37
diff --git a/src/video_core/vulkan_common/vulkan_debug_callback.cpp b/src/video_core/vulkan_common/vulkan_debug_callback.cpp
index 10a001b8f..9de484c29 100644
--- a/src/video_core/vulkan_common/vulkan_debug_callback.cpp
+++ b/src/video_core/vulkan_common/vulkan_debug_callback.cpp
@@ -13,11 +13,39 @@ VkBool32 Callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
13 [[maybe_unused]] void* user_data) { 13 [[maybe_unused]] void* user_data) {
14 // Skip logging known false-positive validation errors 14 // Skip logging known false-positive validation errors
15 switch (static_cast<u32>(data->messageIdNumber)) { 15 switch (static_cast<u32>(data->messageIdNumber)) {
16#ifdef ANDROID
17 case 0xbf9cf353u: // VUID-vkCmdBindVertexBuffers2-pBuffers-04111
18 // The below are due to incorrect reporting of extendedDynamicState
19 case 0x1093bebbu: // VUID-vkCmdSetCullMode-None-03384
20 case 0x9215850fu: // VUID-vkCmdSetDepthTestEnable-None-03352
21 case 0x86bf18dcu: // VUID-vkCmdSetDepthWriteEnable-None-03354
22 case 0x0792ad08u: // VUID-vkCmdSetStencilOp-None-03351
23 case 0x93e1ba4eu: // VUID-vkCmdSetFrontFace-None-03383
24 case 0xac9c13c5u: // VUID-vkCmdSetStencilTestEnable-None-03350
25 case 0xc9a2001bu: // VUID-vkCmdSetDepthBoundsTestEnable-None-03349
26 case 0x8b7159a7u: // VUID-vkCmdSetDepthCompareOp-None-03353
27 // The below are due to incorrect reporting of extendedDynamicState2
28 case 0xb13c8036u: // VUID-vkCmdSetDepthBiasEnable-None-04872
29 case 0xdff2e5c1u: // VUID-vkCmdSetRasterizerDiscardEnable-None-04871
30 case 0x0cc85f41u: // VUID-vkCmdSetPrimitiveRestartEnable-None-04866
31 case 0x01257b492: // VUID-vkCmdSetLogicOpEXT-None-0486
32 // The below are due to incorrect reporting of vertexInputDynamicState
33 case 0x398e0dabu: // VUID-vkCmdSetVertexInputEXT-None-04790
34 // The below are due to incorrect reporting of extendedDynamicState3
35 case 0x970c11a5u: // VUID-vkCmdSetColorWriteMaskEXT-extendedDynamicState3ColorWriteMask-07364
36 case 0x6b453f78u: // VUID-vkCmdSetColorBlendEnableEXT-extendedDynamicState3ColorBlendEnable-07355
37 case 0xf66469d0u: // VUID-vkCmdSetColorBlendEquationEXT-extendedDynamicState3ColorBlendEquation-07356
38 case 0x1d43405eu: // VUID-vkCmdSetLogicOpEnableEXT-extendedDynamicState3LogicOpEnable-07365
39 case 0x638462e8u: // VUID-vkCmdSetDepthClampEnableEXT-extendedDynamicState3DepthClampEnable-07448
40 // Misc
41 case 0xe0a2da61u: // VUID-vkCmdDrawIndexed-format-07753
42#else
16 case 0x682a878au: // VUID-vkCmdBindVertexBuffers2EXT-pBuffers-parameter 43 case 0x682a878au: // VUID-vkCmdBindVertexBuffers2EXT-pBuffers-parameter
17 case 0x99fb7dfdu: // UNASSIGNED-RequiredParameter (vkCmdBindVertexBuffers2EXT pBuffers[0]) 44 case 0x99fb7dfdu: // UNASSIGNED-RequiredParameter (vkCmdBindVertexBuffers2EXT pBuffers[0])
18 case 0xe8616bf2u: // Bound VkDescriptorSet 0x0[] was destroyed. Likely push_descriptor related 45 case 0xe8616bf2u: // Bound VkDescriptorSet 0x0[] was destroyed. Likely push_descriptor related
19 case 0x1608dec0u: // Image layout in vkUpdateDescriptorSet doesn't match descriptor use 46 case 0x1608dec0u: // Image layout in vkUpdateDescriptorSet doesn't match descriptor use
20 case 0x55362756u: // Descriptor binding and framebuffer attachment overlap 47 case 0x55362756u: // Descriptor binding and framebuffer attachment overlap
48#endif
21 return VK_FALSE; 49 return VK_FALSE;
22 default: 50 default:
23 break; 51 break;
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index aea677cb3..0158b6b0d 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -18,6 +18,10 @@
18#include "video_core/vulkan_common/vulkan_device.h" 18#include "video_core/vulkan_common/vulkan_device.h"
19#include "video_core/vulkan_common/vulkan_wrapper.h" 19#include "video_core/vulkan_common/vulkan_wrapper.h"
20 20
21#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
22#include <adrenotools/bcenabler.h>
23#endif
24
21namespace Vulkan { 25namespace Vulkan {
22using namespace Common::Literals; 26using namespace Common::Literals;
23namespace { 27namespace {
@@ -262,6 +266,32 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica
262 return format_properties; 266 return format_properties;
263} 267}
264 268
269#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
270void OverrideBcnFormats(std::unordered_map<VkFormat, VkFormatProperties>& format_properties) {
271 // These properties are extracted from Adreno driver 512.687.0
272 constexpr VkFormatFeatureFlags tiling_features{
273 VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT |
274 VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
275 VK_FORMAT_FEATURE_TRANSFER_DST_BIT};
276
277 constexpr VkFormatFeatureFlags buffer_features{VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT};
278
279 static constexpr std::array bcn_formats{
280 VK_FORMAT_BC1_RGBA_SRGB_BLOCK, VK_FORMAT_BC1_RGBA_UNORM_BLOCK, VK_FORMAT_BC2_SRGB_BLOCK,
281 VK_FORMAT_BC2_UNORM_BLOCK, VK_FORMAT_BC3_SRGB_BLOCK, VK_FORMAT_BC3_UNORM_BLOCK,
282 VK_FORMAT_BC4_SNORM_BLOCK, VK_FORMAT_BC4_UNORM_BLOCK, VK_FORMAT_BC5_SNORM_BLOCK,
283 VK_FORMAT_BC5_UNORM_BLOCK, VK_FORMAT_BC6H_SFLOAT_BLOCK, VK_FORMAT_BC6H_UFLOAT_BLOCK,
284 VK_FORMAT_BC7_SRGB_BLOCK, VK_FORMAT_BC7_UNORM_BLOCK,
285 };
286
287 for (const auto format : bcn_formats) {
288 format_properties[format].linearTilingFeatures = tiling_features;
289 format_properties[format].optimalTilingFeatures = tiling_features;
290 format_properties[format].bufferFeatures = buffer_features;
291 }
292}
293#endif
294
265NvidiaArchitecture GetNvidiaArchitecture(vk::PhysicalDevice physical, 295NvidiaArchitecture GetNvidiaArchitecture(vk::PhysicalDevice physical,
266 const std::set<std::string, std::less<>>& exts) { 296 const std::set<std::string, std::less<>>& exts) {
267 if (exts.contains(VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME)) { 297 if (exts.contains(VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME)) {
@@ -302,6 +332,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
302 const bool is_suitable = GetSuitability(surface != nullptr); 332 const bool is_suitable = GetSuitability(surface != nullptr);
303 333
304 const VkDriverId driver_id = properties.driver.driverID; 334 const VkDriverId driver_id = properties.driver.driverID;
335 const auto device_id = properties.properties.deviceID;
305 const bool is_radv = driver_id == VK_DRIVER_ID_MESA_RADV; 336 const bool is_radv = driver_id == VK_DRIVER_ID_MESA_RADV;
306 const bool is_amd_driver = 337 const bool is_amd_driver =
307 driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE; 338 driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE;
@@ -310,9 +341,12 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
310 const bool is_intel_anv = driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA; 341 const bool is_intel_anv = driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA;
311 const bool is_nvidia = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY; 342 const bool is_nvidia = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY;
312 const bool is_mvk = driver_id == VK_DRIVER_ID_MOLTENVK; 343 const bool is_mvk = driver_id == VK_DRIVER_ID_MOLTENVK;
344 const bool is_qualcomm = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY;
345 const bool is_turnip = driver_id == VK_DRIVER_ID_MESA_TURNIP;
346 const bool is_s8gen2 = device_id == 0x43050a01;
313 347
314 if (is_mvk && !is_suitable) { 348 if ((is_mvk || is_qualcomm || is_turnip) && !is_suitable) {
315 LOG_WARNING(Render_Vulkan, "Unsuitable driver is MoltenVK, continuing anyway"); 349 LOG_WARNING(Render_Vulkan, "Unsuitable driver, continuing anyway");
316 } else if (!is_suitable) { 350 } else if (!is_suitable) {
317 throw vk::Exception(VK_ERROR_INCOMPATIBLE_DRIVER); 351 throw vk::Exception(VK_ERROR_INCOMPATIBLE_DRIVER);
318 } 352 }
@@ -355,6 +389,59 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
355 CollectPhysicalMemoryInfo(); 389 CollectPhysicalMemoryInfo();
356 CollectToolingInfo(); 390 CollectToolingInfo();
357 391
392#ifdef ANDROID
393 if (is_qualcomm || is_turnip) {
394 LOG_WARNING(Render_Vulkan,
395 "Qualcomm and Turnip drivers have broken VK_EXT_custom_border_color");
396 extensions.custom_border_color = false;
397 loaded_extensions.erase(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
398 }
399
400 if (is_qualcomm) {
401 must_emulate_scaled_formats = true;
402
403 LOG_WARNING(Render_Vulkan, "Qualcomm drivers have broken VK_EXT_extended_dynamic_state");
404 extensions.extended_dynamic_state = false;
405 loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
406
407 LOG_WARNING(Render_Vulkan,
408 "Qualcomm drivers have a slow VK_KHR_push_descriptor implementation");
409 extensions.push_descriptor = false;
410 loaded_extensions.erase(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME);
411
412#ifdef ARCHITECTURE_arm64
413 // Patch the driver to enable BCn textures.
414 const auto major = (properties.properties.driverVersion >> 24) << 2;
415 const auto minor = (properties.properties.driverVersion >> 12) & 0xFFFU;
416 const auto vendor = properties.properties.vendorID;
417 const auto patch_status = adrenotools_get_bcn_type(major, minor, vendor);
418
419 if (patch_status == ADRENOTOOLS_BCN_PATCH) {
420 LOG_INFO(Render_Vulkan, "Patching Adreno driver to support BCn texture formats");
421 if (adrenotools_patch_bcn(
422 reinterpret_cast<void*>(dld.vkGetPhysicalDeviceFormatProperties))) {
423 OverrideBcnFormats(format_properties);
424 } else {
425 LOG_ERROR(Render_Vulkan, "Patch failed! Driver code may now crash");
426 }
427 } else if (patch_status == ADRENOTOOLS_BCN_BLOB) {
428 LOG_INFO(Render_Vulkan, "Adreno driver supports BCn textures without patches");
429 } else {
430 LOG_WARNING(Render_Vulkan, "Adreno driver can't be patched to enable BCn textures");
431 }
432#endif // ARCHITECTURE_arm64
433 }
434
435 const bool is_arm = driver_id == VK_DRIVER_ID_ARM_PROPRIETARY;
436 if (is_arm) {
437 must_emulate_scaled_formats = true;
438
439 LOG_WARNING(Render_Vulkan, "ARM drivers have broken VK_EXT_extended_dynamic_state");
440 extensions.extended_dynamic_state = false;
441 loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
442 }
443#endif // ANDROID
444
358 if (is_nvidia) { 445 if (is_nvidia) {
359 const u32 nv_major_version = (properties.properties.driverVersion >> 22) & 0x3ff; 446 const u32 nv_major_version = (properties.properties.driverVersion >> 22) & 0x3ff;
360 const auto arch = GetNvidiaArchitecture(physical, supported_extensions); 447 const auto arch = GetNvidiaArchitecture(physical, supported_extensions);
@@ -388,7 +475,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
388 loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME); 475 loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
389 } 476 }
390 } 477 }
391 if (extensions.extended_dynamic_state2 && is_radv) { 478 if (extensions.extended_dynamic_state2 && (is_radv || is_qualcomm)) {
392 const u32 version = (properties.properties.driverVersion << 3) >> 3; 479 const u32 version = (properties.properties.driverVersion << 3) >> 3;
393 if (version < VK_MAKE_API_VERSION(0, 22, 3, 1)) { 480 if (version < VK_MAKE_API_VERSION(0, 22, 3, 1)) {
394 LOG_WARNING( 481 LOG_WARNING(
@@ -415,7 +502,8 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
415 dynamic_state3_enables = false; 502 dynamic_state3_enables = false;
416 } 503 }
417 } 504 }
418 if (extensions.vertex_input_dynamic_state && is_radv) { 505 if (extensions.vertex_input_dynamic_state && (is_radv || is_qualcomm)) {
506 // Qualcomm S8gen2 drivers do not properly support vertex_input_dynamic_state.
419 // TODO(ameerj): Blacklist only offending driver versions 507 // TODO(ameerj): Blacklist only offending driver versions
420 // TODO(ameerj): Confirm if RDNA1 is affected 508 // TODO(ameerj): Confirm if RDNA1 is affected
421 const bool is_rdna2 = 509 const bool is_rdna2 =
@@ -467,8 +555,8 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
467 LOG_WARNING(Render_Vulkan, "Intel proprietary drivers do not support MSAA image blits"); 555 LOG_WARNING(Render_Vulkan, "Intel proprietary drivers do not support MSAA image blits");
468 cant_blit_msaa = true; 556 cant_blit_msaa = true;
469 } 557 }
470 if (is_intel_anv) { 558 if (is_intel_anv || (is_qualcomm && !is_s8gen2)) {
471 LOG_WARNING(Render_Vulkan, "ANV driver does not support native BGR format"); 559 LOG_WARNING(Render_Vulkan, "Driver does not support native BGR format");
472 must_emulate_bgr565 = true; 560 must_emulate_bgr565 = true;
473 } 561 }
474 if (extensions.push_descriptor && is_intel_anv) { 562 if (extensions.push_descriptor && is_intel_anv) {
@@ -633,7 +721,8 @@ bool Device::ShouldBoostClocks() const {
633 driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE || 721 driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE ||
634 driver_id == VK_DRIVER_ID_MESA_RADV || driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY || 722 driver_id == VK_DRIVER_ID_MESA_RADV || driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY ||
635 driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS || 723 driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS ||
636 driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA; 724 driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA ||
725 driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY || driver_id == VK_DRIVER_ID_MESA_TURNIP;
637 726
638 const bool is_steam_deck = vendor_id == 0x1002 && device_id == 0x163F; 727 const bool is_steam_deck = vendor_id == 0x1002 && device_id == 0x163F;
639 728
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index 5f1c63ff9..b692b4be4 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -295,6 +295,11 @@ public:
295 return features.features.textureCompressionASTC_LDR; 295 return features.features.textureCompressionASTC_LDR;
296 } 296 }
297 297
298 /// Returns true if descriptor aliasing is natively supported.
299 bool IsDescriptorAliasingSupported() const {
300 return GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY;
301 }
302
298 /// Returns true if the device supports float16 natively. 303 /// Returns true if the device supports float16 natively.
299 bool IsFloat16Supported() const { 304 bool IsFloat16Supported() const {
300 return features.shader_float16_int8.shaderFloat16; 305 return features.shader_float16_int8.shaderFloat16;
@@ -495,6 +500,10 @@ public:
495 } 500 }
496 501
497 bool HasTimelineSemaphore() const { 502 bool HasTimelineSemaphore() const {
503 if (GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {
504 // Timeline semaphores do not work properly on all Qualcomm drivers.
505 return false;
506 }
498 return features.timeline_semaphore.timelineSemaphore; 507 return features.timeline_semaphore.timelineSemaphore;
499 } 508 }
500 509
@@ -551,6 +560,10 @@ public:
551 return cant_blit_msaa; 560 return cant_blit_msaa;
552 } 561 }
553 562
563 bool MustEmulateScaledFormats() const {
564 return must_emulate_scaled_formats;
565 }
566
554 bool MustEmulateBGR565() const { 567 bool MustEmulateBGR565() const {
555 return must_emulate_bgr565; 568 return must_emulate_bgr565;
556 } 569 }
@@ -666,6 +679,7 @@ private:
666 bool has_nsight_graphics{}; ///< Has Nsight Graphics attached 679 bool has_nsight_graphics{}; ///< Has Nsight Graphics attached
667 bool supports_d24_depth{}; ///< Supports D24 depth buffers. 680 bool supports_d24_depth{}; ///< Supports D24 depth buffers.
668 bool cant_blit_msaa{}; ///< Does not support MSAA<->MSAA blitting. 681 bool cant_blit_msaa{}; ///< Does not support MSAA<->MSAA blitting.
682 bool must_emulate_scaled_formats{}; ///< Requires scaled vertex format emulation
669 bool must_emulate_bgr565{}; ///< Emulates BGR565 by swizzling RGB565 format. 683 bool must_emulate_bgr565{}; ///< Emulates BGR565 by swizzling RGB565 format.
670 bool dynamic_state3_blending{}; ///< Has all blending features of dynamic_state3. 684 bool dynamic_state3_blending{}; ///< Has all blending features of dynamic_state3.
671 bool dynamic_state3_enables{}; ///< Has all enables features of dynamic_state3. 685 bool dynamic_state3_enables{}; ///< Has all enables features of dynamic_state3.
diff --git a/src/video_core/vulkan_common/vulkan_library.cpp b/src/video_core/vulkan_common/vulkan_library.cpp
index 4eb3913ee..47f6f2a03 100644
--- a/src/video_core/vulkan_common/vulkan_library.cpp
+++ b/src/video_core/vulkan_common/vulkan_library.cpp
@@ -10,29 +10,35 @@
10 10
11namespace Vulkan { 11namespace Vulkan {
12 12
13Common::DynamicLibrary OpenLibrary() { 13std::shared_ptr<Common::DynamicLibrary> OpenLibrary(
14 [[maybe_unused]] Core::Frontend::GraphicsContext* context) {
14 LOG_DEBUG(Render_Vulkan, "Looking for a Vulkan library"); 15 LOG_DEBUG(Render_Vulkan, "Looking for a Vulkan library");
15 Common::DynamicLibrary library; 16#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
17 // Android manages its Vulkan driver from the frontend.
18 return context->GetDriverLibrary();
19#else
20 auto library = std::make_shared<Common::DynamicLibrary>();
16#ifdef __APPLE__ 21#ifdef __APPLE__
17 // Check if a path to a specific Vulkan library has been specified. 22 // Check if a path to a specific Vulkan library has been specified.
18 char* const libvulkan_env = std::getenv("LIBVULKAN_PATH"); 23 char* const libvulkan_env = std::getenv("LIBVULKAN_PATH");
19 if (!libvulkan_env || !library.Open(libvulkan_env)) { 24 if (!libvulkan_env || !library->Open(libvulkan_env)) {
20 // Use the libvulkan.dylib from the application bundle. 25 // Use the libvulkan.dylib from the application bundle.
21 const auto filename = 26 const auto filename =
22 Common::FS::GetBundleDirectory() / "Contents/Frameworks/libvulkan.dylib"; 27 Common::FS::GetBundleDirectory() / "Contents/Frameworks/libvulkan.dylib";
23 void(library.Open(Common::FS::PathToUTF8String(filename).c_str())); 28 void(library->Open(Common::FS::PathToUTF8String(filename).c_str()));
24 } 29 }
25#else 30#else
26 std::string filename = Common::DynamicLibrary::GetVersionedFilename("vulkan", 1); 31 std::string filename = Common::DynamicLibrary::GetVersionedFilename("vulkan", 1);
27 LOG_DEBUG(Render_Vulkan, "Trying Vulkan library: {}", filename); 32 LOG_DEBUG(Render_Vulkan, "Trying Vulkan library: {}", filename);
28 if (!library.Open(filename.c_str())) { 33 if (!library->Open(filename.c_str())) {
29 // Android devices may not have libvulkan.so.1, only libvulkan.so. 34 // Android devices may not have libvulkan.so.1, only libvulkan.so.
30 filename = Common::DynamicLibrary::GetVersionedFilename("vulkan"); 35 filename = Common::DynamicLibrary::GetVersionedFilename("vulkan");
31 LOG_DEBUG(Render_Vulkan, "Trying Vulkan library (second attempt): {}", filename); 36 LOG_DEBUG(Render_Vulkan, "Trying Vulkan library (second attempt): {}", filename);
32 void(library.Open(filename.c_str())); 37 void(library->Open(filename.c_str()));
33 } 38 }
34#endif 39#endif
35 return library; 40 return library;
41#endif
36} 42}
37 43
38} // namespace Vulkan 44} // namespace Vulkan
diff --git a/src/video_core/vulkan_common/vulkan_library.h b/src/video_core/vulkan_common/vulkan_library.h
index 364ca979b..e1734525e 100644
--- a/src/video_core/vulkan_common/vulkan_library.h
+++ b/src/video_core/vulkan_common/vulkan_library.h
@@ -3,10 +3,14 @@
3 3
4#pragma once 4#pragma once
5 5
6#include <memory>
7
6#include "common/dynamic_library.h" 8#include "common/dynamic_library.h"
9#include "core/frontend/graphics_context.h"
7 10
8namespace Vulkan { 11namespace Vulkan {
9 12
10Common::DynamicLibrary OpenLibrary(); 13std::shared_ptr<Common::DynamicLibrary> OpenLibrary(
14 [[maybe_unused]] Core::Frontend::GraphicsContext* context = nullptr);
11 15
12} // namespace Vulkan 16} // namespace Vulkan
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index 59d226113..cc6b6a25a 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -46,6 +46,7 @@
46#include "core/core.h" 46#include "core/core.h"
47#include "core/cpu_manager.h" 47#include "core/cpu_manager.h"
48#include "core/frontend/framebuffer_layout.h" 48#include "core/frontend/framebuffer_layout.h"
49#include "core/frontend/graphics_context.h"
49#include "input_common/drivers/camera.h" 50#include "input_common/drivers/camera.h"
50#include "input_common/drivers/keyboard.h" 51#include "input_common/drivers/keyboard.h"
51#include "input_common/drivers/mouse.h" 52#include "input_common/drivers/mouse.h"
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index f316b598c..431585216 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -515,8 +515,8 @@ void ConfigureGraphics::RetrieveVulkanDevices() try {
515 auto wsi = QtCommon::GetWindowSystemInfo(window); 515 auto wsi = QtCommon::GetWindowSystemInfo(window);
516 516
517 vk::InstanceDispatch dld; 517 vk::InstanceDispatch dld;
518 const Common::DynamicLibrary library = OpenLibrary(); 518 const auto library = OpenLibrary();
519 const vk::Instance instance = CreateInstance(library, dld, VK_API_VERSION_1_1, wsi.type); 519 const vk::Instance instance = CreateInstance(*library, dld, VK_API_VERSION_1_1, wsi.type);
520 const std::vector<VkPhysicalDevice> physical_devices = instance.EnumeratePhysicalDevices(); 520 const std::vector<VkPhysicalDevice> physical_devices = instance.EnumeratePhysicalDevices();
521 vk::SurfaceKHR surface = CreateSurface(instance, wsi); 521 vk::SurfaceKHR surface = CreateSurface(instance, wsi);
522 522
diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp
index 5e1f76339..6eefc94ed 100644
--- a/src/yuzu/startup_checks.cpp
+++ b/src/yuzu/startup_checks.cpp
@@ -25,9 +25,9 @@ void CheckVulkan() {
25 // Just start the Vulkan loader, this will crash if something is wrong 25 // Just start the Vulkan loader, this will crash if something is wrong
26 try { 26 try {
27 Vulkan::vk::InstanceDispatch dld; 27 Vulkan::vk::InstanceDispatch dld;
28 const Common::DynamicLibrary library = Vulkan::OpenLibrary(); 28 const auto library = Vulkan::OpenLibrary();
29 const Vulkan::vk::Instance instance = 29 const Vulkan::vk::Instance instance =
30 Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1); 30 Vulkan::CreateInstance(*library, dld, VK_API_VERSION_1_1);
31 31
32 } catch (const Vulkan::vk::Exception& exception) { 32 } catch (const Vulkan::vk::Exception& exception) {
33 fmt::print(stderr, "Failed to initialize Vulkan: {}\n", exception.what()); 33 fmt::print(stderr, "Failed to initialize Vulkan: {}\n", exception.what());
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index 644a30e59..911d461e4 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -318,7 +318,7 @@ anti_aliasing =
318fullscreen_mode = 318fullscreen_mode =
319 319
320# Aspect ratio 320# Aspect ratio
321# 0: Default (16:9), 1: Force 4:3, 2: Force 21:9, 3: Stretch to Window 321# 0: Default (16:9), 1: Force 4:3, 2: Force 21:9, 3: Force 16:10, 4: Stretch to Window
322aspect_ratio = 322aspect_ratio =
323 323
324# Anisotropic filtering 324# Anisotropic filtering
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.h b/src/yuzu_cmd/emu_window/emu_window_sdl2.h
index d9b453dee..4ad05e0e1 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.h
@@ -4,7 +4,9 @@
4#pragma once 4#pragma once
5 5
6#include <utility> 6#include <utility>
7
7#include "core/frontend/emu_window.h" 8#include "core/frontend/emu_window.h"
9#include "core/frontend/graphics_context.h"
8 10
9struct SDL_Window; 11struct SDL_Window;
10 12