summaryrefslogtreecommitdiffstats
path: root/src/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/android')
-rw-r--r--src/android/.gitignore65
-rw-r--r--src/android/app/build.gradle.kts258
-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.xml86
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt523
-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.kt306
-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.kt136
-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.kt474
-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.kt340
-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.kt230
-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.kt48
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt118
-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.kt1066
-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.kt528
-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.kt350
-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.kt48
-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.cpp301
-rw-r--r--src/android/app/src/main/jni/config.h37
-rw-r--r--src/android/app/src/main/jni/default_ini.h511
-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.cpp850
-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-xhdpi/tv_banner.pngbin0 -> 7764 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_system_update_alt.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-de/strings.xml332
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-it/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-ja/strings.xml335
-rw-r--r--src/android/app/src/main/res/values-ko/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-nb/strings.xml337
-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-pl/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-pt-rBR/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-pt-rPT/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml337
-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-zh-rCN/strings.xml337
-rw-r--r--src/android/app/src/main/res/values-zh-rTW/strings.xml336
-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.xml874
-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/locales_config.xml17
-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.properties17
-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
272 files changed, 25007 insertions, 0 deletions
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 @@
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/
+
+# Keystore files
+# Uncomment the following line if you do not want to check your keystore files in.
+#*.jks
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+
+# CXX compile cache
+app/.cxx
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
new file mode 100644
index 000000000..fe613d339
--- /dev/null
+++ b/src/android/app/build.gradle.kts
@@ -0,0 +1,258 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import android.annotation.SuppressLint
+import org.jetbrains.kotlin.konan.properties.Properties
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("plugin.serialization") version "1.8.21"
+}
+
+/**
+ * Use the number of seconds/10 since Jan 1 2016 as the versionCode.
+ * This lets us upload a new build at most every 10 seconds for the
+ * next 680 years.
+ */
+val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toInt()
+
+@Suppress("UnstableApiUsage")
+android {
+ namespace = "org.yuzu.yuzu_emu"
+
+ compileSdkVersion = "android-33"
+ ndkVersion = "25.2.9519653"
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ packaging {
+ // This is necessary for libadrenotools custom driver loading
+ jniLibs.useLegacyPackaging = true
+ }
+
+ lint {
+ // This is important as it will run lint but not abort on error
+ // Lint has some overly obnoxious "errors" that should really be warnings
+ abortOnError = false
+
+ //Uncomment disable lines for test builds...
+ //disable 'MissingTranslation'bin
+ //disable 'ExtraTranslation'
+ }
+
+ defaultConfig {
+ // TODO If this is ever modified, change application_id in strings.xml
+ applicationId = "org.yuzu.yuzu_emu"
+ minSdk = 30
+ targetSdk = 33
+ versionName = getGitVersion()
+
+ // If you want to use autoVersion for the versionCode, create a property in local.properties
+ // named "autoVersioned" and set it to "true"
+ val properties = Properties()
+ val versionProperty = try {
+ properties.load(project.rootProject.file("local.properties").inputStream())
+ properties.getProperty("autoVersioned") ?: ""
+ } catch (e: Exception) { "" }
+
+ versionCode = if (versionProperty == "true") {
+ autoVersion
+ } else {
+ 1
+ }
+
+ ndk {
+ @SuppressLint("ChromeOsAbiSupport")
+ abiFilters += listOf("arm64-v8a")
+ }
+
+ buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
+ buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
+ }
+
+ // Define build types, which are orthogonal to product flavors.
+ buildTypes {
+
+ // Signed by release key, allowing for upload to Play Store.
+ release {
+ resValue("string", "app_name_suffixed", "yuzu")
+ signingConfig = signingConfigs.getByName("debug")
+ isMinifyEnabled = true
+ isDebuggable = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android.txt"),
+ "proguard-rules.pro"
+ )
+ }
+
+ // builds a release build that doesn't need signing
+ // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
+ register("relWithDebInfo") {
+ resValue("string", "app_name_suffixed", "yuzu Debug Release")
+ signingConfig = signingConfigs.getByName("debug")
+ isMinifyEnabled = true
+ isDebuggable = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android.txt"),
+ "proguard-rules.pro"
+ )
+ versionNameSuffix = "-relWithDebInfo"
+ applicationIdSuffix = ".relWithDebInfo"
+ isJniDebuggable = true
+ }
+
+ // Signed by debug key disallowing distribution on Play Store.
+ // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
+ debug {
+ resValue("string", "app_name_suffixed", "yuzu Debug")
+ isDebuggable = true
+ isJniDebuggable = true
+ versionNameSuffix = "-debug"
+ applicationIdSuffix = ".debug"
+ }
+ }
+
+ flavorDimensions.add("version")
+ productFlavors {
+ create("mainline") {
+ dimension = "version"
+ buildConfigField("Boolean", "PREMIUM", "false")
+ }
+
+ create("ea") {
+ dimension = "version"
+ buildConfigField("Boolean", "PREMIUM", "true")
+ applicationIdSuffix = ".ea"
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ version = "3.22.1"
+ path = file("../../../CMakeLists.txt")
+ }
+ }
+
+ defaultConfig {
+ externalNativeBuild {
+ cmake {
+ arguments(
+ "-DENABLE_QT=0", // Don't use QT
+ "-DENABLE_SDL2=0", // Don't use SDL
+ "-DENABLE_WEB_SERVICE=0", // Don't use telemetry
+ "-DBUNDLE_SPEEX=ON",
+ "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
+ "-DYUZU_USE_BUNDLED_VCPKG=ON",
+ "-DYUZU_USE_BUNDLED_FFMPEG=ON",
+ "-DYUZU_ENABLE_LTO=ON"
+ )
+
+ abiFilters("arm64-v8a", "x86_64")
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("androidx.recyclerview:recyclerview:1.3.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.fragment:fragment-ktx:1.6.0")
+ implementation("androidx.documentfile:documentfile:1.0.1")
+ implementation("com.google.android.material:material:1.9.0")
+ implementation("androidx.preference:preference:1.2.0")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+ implementation("io.coil-kt:coil:2.2.2")
+ implementation("androidx.core:core-splashscreen:1.0.1")
+ implementation("androidx.window:window:1.1.0")
+ implementation("org.ini4j:ini4j:0.5.4")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
+ implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
+ implementation("info.debatty:java-string-similarity:2.0.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
+}
+
+fun getGitVersion(): String {
+ var versionName = "0.0"
+
+ try {
+ versionName = ProcessBuilder("git", "describe", "--always", "--long")
+ .directory(project.rootDir)
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectError(ProcessBuilder.Redirect.PIPE)
+ .start().inputStream.bufferedReader().use { it.readText() }
+ .trim()
+ .replace(Regex("(-0)?-[^-]+$"), "")
+ } catch (e: Exception) {
+ logger.error("Cannot find git, defaulting to dummy version number")
+ }
+
+ if (System.getenv("GITHUB_ACTIONS") != null) {
+ val gitTag = System.getenv("GIT_TAG_NAME")
+ versionName = gitTag ?: versionName
+ }
+
+ return versionName
+}
+
+fun getGitHash(): String {
+ try {
+ val processBuilder = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
+ processBuilder.directory(project.rootDir)
+ val process = processBuilder.start()
+ val inputStream = process.inputStream
+ val errorStream = process.errorStream
+ process.waitFor()
+
+ return if (process.exitValue() == 0) {
+ inputStream.bufferedReader()
+ .use { it.readText().trim() } // return the value of gitHash
+ } else {
+ val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
+ logger.error("Error running git command: $errorMessage")
+ "dummy-hash" // return a dummy hash value in case of an error
+ }
+ } catch (e: Exception) {
+ logger.error("$e: Cannot find git, defaulting to dummy build hash")
+ return "dummy-hash" // return a dummy hash value in case of an error
+ }
+}
+
+fun getBranch(): String {
+ try {
+ val processBuilder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
+ processBuilder.directory(project.rootDir)
+ val process = processBuilder.start()
+ val inputStream = process.inputStream
+ val errorStream = process.errorStream
+ process.waitFor()
+
+ return if (process.exitValue() == 0) {
+ inputStream.bufferedReader()
+ .use { it.readText().trim() } // return the value of gitHash
+ } else {
+ val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
+ logger.error("Error running git command: $errorMessage")
+ "dummy-hash" // return a dummy hash value in case of an error
+ }
+ } catch (e: Exception) {
+ logger.error("$e: Cannot find git, defaulting to dummy build hash")
+ return "dummy-hash" // return a dummy hash value in case of an error
+ }
+}
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 @@
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# To get usable stack traces
+-dontobfuscate
+
+# Prevents crashing when using Wini
+-keep class org.ini4j.spi.IniParser
+-keep class org.ini4j.spi.IniBuilder
+-keep class org.ini4j.spi.IniFormatter
+
+# Suppress warnings for R8
+-dontwarn org.bouncycastle.jsse.BCSSLParameters
+-dontwarn org.bouncycastle.jsse.BCSSLSocket
+-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
+-dontwarn org.conscrypt.Conscrypt$Version
+-dontwarn org.conscrypt.Conscrypt
+-dontwarn org.conscrypt.ConscryptHostnameVerifier
+-dontwarn org.openjsse.javax.net.ssl.SSLParameters
+-dontwarn org.openjsse.javax.net.ssl.SSLSocket
+-dontwarn org.openjsse.net.ssl.OpenJSSE
+-dontwarn java.beans.Introspector
+-dontwarn java.beans.VetoableChangeListener
+-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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="200dp"
+ android:height="200dp"
+ android:viewportWidth="500"
+ android:viewportHeight="500">
+ <path
+ android:fillColor="#C6C6C6"
+ android:fillType="nonZero"
+ 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"
+ android:strokeWidth="1.46"
+ android:strokeColor="#00000000"
+ android:strokeLineCap="butt"
+ android:strokeLineJoin="miter" />
+ <path
+ android:fillColor="#FFDC00"
+ android:fillType="nonZero"
+ 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"
+ android:strokeWidth="1.46"
+ android:strokeColor="#00000000"
+ android:strokeLineCap="butt"
+ android:strokeLineJoin="miter" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="155.3dp"
+ android:height="172.55dp"
+ android:viewportWidth="155.3"
+ android:viewportHeight="172.55">
+ <path
+ android:fillColor="#C6C6C6"
+ 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" />
+ <path
+ android:fillColor="#FFDC00"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="340.97dp"
+ android:height="389.85dp"
+ android:viewportWidth="340.97"
+ android:viewportHeight="389.85">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="#C6C6C6"
+ 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" />
+ <path
+ android:fillColor="#FFDC00"
+ 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" />
+</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..55f62b4b9
--- /dev/null
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
+ <uses-feature android:name="android.hardware.gamepad" android:required="false" />
+ <uses-feature android:name="android.software.leanback" android:required="false" />
+ <uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.NFC" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application
+ android:name="org.yuzu.yuzu_emu.YuzuApplication"
+ android:label="@string/app_name_suffixed"
+ android:icon="@drawable/ic_launcher"
+ android:allowBackup="true"
+ android:hasFragileUserData="true"
+ android:supportsRtl="true"
+ android:isGame="true"
+ android:localeConfig="@xml/locales_config"
+ android:banner="@drawable/tv_banner"
+ android:extractNativeLibs="true"
+ android:fullBackupContent="@xml/data_extraction_rules"
+ android:dataExtractionRules="@xml/data_extraction_rules_api_31"
+ android:enableOnBackInvokedCallback="true">
+
+ <activity
+ android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
+ android:exported="true"
+ android:theme="@style/Theme.Yuzu.Splash.Main">
+
+ <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
+ android:theme="@style/Theme.Yuzu.Main"
+ android:label="@string/preferences_settings"/>
+
+ <activity
+ android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
+ android:theme="@style/Theme.Yuzu.Main"
+ android:launchMode="singleTop"
+ android:screenOrientation="userLandscape"
+ android:exported="true">
+
+ <intent-filter>
+ <action android:name="android.nfc.action.TECH_DISCOVERED" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="application/octet-stream" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.nfc.action.TECH_DISCOVERED"
+ android:resource="@xml/nfc_tech_filter" />
+ </activity>
+
+ <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
+
+ <provider
+ android:name=".features.DocumentProvider"
+ android:authorities="${applicationId}.user"
+ android:grantUriPermissions="true"
+ android:exported="true"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+ <intent-filter>
+ <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+ </intent-filter>
+ </provider>
+
+ </application>
+
+</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..4be9ade14
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -0,0 +1,523 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.Html
+import android.text.method.LinkMovementMethod
+import android.view.Surface
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.Keep
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext
+import org.yuzu.yuzu_emu.activities.EmulationActivity
+import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
+import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
+import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
+import org.yuzu.yuzu_emu.utils.Log.error
+import org.yuzu.yuzu_emu.utils.Log.verbose
+import org.yuzu.yuzu_emu.utils.Log.warning
+import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
+import java.lang.ref.WeakReference
+
+/**
+ * Class which contains methods that interact
+ * with the native side of the Yuzu code.
+ */
+object NativeLibrary {
+ /**
+ * Default controller id for each device
+ */
+ const val Player1Device = 0
+ const val Player2Device = 1
+ const val Player3Device = 2
+ const val Player4Device = 3
+ const val Player5Device = 4
+ const val Player6Device = 5
+ const val Player7Device = 6
+ const val Player8Device = 7
+ const val ConsoleDevice = 8
+
+ /**
+ * Controller type for each device
+ */
+ const val ProController = 3
+ const val Handheld = 4
+ const val JoyconDual = 5
+ const val JoyconLeft = 6
+ const val JoyconRight = 7
+ const val GameCube = 8
+ const val Pokeball = 9
+ const val NES = 10
+ const val SNES = 11
+ const val N64 = 12
+ const val SegaGenesis = 13
+
+ @JvmField
+ var sEmulationActivity = WeakReference<EmulationActivity?>(null)
+
+ init {
+ try {
+ System.loadLibrary("yuzu-android")
+ } catch (ex: UnsatisfiedLinkError) {
+ error("[NativeLibrary] $ex")
+ }
+ }
+
+ @Keep
+ @JvmStatic
+ fun openContentUri(path: String?, openmode: String?): Int {
+ return if (isNativePath(path!!)) {
+ YuzuApplication.documentsTree!!.openContentUri(path, openmode)
+ } else openContentUri(appContext, path, openmode)
+ }
+
+ @Keep
+ @JvmStatic
+ fun getSize(path: String?): Long {
+ return if (isNativePath(path!!)) {
+ YuzuApplication.documentsTree!!.getFileSize(path)
+ } else getFileSize(appContext, path)
+ }
+
+ /**
+ * Returns true if pro controller isn't available and handheld is
+ */
+ external fun isHandheldOnly(): Boolean
+
+ /**
+ * Changes controller type for a specific device.
+ *
+ * @param Device The input descriptor of the gamepad.
+ * @param Type The NpadStyleIndex of the gamepad.
+ */
+ external fun setDeviceType(Device: Int, Type: Int): Boolean
+
+ /**
+ * Handles event when a gamepad is connected.
+ *
+ * @param Device The input descriptor of the gamepad.
+ */
+ external fun onGamePadConnectEvent(Device: Int): Boolean
+
+ /**
+ * Handles event when a gamepad is disconnected.
+ *
+ * @param Device The input descriptor of the gamepad.
+ */
+ external fun onGamePadDisconnectEvent(Device: Int): Boolean
+
+ /**
+ * Handles button press events for a gamepad.
+ *
+ * @param Device The input descriptor of the gamepad.
+ * @param Button Key code identifying which button was pressed.
+ * @param Action Mask identifying which action is happening (button pressed down, or button released).
+ * @return If we handled the button press.
+ */
+ external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
+
+ /**
+ * Handles joystick movement events.
+ *
+ * @param Device The device ID of the gamepad.
+ * @param Axis The axis ID
+ * @param x_axis The value of the x-axis represented by the given ID.
+ * @param y_axis The value of the y-axis represented by the given ID.
+ */
+ external fun onGamePadJoystickEvent(
+ Device: Int,
+ Axis: Int,
+ x_axis: Float,
+ y_axis: Float
+ ): Boolean
+
+ /**
+ * Handles motion events.
+ *
+ * @param delta_timestamp The finger id corresponding to this event
+ * @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor.
+ * @param accel_x,accel_y,accel_z The value of the y-axis
+ */
+ external fun onGamePadMotionEvent(
+ Device: Int,
+ delta_timestamp: Long,
+ gyro_x: Float,
+ gyro_y: Float,
+ gyro_z: Float,
+ accel_x: Float,
+ accel_y: Float,
+ accel_z: Float
+ ): Boolean
+
+ /**
+ * Signals and load a nfc tag
+ *
+ * @param data Byte array containing all the data from a nfc tag
+ */
+ external fun onReadNfcTag(data: ByteArray?): Boolean
+
+ /**
+ * Removes current loaded nfc tag
+ */
+ external fun onRemoveNfcTag(): Boolean
+
+ /**
+ * Handles touch press events.
+ *
+ * @param finger_id The finger id corresponding to this event
+ * @param x_axis The value of the x-axis.
+ * @param y_axis The value of the y-axis.
+ */
+ external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
+
+ /**
+ * Handles touch movement.
+ *
+ * @param x_axis The value of the instantaneous x-axis.
+ * @param y_axis The value of the instantaneous y-axis.
+ */
+ external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
+
+ /**
+ * Handles touch release events.
+ *
+ * @param finger_id The finger id corresponding to this event
+ */
+ external fun onTouchReleased(finger_id: Int)
+
+ external fun reloadSettings()
+
+ external fun getUserSetting(gameID: String?, Section: String?, Key: String?): String?
+
+ external fun setUserSetting(gameID: String?, Section: String?, Key: String?, Value: String?)
+
+ external fun initGameIni(gameID: String?)
+
+ /**
+ * Gets the embedded icon within the given ROM.
+ *
+ * @param filename the file path to the ROM.
+ * @return a byte array containing the JPEG data for the icon.
+ */
+ external fun getIcon(filename: String): ByteArray
+
+ /**
+ * Gets the embedded title of the given ISO/ROM.
+ *
+ * @param filename The file path to the ISO/ROM.
+ * @return the embedded title of the ISO/ROM.
+ */
+ external fun getTitle(filename: String): String
+
+ external fun getDescription(filename: String): String
+
+ external fun getGameId(filename: String): String
+
+ external fun getRegions(filename: String): String
+
+ external fun getCompany(filename: String): String
+
+ external fun isHomebrew(filename: String): Boolean
+
+ external fun setAppDirectory(directory: String)
+
+ external fun installFileToNand(filename: String): Int
+
+ external fun initializeGpuDriver(
+ hookLibDir: String?,
+ customDriverDir: String?,
+ customDriverName: String?,
+ fileRedirectDir: String?
+ )
+
+ external fun reloadKeys(): Boolean
+
+ external fun initializeEmulation()
+
+ external fun defaultCPUCore(): Int
+
+ /**
+ * Begins emulation.
+ */
+ external fun run(path: String?)
+
+ /**
+ * Begins emulation from the specified savestate.
+ */
+ external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
+
+ // Surface Handling
+ external fun surfaceChanged(surf: Surface?)
+
+ external fun surfaceDestroyed()
+
+ /**
+ * Unpauses emulation from a paused state.
+ */
+ external fun unPauseEmulation()
+
+ /**
+ * Pauses emulation.
+ */
+ external fun pauseEmulation()
+
+ /**
+ * Stops emulation.
+ */
+ external fun stopEmulation()
+
+ /**
+ * Resets the in-memory ROM metadata cache.
+ */
+ external fun resetRomMetadata()
+
+ /**
+ * Returns true if emulation is running (or is paused).
+ */
+ external fun isRunning(): Boolean
+
+ /**
+ * Returns the performance stats for the current game
+ */
+ external fun getPerfStats(): DoubleArray
+
+ /**
+ * Notifies the core emulation that the orientation has changed.
+ */
+ external fun notifyOrientationChange(layout_option: Int, rotation: Int)
+
+ enum class CoreError {
+ ErrorSystemFiles,
+ ErrorSavestate,
+ ErrorUnknown
+ }
+
+ private var coreErrorAlertResult = false
+ private val coreErrorAlertLock = Object()
+
+ class CoreErrorDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val title = requireArguments().serializable<String>("title")
+ val message = requireArguments().serializable<String>("message")
+
+ return MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.continue_button, null)
+ .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
+ coreErrorAlertResult = false
+ synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
+ }
+ .create()
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ coreErrorAlertResult = true
+ synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
+ }
+
+ companion object {
+ fun newInstance(title: String?, message: String?): CoreErrorDialogFragment {
+ val frag = CoreErrorDialogFragment()
+ val args = Bundle()
+ args.putString("title", title)
+ args.putString("message", message)
+ frag.arguments = args
+ return frag
+ }
+ }
+ }
+
+ private fun onCoreErrorImpl(title: String, message: String) {
+ val emulationActivity = sEmulationActivity.get()
+ if (emulationActivity == null) {
+ error("[NativeLibrary] EmulationActivity not present")
+ return
+ }
+
+ val fragment = CoreErrorDialogFragment.newInstance(title, message)
+ fragment.show(emulationActivity.supportFragmentManager, "coreError")
+ }
+
+ /**
+ * Handles a core error.
+ *
+ * @return true: continue; false: abort
+ */
+ fun onCoreError(error: CoreError?, details: String): Boolean {
+ val emulationActivity = sEmulationActivity.get()
+ if (emulationActivity == null) {
+ error("[NativeLibrary] EmulationActivity not present")
+ return false
+ }
+
+ val title: String
+ val message: String
+ when (error) {
+ CoreError.ErrorSystemFiles -> {
+ title = emulationActivity.getString(R.string.system_archive_not_found)
+ message = emulationActivity.getString(
+ R.string.system_archive_not_found_message,
+ details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
+ )
+ }
+ CoreError.ErrorSavestate -> {
+ title = emulationActivity.getString(R.string.save_load_error)
+ message = details
+ }
+ CoreError.ErrorUnknown -> {
+ title = emulationActivity.getString(R.string.fatal_error)
+ message = emulationActivity.getString(R.string.fatal_error_message)
+ }
+ else -> {
+ return true
+ }
+ }
+
+ // Show the AlertDialog on the main thread.
+ emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
+
+ // Wait for the lock to notify that it is complete.
+ synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
+
+ return coreErrorAlertResult
+ }
+
+ @Keep
+ @JvmStatic
+ fun exitEmulationActivity(resultCode: Int) {
+ val Success = 0
+ val ErrorNotInitialized = 1
+ val ErrorGetLoader = 2
+ val ErrorSystemFiles = 3
+ val ErrorSharedFont = 4
+ val ErrorVideoCore = 5
+ val ErrorUnknown = 6
+ val ErrorLoader = 7
+
+ val captionId: Int
+ var descriptionId: Int
+ when (resultCode) {
+ ErrorVideoCore -> {
+ captionId = R.string.loader_error_video_core
+ descriptionId = R.string.loader_error_video_core_description
+ }
+ else -> {
+ captionId = R.string.loader_error_encrypted
+ descriptionId = R.string.loader_error_encrypted_roms_description
+ if (!reloadKeys()) {
+ descriptionId = R.string.loader_error_encrypted_keys_description
+ }
+ }
+ }
+
+ val emulationActivity = sEmulationActivity.get()
+ if (emulationActivity == null) {
+ warning("[NativeLibrary] EmulationActivity is null, can't exit.")
+ return
+ }
+
+ val builder = MaterialAlertDialogBuilder(emulationActivity)
+ .setTitle(captionId)
+ .setMessage(
+ Html.fromHtml(
+ emulationActivity.getString(descriptionId),
+ Html.FROM_HTML_MODE_LEGACY
+ )
+ )
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> emulationActivity.finish() }
+ .setOnDismissListener { emulationActivity.finish() }
+ emulationActivity.runOnUiThread {
+ val alert = builder.create()
+ alert.show()
+ (alert.findViewById<View>(android.R.id.message) as TextView).movementMethod =
+ LinkMovementMethod.getInstance()
+ }
+ }
+
+ fun setEmulationActivity(emulationActivity: EmulationActivity?) {
+ verbose("[NativeLibrary] Registering EmulationActivity.")
+ sEmulationActivity = WeakReference(emulationActivity)
+ }
+
+ fun clearEmulationActivity() {
+ verbose("[NativeLibrary] Unregistering EmulationActivity.")
+ sEmulationActivity.clear()
+ }
+
+ /**
+ * Logs the Yuzu version, Android version and, CPU.
+ */
+ external fun logDeviceInfo()
+
+ /**
+ * Submits inline keyboard text. Called on input for buttons that result text.
+ * @param text Text to submit to the inline software keyboard implementation.
+ */
+ external fun submitInlineKeyboardText(text: String?)
+
+ /**
+ * Submits inline keyboard input. Used to indicate keys pressed that are not text.
+ * @param key_code Android Key Code associated with the keyboard input.
+ */
+ external fun submitInlineKeyboardInput(key_code: Int)
+
+ /**
+ * Button type for use in onTouchEvent
+ */
+ object ButtonType {
+ const val BUTTON_A = 0
+ const val BUTTON_B = 1
+ const val BUTTON_X = 2
+ const val BUTTON_Y = 3
+ const val STICK_L = 4
+ const val STICK_R = 5
+ const val TRIGGER_L = 6
+ const val TRIGGER_R = 7
+ const val TRIGGER_ZL = 8
+ const val TRIGGER_ZR = 9
+ const val BUTTON_PLUS = 10
+ const val BUTTON_MINUS = 11
+ const val DPAD_LEFT = 12
+ const val DPAD_UP = 13
+ const val DPAD_RIGHT = 14
+ const val DPAD_DOWN = 15
+ const val BUTTON_SL = 16
+ const val BUTTON_SR = 17
+ const val BUTTON_HOME = 18
+ const val BUTTON_CAPTURE = 19
+ }
+
+ /**
+ * Stick type for use in onTouchEvent
+ */
+ object StickType {
+ const val STICK_L = 0
+ const val STICK_R = 1
+ }
+
+ /**
+ * Button states
+ */
+ object ButtonState {
+ const val RELEASED = 0
+ const val PRESSED = 1
+ }
+
+ /**
+ * Result from installFileToNand
+ */
+ object InstallFileToNandResult {
+ const val Success = 0
+ const val SuccessFileOverwritten = 1
+ const val Error = 2
+ const val ErrorBaseGame = 3
+ const val ErrorFilenameExtension = 4
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu
+
+import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.DocumentsTree
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import java.io.File
+
+fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
+
+class YuzuApplication : Application() {
+ private fun createNotificationChannels() {
+ val emulationChannel = NotificationChannel(
+ getString(R.string.emulation_notification_channel_id),
+ getString(R.string.emulation_notification_channel_name),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ emulationChannel.description = getString(R.string.emulation_notification_channel_description)
+ emulationChannel.setSound(null, null)
+ emulationChannel.vibrationPattern = null
+
+ val noticeChannel = NotificationChannel(
+ getString(R.string.notice_notification_channel_id),
+ getString(R.string.notice_notification_channel_name),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ noticeChannel.description = getString(R.string.notice_notification_channel_description)
+ noticeChannel.setSound(null, null)
+
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ val notificationManager = getSystemService(NotificationManager::class.java)
+ notificationManager.createNotificationChannel(emulationChannel)
+ notificationManager.createNotificationChannel(noticeChannel)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ application = this
+ documentsTree = DocumentsTree()
+ DirectoryInitialization.start(applicationContext)
+ GpuDriverHelper.initializeDriverParameters(applicationContext)
+ NativeLibrary.logDeviceInfo()
+
+ createNotificationChannels();
+ }
+
+ companion object {
+ var documentsTree: DocumentsTree? = null
+ lateinit var application: YuzuApplication
+
+ val appContext: Context
+ get() = application.applicationContext
+ }
+}
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..20a0394f5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -0,0 +1,306 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.activities
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.os.Bundle
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.layout.WindowInfoTracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
+import org.yuzu.yuzu_emu.fragments.EmulationFragment
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
+import org.yuzu.yuzu_emu.utils.ForegroundService
+import org.yuzu.yuzu_emu.utils.InputHandler
+import org.yuzu.yuzu_emu.utils.NfcReader
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+import org.yuzu.yuzu_emu.utils.ThemeHelper
+import kotlin.math.roundToInt
+
+class EmulationActivity : AppCompatActivity(), SensorEventListener {
+ private var controllerMappingHelper: ControllerMappingHelper? = null
+
+ var isActivityRecreated = false
+ private var emulationFragment: EmulationFragment? = null
+ private lateinit var nfcReader: NfcReader
+ private lateinit var inputHandler: InputHandler
+
+ private val gyro = FloatArray(3)
+ private val accel = FloatArray(3)
+ private var motionTimestamp: Long = 0
+ private var flipMotionOrientation: Boolean = false
+
+ private lateinit var game: Game
+
+ private val settingsViewModel: SettingsViewModel by viewModels()
+
+ override fun onDestroy() {
+ stopForegroundService(this)
+ super.onDestroy()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeHelper.setTheme(this)
+
+ settingsViewModel.settings.loadSettings()
+
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ // Get params we were passed
+ game = intent.parcelable(EXTRA_SELECTED_GAME)!!
+ isActivityRecreated = false
+ } else {
+ isActivityRecreated = true
+ restoreState(savedInstanceState)
+ }
+ controllerMappingHelper = ControllerMappingHelper()
+
+ // Set these options now so that the SurfaceView the game renders into is the right size.
+ enableFullscreenImmersive()
+
+ setContentView(R.layout.activity_emulation)
+ window.decorView.setBackgroundColor(getColor(android.R.color.black))
+
+ // Find or create the EmulationFragment
+ emulationFragment =
+ supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
+ if (emulationFragment == null) {
+ emulationFragment = EmulationFragment.newInstance(game)
+ supportFragmentManager.beginTransaction()
+ .add(R.id.frame_emulation_fragment, emulationFragment!!)
+ .commit()
+ }
+ title = game.title
+
+ nfcReader = NfcReader(this)
+ nfcReader.initialize()
+
+ inputHandler = InputHandler()
+ inputHandler.initialize()
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ WindowInfoTracker.getOrCreate(this@EmulationActivity)
+ .windowLayoutInfo(this@EmulationActivity)
+ .collect { emulationFragment?.updateCurrentLayout(this@EmulationActivity, it) }
+ }
+ }
+
+ // Start a foreground service to prevent the app from getting killed in the background
+ val startIntent = Intent(this, ForegroundService::class.java)
+ startForegroundService(startIntent)
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (event.action == KeyEvent.ACTION_DOWN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ // Special case, we do not support multiline input, dismiss the keyboard.
+ val overlayView: View =
+ this.findViewById(R.id.surface_input_overlay)
+ val im =
+ overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ im.hideSoftInputFromWindow(overlayView.windowToken, 0)
+ } else {
+ val textChar = event.unicodeChar
+ if (textChar == 0) {
+ // No text, button input.
+ NativeLibrary.submitInlineKeyboardInput(keyCode)
+ } else {
+ // Text submitted.
+ NativeLibrary.submitInlineKeyboardText(textChar.toChar().toString())
+ }
+ }
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ nfcReader.startScanning()
+ startMotionSensorListener()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ nfcReader.stopScanning()
+ stopMotionSensorListener()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ nfcReader.onNewIntent(intent)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putParcelable(EXTRA_SELECTED_GAME, game)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
+ event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
+ ) {
+ return super.dispatchKeyEvent(event)
+ }
+
+ return inputHandler.dispatchKeyEvent(event)
+ }
+
+ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
+ if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
+ event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
+ ) {
+ return super.dispatchGenericMotionEvent(event)
+ }
+
+ // Don't attempt to do anything if we are disconnecting a device.
+ if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
+ return true
+ }
+
+ return inputHandler.dispatchGenericMotionEvent(event)
+ }
+
+ override fun onSensorChanged(event: SensorEvent) {
+ val rotation = this.display?.rotation
+ if (rotation == Surface.ROTATION_90) {
+ flipMotionOrientation = true
+ }
+ if (rotation == Surface.ROTATION_270) {
+ flipMotionOrientation = false
+ }
+
+ if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
+ if (flipMotionOrientation) {
+ accel[0] = event.values[1] / SensorManager.GRAVITY_EARTH
+ accel[1] = -event.values[0] / SensorManager.GRAVITY_EARTH
+ } else {
+ accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH
+ accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH
+ }
+ accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH
+ }
+ if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
+ // Investigate why sensor value is off by 6x
+ if (flipMotionOrientation) {
+ gyro[0] = -event.values[1] / 6.0f
+ gyro[1] = event.values[0] / 6.0f
+ } else {
+ gyro[0] = event.values[1] / 6.0f
+ gyro[1] = -event.values[0] / 6.0f
+ }
+ gyro[2] = event.values[2] / 6.0f
+ }
+
+ // Only update state on accelerometer data
+ if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) {
+ return
+ }
+ val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
+ motionTimestamp = event.timestamp
+ NativeLibrary.onGamePadMotionEvent(
+ NativeLibrary.Player1Device,
+ deltaTimestamp,
+ gyro[0],
+ gyro[1],
+ gyro[2],
+ accel[0],
+ accel[1],
+ accel[2]
+ )
+ NativeLibrary.onGamePadMotionEvent(
+ NativeLibrary.ConsoleDevice,
+ deltaTimestamp,
+ gyro[0],
+ gyro[1],
+ gyro[2],
+ accel[0],
+ accel[1],
+ accel[2]
+ )
+ }
+
+ override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
+
+ private fun restoreState(savedInstanceState: Bundle) {
+ game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
+ }
+
+ private fun enableFullscreenImmersive() {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ WindowInsetsControllerCompat(window, window.decorView).let { controller ->
+ controller.hide(WindowInsetsCompat.Type.systemBars())
+ controller.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+ }
+
+ private fun startMotionSensorListener() {
+ val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
+ val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+ val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+ sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME)
+ sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME)
+ }
+
+ private fun stopMotionSensorListener() {
+ val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
+ val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+ val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+
+ sensorManager.unregisterListener(this, gyroSensor)
+ sensorManager.unregisterListener(this, accelSensor)
+ }
+
+ companion object {
+ const val EXTRA_SELECTED_GAME = "SelectedGame"
+
+ fun launch(activity: AppCompatActivity, game: Game) {
+ val launcher = Intent(activity, EmulationActivity::class.java)
+ launcher.putExtra(EXTRA_SELECTED_GAME, game)
+ activity.startActivity(launcher)
+ }
+
+ fun stopForegroundService(activity: Activity) {
+ val startIntent = Intent(activity, ForegroundService::class.java)
+ startIntent.action = ForegroundService.ACTION_STOP
+ activity.startForegroundService(startIntent)
+ }
+
+ private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
+ if (view == null) {
+ return true
+ }
+ val viewBounds = Rect()
+ view.getGlobalVisibleRect(viewBounds)
+ return !viewBounds.contains(x.roundToInt(), y.roundToInt())
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.databinding.CardGameBinding
+import org.yuzu.yuzu_emu.activities.EmulationActivity
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
+import org.yuzu.yuzu_emu.model.GamesViewModel
+
+class GameAdapter(private val activity: AppCompatActivity) :
+ ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
+ View.OnClickListener {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
+ // Create a new view.
+ val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ binding.cardGame.setOnClickListener(this)
+
+ // Use that view to create a ViewHolder.
+ return GameViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
+ holder.bind(currentList[position])
+ }
+
+ override fun getItemCount(): Int = currentList.size
+
+ /**
+ * Launches the game that was clicked on.
+ *
+ * @param view The card representing the game the user wants to play.
+ */
+ override fun onClick(view: View) {
+ val holder = view.tag as GameViewHolder
+
+ val gameExists = DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(holder.game.path))?.exists() == true
+ if (!gameExists) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ R.string.loader_error_file_not_found,
+ Toast.LENGTH_LONG
+ ).show()
+
+ ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
+ return
+ }
+
+ val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ preferences.edit()
+ .putLong(
+ holder.game.keyLastPlayedTime,
+ System.currentTimeMillis()
+ )
+ .apply()
+
+ EmulationActivity.launch(activity, holder.game)
+ }
+
+ inner class GameViewHolder(val binding: CardGameBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var game: Game
+
+ init {
+ binding.cardGame.tag = this
+ }
+
+ fun bind(game: Game) {
+ this.game = game
+
+ binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
+ activity.lifecycleScope.launch {
+ val bitmap = decodeGameIcon(game.path)
+ binding.imageGameScreen.load(bitmap) {
+ error(R.drawable.default_icon)
+ }
+ }
+
+ binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
+
+ binding.textGameTitle.postDelayed(
+ {
+ binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
+ binding.textGameTitle.isSelected = true
+ },
+ 3000
+ )
+ }
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback<Game>() {
+ override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
+ return oldItem.gameId == newItem.gameId
+ }
+
+ override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
+ return oldItem == newItem
+ }
+ }
+
+ private fun decodeGameIcon(uri: String): Bitmap? {
+ val data = NativeLibrary.getIcon(uri)
+ return BitmapFactory.decodeByteArray(
+ data,
+ 0,
+ data.size,
+ BitmapFactory.Options()
+ )
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
+import org.yuzu.yuzu_emu.model.HomeSetting
+
+class HomeSettingAdapter(private val activity: AppCompatActivity, var options: List<HomeSetting>) :
+ RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
+ View.OnClickListener {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
+ val binding =
+ CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ binding.root.setOnClickListener(this)
+ return HomeOptionViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int {
+ return options.size
+ }
+
+ override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
+ holder.bind(options[position])
+ }
+
+ override fun onClick(view: View) {
+ val holder = view.tag as HomeOptionViewHolder
+ holder.option.onClick.invoke()
+ }
+
+ inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var option: HomeSetting
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(option: HomeSetting) {
+ this.option = option
+ binding.optionTitle.text = activity.resources.getString(option.titleId)
+ binding.optionDescription.text = activity.resources.getString(option.descriptionId)
+ binding.optionIcon.setImageDrawable(
+ ResourcesCompat.getDrawable(
+ activity.resources,
+ option.iconId,
+ activity.theme
+ )
+ )
+
+ when (option.titleId) {
+ R.string.get_early_access -> binding.optionLayout.background =
+ ContextCompat.getDrawable(
+ binding.optionCard.context,
+ R.drawable.premium_background
+ )
+ }
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
+import org.yuzu.yuzu_emu.model.License
+
+class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
+ RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
+ View.OnClickListener {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
+ val binding =
+ ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ binding.root.setOnClickListener(this)
+ return LicenseViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = licenses.size
+
+ override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
+ holder.bind(licenses[position])
+ }
+
+ override fun onClick(view: View) {
+ val license = (view.tag as LicenseViewHolder).license
+ LicenseBottomSheetDialogFragment.newInstance(license)
+ .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
+ }
+
+ inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
+ lateinit var license: License
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(license: License) {
+ this.license = license
+
+ val context = YuzuApplication.appContext
+ binding.textSettingName.text = context.getString(license.titleId)
+ binding.textSettingDescription.text = context.getString(license.descriptionId)
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
+import org.yuzu.yuzu_emu.databinding.PageSetupBinding
+import org.yuzu.yuzu_emu.model.SetupPage
+
+class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
+ RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
+ val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return SetupPageViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = pages.size
+
+ override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
+ holder.bind(pages[position])
+
+ inner class SetupPageViewHolder(val binding: PageSetupBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var page: SetupPage
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(page: SetupPage) {
+ this.page = page
+ binding.icon.setImageDrawable(
+ ResourcesCompat.getDrawable(
+ activity.resources,
+ page.iconId,
+ activity.theme
+ )
+ )
+ binding.textTitle.text = activity.resources.getString(page.titleId)
+ binding.textDescription.text =
+ Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
+
+ binding.buttonAction.apply {
+ text = activity.resources.getString(page.buttonTextId)
+ if (page.buttonIconId != 0) {
+ icon = ResourcesCompat.getDrawable(
+ activity.resources,
+ page.buttonIconId,
+ activity.theme
+ )
+ }
+ iconGravity =
+ if (page.leftAlignedIcon) {
+ MaterialButton.ICON_GRAVITY_START
+ } else {
+ MaterialButton.ICON_GRAVITY_END
+ }
+ setOnClickListener {
+ page.buttonAction.invoke()
+ }
+ }
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.applets.keyboard
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.view.KeyEvent
+import android.view.View
+import android.view.WindowInsets
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.Keep
+import androidx.core.view.ViewCompat
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment
+import java.io.Serializable
+
+@Keep
+object SoftwareKeyboard {
+ lateinit var data: KeyboardData
+ val dataLock = Object()
+
+ private fun executeNormalImpl(config: KeyboardConfig) {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()
+ data = KeyboardData(SwkbdResult.Cancel.ordinal, "")
+ val fragment = KeyboardDialogFragment.newInstance(config)
+ fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
+ }
+
+ private fun executeInlineImpl(config: KeyboardConfig) {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()
+
+ val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay)
+ val im =
+ overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED)
+
+ // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
+ val handler = Handler(Looper.myLooper()!!)
+ val delayMs = 500
+ handler.postDelayed(object : Runnable {
+ override fun run() {
+ val insets = ViewCompat.getRootWindowInsets(overlayView)
+ val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime())
+ if (isKeyboardVisible) {
+ handler.postDelayed(this, delayMs.toLong())
+ return
+ }
+
+ // No longer visible, submit the result.
+ NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
+ }
+ }, delayMs.toLong())
+ }
+
+ @JvmStatic
+ fun executeNormal(config: KeyboardConfig): KeyboardData {
+ NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) }
+ synchronized(dataLock) {
+ dataLock.wait()
+ }
+ return data
+ }
+
+ @JvmStatic
+ fun executeInline(config: KeyboardConfig) {
+ NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) }
+ }
+
+ // Corresponds to Service::AM::Applets::SwkbdType
+ enum class SwkbdType {
+ Normal,
+ NumberPad,
+ Qwerty,
+ Unknown3,
+ Latin,
+ SimplifiedChinese,
+ TraditionalChinese,
+ Korean
+ }
+
+ // Corresponds to Service::AM::Applets::SwkbdPasswordMode
+ enum class SwkbdPasswordMode {
+ Disabled,
+ Enabled
+ }
+
+ // Corresponds to Service::AM::Applets::SwkbdResult
+ enum class SwkbdResult {
+ Ok,
+ Cancel
+ }
+
+ @Keep
+ data class KeyboardConfig(
+ var ok_text: String? = null,
+ var header_text: String? = null,
+ var sub_text: String? = null,
+ var guide_text: String? = null,
+ var initial_text: String? = null,
+ var left_optional_symbol_key: Short = 0,
+ var right_optional_symbol_key: Short = 0,
+ var max_text_length: Int = 0,
+ var min_text_length: Int = 0,
+ var initial_cursor_position: Int = 0,
+ var type: Int = 0,
+ var password_mode: Int = 0,
+ var text_draw_type: Int = 0,
+ var key_disable_flags: Int = 0,
+ var use_blur_background: Boolean = false,
+ var enable_backspace_button: Boolean = false,
+ var enable_return_button: Boolean = false,
+ var disable_cancel_button: Boolean = false
+ ) : Serializable
+
+ // Corresponds to Frontend::KeyboardData
+ @Keep
+ data class KeyboardData(var result: Int, var text: String)
+}
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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.applets.keyboard.ui
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.InputFilter
+import android.text.InputType
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard
+import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig
+import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
+import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
+
+class KeyboardDialogFragment : DialogFragment() {
+ private lateinit var binding: DialogEditTextBinding
+ private lateinit var config: KeyboardConfig
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = DialogEditTextBinding.inflate(layoutInflater)
+ config = requireArguments().serializable(CONFIG)!!
+
+ // Set up the input
+ binding.editText.hint = config.initial_text
+ binding.editText.isSingleLine = !config.enable_return_button
+ binding.editText.filters =
+ arrayOf<InputFilter>(InputFilter.LengthFilter(config.max_text_length))
+
+ // Handle input type
+ var inputType: Int
+ when (config.type) {
+ SoftwareKeyboard.SwkbdType.Normal.ordinal,
+ SoftwareKeyboard.SwkbdType.Qwerty.ordinal,
+ SoftwareKeyboard.SwkbdType.Unknown3.ordinal,
+ SoftwareKeyboard.SwkbdType.Latin.ordinal,
+ SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal,
+ SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal,
+ SoftwareKeyboard.SwkbdType.Korean.ordinal -> {
+ inputType = InputType.TYPE_CLASS_TEXT
+ if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
+ inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ }
+ SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> {
+ inputType = InputType.TYPE_CLASS_NUMBER
+ if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
+ inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD
+ }
+ }
+ else -> {
+ inputType = InputType.TYPE_CLASS_TEXT
+ if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
+ inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ }
+ }
+ binding.editText.inputType = inputType
+
+ val headerText =
+ config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) }
+ val okText =
+ config.ok_text!!.ifEmpty { resources.getString(R.string.submit) }
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(headerText)
+ .setView(binding.root)
+ .setPositiveButton(okText) { _, _ ->
+ SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal
+ SoftwareKeyboard.data.text = binding.editText.text.toString()
+ }
+ .setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ ->
+ SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal
+ }
+ .create()
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ synchronized(SoftwareKeyboard.dataLock) {
+ SoftwareKeyboard.dataLock.notifyAll()
+ }
+ }
+
+ companion object {
+ const val TAG = "KeyboardDialogFragment"
+ const val CONFIG = "keyboard_config"
+
+ fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment {
+ val frag = KeyboardDialogFragment()
+ val args = Bundle()
+ args.putSerializable(CONFIG, config)
+ frag.arguments = args
+ return frag
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.disk_shader_cache
+
+import androidx.annotation.Keep
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment
+
+@Keep
+object DiskShaderCacheProgress {
+ val finishLock = Object()
+ private lateinit var fragment: ShaderProgressDialogFragment
+
+ private fun prepareDialog() {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
+ emulationActivity.runOnUiThread {
+ fragment = ShaderProgressDialogFragment.newInstance(
+ emulationActivity.getString(R.string.loading),
+ emulationActivity.getString(R.string.preparing_shaders)
+ )
+ fragment.show(emulationActivity.supportFragmentManager, ShaderProgressDialogFragment.TAG)
+ }
+ synchronized(finishLock) { finishLock.wait() }
+ }
+
+ @JvmStatic
+ fun loadProgress(stage: Int, progress: Int, max: Int) {
+ val emulationActivity = NativeLibrary.sEmulationActivity.get()
+ ?: error("[DiskShaderCacheProgress] EmulationActivity not present")
+
+ when (LoadCallbackStage.values()[stage]) {
+ LoadCallbackStage.Prepare -> prepareDialog()
+ LoadCallbackStage.Build -> fragment.onUpdateProgress(
+ emulationActivity.getString(R.string.building_shaders),
+ progress,
+ max
+ )
+ LoadCallbackStage.Complete -> fragment.dismiss()
+ }
+ }
+
+ // Equivalent to VideoCore::LoadCallbackStage
+ enum class LoadCallbackStage {
+ Prepare, Build, Complete
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.disk_shader_cache
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class ShaderProgressViewModel : ViewModel() {
+ private val _progress = MutableLiveData(0)
+ val progress: LiveData<Int> get() = _progress
+
+ private val _max = MutableLiveData(0)
+ val max: LiveData<Int> get() = _max
+
+ private val _message = MutableLiveData("")
+ val message: LiveData<String> get() = _message
+
+ fun setProgress(progress: Int) {
+ _progress.postValue(progress)
+ }
+
+ fun setMax(max: Int) {
+ _max.postValue(max)
+ }
+
+ fun setMessage(msg: String) {
+ _message.postValue(msg)
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.disk_shader_cache.ui
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
+import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
+
+class ShaderProgressDialogFragment : DialogFragment() {
+ private var _binding: DialogProgressBarBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var alertDialog: AlertDialog
+
+ private lateinit var shaderProgressViewModel: ShaderProgressViewModel
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ _binding = DialogProgressBarBinding.inflate(layoutInflater)
+ shaderProgressViewModel =
+ ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
+
+ val title = requireArguments().getString(TITLE)
+ val message = requireArguments().getString(MESSAGE)
+
+ isCancelable = false
+ alertDialog = MaterialAlertDialogBuilder(requireActivity())
+ .setView(binding.root)
+ .setTitle(title)
+ .setMessage(message)
+ .create()
+ return alertDialog
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
+ binding.progressBar.progress = progress
+ setUpdateText()
+ }
+ shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
+ binding.progressBar.max = max
+ setUpdateText()
+ }
+ shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
+ alertDialog.setMessage(msg)
+ }
+ synchronized(DiskShaderCacheProgress.finishLock) { DiskShaderCacheProgress.finishLock.notifyAll() }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ fun onUpdateProgress(msg: String, progress: Int, max: Int) {
+ shaderProgressViewModel.setProgress(progress)
+ shaderProgressViewModel.setMax(max)
+ shaderProgressViewModel.setMessage(msg)
+ }
+
+ private fun setUpdateText() {
+ binding.progressText.text = String.format(
+ "%d/%d",
+ shaderProgressViewModel.progress.value,
+ shaderProgressViewModel.max.value
+ )
+ }
+
+ companion object {
+ const val TAG = "ProgressDialogFragment"
+ const val TITLE = "title"
+ const val MESSAGE = "message"
+
+ fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
+ val frag = ShaderProgressDialogFragment()
+ val args = Bundle()
+ args.putString(TITLE, title)
+ args.putString(MESSAGE, message)
+ frag.arguments = args
+ return frag
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// SPDX-License-Identifier: MPL-2.0
+// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
+
+package org.yuzu.yuzu_emu.features
+
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.provider.DocumentsContract
+import android.provider.DocumentsProvider
+import android.webkit.MimeTypeMap
+import org.yuzu.yuzu_emu.BuildConfig
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.getPublicFilesDir
+import java.io.*
+
+class DocumentProvider : DocumentsProvider() {
+ private val baseDirectory: File
+ get() = File(YuzuApplication.application.getPublicFilesDir().canonicalPath)
+
+ companion object {
+ private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
+ DocumentsContract.Root.COLUMN_ROOT_ID,
+ DocumentsContract.Root.COLUMN_MIME_TYPES,
+ DocumentsContract.Root.COLUMN_FLAGS,
+ DocumentsContract.Root.COLUMN_ICON,
+ DocumentsContract.Root.COLUMN_TITLE,
+ DocumentsContract.Root.COLUMN_SUMMARY,
+ DocumentsContract.Root.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
+ )
+
+ private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ DocumentsContract.Document.COLUMN_FLAGS,
+ DocumentsContract.Document.COLUMN_SIZE
+ )
+
+ const val AUTHORITY : String = BuildConfig.APPLICATION_ID + ".user"
+ const val ROOT_ID: String = "root"
+ }
+
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ /**
+ * @return The [File] that corresponds to the document ID supplied by [getDocumentId]
+ */
+ private fun getFile(documentId: String): File {
+ if (documentId.startsWith(ROOT_ID)) {
+ val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
+ if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
+ return file
+ } else {
+ throw FileNotFoundException("'$documentId' is not in any known root")
+ }
+ }
+
+ /**
+ * @return A unique ID for the provided [File]
+ */
+ private fun getDocumentId(file: File): String {
+ return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
+ }
+
+ override fun queryRoots(projection: Array<out String>?): Cursor {
+ val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
+
+ cursor.newRow().apply {
+ add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
+ add(DocumentsContract.Root.COLUMN_SUMMARY, null)
+ add(
+ DocumentsContract.Root.COLUMN_FLAGS,
+ DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
+ )
+ add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
+ add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
+ add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
+ add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
+ add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
+ }
+
+ return cursor
+ }
+
+ override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
+ val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
+ return includeFile(cursor, documentId, null)
+ }
+
+ override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
+ return documentId?.startsWith(parentDocumentId!!) ?: false
+ }
+
+ /**
+ * @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
+ */
+ private fun File.resolveWithoutConflict(name: String): File {
+ var file = resolve(name)
+ if (file.exists()) {
+ var noConflictId =
+ 1 // Makes sure two files don't have the same name by adding a number to the end
+ val extension = name.substringAfterLast('.')
+ val baseName = name.substringBeforeLast('.')
+ while (file.exists())
+ file = resolve("$baseName (${noConflictId++}).$extension")
+ }
+ return file
+ }
+
+ override fun createDocument(
+ parentDocumentId: String?,
+ mimeType: String?,
+ displayName: String
+ ): String {
+ val parentFile = getFile(parentDocumentId!!)
+ val newFile = parentFile.resolveWithoutConflict(displayName)
+
+ try {
+ if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
+ if (!newFile.mkdir())
+ throw IOException("Failed to create directory")
+ } else {
+ if (!newFile.createNewFile())
+ throw IOException("Failed to create file")
+ }
+ } catch (e: IOException) {
+ throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
+ }
+
+ return getDocumentId(newFile)
+ }
+
+ override fun deleteDocument(documentId: String?) {
+ val file = getFile(documentId!!)
+ if (!file.delete())
+ throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
+ }
+
+ override fun removeDocument(documentId: String, parentDocumentId: String?) {
+ val parent = getFile(parentDocumentId!!)
+ val file = getFile(documentId)
+
+ if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
+ if (!file.delete())
+ throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
+ } else {
+ throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
+ }
+ }
+
+ override fun renameDocument(documentId: String?, displayName: String?): String {
+ if (displayName == null)
+ throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
+
+ val sourceFile = getFile(documentId!!)
+ val sourceParentFile = sourceFile.parentFile
+ ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
+ val destFile = sourceParentFile.resolve(displayName)
+
+ try {
+ if (!sourceFile.renameTo(destFile))
+ throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
+ } catch (e: Exception) {
+ throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
+ }
+
+ return getDocumentId(destFile)
+ }
+
+ private fun copyDocument(
+ sourceDocumentId: String, sourceParentDocumentId: String,
+ targetParentDocumentId: String?
+ ): String {
+ if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
+ throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
+
+ return copyDocument(sourceDocumentId, targetParentDocumentId)
+ }
+
+ override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
+ val parent = getFile(targetParentDocumentId!!)
+ val oldFile = getFile(sourceDocumentId)
+ val newFile = parent.resolveWithoutConflict(oldFile.name)
+
+ try {
+ if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
+ throw IOException("Couldn't create new file")
+
+ FileInputStream(oldFile).use { inStream ->
+ FileOutputStream(newFile).use { outStream ->
+ inStream.copyTo(outStream)
+ }
+ }
+ } catch (e: IOException) {
+ throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
+ }
+
+ return getDocumentId(newFile)
+ }
+
+ override fun moveDocument(
+ sourceDocumentId: String, sourceParentDocumentId: String?,
+ targetParentDocumentId: String?
+ ): String {
+ try {
+ val newDocumentId = copyDocument(
+ sourceDocumentId, sourceParentDocumentId!!,
+ targetParentDocumentId
+ )
+ removeDocument(sourceDocumentId, sourceParentDocumentId)
+ return newDocumentId
+ } catch (e: FileNotFoundException) {
+ throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
+ }
+ }
+
+ private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
+ val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
+ val localFile = file ?: getFile(documentId!!)
+
+ var flags = 0
+ if (localFile.isDirectory && localFile.canWrite()) {
+ flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
+ } else if (localFile.canWrite()) {
+ flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
+
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
+ }
+
+ cursor.newRow().apply {
+ add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
+ add(
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ if (localFile == baseDirectory) context!!.getString(R.string.app_name) else localFile.name
+ )
+ add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
+ add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
+ add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
+ add(DocumentsContract.Document.COLUMN_FLAGS, flags)
+ if (localFile == baseDirectory)
+ add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
+ }
+
+ return cursor
+ }
+
+ private fun getTypeForFile(file: File): Any {
+ return if (file.isDirectory)
+ DocumentsContract.Document.MIME_TYPE_DIR
+ else
+ getTypeForName(file.name)
+ }
+
+ private fun getTypeForName(name: String): Any {
+ val lastDot = name.lastIndexOf('.')
+ if (lastDot >= 0) {
+ val extension = name.substring(lastDot + 1)
+ val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+ if (mime != null)
+ return mime
+ }
+ return "application/octect-stream"
+ }
+
+ override fun queryChildDocuments(
+ parentDocumentId: String?,
+ projection: Array<out String>?,
+ sortOrder: String?
+ ): Cursor {
+ var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
+
+ val parent = getFile(parentDocumentId!!)
+ for (file in parent.listFiles()!!)
+ cursor = includeFile(cursor, null, file)
+
+ return cursor
+ }
+
+ override fun openDocument(
+ documentId: String?,
+ mode: String?,
+ signal: CancellationSignal?
+ ): ParcelFileDescriptor {
+ val file = documentId?.let { getFile(it) }
+ val accessMode = ParcelFileDescriptor.parseMode(mode)
+ return ParcelFileDescriptor.open(file, accessMode)
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+interface AbstractBooleanSetting : AbstractSetting {
+ var boolean: Boolean
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+interface AbstractFloatSetting : AbstractSetting {
+ var float: Float
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+interface AbstractIntSetting : AbstractSetting {
+ var int: Int
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+interface AbstractSetting {
+ val key: String?
+ val section: String?
+ val isRuntimeEditable: Boolean
+ val valueAsString: String
+ val defaultValue: Any
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+interface AbstractStringSetting : AbstractSetting {
+ var string: String
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+enum class BooleanSetting(
+ override val key: String,
+ override val section: String,
+ override val defaultValue: Boolean
+) : AbstractBooleanSetting {
+ USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
+
+ override var boolean: Boolean = defaultValue
+
+ override val valueAsString: String
+ get() = boolean.toString()
+
+ override val isRuntimeEditable: Boolean
+ get() {
+ for (setting in NOT_RUNTIME_EDITABLE) {
+ if (setting == this) {
+ return false
+ }
+ }
+ return true
+ }
+
+ companion object {
+ private val NOT_RUNTIME_EDITABLE = listOf(
+ USE_CUSTOM_RTC
+ )
+
+ fun from(key: String): BooleanSetting? =
+ BooleanSetting.values().firstOrNull { it.key == key }
+
+ fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+enum class FloatSetting(
+ override val key: String,
+ override val section: String,
+ override val defaultValue: Float
+) : AbstractFloatSetting {
+ // No float settings currently exist
+ EMPTY_SETTING("", "", 0f);
+
+ override var float: Float = defaultValue
+
+ override val valueAsString: String
+ get() = float.toString()
+
+ override val isRuntimeEditable: Boolean
+ get() {
+ for (setting in NOT_RUNTIME_EDITABLE) {
+ if (setting == this) {
+ return false
+ }
+ }
+ return true
+ }
+
+ companion object {
+ private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
+
+ fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
+
+ fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
+ }
+}
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..fa84f94f5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+enum class IntSetting(
+ override val key: String,
+ override val section: String,
+ override val defaultValue: Int
+) : AbstractIntSetting {
+ RENDERER_USE_SPEED_LIMIT(
+ "use_speed_limit",
+ Settings.SECTION_RENDERER,
+ 1
+ ),
+ USE_DOCKED_MODE(
+ "use_docked_mode",
+ Settings.SECTION_SYSTEM,
+ 0
+ ),
+ RENDERER_USE_DISK_SHADER_CACHE(
+ "use_disk_shader_cache",
+ Settings.SECTION_RENDERER,
+ 1
+ ),
+ RENDERER_FORCE_MAX_CLOCK(
+ "force_max_clock",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_ASYNCHRONOUS_SHADERS(
+ "use_asynchronous_shaders",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_REACTIVE_FLUSHING(
+ "use_reactive_flushing",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_DEBUG(
+ "debug",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_SPEED_LIMIT(
+ "speed_limit",
+ Settings.SECTION_RENDERER,
+ 100
+ ),
+ CPU_ACCURACY(
+ "cpu_accuracy",
+ Settings.SECTION_CPU,
+ 0
+ ),
+ REGION_INDEX(
+ "region_index",
+ Settings.SECTION_SYSTEM,
+ -1
+ ),
+ LANGUAGE_INDEX(
+ "language_index",
+ Settings.SECTION_SYSTEM,
+ 1
+ ),
+ RENDERER_BACKEND(
+ "backend",
+ Settings.SECTION_RENDERER,
+ 1
+ ),
+ RENDERER_ACCURACY(
+ "gpu_accuracy",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_RESOLUTION(
+ "resolution_setup",
+ Settings.SECTION_RENDERER,
+ 2
+ ),
+ RENDERER_VSYNC(
+ "use_vsync",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_SCALING_FILTER(
+ "scaling_filter",
+ Settings.SECTION_RENDERER,
+ 1
+ ),
+ RENDERER_ANTI_ALIASING(
+ "anti_aliasing",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ RENDERER_ASPECT_RATIO(
+ "aspect_ratio",
+ Settings.SECTION_RENDERER,
+ 0
+ ),
+ AUDIO_VOLUME(
+ "volume",
+ Settings.SECTION_AUDIO,
+ 100
+ );
+
+ override var int: Int = defaultValue
+
+ override val valueAsString: String
+ get() = int.toString()
+
+ override val isRuntimeEditable: Boolean
+ get() {
+ for (setting in NOT_RUNTIME_EDITABLE) {
+ if (setting == this) {
+ return false
+ }
+ }
+ return true
+ }
+
+ companion object {
+ private val NOT_RUNTIME_EDITABLE = listOf(
+ RENDERER_USE_DISK_SHADER_CACHE,
+ RENDERER_ASYNCHRONOUS_SHADERS,
+ RENDERER_DEBUG,
+ RENDERER_BACKEND,
+ RENDERER_RESOLUTION,
+ RENDERER_VSYNC
+ )
+
+ fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
+
+ fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+/**
+ * A semantically-related group of Settings objects. These Settings are
+ * internally stored as a HashMap.
+ */
+class SettingSection(val name: String) {
+ val settings = HashMap<String, AbstractSetting>()
+
+ /**
+ * Convenience method; inserts a value directly into the backing HashMap.
+ *
+ * @param setting The Setting to be inserted.
+ */
+ fun putSetting(setting: AbstractSetting) {
+ settings[setting.key!!] = setting
+ }
+
+ /**
+ * Convenience method; gets a value directly from the backing HashMap.
+ *
+ * @param key Used to retrieve the Setting.
+ * @return A Setting object (you should probably cast this before using)
+ */
+ fun getSetting(key: String): AbstractSetting? {
+ return settings[key]
+ }
+
+ fun mergeSection(settingSection: SettingSection) {
+ for (setting in settingSection.settings.values) {
+ putSetting(setting)
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+import android.text.TextUtils
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import java.util.*
+
+class Settings {
+ private var gameId: String? = null
+
+ var isLoaded = false
+
+ /**
+ * A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
+ * when getting a key not already in the map
+ */
+ class SettingsSectionMap : HashMap<String, SettingSection?>() {
+ override operator fun get(key: String): SettingSection? {
+ if (!super.containsKey(key)) {
+ val section = SettingSection(key)
+ super.put(key, section)
+ return section
+ }
+ return super.get(key)
+ }
+ }
+
+ var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
+
+ fun getSection(sectionName: String): SettingSection? {
+ return sections[sectionName]
+ }
+
+ val isEmpty: Boolean
+ get() = sections.isEmpty()
+
+ fun loadSettings(view: SettingsActivityView? = null) {
+ sections = SettingsSectionMap()
+ loadYuzuSettings(view)
+ if (!TextUtils.isEmpty(gameId)) {
+ loadCustomGameSettings(gameId!!, view)
+ }
+ isLoaded = true
+ }
+
+ private fun loadYuzuSettings(view: SettingsActivityView?) {
+ for ((fileName) in configFileSectionsMap) {
+ sections.putAll(SettingsFile.readFile(fileName, view))
+ }
+ }
+
+ private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
+ // Custom game settings
+ mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
+ }
+
+ private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
+ for ((key, updatedSection) in updatedSections) {
+ if (sections.containsKey(key)) {
+ val originalSection = sections[key]
+ originalSection!!.mergeSection(updatedSection!!)
+ } else {
+ sections[key] = updatedSection
+ }
+ }
+ }
+
+ fun loadSettings(gameId: String, view: SettingsActivityView) {
+ this.gameId = gameId
+ loadSettings(view)
+ }
+
+ fun saveSettings(view: SettingsActivityView) {
+ if (TextUtils.isEmpty(gameId)) {
+ view.showToastMessage(
+ YuzuApplication.appContext.getString(R.string.ini_saved),
+ false
+ )
+
+ for ((fileName, sectionNames) in configFileSectionsMap) {
+ val iniSections = TreeMap<String, SettingSection>()
+ for (section in sectionNames) {
+ iniSections[section] = sections[section]!!
+ }
+
+ SettingsFile.saveFile(fileName, iniSections, view)
+ }
+ } else {
+ // Custom game settings
+ view.showToastMessage(
+ YuzuApplication.appContext.getString(R.string.gameid_saved, gameId),
+ false
+ )
+
+ SettingsFile.saveCustomGameSettings(gameId, sections)
+ }
+ }
+
+ companion object {
+ const val SECTION_GENERAL = "General"
+ const val SECTION_SYSTEM = "System"
+ const val SECTION_RENDERER = "Renderer"
+ const val SECTION_AUDIO = "Audio"
+ const val SECTION_CPU = "Cpu"
+ const val SECTION_THEME = "Theme"
+ const val SECTION_DEBUG = "Debug"
+
+ const val PREF_OVERLAY_INIT = "OverlayInit"
+ const val PREF_CONTROL_SCALE = "controlScale"
+ const val PREF_CONTROL_OPACITY = "controlOpacity"
+ const val PREF_TOUCH_ENABLED = "isTouchEnabled"
+ const val PREF_BUTTON_TOGGLE_0 = "buttonToggle0"
+ const val PREF_BUTTON_TOGGLE_1 = "buttonToggle1"
+ const val PREF_BUTTON_TOGGLE_2 = "buttonToggle2"
+ const val PREF_BUTTON_TOGGLE_3 = "buttonToggle3"
+ const val PREF_BUTTON_TOGGLE_4 = "buttonToggle4"
+ const val PREF_BUTTON_TOGGLE_5 = "buttonToggle5"
+ const val PREF_BUTTON_TOGGLE_6 = "buttonToggle6"
+ const val PREF_BUTTON_TOGGLE_7 = "buttonToggle7"
+ const val PREF_BUTTON_TOGGLE_8 = "buttonToggle8"
+ const val PREF_BUTTON_TOGGLE_9 = "buttonToggle9"
+ const val PREF_BUTTON_TOGGLE_10 = "buttonToggle10"
+ const val PREF_BUTTON_TOGGLE_11 = "buttonToggle11"
+ const val PREF_BUTTON_TOGGLE_12 = "buttonToggle12"
+ const val PREF_BUTTON_TOGGLE_13 = "buttonToggle13"
+ const val PREF_BUTTON_TOGGLE_14 = "buttonToggle14"
+
+ const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
+ const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
+ const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
+ const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
+ const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
+ const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
+
+ const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
+ const val PREF_THEME = "Theme"
+ const val PREF_THEME_MODE = "ThemeMode"
+ const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
+
+ private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
+
+ init {
+ configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
+ listOf(
+ SECTION_GENERAL,
+ SECTION_SYSTEM,
+ SECTION_RENDERER,
+ SECTION_AUDIO,
+ SECTION_CPU
+ )
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+import androidx.lifecycle.ViewModel
+
+class SettingsViewModel : ViewModel() {
+ val settings = Settings()
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model
+
+enum class StringSetting(
+ override val key: String,
+ override val section: String,
+ override val defaultValue: String
+) : AbstractStringSetting {
+ CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0");
+
+ override var string: String = defaultValue
+
+ override val valueAsString: String
+ get() = string
+
+ override val isRuntimeEditable: Boolean
+ get() {
+ for (setting in NOT_RUNTIME_EDITABLE) {
+ if (setting == this) {
+ return false
+ }
+ }
+ return true
+ }
+
+ companion object {
+ private val NOT_RUNTIME_EDITABLE = listOf(
+ CUSTOM_RTC
+ )
+
+ fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
+
+ fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
+
+class DateTimeSetting(
+ setting: AbstractSetting?,
+ titleId: Int,
+ descriptionId: Int,
+ val key: String? = null,
+ private val defaultValue: String? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_DATETIME_SETTING
+
+ val value: String
+ get() = if (setting != null) {
+ val setting = setting as AbstractStringSetting
+ setting.string
+ } else {
+ defaultValue!!
+ }
+
+ fun setSelectedValue(datetime: String): AbstractStringSetting {
+ val stringSetting = setting as AbstractStringSetting
+ stringSetting.string = datetime
+ return stringSetting
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+
+class HeaderSetting(
+ setting: AbstractSetting?,
+ titleId: Int,
+ descriptionId: Int
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_HEADER
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+class RunnableSetting(
+ titleId: Int,
+ descriptionId: Int,
+ val isRuntimeRunnable: Boolean,
+ val runnable: () -> Unit
+) : SettingsItem(null, titleId, descriptionId) {
+ override val type = TYPE_RUNNABLE
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+
+/**
+ * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
+ * Each one corresponds to a [AbstractSetting] object, so this class's subclasses
+ * should vaguely correspond to those subclasses. There are a few with multiple analogues
+ * and a few with none (Headers, for example, do not correspond to anything in the ini
+ * file.)
+ */
+abstract class SettingsItem(
+ var setting: AbstractSetting?,
+ val nameId: Int,
+ val descriptionId: Int
+) {
+ abstract val type: Int
+
+ val isEditable: Boolean
+ get() {
+ if (!NativeLibrary.isRunning()) return true
+ return setting?.isRuntimeEditable ?: false
+ }
+
+ companion object {
+ const val TYPE_HEADER = 0
+ const val TYPE_SWITCH = 1
+ const val TYPE_SINGLE_CHOICE = 2
+ const val TYPE_SLIDER = 3
+ const val TYPE_SUBMENU = 4
+ const val TYPE_STRING_SINGLE_CHOICE = 5
+ const val TYPE_DATETIME_SETTING = 6
+ const val TYPE_RUNNABLE = 7
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+
+class SingleChoiceSetting(
+ setting: AbstractIntSetting?,
+ titleId: Int,
+ descriptionId: Int,
+ val choicesId: Int,
+ val valuesId: Int,
+ val key: String? = null,
+ val defaultValue: Int? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_SINGLE_CHOICE
+
+ val selectedValue: Int
+ get() = if (setting != null) {
+ val setting = setting as AbstractIntSetting
+ setting.int
+ } else {
+ defaultValue!!
+ }
+
+ /**
+ * Write a value to the backing int. If that int was previously null,
+ * initializes a new one and returns it, so it can be added to the Hashmap.
+ *
+ * @param selection New value of the int.
+ * @return the existing setting with the new value applied.
+ */
+ fun setSelectedValue(selection: Int): AbstractIntSetting {
+ val intSetting = setting as AbstractIntSetting
+ intSetting.int = selection
+ return intSetting
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+import org.yuzu.yuzu_emu.utils.Log
+import kotlin.math.roundToInt
+
+class SliderSetting(
+ setting: AbstractSetting?,
+ titleId: Int,
+ descriptionId: Int,
+ val min: Int,
+ val max: Int,
+ val units: String,
+ val key: String? = null,
+ val defaultValue: Int? = null,
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_SLIDER
+
+ val selectedValue: Int
+ get() {
+ val setting = setting ?: return defaultValue!!
+ return when (setting) {
+ is AbstractIntSetting -> setting.int
+ is AbstractFloatSetting -> setting.float.roundToInt()
+ else -> {
+ Log.error("[SliderSetting] Error casting setting type.")
+ -1
+ }
+ }
+ }
+
+ /**
+ * Write a value to the backing int. If that int was previously null,
+ * initializes a new one and returns it, so it can be added to the Hashmap.
+ *
+ * @param selection New value of the int.
+ * @return the existing setting with the new value applied.
+ */
+ fun setSelectedValue(selection: Int): AbstractIntSetting {
+ val intSetting = setting as AbstractIntSetting
+ intSetting.int = selection
+ return intSetting
+ }
+
+ /**
+ * Write a value to the backing float. If that float was previously null,
+ * initializes a new one and returns it, so it can be added to the Hashmap.
+ *
+ * @param selection New value of the float.
+ * @return the existing setting with the new value applied.
+ */
+ fun setSelectedValue(selection: Float): AbstractFloatSetting {
+ val floatSetting = setting as AbstractFloatSetting
+ floatSetting.float = selection
+ return floatSetting
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
+import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+
+class StringSingleChoiceSetting(
+ val key: String? = null,
+ setting: AbstractSetting?,
+ titleId: Int,
+ descriptionId: Int,
+ val choicesId: Array<String>,
+ private val valuesId: Array<String>?,
+ private val defaultValue: String? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_STRING_SINGLE_CHOICE
+
+ fun getValueAt(index: Int): String? {
+ if (valuesId == null) return null
+ return if (index >= 0 && index < valuesId.size) {
+ valuesId[index]
+ } else ""
+ }
+
+ val selectedValue: String
+ get() = if (setting != null) {
+ val setting = setting as AbstractStringSetting
+ setting.string
+ } else {
+ defaultValue!!
+ }
+ val selectValueIndex: Int
+ get() {
+ val selectedValue = selectedValue
+ for (i in valuesId!!.indices) {
+ if (valuesId[i] == selectedValue) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Write a value to the backing int. If that int was previously null,
+ * initializes a new one and returns it, so it can be added to the Hashmap.
+ *
+ * @param selection New value of the int.
+ * @return the existing setting with the new value applied.
+ */
+ fun setSelectedValue(selection: String): AbstractStringSetting {
+ val stringSetting = setting as AbstractStringSetting
+ stringSetting.string = selection
+ return stringSetting
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+
+class SubmenuSetting(
+ titleId: Int,
+ descriptionId: Int,
+ val menuKey: String
+) : SettingsItem(null, titleId, descriptionId) {
+ override val type = TYPE_SUBMENU
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+
+class SwitchSetting(
+ setting: AbstractSetting,
+ titleId: Int,
+ descriptionId: Int,
+ val key: String? = null,
+ val defaultValue: Any? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_SWITCH
+
+ val isChecked: Boolean
+ get() {
+ if (setting == null) {
+ return defaultValue as Boolean
+ }
+
+ // Try integer setting
+ try {
+ val setting = setting as AbstractIntSetting
+ return setting.int == 1
+ } catch (_: ClassCastException) {
+ }
+
+ // Try boolean setting
+ try {
+ val setting = setting as AbstractBooleanSetting
+ return setting.boolean
+ } catch (_: ClassCastException) {
+ }
+ return defaultValue as Boolean
+ }
+
+ /**
+ * Write a value to the backing boolean. If that boolean was previously null,
+ * initializes a new one and returns it, so it can be added to the Hashmap.
+ *
+ * @param checked Pretty self explanatory.
+ * @return the existing setting with the new value applied.
+ */
+ fun setChecked(checked: Boolean): AbstractSetting {
+ // Try integer setting
+ try {
+ val setting = setting as AbstractIntSetting
+ setting.int = if (checked) 1 else 0
+ return setting
+ } catch (_: ClassCastException) {
+ }
+
+ // Try boolean setting
+ val setting = setting as AbstractBooleanSetting
+ setting.boolean = checked
+ return setting
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.View
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.updatePadding
+import com.google.android.material.color.MaterialColors
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
+import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
+import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
+import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.utils.*
+import java.io.IOException
+
+class SettingsActivity : AppCompatActivity(), SettingsActivityView {
+ private val presenter = SettingsActivityPresenter(this)
+
+ private lateinit var binding: ActivitySettingsBinding
+
+ private val settingsViewModel: SettingsViewModel by viewModels()
+
+ override val settings: Settings get() = settingsViewModel.settings
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeHelper.setTheme(this)
+
+ super.onCreate(savedInstanceState)
+
+ binding = ActivitySettingsBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ val launcher = intent
+ val gameID = launcher.getStringExtra(ARG_GAME_ID)
+ val menuTag = launcher.getStringExtra(ARG_MENU_TAG)
+ presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
+
+ // Show "Back" button in the action bar for navigation
+ setSupportActionBar(binding.toolbarSettings)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+ if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) {
+ binding.navigationBarShade.setBackgroundColor(
+ ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.navigationBarShade,
+ com.google.android.material.R.attr.colorSurface
+ ),
+ ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ )
+ }
+
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() = navigateBack()
+ })
+
+ setInsets()
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ navigateBack()
+ return true
+ }
+
+ private fun navigateBack() {
+ if (supportFragmentManager.backStackEntryCount > 0) {
+ supportFragmentManager.popBackStack()
+ } else {
+ finish()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val inflater = menuInflater
+ inflater.inflate(R.menu.menu_settings, menu)
+ return true
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ // Critical: If super method is not called, rotations will be busted.
+ super.onSaveInstanceState(outState)
+ presenter.saveState(outState)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ presenter.onStart()
+ }
+
+ /**
+ * If this is called, the user has left the settings screen (potentially through the
+ * home button) and will expect their changes to be persisted. So we kick off an
+ * IntentService which will do so on a background thread.
+ */
+ override fun onStop() {
+ super.onStop()
+ presenter.onStop(isFinishing)
+ }
+
+ override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
+ if (!addToStack && settingsFragment != null) {
+ return
+ }
+
+ val transaction = supportFragmentManager.beginTransaction()
+ if (addToStack) {
+ if (areSystemAnimationsEnabled()) {
+ transaction.setCustomAnimations(
+ R.anim.anim_settings_fragment_in,
+ R.anim.anim_settings_fragment_out,
+ 0,
+ R.anim.anim_pop_settings_fragment_out
+ )
+ }
+ transaction.addToBackStack(null)
+ }
+ transaction.replace(
+ R.id.frame_content,
+ SettingsFragment.newInstance(menuTag, gameId),
+ FRAGMENT_TAG
+ )
+ transaction.commit()
+ }
+
+ private fun areSystemAnimationsEnabled(): Boolean {
+ val duration = android.provider.Settings.Global.getFloat(
+ contentResolver,
+ android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
+ )
+ val transition = android.provider.Settings.Global.getFloat(
+ contentResolver,
+ android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
+ )
+ return duration != 0f && transition != 0f
+ }
+
+ override fun onSettingsFileLoaded() {
+ val fragment: SettingsFragmentView? = settingsFragment
+ fragment?.loadSettingsList()
+ }
+
+ override fun onSettingsFileNotFound() {
+ val fragment: SettingsFragmentView? = settingsFragment
+ fragment?.loadSettingsList()
+ }
+
+ override fun showToastMessage(message: String, is_long: Boolean) {
+ Toast.makeText(
+ this,
+ message,
+ if (is_long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ override fun onSettingChanged() {
+ presenter.onSettingChanged()
+ }
+
+ fun onSettingsReset() {
+ // Prevents saving to a non-existent settings file
+ presenter.onSettingsReset()
+
+ // Reset the static memory representation of each setting
+ BooleanSetting.clear()
+ FloatSetting.clear()
+ IntSetting.clear()
+ StringSetting.clear()
+
+ // Delete settings file because the user may have changed values that do not exist in the UI
+ val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
+ if (!settingsFile.delete()) {
+ throw IOException("Failed to delete $settingsFile")
+ }
+
+ showToastMessage(getString(R.string.settings_reset), true)
+ finish()
+ }
+
+ fun setToolbarTitle(title: String) {
+ binding.toolbarSettingsLayout.title = title
+ }
+
+ private val settingsFragment: SettingsFragment?
+ get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
+
+ private fun setInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.frameContent) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ view.updatePadding(
+ left = barInsets.left + cutoutInsets.left,
+ right = barInsets.right + cutoutInsets.right
+ )
+
+ val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
+ mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
+ mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
+ binding.appbarSettings.layoutParams = mlpAppBar
+
+ val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
+ mlpShade.height = barInsets.bottom
+ binding.navigationBarShade.layoutParams = mlpShade
+
+ windowInsets
+ }
+ }
+
+ companion object {
+ private const val ARG_MENU_TAG = "menu_tag"
+ private const val ARG_GAME_ID = "game_id"
+ private const val FRAGMENT_TAG = "settings"
+
+ fun launch(context: Context, menuTag: String?, gameId: String?) {
+ val settings = Intent(context, SettingsActivity::class.java)
+ settings.putExtra(ARG_MENU_TAG, menuTag)
+ settings.putExtra(ARG_GAME_ID, gameId)
+ context.startActivity(settings)
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.text.TextUtils
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.Log
+import java.io.File
+
+class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
+ val settings: Settings get() = activityView.settings
+
+ private var shouldSave = false
+ private lateinit var menuTag: String
+ private lateinit var gameId: String
+
+ fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
+ this.menuTag = menuTag
+ this.gameId = gameId
+ if (savedInstanceState != null) {
+ shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
+ }
+ }
+
+ fun onStart() {
+ prepareDirectoriesIfNeeded()
+ }
+
+ private fun loadSettingsUI() {
+ if (!settings.isLoaded) {
+ if (!TextUtils.isEmpty(gameId)) {
+ settings.loadSettings(gameId, activityView)
+ } else {
+ settings.loadSettings(activityView)
+ }
+ }
+ activityView.showSettingsFragment(menuTag, false, gameId)
+ activityView.onSettingsFileLoaded()
+ }
+
+ private fun prepareDirectoriesIfNeeded() {
+ val configFile =
+ File(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini")
+ if (!configFile.exists()) {
+ Log.error(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini")
+ Log.error("yuzu config file could not be found!")
+ }
+
+ if (!DirectoryInitialization.areDirectoriesReady) {
+ DirectoryInitialization.start(activityView as Context)
+ }
+ loadSettingsUI()
+ }
+
+ fun onStop(finishing: Boolean) {
+ if (finishing && shouldSave) {
+ Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
+ settings.saveSettings(activityView)
+ }
+ NativeLibrary.reloadSettings()
+ }
+
+ fun onSettingChanged() {
+ shouldSave = true
+ }
+
+ fun onSettingsReset() {
+ shouldSave = false
+ }
+
+ fun saveState(outState: Bundle) {
+ outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
+ }
+
+ companion object {
+ private const val KEY_SHOULD_SAVE = "should_save"
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+
+/**
+ * Abstraction for the Activity that manages SettingsFragments.
+ */
+interface SettingsActivityView {
+ /**
+ * Show a new SettingsFragment.
+ *
+ * @param menuTag Identifier for the settings group that should be displayed.
+ * @param addToStack Whether or not this fragment should replace a previous one.
+ */
+ fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
+
+ /**
+ * Called by a contained Fragment to get access to the Setting HashMap
+ * loaded from disk, so that each Fragment doesn't need to perform its own
+ * read operation.
+ *
+ * @return A HashMap of Settings.
+ */
+ val settings: Settings
+
+ /**
+ * Called when a load operation completes.
+ */
+ fun onSettingsFileLoaded()
+
+ /**
+ * Called when a load operation fails.
+ */
+ fun onSettingsFileNotFound()
+
+ /**
+ * Display a popup text message on screen.
+ *
+ * @param message The contents of the onscreen message.
+ * @param is_long Whether this should be a long Toast or short one.
+ */
+ fun showToastMessage(message: String, is_long: Boolean)
+
+ /**
+ * End the activity.
+ */
+ fun finish()
+
+ /**
+ * Called by a containing Fragment to tell the Activity that a setting was changed;
+ * unless this has been called, the Activity will not save to disk.
+ */
+ fun onSettingChanged()
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.Context
+import android.content.DialogInterface
+import android.icu.util.Calendar
+import android.icu.util.TimeZone
+import android.text.format.DateFormat
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.setFragmentResultListener
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
+import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
+import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
+import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.*
+import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
+
+class SettingsAdapter(
+ private val fragmentView: SettingsFragmentView,
+ private val context: Context
+) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
+ private var settings: ArrayList<SettingsItem>? = null
+ private var clickedItem: SettingsItem? = null
+ private var clickedPosition: Int
+ private var dialog: AlertDialog? = null
+ private var sliderProgress = 0
+ private var textSliderValue: TextView? = null
+
+ private var defaultCancelListener =
+ DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
+
+ init {
+ clickedPosition = -1
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ return when (viewType) {
+ SettingsItem.TYPE_HEADER -> {
+ HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_SWITCH -> {
+ SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
+ SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_SLIDER -> {
+ SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_SUBMENU -> {
+ SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_DATETIME_SETTING -> {
+ DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
+ SettingsItem.TYPE_RUNNABLE -> {
+ RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
+ else -> {
+ // TODO: Create an error view since we can't return null now
+ HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ private fun getItem(position: Int): SettingsItem {
+ return settings!![position]
+ }
+
+ override fun getItemCount(): Int {
+ return if (settings != null) {
+ settings!!.size
+ } else {
+ 0
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return getItem(position).type
+ }
+
+ fun setSettingsList(settings: ArrayList<SettingsItem>?) {
+ this.settings = settings
+ notifyDataSetChanged()
+ }
+
+ fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
+ val setting = item.setChecked(checked)
+ fragmentView.putSetting(setting)
+ fragmentView.onSettingChanged()
+ }
+
+ private fun onSingleChoiceClick(item: SingleChoiceSetting) {
+ clickedItem = item
+ val value = getSelectionForSingleChoiceValue(item)
+ dialog = MaterialAlertDialogBuilder(context)
+ .setTitle(item.nameId)
+ .setSingleChoiceItems(item.choicesId, value, this)
+ .show()
+ }
+
+ fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
+ clickedPosition = position
+ onSingleChoiceClick(item)
+ }
+
+ private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
+ clickedItem = item
+ dialog = MaterialAlertDialogBuilder(context)
+ .setTitle(item.nameId)
+ .setSingleChoiceItems(item.choicesId, item.selectValueIndex, this)
+ .show()
+ }
+
+ fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
+ clickedPosition = position
+ onStringSingleChoiceClick(item)
+ }
+
+ fun onDateTimeClick(item: DateTimeSetting, position: Int) {
+ clickedItem = item
+ clickedPosition = position
+ val storedTime = java.lang.Long.decode(item.value) * 1000
+
+ // Helper to extract hour and minute from epoch time
+ val calendar: Calendar = Calendar.getInstance()
+ calendar.timeInMillis = storedTime
+ calendar.timeZone = TimeZone.getTimeZone("UTC")
+
+ var timeFormat: Int = TimeFormat.CLOCK_12H
+ if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) {
+ timeFormat = TimeFormat.CLOCK_24H
+ }
+
+ val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker()
+ .setSelection(storedTime)
+ .setTitleText(R.string.select_rtc_date)
+ .build()
+ val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
+ .setTimeFormat(timeFormat)
+ .setHour(calendar.get(Calendar.HOUR_OF_DAY))
+ .setMinute(calendar.get(Calendar.MINUTE))
+ .setTitleText(R.string.select_rtc_time)
+ .build()
+
+ datePicker.addOnPositiveButtonClickListener {
+ timePicker.show(
+ (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
+ "TimePicker"
+ )
+ }
+ timePicker.addOnPositiveButtonClickListener {
+ var epochTime: Long = datePicker.selection!! / 1000
+ epochTime += timePicker.hour.toLong() * 60 * 60
+ epochTime += timePicker.minute.toLong() * 60
+ val rtcString = epochTime.toString()
+ if (item.value != rtcString) {
+ fragmentView.onSettingChanged()
+ }
+ notifyItemChanged(clickedPosition)
+ val setting = item.setSelectedValue(rtcString)
+ fragmentView.putSetting(setting)
+ clickedItem = null
+ }
+ datePicker.show(
+ (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
+ "DatePicker"
+ )
+ }
+
+ fun onSliderClick(item: SliderSetting, position: Int) {
+ clickedItem = item
+ clickedPosition = position
+ sliderProgress = item.selectedValue
+
+ val inflater = LayoutInflater.from(context)
+ val sliderBinding = DialogSliderBinding.inflate(inflater)
+
+ textSliderValue = sliderBinding.textValue
+ textSliderValue!!.text = sliderProgress.toString()
+ sliderBinding.textUnits.text = item.units
+
+ sliderBinding.slider.apply {
+ valueFrom = item.min.toFloat()
+ valueTo = item.max.toFloat()
+ value = sliderProgress.toFloat()
+ addOnChangeListener { _: Slider, value: Float, _: Boolean ->
+ sliderProgress = value.toInt()
+ textSliderValue!!.text = sliderProgress.toString()
+ }
+ }
+
+ dialog = MaterialAlertDialogBuilder(context)
+ .setTitle(item.nameId)
+ .setView(sliderBinding.root)
+ .setPositiveButton(android.R.string.ok, this)
+ .setNegativeButton(android.R.string.cancel, defaultCancelListener)
+ .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
+ sliderBinding.slider.value = item.defaultValue!!.toFloat()
+ onClick(dialog, which)
+ }
+ .show()
+ }
+
+ fun onSubmenuClick(item: SubmenuSetting) {
+ fragmentView.loadSubMenu(item.menuKey)
+ }
+
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ when (clickedItem) {
+ is SingleChoiceSetting -> {
+ val scSetting = clickedItem as SingleChoiceSetting
+ val value = getValueForSingleChoiceSelection(scSetting, which)
+ if (scSetting.selectedValue != value) {
+ fragmentView.onSettingChanged()
+ }
+
+ // Get the backing Setting, which may be null (if for example it was missing from the file)
+ val setting = scSetting.setSelectedValue(value)
+ fragmentView.putSetting(setting)
+ closeDialog()
+ }
+
+ is StringSingleChoiceSetting -> {
+ val scSetting = clickedItem as StringSingleChoiceSetting
+ val value = scSetting.getValueAt(which)
+ if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
+ val setting = scSetting.setSelectedValue(value!!)
+ fragmentView.putSetting(setting)
+ closeDialog()
+ }
+
+ is SliderSetting -> {
+ val sliderSetting = clickedItem as SliderSetting
+ if (sliderSetting.selectedValue != sliderProgress) {
+ fragmentView.onSettingChanged()
+ }
+ if (sliderSetting.setting is FloatSetting) {
+ val value = sliderProgress.toFloat()
+ val setting = sliderSetting.setSelectedValue(value)
+ fragmentView.putSetting(setting)
+ } else {
+ val setting = sliderSetting.setSelectedValue(sliderProgress)
+ fragmentView.putSetting(setting)
+ }
+ closeDialog()
+ }
+ }
+ clickedItem = null
+ sliderProgress = -1
+ }
+
+ fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
+ MaterialAlertDialogBuilder(context)
+ .setMessage(R.string.reset_setting_confirmation)
+ .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
+ when (setting) {
+ is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
+ is AbstractFloatSetting -> setting.float = setting.defaultValue as Float
+ is AbstractIntSetting -> setting.int = setting.defaultValue as Int
+ is AbstractStringSetting -> setting.string = setting.defaultValue as String
+ }
+ notifyItemChanged(position)
+ fragmentView.onSettingChanged()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+
+ return true
+ }
+
+ fun closeDialog() {
+ if (dialog != null) {
+ if (clickedPosition != -1) {
+ notifyItemChanged(clickedPosition)
+ clickedPosition = -1
+ }
+ dialog!!.dismiss()
+ dialog = null
+ }
+ }
+
+ private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
+ val valuesId = item.valuesId
+ return if (valuesId > 0) {
+ val valuesArray = context.resources.getIntArray(valuesId)
+ valuesArray[which]
+ } else {
+ which
+ }
+ }
+
+ private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
+ val value = item.selectedValue
+ val valuesId = item.valuesId
+ if (valuesId > 0) {
+ val valuesArray = context.resources.getIntArray(valuesId)
+ for (index in valuesArray.indices) {
+ val current = valuesArray[index]
+ if (current == value) {
+ return index
+ }
+ }
+ } else {
+ return value
+ }
+ return -1
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+
+class SettingsFragment : Fragment(), SettingsFragmentView {
+ override var activityView: SettingsActivityView? = null
+
+ private val fragmentPresenter = SettingsFragmentPresenter(this)
+ private var settingsAdapter: SettingsAdapter? = null
+
+ private var _binding: FragmentSettingsBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ activityView = requireActivity() as SettingsActivityView
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
+ val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
+ fragmentPresenter.onCreate(menuTag!!, gameId!!)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSettingsBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ settingsAdapter = SettingsAdapter(this, requireActivity())
+ val dividerDecoration = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
+ dividerDecoration.isLastItemDecorated = false
+ binding.listSettings.apply {
+ adapter = settingsAdapter
+ layoutManager = LinearLayoutManager(activity)
+ addItemDecoration(dividerDecoration)
+ }
+ fragmentPresenter.onViewCreated()
+
+ setInsets()
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ activityView = null
+ if (settingsAdapter != null) {
+ settingsAdapter!!.closeDialog()
+ }
+ }
+
+ override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
+ settingsAdapter!!.setSettingsList(settingsList)
+ }
+
+ override fun loadSettingsList() {
+ fragmentPresenter.loadSettingsList()
+ }
+
+ override fun loadSubMenu(menuKey: String) {
+ activityView!!.showSettingsFragment(
+ menuKey,
+ true,
+ requireArguments().getString(ARGUMENT_GAME_ID)!!
+ )
+ }
+
+ override fun showToastMessage(message: String?, is_long: Boolean) {
+ activityView!!.showToastMessage(message!!, is_long)
+ }
+
+ override fun putSetting(setting: AbstractSetting) {
+ fragmentPresenter.putSetting(setting)
+ }
+
+ override fun onSettingChanged() {
+ activityView!!.onSettingChanged()
+ }
+
+ private fun setInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.listSettings) { view: View, windowInsets: WindowInsetsCompat ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ view.updatePadding(bottom = insets.bottom)
+ windowInsets
+ }
+ }
+
+ companion object {
+ private const val ARGUMENT_MENU_TAG = "menu_tag"
+ private const val ARGUMENT_GAME_ID = "game_id"
+
+ fun newInstance(menuTag: String?, gameId: String?): Fragment {
+ val fragment = SettingsFragment()
+ val arguments = Bundle()
+ arguments.putString(ARGUMENT_MENU_TAG, menuTag)
+ arguments.putString(ARGUMENT_GAME_ID, gameId)
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
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..1ceaa6fb4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -0,0 +1,474 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.SharedPreferences
+import android.os.Build
+import android.text.TextUtils
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
+import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.*
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
+import org.yuzu.yuzu_emu.utils.ThemeHelper
+
+class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
+ private var menuTag: String? = null
+ private lateinit var gameId: String
+ private var settingsList: ArrayList<SettingsItem>? = null
+
+ private val settingsActivity get() = fragmentView.activityView as SettingsActivity
+ private val settings get() = fragmentView.activityView!!.settings
+
+ private lateinit var preferences: SharedPreferences
+
+ fun onCreate(menuTag: String, gameId: String) {
+ this.gameId = gameId
+ this.menuTag = menuTag
+ }
+
+ fun onViewCreated() {
+ preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ loadSettingsList()
+ }
+
+ fun putSetting(setting: AbstractSetting) {
+ if (setting.section == null) {
+ return
+ }
+
+ val section = settings.getSection(setting.section!!)!!
+ if (section.getSetting(setting.key!!) == null) {
+ section.putSetting(setting)
+ }
+ }
+
+ fun loadSettingsList() {
+ if (!TextUtils.isEmpty(gameId)) {
+ settingsActivity.setToolbarTitle("Game Settings: $gameId")
+ }
+ val sl = ArrayList<SettingsItem>()
+ if (menuTag == null) {
+ return
+ }
+ when (menuTag) {
+ SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
+ Settings.SECTION_GENERAL -> addGeneralSettings(sl)
+ Settings.SECTION_SYSTEM -> addSystemSettings(sl)
+ Settings.SECTION_RENDERER -> addGraphicsSettings(sl)
+ Settings.SECTION_AUDIO -> addAudioSettings(sl)
+ Settings.SECTION_THEME -> addThemeSettings(sl)
+ Settings.SECTION_DEBUG -> addDebugSettings(sl)
+ else -> {
+ fragmentView.showToastMessage("Unimplemented menu", false)
+ return
+ }
+ }
+ settingsList = sl
+ fragmentView.showSettingsList(settingsList!!)
+ }
+
+ private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings))
+ sl.apply {
+ add(
+ SubmenuSetting(
+ R.string.preferences_general,
+ 0,
+ Settings.SECTION_GENERAL
+ )
+ )
+ add(
+ SubmenuSetting(
+ R.string.preferences_system,
+ 0,
+ Settings.SECTION_SYSTEM
+ )
+ )
+ add(
+ SubmenuSetting(
+ R.string.preferences_graphics,
+ 0,
+ Settings.SECTION_RENDERER
+ )
+ )
+ add(
+ SubmenuSetting(
+ R.string.preferences_audio,
+ 0,
+ Settings.SECTION_AUDIO
+ )
+ )
+ add(
+ SubmenuSetting(
+ R.string.preferences_debug,
+ 0,
+ Settings.SECTION_DEBUG
+ )
+ )
+ add(
+ RunnableSetting(
+ R.string.reset_to_default,
+ 0,
+ false
+ ) {
+ ResetSettingsDialogFragment().show(
+ settingsActivity.supportFragmentManager,
+ ResetSettingsDialogFragment.TAG
+ )
+ }
+ )
+ }
+ }
+
+ private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general))
+ sl.apply {
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_USE_SPEED_LIMIT,
+ R.string.frame_limit_enable,
+ R.string.frame_limit_enable_description,
+ IntSetting.RENDERER_USE_SPEED_LIMIT.key,
+ IntSetting.RENDERER_USE_SPEED_LIMIT.defaultValue
+ )
+ )
+ add(
+ SliderSetting(
+ IntSetting.RENDERER_SPEED_LIMIT,
+ R.string.frame_limit_slider,
+ R.string.frame_limit_slider_description,
+ 1,
+ 200,
+ "%",
+ IntSetting.RENDERER_SPEED_LIMIT.key,
+ IntSetting.RENDERER_SPEED_LIMIT.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.CPU_ACCURACY,
+ R.string.cpu_accuracy,
+ 0,
+ R.array.cpuAccuracyNames,
+ R.array.cpuAccuracyValues,
+ IntSetting.CPU_ACCURACY.key,
+ IntSetting.CPU_ACCURACY.defaultValue
+ )
+ )
+ }
+ }
+
+ private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system))
+ sl.apply {
+ add(
+ SwitchSetting(
+ IntSetting.USE_DOCKED_MODE,
+ R.string.use_docked_mode,
+ R.string.use_docked_mode_description,
+ IntSetting.USE_DOCKED_MODE.key,
+ IntSetting.USE_DOCKED_MODE.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.REGION_INDEX,
+ R.string.emulated_region,
+ 0,
+ R.array.regionNames,
+ R.array.regionValues,
+ IntSetting.REGION_INDEX.key,
+ IntSetting.REGION_INDEX.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.LANGUAGE_INDEX,
+ R.string.emulated_language,
+ 0,
+ R.array.languageNames,
+ R.array.languageValues,
+ IntSetting.LANGUAGE_INDEX.key,
+ IntSetting.LANGUAGE_INDEX.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ BooleanSetting.USE_CUSTOM_RTC,
+ R.string.use_custom_rtc,
+ R.string.use_custom_rtc_description,
+ BooleanSetting.USE_CUSTOM_RTC.key,
+ BooleanSetting.USE_CUSTOM_RTC.defaultValue
+ )
+ )
+ add(
+ DateTimeSetting(
+ StringSetting.CUSTOM_RTC,
+ R.string.set_custom_rtc,
+ 0,
+ StringSetting.CUSTOM_RTC.key,
+ StringSetting.CUSTOM_RTC.defaultValue
+ )
+ )
+ }
+ }
+
+ private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
+ sl.apply {
+
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_ACCURACY,
+ R.string.renderer_accuracy,
+ 0,
+ R.array.rendererAccuracyNames,
+ R.array.rendererAccuracyValues,
+ IntSetting.RENDERER_ACCURACY.key,
+ IntSetting.RENDERER_ACCURACY.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_RESOLUTION,
+ R.string.renderer_resolution,
+ 0,
+ R.array.rendererResolutionNames,
+ R.array.rendererResolutionValues,
+ IntSetting.RENDERER_RESOLUTION.key,
+ IntSetting.RENDERER_RESOLUTION.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_VSYNC,
+ R.string.renderer_vsync,
+ 0,
+ R.array.rendererVSyncNames,
+ R.array.rendererVSyncValues,
+ IntSetting.RENDERER_VSYNC.key,
+ IntSetting.RENDERER_VSYNC.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_SCALING_FILTER,
+ R.string.renderer_scaling_filter,
+ 0,
+ R.array.rendererScalingFilterNames,
+ R.array.rendererScalingFilterValues,
+ IntSetting.RENDERER_SCALING_FILTER.key,
+ IntSetting.RENDERER_SCALING_FILTER.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_ANTI_ALIASING,
+ R.string.renderer_anti_aliasing,
+ 0,
+ R.array.rendererAntiAliasingNames,
+ R.array.rendererAntiAliasingValues,
+ IntSetting.RENDERER_ANTI_ALIASING.key,
+ IntSetting.RENDERER_ANTI_ALIASING.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_ASPECT_RATIO,
+ R.string.renderer_aspect_ratio,
+ 0,
+ R.array.rendererAspectRatioNames,
+ R.array.rendererAspectRatioValues,
+ IntSetting.RENDERER_ASPECT_RATIO.key,
+ IntSetting.RENDERER_ASPECT_RATIO.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_USE_DISK_SHADER_CACHE,
+ R.string.use_disk_shader_cache,
+ R.string.use_disk_shader_cache_description,
+ IntSetting.RENDERER_USE_DISK_SHADER_CACHE.key,
+ IntSetting.RENDERER_USE_DISK_SHADER_CACHE.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_FORCE_MAX_CLOCK,
+ R.string.renderer_force_max_clock,
+ R.string.renderer_force_max_clock_description,
+ IntSetting.RENDERER_FORCE_MAX_CLOCK.key,
+ IntSetting.RENDERER_FORCE_MAX_CLOCK.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_ASYNCHRONOUS_SHADERS,
+ R.string.renderer_asynchronous_shaders,
+ R.string.renderer_asynchronous_shaders_description,
+ IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.key,
+ IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_REACTIVE_FLUSHING,
+ R.string.renderer_reactive_flushing,
+ R.string.renderer_reactive_flushing_description,
+ IntSetting.RENDERER_REACTIVE_FLUSHING.key,
+ IntSetting.RENDERER_REACTIVE_FLUSHING.defaultValue
+ )
+ )
+ }
+ }
+
+ private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio))
+ sl.add(
+ SliderSetting(
+ IntSetting.AUDIO_VOLUME,
+ R.string.audio_volume,
+ R.string.audio_volume_description,
+ 0,
+ 100,
+ "%",
+ IntSetting.AUDIO_VOLUME.key,
+ IntSetting.AUDIO_VOLUME.defaultValue
+ )
+ )
+ }
+
+ private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme))
+ sl.apply {
+ val theme: AbstractIntSetting = object : AbstractIntSetting {
+ override var int: Int
+ get() = preferences.getInt(Settings.PREF_THEME, 0)
+ set(value) {
+ preferences.edit()
+ .putInt(Settings.PREF_THEME, value)
+ .apply()
+ settingsActivity.recreate()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getInt(Settings.PREF_THEME, 0).toString()
+ override val defaultValue: Any = 0
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ add(
+ SingleChoiceSetting(
+ theme,
+ R.string.change_app_theme,
+ 0,
+ R.array.themeEntriesA12,
+ R.array.themeValuesA12
+ )
+ )
+ } else {
+ add(
+ SingleChoiceSetting(
+ theme,
+ R.string.change_app_theme,
+ 0,
+ R.array.themeEntries,
+ R.array.themeValues
+ )
+ )
+ }
+
+ val themeMode: AbstractIntSetting = object : AbstractIntSetting {
+ override var int: Int
+ get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
+ set(value) {
+ preferences.edit()
+ .putInt(Settings.PREF_THEME_MODE, value)
+ .apply()
+ ThemeHelper.setThemeMode(settingsActivity)
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString()
+ override val defaultValue: Any = -1
+ }
+
+ add(
+ SingleChoiceSetting(
+ themeMode,
+ R.string.change_theme_mode,
+ 0,
+ R.array.themeModeEntries,
+ R.array.themeModeValues
+ )
+ )
+
+ val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
+ override var boolean: Boolean
+ get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
+ .apply()
+ settingsActivity.recreate()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+ .toString()
+ override val defaultValue: Any = false
+ }
+
+ add(
+ SwitchSetting(
+ blackBackgrounds,
+ R.string.use_black_backgrounds,
+ R.string.use_black_backgrounds_description
+ )
+ )
+ }
+ }
+
+ private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug))
+ sl.apply {
+ add(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_BACKEND,
+ R.string.renderer_api,
+ 0,
+ R.array.rendererApiNames,
+ R.array.rendererApiValues,
+ IntSetting.RENDERER_BACKEND.key,
+ IntSetting.RENDERER_BACKEND.defaultValue
+ )
+ )
+ add(
+ SwitchSetting(
+ IntSetting.RENDERER_DEBUG,
+ R.string.renderer_debug,
+ R.string.renderer_debug_description,
+ IntSetting.RENDERER_DEBUG.key,
+ IntSetting.RENDERER_DEBUG.defaultValue
+ )
+ )
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+
+/**
+ * Abstraction for a screen showing a list of settings. Instances of
+ * this type of view will each display a layer of the setting hierarchy.
+ */
+interface SettingsFragmentView {
+ /**
+ * Pass an ArrayList to the View so that it can be displayed on screen.
+ *
+ * @param settingsList The result of converting the HashMap to an ArrayList
+ */
+ fun showSettingsList(settingsList: ArrayList<SettingsItem>)
+
+ /**
+ * Instructs the Fragment to load the settings screen.
+ */
+ fun loadSettingsList()
+
+ /**
+ * @return The Fragment's containing activity.
+ */
+ val activityView: SettingsActivityView?
+
+ /**
+ * Tell the Fragment to tell the containing Activity to show a new
+ * Fragment containing a submenu of settings.
+ *
+ * @param menuKey Identifier for the settings group that should be shown.
+ */
+ fun loadSubMenu(menuKey: String)
+
+ /**
+ * Tell the Fragment to tell the containing activity to display a toast message.
+ *
+ * @param message Text to be shown in the Toast
+ * @param is_long Whether this should be a long Toast or short one.
+ */
+ fun showToastMessage(message: String?, is_long: Boolean)
+
+ /**
+ * Have the fragment add a setting to the HashMap.
+ *
+ * @param setting The (possibly previously missing) new setting.
+ */
+ fun putSetting(setting: AbstractSetting)
+
+ /**
+ * Have the fragment tell the containing Activity that a setting was modified.
+ */
+ fun onSettingChanged()
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: DateTimeSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as DateTimeSetting
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ } else {
+ val epochTime = setting.value.toLong()
+ val instant = Instant.ofEpochMilli(epochTime * 1000)
+ val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
+ val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
+ binding.textSettingDescription.text = dateFormatter.format(zonedTime)
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ if (setting.isEditable) {
+ adapter.onDateTimeClick(setting, bindingAdapterPosition)
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+ }
+ return false
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+
+ init {
+ itemView.setOnClickListener(null)
+ }
+
+ override fun bind(item: SettingsItem) {
+ binding.textHeaderName.setText(item.nameId)
+ }
+
+ override fun onClick(clicked: View) {
+ // no-op
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ // no-op
+ return true
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: RunnableSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as RunnableSetting
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ } else {
+ binding.textSettingDescription.visibility = View.GONE
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
+ setting.runnable.invoke()
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ // no-op
+ return true
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) :
+ RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
+
+ init {
+ itemView.setOnClickListener(this)
+ itemView.setOnLongClickListener(this)
+ }
+
+ /**
+ * Called by the adapter to set this ViewHolder's child views to display the list item
+ * it must now represent.
+ *
+ * @param item The list item that should be represented by this ViewHolder.
+ */
+ abstract fun bind(item: SettingsItem)
+
+ /**
+ * Called when this ViewHolder's view is clicked on. Implementations should usually pass
+ * this event up to the adapter.
+ *
+ * @param clicked The view that was clicked on.
+ */
+ abstract override fun onClick(clicked: View)
+
+ abstract override fun onLongClick(clicked: View): Boolean
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: SettingsItem
+
+ override fun bind(item: SettingsItem) {
+ setting = item
+ binding.textSettingName.setText(item.nameId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ } else if (item is SingleChoiceSetting) {
+ val resMgr = binding.textSettingDescription.context.resources
+ val values = resMgr.getIntArray(item.valuesId)
+ for (i in values.indices) {
+ if (values[i] == item.selectedValue) {
+ binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i]
+ }
+ }
+ } else {
+ binding.textSettingDescription.visibility = View.GONE
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ if (!setting.isEditable) {
+ return
+ }
+
+ if (setting is SingleChoiceSetting) {
+ adapter.onSingleChoiceClick(
+ (setting as SingleChoiceSetting),
+ bindingAdapterPosition
+ )
+ } else if (setting is StringSingleChoiceSetting) {
+ adapter.onStringSingleChoiceClick(
+ (setting as StringSingleChoiceSetting),
+ bindingAdapterPosition
+ )
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+ }
+ return false
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: SliderSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as SliderSetting
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ } else {
+ binding.textSettingDescription.visibility = View.GONE
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ if (setting.isEditable) {
+ adapter.onSliderClick(setting, bindingAdapterPosition)
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+ }
+ return false
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var item: SubmenuSetting
+
+ override fun bind(item: SettingsItem) {
+ this.item = item as SubmenuSetting
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ } else {
+ binding.textSettingDescription.visibility = View.GONE
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ adapter.onSubmenuClick(item)
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ // no-op
+ return true
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import android.widget.CompoundButton
+import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+
+class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+
+ private lateinit var setting: SwitchSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as SwitchSetting
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ } else {
+ binding.textSettingDescription.text = ""
+ binding.textSettingDescription.visibility = View.GONE
+ }
+ binding.switchWidget.isChecked = setting.isChecked
+ binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
+ adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
+ }
+
+ binding.switchWidget.isEnabled = setting.isEditable
+ }
+
+ override fun onClick(clicked: View) {
+ if (setting.isEditable) {
+ binding.switchWidget.toggle()
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+ }
+ return false
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.utils
+
+import org.ini4j.Wini
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.*
+import org.yuzu.yuzu_emu.features.settings.model.Settings.SettingsSectionMap
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
+import org.yuzu.yuzu_emu.utils.BiMap
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.Log
+import java.io.*
+import java.util.*
+
+/**
+ * Contains static methods for interacting with .ini files in which settings are stored.
+ */
+object SettingsFile {
+ const val FILE_NAME_CONFIG = "config"
+
+ private var sectionsMap = BiMap<String?, String?>()
+
+ /**
+ * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
+ * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
+ * failed.
+ *
+ * @param ini The ini file to load the settings from
+ * @param isCustomGame
+ * @param view The current view.
+ * @return An Observable that emits a HashMap of the file's contents, then completes.
+ */
+ private fun readFile(
+ ini: File?,
+ isCustomGame: Boolean,
+ view: SettingsActivityView? = null
+ ): HashMap<String, SettingSection?> {
+ val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
+ var reader: BufferedReader? = null
+ try {
+ reader = BufferedReader(FileReader(ini))
+ var current: SettingSection? = null
+ var line: String?
+ while (reader.readLine().also { line = it } != null) {
+ if (line!!.startsWith("[") && line!!.endsWith("]")) {
+ current = sectionFromLine(line!!, isCustomGame)
+ sections[current.name] = current
+ } else if (current != null) {
+ val setting = settingFromLine(line!!)
+ if (setting != null) {
+ current.putSetting(setting)
+ }
+ }
+ }
+ } catch (e: FileNotFoundException) {
+ Log.error("[SettingsFile] File not found: " + e.message)
+ view?.onSettingsFileNotFound()
+ } catch (e: IOException) {
+ Log.error("[SettingsFile] Error reading from: " + e.message)
+ view?.onSettingsFileNotFound()
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close()
+ } catch (e: IOException) {
+ Log.error("[SettingsFile] Error closing: " + e.message)
+ }
+ }
+ }
+ return sections
+ }
+
+ fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
+ return readFile(getSettingsFile(fileName), false, view)
+ }
+
+ fun readFile(fileName: String): HashMap<String, SettingSection?> =
+ readFile(getSettingsFile(fileName), false)
+
+ /**
+ * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
+ * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
+ * failed.
+ *
+ * @param gameId the id of the game to load it's settings.
+ * @param view The current view.
+ */
+ fun readCustomGameSettings(
+ gameId: String,
+ view: SettingsActivityView?
+ ): HashMap<String, SettingSection?> {
+ return readFile(getCustomGameSettingsFile(gameId), true, view)
+ }
+
+ /**
+ * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
+ * telling why it failed.
+ *
+ * @param fileName The target filename without a path or extension.
+ * @param sections The HashMap containing the Settings we want to serialize.
+ * @param view The current view.
+ */
+ fun saveFile(
+ fileName: String,
+ sections: TreeMap<String, SettingSection>,
+ view: SettingsActivityView
+ ) {
+ val ini = getSettingsFile(fileName)
+ try {
+ val writer = Wini(ini)
+ val keySet: Set<String> = sections.keys
+ for (key in keySet) {
+ val section = sections[key]
+ writeSection(writer, section!!)
+ }
+ writer.store()
+ } catch (e: IOException) {
+ Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message)
+ view.showToastMessage(
+ YuzuApplication.appContext
+ .getString(R.string.error_saving, fileName, e.message),
+ false
+ )
+ }
+ }
+
+ fun saveCustomGameSettings(gameId: String?, sections: HashMap<String, SettingSection?>) {
+ val sortedSections: Set<String> = TreeSet(sections.keys)
+ for (sectionKey in sortedSections) {
+ val section = sections[sectionKey]
+ val settings = section!!.settings
+ val sortedKeySet: Set<String> = TreeSet(settings.keys)
+ for (settingKey in sortedKeySet) {
+ val setting = settings[settingKey]
+ NativeLibrary.setUserSetting(
+ gameId, mapSectionNameFromIni(
+ section.name
+ ), setting!!.key, setting.valueAsString
+ )
+ }
+ }
+ }
+
+ private fun mapSectionNameFromIni(generalSectionName: String): String? {
+ return if (sectionsMap.getForward(generalSectionName) != null) {
+ sectionsMap.getForward(generalSectionName)
+ } else generalSectionName
+ }
+
+ private fun mapSectionNameToIni(generalSectionName: String): String {
+ return if (sectionsMap.getBackward(generalSectionName) != null) {
+ sectionsMap.getBackward(generalSectionName).toString()
+ } else generalSectionName
+ }
+
+ fun getSettingsFile(fileName: String): File {
+ return File(
+ DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini"
+ )
+ }
+
+ private fun getCustomGameSettingsFile(gameId: String): File {
+ return File(DirectoryInitialization.userDirectory + "/GameSettings/" + gameId + ".ini")
+ }
+
+ private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
+ var sectionName: String = line.substring(1, line.length - 1)
+ if (isCustomGame) {
+ sectionName = mapSectionNameToIni(sectionName)
+ }
+ return SettingSection(sectionName)
+ }
+
+ /**
+ * For a line of text, determines what type of data is being represented, and returns
+ * a Setting object containing this data.
+ *
+ * @param line The line of text being parsed.
+ * @return A typed Setting containing the key/value contained in the line.
+ */
+ private fun settingFromLine(line: String): AbstractSetting? {
+ val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ if (splitLine.size != 2) {
+ return null
+ }
+ val key = splitLine[0].trim { it <= ' ' }
+ val value = splitLine[1].trim { it <= ' ' }
+ if (value.isEmpty()) {
+ return null
+ }
+
+ val booleanSetting = BooleanSetting.from(key)
+ if (booleanSetting != null) {
+ booleanSetting.boolean = value.toBoolean()
+ return booleanSetting
+ }
+
+ val intSetting = IntSetting.from(key)
+ if (intSetting != null) {
+ intSetting.int = value.toInt()
+ return intSetting
+ }
+
+ val floatSetting = FloatSetting.from(key)
+ if (floatSetting != null) {
+ floatSetting.float = value.toFloat()
+ return floatSetting
+ }
+
+ val stringSetting = StringSetting.from(key)
+ if (stringSetting != null) {
+ stringSetting.string = value
+ return stringSetting
+ }
+
+ return null
+ }
+
+ /**
+ * Writes the contents of a Section HashMap to disk.
+ *
+ * @param parser A Wini pointed at a file on disk.
+ * @param section A section containing settings to be written to the file.
+ */
+ private fun writeSection(parser: Wini, section: SettingSection) {
+ // Write the section header.
+ val header = section.name
+
+ // Write this section's values.
+ val settings = section.settings
+ val keySet: Set<String> = settings.keys
+ for (key in keySet) {
+ val setting = settings[key]
+ parser.put(header, setting!!.key, setting.valueAsString)
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.Toast
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.BuildConfig
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
+import org.yuzu.yuzu_emu.model.HomeViewModel
+
+class AboutFragment : Fragment() {
+ private var _binding: FragmentAboutBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentAboutBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarAbout.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ binding.imageLogo.setOnLongClickListener {
+ Toast.makeText(
+ requireContext(),
+ R.string.gaia_is_not_real,
+ Toast.LENGTH_SHORT
+ ).show()
+ true
+ }
+
+ binding.buttonContributors.setOnClickListener { openLink(getString(R.string.contributors_link)) }
+ binding.buttonLicenses.setOnClickListener {
+ exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
+ }
+
+ binding.textBuildHash.text = BuildConfig.GIT_HASH
+ binding.buttonBuildHash.setOnClickListener {
+ val clipBoard =
+ requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH)
+ clipBoard.setPrimaryClip(clip)
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Toast.makeText(
+ requireContext(),
+ R.string.copied_to_clipboard,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
+ binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
+ binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
+
+ setInsets()
+ }
+
+ private fun openLink(link: String) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ startActivity(intent)
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.appbarAbout.layoutParams as MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.appbarAbout.layoutParams = mlpAppBar
+
+ val mlpScrollAbout = binding.scrollAbout.layoutParams as MarginLayoutParams
+ mlpScrollAbout.leftMargin = leftInsets
+ mlpScrollAbout.rightMargin = rightInsets
+ binding.scrollAbout.layoutParams = mlpScrollAbout
+
+ binding.contentAbout.updatePadding(bottom = barInsets.bottom)
+
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.FragmentEarlyAccessBinding
+import org.yuzu.yuzu_emu.model.HomeViewModel
+
+class EarlyAccessFragment : Fragment() {
+ private var _binding: FragmentEarlyAccessBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentEarlyAccessBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarAbout.setNavigationOnClickListener {
+ parentFragmentManager.primaryNavigationFragment?.findNavController()?.popBackStack()
+ }
+
+ binding.getEarlyAccessButton.setOnClickListener { openLink(getString(R.string.play_store_link)) }
+
+ setInsets()
+ }
+
+ private fun openLink(link: String) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ startActivity(intent)
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.appbarEa.layoutParams as ViewGroup.MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.appbarEa.layoutParams = mlpAppBar
+
+ binding.scrollEa.updatePadding(
+ left = leftInsets,
+ right = rightInsets,
+ bottom = barInsets.bottom
+ )
+
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.content.pm.ActivityInfo
+import android.content.res.Resources
+import android.graphics.Color
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Rational
+import android.util.TypedValue
+import android.view.*
+import android.widget.TextView
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.Insets
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.preference.PreferenceManager
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.activities.EmulationActivity
+import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
+import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
+import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.utils.*
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+
+class EmulationFragment : Fragment(), SurfaceHolder.Callback {
+ private lateinit var preferences: SharedPreferences
+ private lateinit var emulationState: EmulationState
+ private var emulationActivity: EmulationActivity? = null
+ private var perfStatsUpdater: (() -> Unit)? = null
+
+ private var _binding: FragmentEmulationBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var game: Game
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is EmulationActivity) {
+ emulationActivity = context
+ NativeLibrary.setEmulationActivity(context)
+ } else {
+ throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
+ }
+ }
+
+ /**
+ * Initialize anything that doesn't depend on the layout / views in here.
+ */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // So this fragment doesn't restart on configuration changes; i.e. rotation.
+ retainInstance = true
+ preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ game = requireArguments().parcelable(EmulationActivity.EXTRA_SELECTED_GAME)!!
+ emulationState = EmulationState(game.path)
+ }
+
+ /**
+ * Initialize the UI and start emulation in here.
+ */
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentEmulationBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.surfaceEmulation.holder.addCallback(this)
+ binding.showFpsText.setTextColor(Color.YELLOW)
+ binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
+
+ // Setup overlay.
+ updateShowFpsOverlay()
+
+ binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
+ game.title
+ binding.inGameMenu.setNavigationItemSelectedListener {
+ when (it.itemId) {
+ R.id.menu_pause_emulation -> {
+ if (emulationState.isPaused) {
+ emulationState.run(false)
+ it.title = resources.getString(R.string.emulation_pause)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_pause,
+ requireContext().theme
+ )
+ } else {
+ emulationState.pause()
+ it.title = resources.getString(R.string.emulation_unpause)
+ it.icon = ResourcesCompat.getDrawable(
+ resources,
+ R.drawable.ic_play,
+ requireContext().theme
+ )
+ }
+ true
+ }
+
+ R.id.menu_settings -> {
+ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
+ true
+ }
+
+ R.id.menu_overlay_controls -> {
+ showOverlayOptions()
+ true
+ }
+
+ R.id.menu_exit -> {
+ emulationState.stop()
+ requireActivity().finish()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ setInsets()
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ requireActivity(),
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open()
+ }
+ })
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!DirectoryInitialization.areDirectoriesReady) {
+ DirectoryInitialization.start(requireContext())
+ }
+
+ binding.surfaceEmulation.setAspectRatio(
+ when (IntSetting.RENDERER_ASPECT_RATIO.int) {
+ 0 -> Rational(16, 9)
+ 1 -> Rational(4, 3)
+ 2 -> Rational(21, 9)
+ 3 -> Rational(16, 10)
+ 4 -> null // Stretch
+ else -> Rational(16, 9)
+ }
+ )
+
+ emulationState.run(emulationActivity!!.isActivityRecreated)
+ }
+
+ override fun onPause() {
+ if (emulationState.isRunning) {
+ emulationState.pause()
+ }
+ super.onPause()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ override fun onDetach() {
+ NativeLibrary.clearEmulationActivity()
+ super.onDetach()
+ }
+
+ private fun refreshInputOverlay() {
+ binding.surfaceInputOverlay.refreshControls()
+ }
+
+ private fun resetInputOverlay() {
+ preferences.edit()
+ .remove(Settings.PREF_CONTROL_SCALE)
+ .remove(Settings.PREF_CONTROL_OPACITY)
+ .apply()
+ binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() }
+ }
+
+ private fun updateShowFpsOverlay() {
+ if (EmulationMenuSettings.showFps) {
+ val SYSTEM_FPS = 0
+ val FPS = 1
+ val FRAMETIME = 2
+ val SPEED = 3
+ perfStatsUpdater = {
+ val perfStats = NativeLibrary.getPerfStats()
+ if (perfStats[FPS] > 0 && _binding != null) {
+ binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
+ }
+
+ if (!emulationState.isStopped) {
+ perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
+ }
+ }
+ perfStatsUpdateHandler.post(perfStatsUpdater!!)
+ binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
+ binding.showFpsText.visibility = View.VISIBLE
+ } else {
+ if (perfStatsUpdater != null) {
+ perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
+ }
+ binding.showFpsText.visibility = View.GONE
+ }
+ }
+
+ private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
+
+ fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
+ val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
+ if (it.isSeparating) {
+ emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
+ binding.surfaceEmulation.layoutParams.height = it.bounds.top
+ binding.inGameMenu.layoutParams.height = it.bounds.bottom
+ binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
+ binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
+ }
+ }
+ it.isSeparating
+ } ?: false
+ if (!isFolding) {
+ binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.overlayContainer.updatePadding(0, 0, 0, 0)
+ emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ }
+ binding.surfaceInputOverlay.requestLayout()
+ binding.inGameMenu.requestLayout()
+ binding.overlayContainer.requestLayout()
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ // We purposely don't do anything here.
+ // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+ Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height)
+ emulationState.newSurface(holder.surface)
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ emulationState.clearSurface()
+ }
+
+ private fun showOverlayOptions() {
+ val anchor = binding.inGameMenu.findViewById<View>(R.id.menu_overlay_controls)
+ val popup = PopupMenu(requireContext(), anchor)
+
+ popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
+
+ popup.menu.apply {
+ findItem(R.id.menu_toggle_fps).isChecked = EmulationMenuSettings.showFps
+ findItem(R.id.menu_rel_stick_center).isChecked = EmulationMenuSettings.joystickRelCenter
+ findItem(R.id.menu_dpad_slide).isChecked = EmulationMenuSettings.dpadSlide
+ findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
+ findItem(R.id.menu_haptics).isChecked = EmulationMenuSettings.hapticFeedback
+ }
+
+ popup.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_toggle_fps -> {
+ it.isChecked = !it.isChecked
+ EmulationMenuSettings.showFps = it.isChecked
+ updateShowFpsOverlay()
+ true
+ }
+
+ R.id.menu_edit_overlay -> {
+ binding.drawerLayout.close()
+ binding.surfaceInputOverlay.requestFocus()
+ startConfiguringControls()
+ true
+ }
+
+ R.id.menu_adjust_overlay -> {
+ adjustOverlay()
+ true
+ }
+
+ R.id.menu_toggle_controls -> {
+ val preferences =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ val optionsArray = BooleanArray(15)
+ for (i in 0..14) {
+ optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 13)
+ }
+
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.emulation_toggle_controls)
+ .setMultiChoiceItems(
+ R.array.gamepadButtons,
+ optionsArray
+ ) { _, indexSelected, isChecked ->
+ preferences.edit()
+ .putBoolean("buttonToggle$indexSelected", isChecked)
+ .apply()
+ }
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ refreshInputOverlay()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
+ .show()
+
+ // Override normal behaviour so the dialog doesn't close
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
+ .setOnClickListener {
+ val isChecked = !optionsArray[0]
+ for (i in 0..14) {
+ optionsArray[i] = isChecked
+ dialog.listView.setItemChecked(i, isChecked)
+ preferences.edit()
+ .putBoolean("buttonToggle$i", isChecked)
+ .apply()
+ }
+ }
+ true
+ }
+
+ R.id.menu_show_overlay -> {
+ it.isChecked = !it.isChecked
+ EmulationMenuSettings.showOverlay = it.isChecked
+ refreshInputOverlay()
+ true
+ }
+
+ R.id.menu_rel_stick_center -> {
+ it.isChecked = !it.isChecked
+ EmulationMenuSettings.joystickRelCenter = it.isChecked
+ true
+ }
+
+ R.id.menu_dpad_slide -> {
+ it.isChecked = !it.isChecked
+ EmulationMenuSettings.dpadSlide = it.isChecked
+ true
+ }
+
+ R.id.menu_haptics -> {
+ it.isChecked = !it.isChecked
+ EmulationMenuSettings.hapticFeedback = it.isChecked
+ true
+ }
+
+ R.id.menu_reset_overlay -> {
+ binding.drawerLayout.close()
+ resetInputOverlay()
+ true
+ }
+
+ else -> true
+ }
+ }
+
+ popup.show()
+ }
+
+ private fun startConfiguringControls() {
+ binding.doneControlConfig.visibility = View.VISIBLE
+ binding.surfaceInputOverlay.setIsInEditMode(true)
+ }
+
+ private fun stopConfiguringControls() {
+ binding.doneControlConfig.visibility = View.GONE
+ binding.surfaceInputOverlay.setIsInEditMode(false)
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun adjustOverlay() {
+ val adjustBinding = DialogOverlayAdjustBinding.inflate(layoutInflater)
+ adjustBinding.apply {
+ inputScaleSlider.apply {
+ valueTo = 150F
+ value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
+ addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
+ inputScaleValue.text = "${value.toInt()}%"
+ setControlScale(value.toInt())
+ })
+ }
+ inputOpacitySlider.apply {
+ valueTo = 100F
+ value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
+ addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
+ inputOpacityValue.text = "${value.toInt()}%"
+ setControlOpacity(value.toInt())
+ })
+ }
+ inputScaleValue.text = "${inputScaleSlider.value.toInt()}%"
+ inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%"
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.emulation_control_adjust)
+ .setView(adjustBinding.root)
+ .setPositiveButton(android.R.string.ok, null)
+ .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
+ setControlScale(50)
+ setControlOpacity(100)
+ }
+ .show()
+ }
+
+ private fun setControlScale(scale: Int) {
+ preferences.edit()
+ .putInt(Settings.PREF_CONTROL_SCALE, scale)
+ .apply()
+ refreshInputOverlay()
+ }
+
+ private fun setControlOpacity(opacity: Int) {
+ preferences.edit()
+ .putInt(Settings.PREF_CONTROL_OPACITY, opacity)
+ .apply()
+ refreshInputOverlay()
+ }
+
+ private fun setInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat ->
+ val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ var left = 0
+ var right = 0
+ if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ left = cutInsets.left
+ } else {
+ right = cutInsets.right
+ }
+
+ v.setPadding(left, cutInsets.top, right, 0)
+
+ // Ensure FPS text doesn't get cut off by rounded display corners
+ val sidePadding = resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
+ if (cutInsets.left == 0) {
+ binding.showFpsText.setPadding(
+ sidePadding,
+ cutInsets.top,
+ cutInsets.right,
+ cutInsets.bottom
+ )
+ } else {
+ binding.showFpsText.setPadding(
+ cutInsets.left,
+ cutInsets.top,
+ cutInsets.right,
+ cutInsets.bottom
+ )
+ }
+ windowInsets
+ }
+ }
+
+ private class EmulationState(private val gamePath: String) {
+ private var state: State
+ private var surface: Surface? = null
+ private var runWhenSurfaceIsValid = false
+
+ init {
+ // Starting state is stopped.
+ state = State.STOPPED
+ }
+
+ @get:Synchronized
+ val isStopped: Boolean
+ get() = state == State.STOPPED
+
+ // Getters for the current state
+ @get:Synchronized
+ val isPaused: Boolean
+ get() = state == State.PAUSED
+
+ @get:Synchronized
+ val isRunning: Boolean
+ get() = state == State.RUNNING
+
+ @Synchronized
+ fun stop() {
+ if (state != State.STOPPED) {
+ Log.debug("[EmulationFragment] Stopping emulation.")
+ NativeLibrary.stopEmulation()
+ state = State.STOPPED
+ } else {
+ Log.warning("[EmulationFragment] Stop called while already stopped.")
+ }
+ }
+
+ // State changing methods
+ @Synchronized
+ fun pause() {
+ if (state != State.PAUSED) {
+ Log.debug("[EmulationFragment] Pausing emulation.")
+
+ NativeLibrary.pauseEmulation()
+
+ state = State.PAUSED
+ } else {
+ Log.warning("[EmulationFragment] Pause called while already paused.")
+ }
+ }
+
+ @Synchronized
+ fun run(isActivityRecreated: Boolean) {
+ if (isActivityRecreated) {
+ if (NativeLibrary.isRunning()) {
+ state = State.PAUSED
+ }
+ } else {
+ Log.debug("[EmulationFragment] activity resumed or fresh start")
+ }
+
+ // If the surface is set, run now. Otherwise, wait for it to get set.
+ if (surface != null) {
+ runWithValidSurface()
+ } else {
+ runWhenSurfaceIsValid = true
+ }
+ }
+
+ // Surface callbacks
+ @Synchronized
+ fun newSurface(surface: Surface?) {
+ this.surface = surface
+ if (runWhenSurfaceIsValid) {
+ runWithValidSurface()
+ }
+ }
+
+ @Synchronized
+ fun clearSurface() {
+ if (surface == null) {
+ Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
+ } else {
+ surface = null
+ Log.debug("[EmulationFragment] Surface destroyed.")
+ when (state) {
+ State.RUNNING -> {
+ state = State.PAUSED
+ }
+
+ State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.")
+ else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.")
+ }
+ }
+ }
+
+ private fun runWithValidSurface() {
+ runWhenSurfaceIsValid = false
+ when (state) {
+ State.STOPPED -> {
+ NativeLibrary.surfaceChanged(surface)
+ val emulationThread = Thread({
+ Log.debug("[EmulationFragment] Starting emulation thread.")
+ NativeLibrary.run(gamePath)
+ }, "NativeEmulation")
+ emulationThread.start()
+ }
+
+ State.PAUSED -> {
+ Log.debug("[EmulationFragment] Resuming emulation.")
+ NativeLibrary.surfaceChanged(surface)
+ NativeLibrary.unPauseEmulation()
+ }
+
+ else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
+ }
+ state = State.RUNNING
+ }
+
+ private enum class State {
+ STOPPED, RUNNING, PAUSED
+ }
+ }
+
+ companion object {
+ private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
+
+ fun newInstance(game: Game): EmulationFragment {
+ val args = Bundle()
+ args.putParcelable(EmulationActivity.EXTRA_SELECTED_GAME, game)
+ val fragment = EmulationFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
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..536163eb6
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -0,0 +1,340 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.Manifest
+import android.content.ActivityNotFoundException
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.provider.DocumentsContract
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.documentfile.provider.DocumentFile
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.BuildConfig
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
+import org.yuzu.yuzu_emu.features.DocumentProvider
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.model.HomeSetting
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+
+class HomeSettingsFragment : Fragment() {
+ private var _binding: FragmentHomeSettingsBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var mainActivity: MainActivity
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentHomeSettingsBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ mainActivity = requireActivity() as MainActivity
+
+ val optionsList: MutableList<HomeSetting> = mutableListOf(
+ HomeSetting(
+ R.string.advanced_settings,
+ R.string.settings_description,
+ R.drawable.ic_settings
+ ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") },
+ HomeSetting(
+ R.string.open_user_folder,
+ R.string.open_user_folder_description,
+ R.drawable.ic_folder_open
+ ) { openFileManager() },
+ HomeSetting(
+ R.string.preferences_theme,
+ R.string.theme_and_color_description,
+ R.drawable.ic_palette
+ ) { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") },
+ HomeSetting(
+ R.string.install_gpu_driver,
+ R.string.install_gpu_driver_description,
+ R.drawable.ic_exit
+ ) { driverInstaller() },
+ HomeSetting(
+ R.string.install_amiibo_keys,
+ R.string.install_amiibo_keys_description,
+ R.drawable.ic_nfc
+ ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) },
+ HomeSetting(
+ R.string.install_game_content,
+ R.string.install_game_content_description,
+ R.drawable.ic_system_update_alt
+ ) { mainActivity.installGameUpdate.launch(arrayOf("*/*")) },
+ HomeSetting(
+ R.string.select_games_folder,
+ R.string.select_games_folder_description,
+ R.drawable.ic_add
+ ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
+ HomeSetting(
+ R.string.manage_save_data,
+ R.string.import_export_saves_description,
+ R.drawable.ic_save
+ ) {
+ ImportExportSavesFragment().show(
+ parentFragmentManager,
+ ImportExportSavesFragment.TAG
+ )
+ },
+ HomeSetting(
+ R.string.install_prod_keys,
+ R.string.install_prod_keys_description,
+ R.drawable.ic_unlock
+ ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
+ HomeSetting(
+ R.string.install_firmware,
+ R.string.install_firmware_description,
+ R.drawable.ic_firmware
+ ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) },
+ HomeSetting(
+ R.string.share_log,
+ R.string.share_log_description,
+ R.drawable.ic_log
+ ) { shareLog() },
+ HomeSetting(
+ R.string.about,
+ R.string.about_description,
+ R.drawable.ic_info_outline
+ ) {
+ exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ parentFragmentManager.primaryNavigationFragment?.findNavController()
+ ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
+ }
+ )
+
+ if (!BuildConfig.PREMIUM) {
+ optionsList.add(
+ 0,
+ HomeSetting(
+ R.string.get_early_access,
+ R.string.get_early_access_description,
+ R.drawable.ic_diamond
+ ) {
+ exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ parentFragmentManager.primaryNavigationFragment?.findNavController()
+ ?.navigate(R.id.action_homeSettingsFragment_to_earlyAccessFragment)
+ }
+ )
+ }
+
+ binding.homeSettingsList.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = HomeSettingAdapter(requireActivity() as AppCompatActivity, optionsList)
+ }
+
+ setInsets()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ exitTransition = null
+ homeViewModel.setNavigationVisibility(visible = true, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = true)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun openFileManager() {
+ // First, try to open the user data folder directly
+ try {
+ startActivity(getFileManagerIntentOnDocumentProvider(Intent.ACTION_VIEW))
+ return
+ } catch (_: ActivityNotFoundException) {
+ }
+
+ try {
+ startActivity(getFileManagerIntentOnDocumentProvider("android.provider.action.BROWSE"))
+ return
+ } catch (_: ActivityNotFoundException) {
+ }
+
+ // Just try to open the file manager, try the package name used on "normal" phones
+ try {
+ startActivity(getFileManagerIntent("com.google.android.documentsui"))
+ showNoLinkNotification()
+ return
+ } catch (_: ActivityNotFoundException) {
+ }
+
+ try {
+ // Next, try the AOSP package name
+ startActivity(getFileManagerIntent("com.android.documentsui"))
+ showNoLinkNotification()
+ return
+ } catch (_: ActivityNotFoundException) {
+ }
+
+ Toast.makeText(
+ requireContext(),
+ resources.getString(R.string.no_file_manager),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ private fun getFileManagerIntent(packageName: String): Intent {
+ // Fragile, but some phones don't expose the system file manager in any better way
+ val intent = Intent(Intent.ACTION_MAIN)
+ intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ return intent
+ }
+
+ private fun getFileManagerIntentOnDocumentProvider(action: String): Intent {
+ val authority = "${requireContext().packageName}.user"
+ val intent = Intent(action)
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
+ intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID)
+ intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ return intent
+ }
+
+ private fun showNoLinkNotification() {
+ val builder = NotificationCompat.Builder(
+ requireContext(),
+ getString(R.string.notice_notification_channel_id)
+ )
+ .setSmallIcon(R.drawable.ic_stat_notification_logo)
+ .setContentTitle(getString(R.string.notification_no_directory_link))
+ .setContentText(getString(R.string.notification_no_directory_link_description))
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setAutoCancel(true)
+ // TODO: Make the click action for this notification lead to a help article
+
+ with(NotificationManagerCompat.from(requireContext())) {
+ if (ActivityCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ Toast.makeText(
+ requireContext(),
+ resources.getString(R.string.notification_permission_not_granted),
+ Toast.LENGTH_LONG
+ ).show()
+ return
+ }
+ notify(0, builder.build())
+ }
+ }
+
+ private fun driverInstaller() {
+ // Get the driver name for the dialog message.
+ var driverName = GpuDriverHelper.customDriverName
+ if (driverName == null) {
+ driverName = getString(R.string.system_gpu_driver)
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(getString(R.string.select_gpu_driver_title))
+ .setMessage(driverName)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
+ GpuDriverHelper.installDefaultDriver(requireContext())
+ Toast.makeText(
+ requireContext(),
+ R.string.select_gpu_driver_use_default,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
+ mainActivity.getDriver.launch(arrayOf("application/zip"))
+ }
+ .show()
+ }
+
+ private fun shareLog() {
+ val file = DocumentFile.fromSingleUri(
+ mainActivity,
+ DocumentsContract.buildDocumentUri(
+ DocumentProvider.AUTHORITY,
+ "${DocumentProvider.ROOT_ID}/log/yuzu_log.txt"
+ )
+ )!!
+ if (file.exists()) {
+ val intent = Intent(Intent.ACTION_SEND)
+ .setDataAndType(file.uri, FileUtil.TEXT_PLAIN)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .putExtra(Intent.EXTRA_STREAM, file.uri)
+ startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
+ } else {
+ Toast.makeText(
+ requireContext(),
+ getText(R.string.share_log_missing),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
+ val spacingNavigationRail =
+ resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ binding.scrollViewSettings.updatePadding(
+ top = barInsets.top,
+ bottom = barInsets.bottom
+ )
+
+ val mlpScrollSettings = binding.scrollViewSettings.layoutParams as MarginLayoutParams
+ mlpScrollSettings.leftMargin = leftInsets
+ mlpScrollSettings.rightMargin = rightInsets
+ binding.scrollViewSettings.layoutParams = mlpScrollSettings
+
+ binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation)
+
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail)
+ } else {
+ binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
+ }
+
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.DocumentsContract
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.documentfile.provider.DocumentFile
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.DocumentProvider
+import org.yuzu.yuzu_emu.getPublicFilesDir
+import org.yuzu.yuzu_emu.utils.FileUtil
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.FilenameFilter
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+class ImportExportSavesFragment : DialogFragment() {
+ private val context = YuzuApplication.appContext
+ private val savesFolder =
+ "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
+
+ // Get first subfolder in saves folder (should be the user folder)
+ private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
+ private var lastZipCreated: File? = null
+
+ private lateinit var startForResultExportSave: ActivityResultLauncher<Intent>
+ private lateinit var documentPicker: ActivityResultLauncher<Array<String>>
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val activity = requireActivity() as AppCompatActivity
+
+ val activityResultRegistry = requireActivity().activityResultRegistry
+ startForResultExportSave = activityResultRegistry.register(
+ "startForResultExportSaveKey",
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
+ }
+ documentPicker = activityResultRegistry.register(
+ "documentPickerKey",
+ ActivityResultContracts.OpenDocument()
+ ) {
+ it?.let { uri -> importSave(uri, activity) }
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return if (savesFolderRoot == "") {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.manage_save_data)
+ .setMessage(R.string.import_export_saves_no_profile)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ } else {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.manage_save_data)
+ .setMessage(R.string.manage_save_data_description)
+ .setNegativeButton(R.string.export_saves) { _, _ ->
+ exportSave()
+ }
+ .setPositiveButton(R.string.import_saves) { _, _ ->
+ documentPicker.launch(arrayOf("application/zip"))
+ }
+ .setNeutralButton(android.R.string.cancel, null)
+ .show()
+ }
+ }
+
+ /**
+ * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
+ * @return true if the zip file is successfully created, false otherwise.
+ */
+ private fun zipSave(): Boolean {
+ try {
+ val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp")
+ tempFolder.mkdirs()
+ val saveFolder = File(savesFolderRoot)
+ val outputZipFile = File(
+ tempFolder,
+ "yuzu saves - ${
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+ }.zip"
+ )
+ outputZipFile.createNewFile()
+ ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
+ saveFolder.walkTopDown().forEach { file ->
+ val zipFileName =
+ file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
+ if (zipFileName == "")
+ return@forEach
+ val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
+ zos.putNextEntry(entry)
+ if (file.isFile)
+ file.inputStream().use { fis -> fis.copyTo(zos) }
+ }
+ }
+ lastZipCreated = outputZipFile
+ } catch (e: Exception) {
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
+ */
+ private fun exportSave() {
+ CoroutineScope(Dispatchers.IO).launch {
+ val wasZipCreated = zipSave()
+ val lastZipFile = lastZipCreated
+ if (!wasZipCreated || lastZipFile == null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show()
+ }
+ return@launch
+ }
+
+ withContext(Dispatchers.Main) {
+ val file = DocumentFile.fromSingleUri(
+ context, DocumentsContract.buildDocumentUri(
+ DocumentProvider.AUTHORITY,
+ "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
+ )
+ )!!
+ val intent = Intent(Intent.ACTION_SEND)
+ .setDataAndType(file.uri, "application/zip")
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .putExtra(Intent.EXTRA_STREAM, file.uri)
+ startForResultExportSave.launch(Intent.createChooser(intent, "Share save file"))
+ }
+ }
+ }
+
+ /**
+ * Imports the save files contained in the zip file, and replaces any existing ones with the new save file.
+ * @param zipUri The Uri of the zip file containing the save file(s) to import.
+ */
+ private fun importSave(zipUri: Uri, activity: AppCompatActivity) {
+ val inputZip = context.contentResolver.openInputStream(zipUri)
+ // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
+ var validZip = false
+ val savesFolder = File(savesFolderRoot)
+ val cacheSaveDir = File("${context.cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ if (inputZip == null) {
+ Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
+ .show()
+ return
+ }
+
+ val filterTitleId =
+ FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
+
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ FileUtil.unzip(inputZip, cacheSaveDir)
+ cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
+ File(savesFolder, savePath).deleteRecursively()
+ File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
+ validZip = true
+ }
+
+ withContext(Dispatchers.Main) {
+ if (!validZip) {
+ MessageDialogFragment.newInstance(
+ R.string.save_file_invalid_zip_structure,
+ R.string.save_file_invalid_zip_structure_description
+ ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
+ return@withContext
+ }
+ Toast.makeText(
+ context,
+ context.getString(R.string.save_file_imported_success),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ cacheSaveDir.deleteRecursively()
+ }
+ } catch (e: Exception) {
+ Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+
+ companion object {
+ const val TAG = "ImportExportSavesFragment"
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.model.TaskViewModel
+
+
+class IndeterminateProgressDialogFragment : DialogFragment() {
+ private val taskViewModel: TaskViewModel by activityViewModels()
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val titleId = requireArguments().getInt(TITLE)
+
+ val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
+ progressBinding.progressBar.isIndeterminate = true
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setTitle(titleId)
+ .setView(progressBinding.root)
+ .create()
+ dialog.setCanceledOnTouchOutside(false)
+
+ taskViewModel.isComplete.observe(this) { complete ->
+ if (complete) {
+ dialog.dismiss()
+ when (val result = taskViewModel.result.value) {
+ is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
+ is MessageDialogFragment -> result.show(
+ parentFragmentManager,
+ MessageDialogFragment.TAG
+ )
+ }
+ taskViewModel.clear()
+ }
+ }
+
+ if (taskViewModel.isRunning.value == false) {
+ taskViewModel.runTask()
+ }
+ return dialog
+ }
+
+ companion object {
+ const val TAG = "IndeterminateProgressDialogFragment"
+
+ private const val TITLE = "Title"
+
+ fun newInstance(
+ activity: AppCompatActivity,
+ titleId: Int,
+ task: () -> Any
+ ): IndeterminateProgressDialogFragment {
+ val dialog = IndeterminateProgressDialogFragment()
+ val args = Bundle()
+ ViewModelProvider(activity)[TaskViewModel::class.java].task = task
+ args.putInt(TITLE, titleId)
+ dialog.arguments = args
+ return dialog
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.yuzu.yuzu_emu.databinding.DialogLicenseBinding
+import org.yuzu.yuzu_emu.model.License
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+
+class LicenseBottomSheetDialogFragment : BottomSheetDialogFragment() {
+ private var _binding: DialogLicenseBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = DialogLicenseBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ BottomSheetBehavior.from<View>(view.parent as View).state =
+ BottomSheetBehavior.STATE_HALF_EXPANDED
+
+ val license = requireArguments().parcelable<License>(LICENSE)!!
+
+ binding.apply {
+ textTitle.setText(license.titleId)
+ textLink.setText(license.linkId)
+ textCopyright.setText(license.copyrightId)
+ textLicense.setText(license.licenseId)
+ }
+ }
+
+ companion object {
+ const val TAG = "LicenseBottomSheetDialogFragment"
+
+ const val LICENSE = "License"
+
+ fun newInstance(
+ license: License
+ ): LicenseBottomSheetDialogFragment {
+ val dialog = LicenseBottomSheetDialogFragment()
+ val bundle = Bundle()
+ bundle.putParcelable(LICENSE, license)
+ dialog.arguments = bundle
+ return dialog
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.LicenseAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentLicensesBinding
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.License
+
+class LicensesFragment : Fragment() {
+ private var _binding: FragmentLicensesBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentLicensesBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarLicenses.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ val licenses = listOf(
+ License(
+ R.string.license_fidelityfx_fsr,
+ R.string.license_fidelityfx_fsr_description,
+ R.string.license_fidelityfx_fsr_link,
+ R.string.license_fidelityfx_fsr_copyright,
+ R.string.license_fidelityfx_fsr_text
+ ),
+ License(
+ R.string.license_cubeb,
+ R.string.license_cubeb_description,
+ R.string.license_cubeb_link,
+ R.string.license_cubeb_copyright,
+ R.string.license_cubeb_text
+ ),
+ License(
+ R.string.license_dynarmic,
+ R.string.license_dynarmic_description,
+ R.string.license_dynarmic_link,
+ R.string.license_dynarmic_copyright,
+ R.string.license_dynarmic_text
+ ),
+ License(
+ R.string.license_ffmpeg,
+ R.string.license_ffmpeg_description,
+ R.string.license_ffmpeg_link,
+ R.string.license_ffmpeg_copyright,
+ R.string.license_ffmpeg_text
+ ),
+ License(
+ R.string.license_opus,
+ R.string.license_opus_description,
+ R.string.license_opus_link,
+ R.string.license_opus_copyright,
+ R.string.license_opus_text
+ ),
+ License(
+ R.string.license_sirit,
+ R.string.license_sirit_description,
+ R.string.license_sirit_link,
+ R.string.license_sirit_copyright,
+ R.string.license_sirit_text
+ ),
+ License(
+ R.string.license_adreno_tools,
+ R.string.license_adreno_tools_description,
+ R.string.license_adreno_tools_link,
+ R.string.license_adreno_tools_copyright,
+ R.string.license_adreno_tools_text
+ )
+ )
+
+ binding.listLicenses.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = LicenseAdapter(requireActivity() as AppCompatActivity, licenses)
+ }
+
+ setInsets()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.appbarLicenses.layoutParams as MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.appbarLicenses.layoutParams = mlpAppBar
+
+ val mlpScrollAbout = binding.listLicenses.layoutParams as MarginLayoutParams
+ mlpScrollAbout.leftMargin = leftInsets
+ mlpScrollAbout.rightMargin = rightInsets
+ binding.listLicenses.layoutParams = mlpScrollAbout
+
+ binding.listLicenses.updatePadding(bottom = barInsets.bottom)
+
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+
+class MessageDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val titleId = requireArguments().getInt(TITLE)
+ val descriptionId = requireArguments().getInt(DESCRIPTION)
+ val helpLinkId = requireArguments().getInt(HELP_LINK)
+
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.close, null)
+ .setTitle(titleId)
+ .setMessage(descriptionId)
+
+ if (helpLinkId != 0) {
+ dialog.setNeutralButton(R.string.learn_more) { _, _ ->
+ openLink(getString(helpLinkId))
+ }
+ }
+
+ return dialog.show()
+ }
+
+ private fun openLink(link: String) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ startActivity(intent)
+ }
+
+ companion object {
+ const val TAG = "MessageDialogFragment"
+
+ private const val TITLE = "Title"
+ private const val DESCRIPTION = "Description"
+ private const val HELP_LINK = "Link"
+
+ fun newInstance(
+ titleId: Int,
+ descriptionId: Int,
+ helpLinkId: Int = 0
+ ): MessageDialogFragment {
+ val dialog = MessageDialogFragment()
+ val bundle = Bundle()
+ bundle.apply {
+ putInt(TITLE, titleId)
+ putInt(DESCRIPTION, descriptionId)
+ putInt(HELP_LINK, helpLinkId)
+ }
+ dialog.arguments = bundle
+ return dialog
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+
+class PermissionDeniedDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return MaterialAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.home_settings) { _: DialogInterface?, _: Int ->
+ openSettings()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .setTitle(R.string.permission_denied)
+ .setMessage(R.string.permission_denied_description)
+ .show()
+ }
+
+ private fun openSettings() {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", requireActivity().packageName, null)
+ intent.data = uri
+ startActivity(intent)
+ }
+
+ companion object {
+ const val TAG = "PermissionDeniedDialogFragment"
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
+
+class ResetSettingsDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val settingsActivity = requireActivity() as SettingsActivity
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.reset_all_settings)
+ .setMessage(R.string.reset_all_settings_description)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ settingsActivity.onSettingsReset()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ companion object {
+ const val TAG = "ResetSettingsDialogFragment"
+ }
+}
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..adbe3696b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -0,0 +1,230 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.preference.PreferenceManager
+import info.debatty.java.stringsimilarity.Jaccard
+import info.debatty.java.stringsimilarity.JaroWinkler
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.GameAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding
+import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.Log
+import java.util.Locale
+
+class SearchFragment : Fragment() {
+ private var _binding: FragmentSearchBinding? = null
+ private val binding get() = _binding!!
+
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ private lateinit var preferences: SharedPreferences
+
+ companion object {
+ private const val SEARCH_TEXT = "SearchText"
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSearchBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = true, animated = false)
+ preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ if (savedInstanceState != null) {
+ binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
+ }
+
+ binding.gridGamesSearch.apply {
+ layoutManager = AutofitGridLayoutManager(
+ requireContext(),
+ requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
+ )
+ adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ }
+
+ binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
+
+ binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
+ if (text.toString().isNotEmpty()) {
+ binding.clearButton.visibility = View.VISIBLE
+ } else {
+ binding.clearButton.visibility = View.INVISIBLE
+ }
+ filterAndSearch()
+ }
+
+ gamesViewModel.apply {
+ searchFocused.observe(viewLifecycleOwner) { searchFocused ->
+ if (searchFocused) {
+ focusSearch()
+ gamesViewModel.setSearchFocused(false)
+ }
+ }
+
+ games.observe(viewLifecycleOwner) { filterAndSearch() }
+ searchedGames.observe(viewLifecycleOwner) {
+ (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
+ if (it.isEmpty()) {
+ binding.noResultsView.visibility = View.VISIBLE
+ } else {
+ binding.noResultsView.visibility = View.GONE
+ }
+ }
+ }
+
+ binding.clearButton.setOnClickListener { binding.searchText.setText("") }
+
+ binding.searchBackground.setOnClickListener { focusSearch() }
+
+ setInsets()
+ filterAndSearch()
+ }
+
+ private inner class ScoredGame(val score: Double, val item: Game)
+
+ private fun filterAndSearch() {
+ val baseList = gamesViewModel.games.value!!
+ val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
+ R.id.chip_recently_played -> {
+ baseList.filter {
+ val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
+ lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
+ }
+ }
+
+ R.id.chip_recently_added -> {
+ baseList.filter {
+ val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
+ addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
+ }
+ }
+
+ R.id.chip_homebrew -> baseList.filter { it.isHomebrew }
+
+ R.id.chip_retail -> baseList.filter {
+ FileUtil.hasExtension(it.path, "xci")
+ || FileUtil.hasExtension(it.path, "nsp")
+ }
+
+ else -> baseList
+ }
+
+ if (binding.searchText.text.toString().isEmpty()
+ && binding.chipGroup.checkedChipId != View.NO_ID
+ ) {
+ gamesViewModel.setSearchedGames(filteredList)
+ return
+ }
+
+ val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
+ val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
+ val sortedList: List<Game> = filteredList.mapNotNull { game ->
+ val title = game.title.lowercase(Locale.getDefault())
+ val score = searchAlgorithm.similarity(searchTerm, title)
+ if (score > 0.03) {
+ ScoredGame(score, game)
+ } else {
+ null
+ }
+ }.sortedByDescending { it.score }.map { it.item }
+ gamesViewModel.setSearchedGames(sortedList)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (_binding != null) {
+ outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
+ }
+ }
+
+ private fun focusSearch() {
+ if (_binding != null) {
+ binding.searchText.requestFocus()
+ val imm =
+ requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
+ imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
+ }
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
+ val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
+ val spacingNavigationRail =
+ resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
+ val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
+
+ binding.constraintSearch.updatePadding(
+ left = barInsets.left + cutoutInsets.left,
+ top = barInsets.top,
+ right = barInsets.right + cutoutInsets.right
+ )
+
+ binding.gridGamesSearch.updatePadding(
+ top = extraListSpacing,
+ bottom = barInsets.bottom + spacingNavigation + extraListSpacing
+ )
+ binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
+
+ val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ binding.frameSearch.updatePadding(left = spacingNavigationRail)
+ binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
+ binding.noResultsView.updatePadding(left = spacingNavigationRail)
+ binding.chipGroup.updatePadding(
+ left = chipSpacing + spacingNavigationRail,
+ right = chipSpacing
+ )
+ mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
+ mlpDivider.rightMargin = chipSpacing
+ } else {
+ binding.frameSearch.updatePadding(right = spacingNavigationRail)
+ binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
+ binding.noResultsView.updatePadding(right = spacingNavigationRail)
+ binding.chipGroup.updatePadding(
+ left = chipSpacing,
+ right = chipSpacing + spacingNavigationRail
+ )
+ mlpDivider.leftMargin = chipSpacing
+ mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
+ }
+ binding.divider.layoutParams = mlpDivider
+
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.Manifest
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import androidx.preference.PreferenceManager
+import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
+import com.google.android.material.transition.MaterialFadeThrough
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.SetupAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.SetupPage
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.GameHelper
+import java.io.File
+
+class SetupFragment : Fragment() {
+ private var _binding: FragmentSetupBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ private lateinit var mainActivity: MainActivity
+
+ private lateinit var hasBeenWarned: BooleanArray
+
+ companion object {
+ const val KEY_NEXT_VISIBILITY = "NextButtonVisibility"
+ const val KEY_BACK_VISIBILITY = "BackButtonVisibility"
+ const val KEY_HAS_BEEN_WARNED = "HasBeenWarned"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ exitTransition = MaterialFadeThrough()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSetupBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ mainActivity = requireActivity() as MainActivity
+
+ homeViewModel.setNavigationVisibility(visible = false, animated = false)
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (binding.viewPager2.currentItem > 0) {
+ pageBackward()
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+
+ requireActivity().window.navigationBarColor =
+ ContextCompat.getColor(requireContext(), android.R.color.transparent)
+
+ val pages = mutableListOf<SetupPage>()
+ pages.apply {
+ add(
+ SetupPage(
+ R.drawable.ic_yuzu_title,
+ R.string.welcome,
+ R.string.welcome_description,
+ 0,
+ true,
+ R.string.get_started,
+ { pageForward() },
+ false
+ )
+ )
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ add(
+ SetupPage(
+ R.drawable.ic_notification,
+ R.string.notifications,
+ R.string.notifications_description,
+ 0,
+ false,
+ R.string.give_permission,
+ { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) },
+ true,
+ R.string.notification_warning,
+ R.string.notification_warning_description,
+ 0,
+ {
+ NotificationManagerCompat.from(requireContext())
+ .areNotificationsEnabled()
+ }
+ )
+ )
+ }
+
+ add(
+ SetupPage(
+ R.drawable.ic_key,
+ R.string.keys,
+ R.string.keys_description,
+ R.drawable.ic_add,
+ true,
+ R.string.select_keys,
+ { mainActivity.getProdKey.launch(arrayOf("*/*")) },
+ true,
+ R.string.install_prod_keys_warning,
+ R.string.install_prod_keys_warning_description,
+ R.string.install_prod_keys_warning_help,
+ { File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() }
+ )
+ )
+ add(
+ SetupPage(
+ R.drawable.ic_controller,
+ R.string.games,
+ R.string.games_description,
+ R.drawable.ic_add,
+ true,
+ R.string.add_games,
+ { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
+ true,
+ R.string.add_games_warning,
+ R.string.add_games_warning_description,
+ R.string.add_games_warning_help,
+ {
+ val preferences =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
+ }
+ )
+ )
+ add(
+ SetupPage(
+ R.drawable.ic_check,
+ R.string.done,
+ R.string.done_description,
+ R.drawable.ic_arrow_forward,
+ false,
+ R.string.text_continue,
+ { finishSetup() },
+ false
+ )
+ )
+ }
+
+ binding.viewPager2.apply {
+ adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
+ offscreenPageLimit = 2
+ isUserInputEnabled = false
+ }
+
+ binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
+ var previousPosition: Int = 0
+
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+
+ if (position == 1 && previousPosition == 0) {
+ showView(binding.buttonNext)
+ showView(binding.buttonBack)
+ } else if (position == 0 && previousPosition == 1) {
+ hideView(binding.buttonBack)
+ hideView(binding.buttonNext)
+ } else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
+ hideView(binding.buttonNext)
+ } else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
+ showView(binding.buttonNext)
+ }
+
+ previousPosition = position
+ }
+ })
+
+ binding.buttonNext.setOnClickListener {
+ val index = binding.viewPager2.currentItem
+ val currentPage = pages[index]
+
+ // Checks if the user has completed the task on the current page
+ if (currentPage.hasWarning) {
+ if (currentPage.taskCompleted.invoke()) {
+ pageForward()
+ return@setOnClickListener
+ }
+
+ if (!hasBeenWarned[index]) {
+ SetupWarningDialogFragment.newInstance(
+ currentPage.warningTitleId,
+ currentPage.warningDescriptionId,
+ currentPage.warningHelpLinkId,
+ index
+ ).show(childFragmentManager, SetupWarningDialogFragment.TAG)
+ return@setOnClickListener
+ }
+ }
+ pageForward()
+ }
+ binding.buttonBack.setOnClickListener { pageBackward() }
+
+ if (savedInstanceState != null) {
+ val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
+ val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
+ hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
+
+ if (nextIsVisible) {
+ binding.buttonNext.visibility = View.VISIBLE
+ }
+ if (backIsVisible) {
+ binding.buttonBack.visibility = View.VISIBLE
+ }
+ } else {
+ hasBeenWarned = BooleanArray(pages.size)
+ }
+
+ setInsets()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
+ outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
+ outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private val permissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (!it && !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
+ PermissionDeniedDialogFragment().show(
+ childFragmentManager,
+ PermissionDeniedDialogFragment.TAG
+ )
+ }
+ }
+
+ private fun finishSetup() {
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
+ .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
+ .apply()
+ mainActivity.finishSetup(binding.root.findNavController())
+ }
+
+ private fun showView(view: View) {
+ view.apply {
+ alpha = 0f
+ visibility = View.VISIBLE
+ isClickable = true
+ }.animate().apply {
+ duration = 300
+ alpha(1f)
+ }.start()
+ }
+
+ private fun hideView(view: View) {
+ if (view.visibility == View.INVISIBLE) {
+ return
+ }
+
+ view.apply {
+ alpha = 1f
+ isClickable = false
+ }.animate().apply {
+ duration = 300
+ alpha(0f)
+ }.withEndAction {
+ view.visibility = View.INVISIBLE
+ }
+ }
+
+ fun pageForward() {
+ binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
+ }
+
+ fun pageBackward() {
+ binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
+ }
+
+ fun setPageWarned(page: Int) {
+ hasBeenWarned[page] = true
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ view.setPadding(
+ barInsets.left + cutoutInsets.left,
+ barInsets.top + cutoutInsets.top,
+ barInsets.right + cutoutInsets.right,
+ barInsets.bottom + cutoutInsets.bottom
+ )
+ windowInsets
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+
+class SetupWarningDialogFragment : DialogFragment() {
+ private var titleId: Int = 0
+ private var descriptionId: Int = 0
+ private var helpLinkId: Int = 0
+ private var page: Int = 0
+
+ private lateinit var setupFragment: SetupFragment
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ titleId = requireArguments().getInt(TITLE)
+ descriptionId = requireArguments().getInt(DESCRIPTION)
+ helpLinkId = requireArguments().getInt(HELP_LINK)
+ page = requireArguments().getInt(PAGE)
+
+ setupFragment = requireParentFragment() as SetupFragment
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
+ setupFragment.pageForward()
+ setupFragment.setPageWarned(page)
+ }
+ .setNegativeButton(R.string.warning_cancel, null)
+
+ if (titleId != 0) {
+ builder.setTitle(titleId)
+ } else {
+ builder.setTitle("")
+ }
+ if (descriptionId != 0) {
+ builder.setMessage(descriptionId)
+ }
+ if (helpLinkId != 0) {
+ builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
+ val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
+ startActivity(intent)
+ }
+ }
+
+ return builder.show()
+ }
+
+ companion object {
+ const val TAG = "SetupWarningDialogFragment"
+
+ private const val TITLE = "Title"
+ private const val DESCRIPTION = "Description"
+ private const val HELP_LINK = "HelpLink"
+ private const val PAGE = "Page"
+
+ fun newInstance(
+ titleId: Int,
+ descriptionId: Int,
+ helpLinkId: Int,
+ page: Int
+ ): SetupWarningDialogFragment {
+ val dialog = SetupWarningDialogFragment()
+ val bundle = Bundle()
+ bundle.apply {
+ putInt(TITLE, titleId)
+ putInt(DESCRIPTION, descriptionId)
+ putInt(HELP_LINK, helpLinkId)
+ putInt(PAGE, page)
+ }
+ dialog.arguments = bundle
+ return dialog
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.layout
+
+import android.content.Context
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Recycler
+import org.yuzu.yuzu_emu.R
+
+/**
+ * Cut down version of the solution provided here
+ * https://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
+ */
+class AutofitGridLayoutManager(
+ context: Context,
+ columnWidth: Int
+) : GridLayoutManager(context, 1) {
+ private var columnWidth = 0
+ private var isColumnWidthChanged = true
+ private var lastWidth = 0
+ private var lastHeight = 0
+
+ init {
+ setColumnWidth(checkedColumnWidth(context, columnWidth))
+ }
+
+ private fun checkedColumnWidth(context: Context, columnWidth: Int): Int {
+ var newColumnWidth = columnWidth
+ if (newColumnWidth <= 0) {
+ newColumnWidth = context.resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
+ }
+ return newColumnWidth
+ }
+
+ private fun setColumnWidth(newColumnWidth: Int) {
+ if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
+ columnWidth = newColumnWidth
+ isColumnWidthChanged = true
+ }
+ }
+
+ override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
+ val width = width
+ val height = height
+ if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
+ val totalSpace: Int = if (orientation == VERTICAL) {
+ width - paddingRight - paddingLeft
+ } else {
+ height - paddingTop - paddingBottom
+ }
+ val spanCount = 1.coerceAtLeast(totalSpace / columnWidth)
+ setSpanCount(spanCount)
+ isColumnWidthChanged = false
+ }
+ lastWidth = width
+ lastHeight = height
+ super.onLayoutChildren(recycler, state)
+ }
+}
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..35d8000c5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+import java.util.HashSet
+
+@Parcelize
+@Serializable
+class Game(
+ val title: String,
+ val description: String,
+ val regions: String,
+ val path: String,
+ val gameId: String,
+ val company: String,
+ val isHomebrew: Boolean
+) : Parcelable {
+ val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
+ val keyLastPlayedTime get() = "${gameId}_LastPlayed"
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is Game)
+ return false
+
+ return hashCode() == other.hashCode()
+ }
+
+ override fun hashCode(): Int {
+ var result = title.hashCode()
+ result = 31 * result + description.hashCode()
+ result = 31 * result + regions.hashCode()
+ result = 31 * result + path.hashCode()
+ result = 31 * result + gameId.hashCode()
+ result = 31 * result + company.hashCode()
+ result = 31 * result + isHomebrew.hashCode()
+ return result
+ }
+
+ companion object {
+ val extensions: Set<String> = HashSet(
+ listOf(".xci", ".nsp", ".nca", ".nro")
+ )
+ }
+}
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..d9b301210
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.preference.PreferenceManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.MissingFieldException
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.utils.GameHelper
+import java.util.Locale
+
+@OptIn(ExperimentalSerializationApi::class)
+class GamesViewModel : ViewModel() {
+ private val _games = MutableLiveData<List<Game>>(emptyList())
+ val games: LiveData<List<Game>> get() = _games
+
+ private val _searchedGames = MutableLiveData<List<Game>>(emptyList())
+ val searchedGames: LiveData<List<Game>> get() = _searchedGames
+
+ private val _isReloading = MutableLiveData(false)
+ val isReloading: LiveData<Boolean> get() = _isReloading
+
+ private val _shouldSwapData = MutableLiveData(false)
+ val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData
+
+ private val _shouldScrollToTop = MutableLiveData(false)
+ val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
+
+ private val _searchFocused = MutableLiveData(false)
+ val searchFocused: LiveData<Boolean> get() = _searchFocused
+
+ init {
+ // Ensure keys are loaded so that ROM metadata can be decrypted.
+ NativeLibrary.reloadKeys()
+
+ // Retrieve list of cached games
+ val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ .getStringSet(GameHelper.KEY_GAMES, emptySet())
+ if (storedGames!!.isNotEmpty()) {
+ val deserializedGames = mutableSetOf<Game>()
+ storedGames.forEach {
+ val game: Game
+ try {
+ game = Json.decodeFromString(it)
+ } catch (e: MissingFieldException) {
+ return@forEach
+ }
+
+ val gameExists =
+ DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(game.path))
+ ?.exists()
+ if (gameExists == true) {
+ deserializedGames.add(game)
+ }
+ }
+ setGames(deserializedGames.toList())
+ }
+ reloadGames(false)
+ }
+
+ fun setGames(games: List<Game>) {
+ val sortedList = games.sortedWith(
+ compareBy(
+ { it.title.lowercase(Locale.getDefault()) },
+ { it.path }
+ )
+ )
+
+ _games.postValue(sortedList)
+ }
+
+ fun setSearchedGames(games: List<Game>) {
+ _searchedGames.postValue(games)
+ }
+
+ fun setShouldSwapData(shouldSwap: Boolean) {
+ _shouldSwapData.postValue(shouldSwap)
+ }
+
+ fun setShouldScrollToTop(shouldScroll: Boolean) {
+ _shouldScrollToTop.postValue(shouldScroll)
+ }
+
+ fun setSearchFocused(searchFocused: Boolean) {
+ _searchFocused.postValue(searchFocused)
+ }
+
+ fun reloadGames(directoryChanged: Boolean) {
+ if (isReloading.value == true)
+ return
+ _isReloading.postValue(true)
+
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeLibrary.resetRomMetadata()
+ setGames(GameHelper.getGames())
+ _isReloading.postValue(false)
+
+ if (directoryChanged) {
+ setShouldSwapData(true)
+ }
+ }
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+data class HomeSetting(
+ val titleId: Int,
+ val descriptionId: Int,
+ val iconId: Int,
+ val onClick: () -> Unit
+)
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class HomeViewModel : ViewModel() {
+ private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
+ val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible
+
+ private val _statusBarShadeVisible = MutableLiveData(true)
+ val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
+
+ var navigatedToSetup = false
+
+ init {
+ _navigationVisible.value = Pair(false, false)
+ }
+
+ fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
+ if (_navigationVisible.value?.first == visible) {
+ return
+ }
+ _navigationVisible.value = Pair(visible, animated)
+ }
+
+ fun setStatusBarShadeVisibility(visible: Boolean) {
+ if (_statusBarShadeVisible.value == visible) {
+ return
+ }
+ _statusBarShadeVisible.value = visible
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class License(
+ val titleId: Int,
+ val descriptionId: Int,
+ val linkId: Int,
+ val copyrightId: Int,
+ val licenseId: Int
+) : 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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.net.Uri
+import android.provider.DocumentsContract
+
+class MinimalDocumentFile(val filename: String, mimeType: String, val uri: Uri) {
+ val isDirectory: Boolean = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+data class SetupPage(
+ val iconId: Int,
+ val titleId: Int,
+ val descriptionId: Int,
+ val buttonIconId: Int,
+ val leftAlignedIcon: Boolean,
+ val buttonTextId: Int,
+ val buttonAction: () -> Unit,
+ val hasWarning: Boolean,
+ val warningTitleId: Int = 0,
+ val warningDescriptionId: Int = 0,
+ val warningHelpLinkId: Int = 0,
+ val taskCompleted: () -> Boolean = { true }
+)
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class TaskViewModel : ViewModel() {
+ private val _result = MutableLiveData<Any>()
+ val result: LiveData<Any> = _result
+
+ private val _isComplete = MutableLiveData<Boolean>()
+ val isComplete: LiveData<Boolean> = _isComplete
+
+ private val _isRunning = MutableLiveData<Boolean>()
+ val isRunning: LiveData<Boolean> = _isRunning
+
+ lateinit var task: () -> Any
+
+ init {
+ clear()
+ }
+
+ fun clear() {
+ _result.value = Any()
+ _isComplete.value = false
+ _isRunning.value = false
+ }
+
+ fun runTask() {
+ if (_isRunning.value == true) {
+ return
+ }
+ _isRunning.value = true
+
+ viewModelScope.launch(Dispatchers.IO) {
+ val res = task()
+ _result.postValue(res)
+ _isComplete.postValue(true)
+ }
+ }
+}
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..aa424c768
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -0,0 +1,1066 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.app.Activity
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.VectorDrawable
+import android.os.Build
+import android.util.AttributeSet
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
+import android.view.SurfaceView
+import android.view.View
+import android.view.View.OnTouchListener
+import android.view.WindowInsets
+import androidx.core.content.ContextCompat
+import androidx.preference.PreferenceManager
+import androidx.window.layout.WindowMetricsCalculator
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
+import org.yuzu.yuzu_emu.NativeLibrary.StickType
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Draws the interactive input overlay on top of the
+ * [SurfaceView] that is rendering emulation.
+ */
+class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs),
+ OnTouchListener {
+ private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
+ private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
+ private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
+
+ private var inEditMode = false
+ private var buttonBeingConfigured: InputOverlayDrawableButton? = null
+ private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
+ private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
+
+ private lateinit var windowInsets: WindowInsets
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+
+ windowInsets = rootWindowInsets
+
+ if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
+ defaultOverlay()
+ }
+
+ // Load the controls.
+ refreshControls()
+
+ // Set the on touch listener.
+ setOnTouchListener(this)
+
+ // Force draw
+ setWillNotDraw(false)
+
+ // Request focus for the overlay so it has priority on presses.
+ requestFocus()
+ }
+
+ override fun draw(canvas: Canvas) {
+ super.draw(canvas)
+ for (button in overlayButtons) {
+ button.draw(canvas)
+ }
+ for (dpad in overlayDpads) {
+ dpad.draw(canvas)
+ }
+ for (joystick in overlayJoysticks) {
+ joystick.draw(canvas)
+ }
+ }
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ if (inEditMode) {
+ return onTouchWhileEditing(event)
+ }
+
+ var shouldUpdateView = false
+ val playerIndex =
+ if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
+
+ for (button in overlayButtons) {
+ if (!button.updateStatus(event)) {
+ continue
+ }
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ button.buttonId,
+ button.status
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ for (dpad in overlayDpads) {
+ if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) {
+ continue
+ }
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.upId,
+ dpad.upStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.downId,
+ dpad.downStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.leftId,
+ dpad.leftStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.rightId,
+ dpad.rightStatus
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ for (joystick in overlayJoysticks) {
+ if (!joystick.updateStatus(event)) {
+ continue
+ }
+ val axisID = joystick.joystickId
+ NativeLibrary.onGamePadJoystickEvent(
+ playerIndex,
+ axisID,
+ joystick.xAxis,
+ joystick.realYAxis
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ joystick.buttonId,
+ joystick.buttonStatus
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ if (shouldUpdateView)
+ invalidate()
+
+ if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) {
+ return true
+ }
+
+ val pointerIndex = event.actionIndex
+ val xPosition = event.getX(pointerIndex).toInt()
+ val yPosition = event.getY(pointerIndex).toInt()
+ val pointerId = event.getPointerId(pointerIndex)
+ val motionEvent = event.action and MotionEvent.ACTION_MASK
+ val isActionDown =
+ motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+ val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
+ val isActionUp =
+ motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+
+ if (isActionDown && !isTouchInputConsumed(pointerId)) {
+ NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
+ }
+
+ if (isActionMove) {
+ for (i in 0 until event.pointerCount) {
+ val fingerId = event.getPointerId(i)
+ if (isTouchInputConsumed(fingerId)) {
+ continue
+ }
+ NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i))
+ }
+ }
+
+ if (isActionUp && !isTouchInputConsumed(pointerId)) {
+ NativeLibrary.onTouchReleased(pointerId)
+ }
+
+ return true
+ }
+
+ private fun playHaptics(event: MotionEvent) {
+ if (EmulationMenuSettings.hapticFeedback) {
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP ->
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
+ }
+ }
+ }
+
+ private fun isTouchInputConsumed(track_id: Int): Boolean {
+ for (button in overlayButtons) {
+ if (button.trackId == track_id) {
+ return true
+ }
+ }
+ for (dpad in overlayDpads) {
+ if (dpad.trackId == track_id) {
+ return true
+ }
+ }
+ for (joystick in overlayJoysticks) {
+ if (joystick.trackId == track_id) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun onTouchWhileEditing(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val fingerPositionX = event.getX(pointerIndex).toInt()
+ val fingerPositionY = event.getY(pointerIndex).toInt()
+
+ // TODO: Provide support for portrait layout
+ //val orientation =
+ // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
+
+ for (button in overlayButtons) {
+ // Determine the button state to apply based on the MotionEvent action flag.
+ when (event.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ // If no button is being moved now, remember the currently touched button to move.
+ if (buttonBeingConfigured == null &&
+ button.bounds.contains(
+ fingerPositionX,
+ fingerPositionY
+ )
+ ) {
+ buttonBeingConfigured = button
+ buttonBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
+ buttonBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
+ // Persist button position by saving new place.
+ saveControlPosition(
+ buttonBeingConfigured!!.buttonId,
+ buttonBeingConfigured!!.bounds.centerX(),
+ buttonBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ buttonBeingConfigured = null
+ }
+ }
+ }
+
+ for (dpad in overlayDpads) {
+ // Determine the button state to apply based on the MotionEvent action flag.
+ when (event.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ // If no button is being moved now, remember the currently touched button to move.
+ if (buttonBeingConfigured == null &&
+ dpad.bounds.contains(fingerPositionX, fingerPositionY)
+ ) {
+ dpadBeingConfigured = dpad
+ dpadBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
+ dpadBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
+ // Persist button position by saving new place.
+ saveControlPosition(
+ dpadBeingConfigured!!.upId,
+ dpadBeingConfigured!!.bounds.centerX(),
+ dpadBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ dpadBeingConfigured = null
+ }
+ }
+ }
+
+ for (joystick in overlayJoysticks) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
+ joystick.bounds.contains(
+ fingerPositionX,
+ fingerPositionY
+ )
+ ) {
+ joystickBeingConfigured = joystick
+ joystickBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
+ joystickBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
+ saveControlPosition(
+ joystickBeingConfigured!!.buttonId,
+ joystickBeingConfigured!!.bounds.centerX(),
+ joystickBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ joystickBeingConfigured = null
+ }
+ }
+ }
+
+ return true
+ }
+
+ private fun addOverlayControls(orientation: String) {
+ val windowSize = getSafeScreenSize(context)
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_a,
+ R.drawable.facebutton_a_depressed,
+ ButtonType.BUTTON_A,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_b,
+ R.drawable.facebutton_b_depressed,
+ ButtonType.BUTTON_B,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_x,
+ R.drawable.facebutton_x_depressed,
+ ButtonType.BUTTON_X,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_y,
+ R.drawable.facebutton_y_depressed,
+ ButtonType.BUTTON_Y,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.l_shoulder,
+ R.drawable.l_shoulder_depressed,
+ ButtonType.TRIGGER_L,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.r_shoulder,
+ R.drawable.r_shoulder_depressed,
+ ButtonType.TRIGGER_R,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.zl_trigger,
+ R.drawable.zl_trigger_depressed,
+ ButtonType.TRIGGER_ZL,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.zr_trigger,
+ R.drawable.zr_trigger_depressed,
+ ButtonType.TRIGGER_ZR,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_plus,
+ R.drawable.facebutton_plus_depressed,
+ ButtonType.BUTTON_PLUS,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_minus,
+ R.drawable.facebutton_minus_depressed,
+ ButtonType.BUTTON_MINUS,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) {
+ overlayDpads.add(
+ initializeOverlayDpad(
+ context,
+ windowSize,
+ R.drawable.dpad_standard,
+ R.drawable.dpad_standard_cardinal_depressed,
+ R.drawable.dpad_standard_diagonal_depressed,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) {
+ overlayJoysticks.add(
+ initializeOverlayJoystick(
+ context,
+ windowSize,
+ R.drawable.joystick_range,
+ R.drawable.joystick,
+ R.drawable.joystick_depressed,
+ StickType.STICK_L,
+ ButtonType.STICK_L,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) {
+ overlayJoysticks.add(
+ initializeOverlayJoystick(
+ context,
+ windowSize,
+ R.drawable.joystick_range,
+ R.drawable.joystick,
+ R.drawable.joystick_depressed,
+ StickType.STICK_R,
+ ButtonType.STICK_R,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_home,
+ R.drawable.facebutton_home_depressed,
+ ButtonType.BUTTON_HOME,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_screenshot,
+ R.drawable.facebutton_screenshot_depressed,
+ ButtonType.BUTTON_CAPTURE,
+ orientation
+ )
+ )
+ }
+ }
+
+ fun refreshControls() {
+ // Remove all the overlay buttons from the HashSet.
+ overlayButtons.clear()
+ overlayDpads.clear()
+ overlayJoysticks.clear()
+ val orientation =
+ if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
+
+ // Add all the enabled overlay items back to the HashSet.
+ if (EmulationMenuSettings.showOverlay) {
+ addOverlayControls(orientation)
+ }
+ invalidate()
+ }
+
+ private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
+ val windowSize = getSafeScreenSize(context)
+ val min = windowSize.first
+ val max = windowSize.second
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
+ .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
+ .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
+ .apply()
+ }
+
+ fun setIsInEditMode(editMode: Boolean) {
+ inEditMode = editMode
+ }
+
+ private fun defaultOverlay() {
+ if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
+ defaultOverlayLandscape()
+ }
+
+ resetButtonPlacement()
+ preferences.edit()
+ .putBoolean(Settings.PREF_OVERLAY_INIT, true)
+ .apply()
+ }
+
+ fun resetButtonPlacement() {
+ defaultOverlayLandscape()
+ refreshControls()
+ }
+
+ private fun defaultOverlayLandscape() {
+ // Each value represents the position of the button in relation to the screen size without insets.
+ preferences.edit()
+ .putFloat(
+ ButtonType.BUTTON_A.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_A.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_B.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_B.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_X.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_X.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_Y.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_Y.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZL.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZL.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZR.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZR.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.DPAD_UP.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.DPAD_UP.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_L.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_L.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_R.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_R.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_PLUS.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_PLUS.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_MINUS.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_MINUS.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_HOME.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_HOME.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_CAPTURE.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
+ .toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_CAPTURE.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
+ .toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_R.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_R.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_L.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_L.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
+ )
+ .apply()
+ }
+
+ override fun isInEditMode(): Boolean {
+ return inEditMode
+ }
+
+ companion object {
+ private val preferences: SharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ /**
+ * Resizes a [Bitmap] by a given scale factor
+ *
+ * @param context Context for getting the vector drawable
+ * @param drawableId The ID of the drawable to scale.
+ * @param scale The scale factor for the bitmap.
+ * @return The scaled [Bitmap]
+ */
+ private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
+ val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
+
+ val bitmap = Bitmap.createBitmap(
+ (vectorDrawable.intrinsicWidth * scale).toInt(),
+ (vectorDrawable.intrinsicHeight * scale).toInt(),
+ Bitmap.Config.ARGB_8888
+ )
+
+ val dm = context.resources.displayMetrics
+ val minScreenDimension = min(dm.widthPixels, dm.heightPixels)
+
+ val maxBitmapDimension = max(bitmap.width, bitmap.height)
+ val bitmapScale = scale * minScreenDimension / maxBitmapDimension
+
+ val scaledBitmap = Bitmap.createScaledBitmap(
+ bitmap,
+ (bitmap.width * bitmapScale).toInt(),
+ (bitmap.height * bitmapScale).toInt(),
+ true
+ )
+
+ val canvas = Canvas(scaledBitmap)
+ vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
+ vectorDrawable.draw(canvas)
+ return scaledBitmap
+ }
+
+ /**
+ * Gets the safe screen size for drawing the overlay
+ *
+ * @param context Context for getting the window metrics
+ * @return A pair of points, the first being the top left corner of the safe area,
+ * the second being the bottom right corner of the safe area
+ */
+ private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
+ // Get screen size
+ val windowMetrics =
+ WindowMetricsCalculator.getOrCreate()
+ .computeCurrentWindowMetrics(context as Activity)
+ var maxY = windowMetrics.bounds.height().toFloat()
+ var maxX = windowMetrics.bounds.width().toFloat()
+ var minY = 0
+ var minX = 0
+
+ // If we have API access, calculate the safe area to draw the overlay
+ var cutoutLeft = 0
+ var cutoutBottom = 0
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
+ if (insets != null) {
+ if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
+ insets.boundingRectTop.bottom.toFloat() else maxY
+ if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
+ insets.boundingRectRight.left.toFloat() else maxX
+
+ minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
+ minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
+
+ cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
+ cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
+ }
+ }
+
+ // This makes sure that if we have an inset on one side of the screen, we mirror it on
+ // the other side. Since removing space from one of the max values messes with the scale,
+ // we also have to account for it using our min values.
+ if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
+ if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
+ if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
+ maxX -= (minX * 2)
+ } else if (minX > 0) {
+ maxX -= minX
+ }
+ if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
+ maxY -= (minY * 2)
+ } else if (minY > 0) {
+ maxY -= minY
+ }
+
+ return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
+ }
+
+ /**
+ * Initializes an InputOverlayDrawableButton, given by resId, with all of the
+ * parameters set for it to be properly shown on the InputOverlay.
+ *
+ *
+ * This works due to the way the X and Y coordinates are stored within
+ * the [SharedPreferences].
+ *
+ *
+ * In the input overlay configuration menu,
+ * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
+ * the X and Y coordinates of the button at the END of its touch event
+ * (when you remove your finger/stylus from the touchscreen) are then stored
+ * within a SharedPreferences instance so that those values can be retrieved here.
+ *
+ *
+ * This has a few benefits over the conventional way of storing the values
+ * (ie. within the yuzu ini file).
+ *
+ * * No native calls
+ * * Keeps Android-only values inside the Android environment
+ *
+ *
+ *
+ * Technically no modifications should need to be performed on the returned
+ * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
+ * for Android to call the onDraw method.
+ *
+ * @param context The current [Context].
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
+ * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
+ * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
+ * @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
+ */
+ private fun initializeOverlayButton(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ defaultResId: Int,
+ pressedResId: Int,
+ buttonId: Int,
+ orientation: String
+ ): InputOverlayDrawableButton {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on button ID and user preference
+ var scale: Float = when (buttonId) {
+ ButtonType.BUTTON_HOME,
+ ButtonType.BUTTON_CAPTURE,
+ ButtonType.BUTTON_PLUS,
+ ButtonType.BUTTON_MINUS -> 0.07f
+
+ ButtonType.TRIGGER_L,
+ ButtonType.TRIGGER_R,
+ ButtonType.TRIGGER_ZL,
+ ButtonType.TRIGGER_ZR -> 0.26f
+
+ else -> 0.11f
+ }
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableButton.
+ val defaultStateBitmap = getBitmap(context, defaultResId, scale)
+ val pressedStateBitmap = getBitmap(context, pressedResId, scale)
+ val overlayDrawable =
+ InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val xKey = "$buttonId$orientation-X"
+ val yKey = "$buttonId$orientation-Y"
+ val drawableXPercent = sPrefs.getFloat(xKey, 0f)
+ val drawableYPercent = sPrefs.getFloat(yKey, 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val width = overlayDrawable.width
+ val height = overlayDrawable.height
+
+ // Now set the bounds for the InputOverlayDrawableButton.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
+ overlayDrawable.setBounds(
+ drawableX - (width / 2),
+ drawableY - (height / 2),
+ drawableX + (width / 2),
+ drawableY + (height / 2)
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(
+ drawableX - (width / 2),
+ drawableY - (height / 2)
+ )
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+
+ /**
+ * Initializes an [InputOverlayDrawableDpad]
+ *
+ * @param context The current [Context].
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param defaultResId The [Bitmap] resource ID of the default state.
+ * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
+ * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
+ * @return the initialized [InputOverlayDrawableDpad]
+ */
+ private fun initializeOverlayDpad(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ defaultResId: Int,
+ pressedOneDirectionResId: Int,
+ pressedTwoDirectionsResId: Int,
+ orientation: String
+ ): InputOverlayDrawableDpad {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on button ID and user preference
+ var scale = 0.25f
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableDpad.
+ val defaultStateBitmap =
+ getBitmap(context, defaultResId, scale)
+ val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
+ val pressedTwoDirectionsStateBitmap =
+ getBitmap(context, pressedTwoDirectionsResId, scale)
+
+ val overlayDrawable = InputOverlayDrawableDpad(
+ res,
+ defaultStateBitmap,
+ pressedOneDirectionStateBitmap,
+ pressedTwoDirectionsStateBitmap,
+ ButtonType.DPAD_UP,
+ ButtonType.DPAD_DOWN,
+ ButtonType.DPAD_LEFT,
+ ButtonType.DPAD_RIGHT
+ )
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f)
+ val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val width = overlayDrawable.width
+ val height = overlayDrawable.height
+
+ // Now set the bounds for the InputOverlayDrawableDpad.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
+ overlayDrawable.setBounds(
+ drawableX - (width / 2),
+ drawableY - (height / 2),
+ drawableX + (width / 2),
+ drawableY + (height / 2)
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+
+ /**
+ * Initializes an [InputOverlayDrawableJoystick]
+ *
+ * @param context The current [Context]
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
+ * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
+ * @param pressedResInner Resource ID for the pressed inner image of the joystick.
+ * @param joystick Identifier for which joystick this is.
+ * @param button Identifier for which joystick button this is.
+ * @return the initialized [InputOverlayDrawableJoystick].
+ */
+ private fun initializeOverlayJoystick(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ resOuter: Int,
+ defaultResInner: Int,
+ pressedResInner: Int,
+ joystick: Int,
+ button: Int,
+ orientation: String
+ ): InputOverlayDrawableJoystick {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on user preference
+ var scale = 0.3f
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableJoystick.
+ val bitmapOuter = getBitmap(context, resOuter, scale)
+ val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
+ val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f)
+ val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val outerScale = 1.66f
+
+ // Now set the bounds for the InputOverlayDrawableJoystick.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
+ val outerSize = bitmapOuter.width
+ val outerRect = Rect(
+ drawableX - (outerSize / 2),
+ drawableY - (outerSize / 2),
+ drawableX + (outerSize / 2),
+ drawableY + (outerSize / 2)
+ )
+ val innerRect =
+ Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
+
+ // Send the drawableId to the joystick so it can be referenced when saving control position.
+ val overlayDrawable = InputOverlayDrawableJoystick(
+ res,
+ bitmapOuter,
+ bitmapInnerDefault,
+ bitmapInnerPressed,
+ outerRect,
+ innerRect,
+ joystick,
+ button
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(drawableX, drawableY)
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res [Resources] instance.
+ * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
+ * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
+ * @param buttonId Identifier for this type of button.
+ */
+class InputOverlayDrawableButton(
+ res: Resources,
+ defaultStateBitmap: Bitmap,
+ pressedStateBitmap: Bitmap,
+ val buttonId: Int
+) {
+ // The ID value what motion event is tracking
+ var trackId: Int
+
+ // The drawable position on the screen
+ private var buttonPositionX = 0
+ private var buttonPositionY = 0
+
+ val width: Int
+ val height: Int
+
+ private val defaultStateBitmap: BitmapDrawable
+ private val pressedStateBitmap: BitmapDrawable
+ private var pressedState = false
+
+ private var previousTouchX = 0
+ private var previousTouchY = 0
+ var controlPositionX = 0
+ var controlPositionY = 0
+
+ init {
+ this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
+ this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap)
+ trackId = -1
+ width = this.defaultStateBitmap.intrinsicWidth
+ height = this.defaultStateBitmap.intrinsicHeight
+ }
+
+ /**
+ * Updates button status based on the motion event.
+ *
+ * @return true if value was changed
+ */
+ fun updateStatus(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val xPosition = event.getX(pointerIndex).toInt()
+ val yPosition = event.getY(pointerIndex).toInt()
+ val pointerId = event.getPointerId(pointerIndex)
+ val motionEvent = event.action and MotionEvent.ACTION_MASK
+ val isActionDown =
+ motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+ val isActionUp =
+ motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+
+ if (isActionDown) {
+ if (!bounds.contains(xPosition, yPosition)) {
+ return false
+ }
+ pressedState = true
+ trackId = pointerId
+ return true
+ }
+
+ if (isActionUp) {
+ if (trackId != pointerId) {
+ return false
+ }
+ pressedState = false
+ trackId = -1
+ return true
+ }
+
+ return false
+ }
+
+ fun setPosition(x: Int, y: Int) {
+ buttonPositionX = x
+ buttonPositionY = y
+ }
+
+ fun draw(canvas: Canvas?) {
+ currentStateBitmapDrawable.draw(canvas!!)
+ }
+
+ private val currentStateBitmapDrawable: BitmapDrawable
+ get() = if (pressedState) pressedStateBitmap else defaultStateBitmap
+
+ fun onConfigureTouch(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val fingerPositionX = event.getX(pointerIndex).toInt()
+ val fingerPositionY = event.getY(pointerIndex).toInt()
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ controlPositionX = fingerPositionX - (width / 2)
+ controlPositionY = fingerPositionY - (height / 2)
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ controlPositionX += fingerPositionX - previousTouchX
+ controlPositionY += fingerPositionY - previousTouchY
+ setBounds(
+ controlPositionX,
+ controlPositionY,
+ width + controlPositionX,
+ height + controlPositionY
+ )
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ }
+ }
+ return true
+ }
+
+ fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
+ defaultStateBitmap.setBounds(left, top, right, bottom)
+ pressedStateBitmap.setBounds(left, top, right, bottom)
+ }
+
+ fun setOpacity(value: Int) {
+ defaultStateBitmap.alpha = value
+ pressedStateBitmap.alpha = value
+ }
+
+ val status: Int
+ get() = if (pressedState) ButtonState.PRESSED else ButtonState.RELEASED
+ val bounds: Rect
+ get() = defaultStateBitmap.bounds
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res [Resources] instance.
+ * @param defaultStateBitmap [Bitmap] of the default state.
+ * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction.
+ * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
+ * @param buttonUp Identifier for the up button.
+ * @param buttonDown Identifier for the down button.
+ * @param buttonLeft Identifier for the left button.
+ * @param buttonRight Identifier for the right button.
+ */
+class InputOverlayDrawableDpad(
+ res: Resources,
+ defaultStateBitmap: Bitmap,
+ pressedOneDirectionStateBitmap: Bitmap,
+ pressedTwoDirectionsStateBitmap: Bitmap,
+ buttonUp: Int,
+ buttonDown: Int,
+ buttonLeft: Int,
+ buttonRight: Int
+) {
+ /**
+ * Gets one of the InputOverlayDrawableDpad's button IDs.
+ *
+ * @return the requested InputOverlayDrawableDpad's button ID.
+ */
+ // The ID identifying what type of button this Drawable represents.
+ val upId: Int
+ val downId: Int
+ val leftId: Int
+ val rightId: Int
+ var trackId: Int
+
+ val width: Int
+ val height: Int
+
+ private val defaultStateBitmap: BitmapDrawable
+ private val pressedOneDirectionStateBitmap: BitmapDrawable
+ private val pressedTwoDirectionsStateBitmap: BitmapDrawable
+
+ private var previousTouchX = 0
+ private var previousTouchY = 0
+ private var controlPositionX = 0
+ private var controlPositionY = 0
+
+ private var upButtonState = false
+ private var downButtonState = false
+ private var leftButtonState = false
+ private var rightButtonState = false
+
+ init {
+ this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
+ this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap)
+ this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
+ width = this.defaultStateBitmap.intrinsicWidth
+ height = this.defaultStateBitmap.intrinsicHeight
+ upId = buttonUp
+ downId = buttonDown
+ leftId = buttonLeft
+ rightId = buttonRight
+ trackId = -1
+ }
+
+ fun updateStatus(event: MotionEvent, dpad_slide: Boolean): Boolean {
+ val pointerIndex = event.actionIndex
+ val xPosition = event.getX(pointerIndex).toInt()
+ val yPosition = event.getY(pointerIndex).toInt()
+ val pointerId = event.getPointerId(pointerIndex)
+ val motionEvent = event.action and MotionEvent.ACTION_MASK
+ val isActionDown =
+ motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+ val isActionUp =
+ motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+ if (isActionDown) {
+ if (!bounds.contains(xPosition, yPosition)) {
+ return false
+ }
+ trackId = pointerId
+ }
+ if (isActionUp) {
+ if (trackId != pointerId) {
+ return false
+ }
+ trackId = -1
+ upButtonState = false
+ downButtonState = false
+ leftButtonState = false
+ rightButtonState = false
+ return true
+ }
+ if (trackId == -1) {
+ return false
+ }
+ if (!dpad_slide && !isActionDown) {
+ return false
+ }
+ for (i in 0 until event.pointerCount) {
+ if (trackId != event.getPointerId(i)) {
+ continue
+ }
+
+ var touchX = event.getX(i)
+ var touchY = event.getY(i)
+ var maxY = bounds.bottom.toFloat()
+ var maxX = bounds.right.toFloat()
+ touchX -= bounds.centerX().toFloat()
+ maxX -= bounds.centerX().toFloat()
+ touchY -= bounds.centerY().toFloat()
+ maxY -= bounds.centerY().toFloat()
+ val axisX = touchX / maxX
+ val axisY = touchY / maxY
+ val oldUpState = upButtonState
+ val oldDownState = downButtonState
+ val oldLeftState = leftButtonState
+ val oldRightState = rightButtonState
+
+ upButtonState = axisY < -VIRT_AXIS_DEADZONE
+ downButtonState = axisY > VIRT_AXIS_DEADZONE
+ leftButtonState = axisX < -VIRT_AXIS_DEADZONE
+ rightButtonState = axisX > VIRT_AXIS_DEADZONE
+ return oldUpState != upButtonState || oldDownState != downButtonState || oldLeftState != leftButtonState || oldRightState != rightButtonState
+ }
+ return false
+ }
+
+ fun draw(canvas: Canvas) {
+ val px = controlPositionX + width / 2
+ val py = controlPositionY + height / 2
+
+ // Pressed up
+ if (upButtonState && !leftButtonState && !rightButtonState) {
+ pressedOneDirectionStateBitmap.draw(canvas)
+ return
+ }
+
+ // Pressed down
+ if (downButtonState && !leftButtonState && !rightButtonState) {
+ canvas.save()
+ canvas.rotate(180f, px.toFloat(), py.toFloat())
+ pressedOneDirectionStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Pressed left
+ if (leftButtonState && !upButtonState && !downButtonState) {
+ canvas.save()
+ canvas.rotate(270f, px.toFloat(), py.toFloat())
+ pressedOneDirectionStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Pressed right
+ if (rightButtonState && !upButtonState && !downButtonState) {
+ canvas.save()
+ canvas.rotate(90f, px.toFloat(), py.toFloat())
+ pressedOneDirectionStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Pressed up left
+ if (upButtonState && leftButtonState && !rightButtonState) {
+ pressedTwoDirectionsStateBitmap.draw(canvas)
+ return
+ }
+
+ // Pressed up right
+ if (upButtonState && !leftButtonState && rightButtonState) {
+ canvas.save()
+ canvas.rotate(90f, px.toFloat(), py.toFloat())
+ pressedTwoDirectionsStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Pressed down right
+ if (downButtonState && !leftButtonState && rightButtonState) {
+ canvas.save()
+ canvas.rotate(180f, px.toFloat(), py.toFloat())
+ pressedTwoDirectionsStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Pressed down left
+ if (downButtonState && leftButtonState && !rightButtonState) {
+ canvas.save()
+ canvas.rotate(270f, px.toFloat(), py.toFloat())
+ pressedTwoDirectionsStateBitmap.draw(canvas)
+ canvas.restore()
+ return
+ }
+
+ // Not pressed
+ defaultStateBitmap.draw(canvas)
+ }
+
+ val upStatus: Int
+ get() = if (upButtonState) ButtonState.PRESSED else ButtonState.RELEASED
+ val downStatus: Int
+ get() = if (downButtonState) ButtonState.PRESSED else ButtonState.RELEASED
+ val leftStatus: Int
+ get() = if (leftButtonState) ButtonState.PRESSED else ButtonState.RELEASED
+ val rightStatus: Int
+ get() = if (rightButtonState) ButtonState.PRESSED else ButtonState.RELEASED
+
+ fun onConfigureTouch(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val fingerPositionX = event.getX(pointerIndex).toInt()
+ val fingerPositionY = event.getY(pointerIndex).toInt()
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ controlPositionX += fingerPositionX - previousTouchX
+ controlPositionY += fingerPositionY - previousTouchY
+ setBounds(
+ controlPositionX,
+ controlPositionY,
+ width + controlPositionX,
+ height + controlPositionY
+ )
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ }
+ }
+ return true
+ }
+
+ fun setPosition(x: Int, y: Int) {
+ controlPositionX = x
+ controlPositionY = y
+ }
+
+ fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
+ defaultStateBitmap.setBounds(left, top, right, bottom)
+ pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom)
+ pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom)
+ }
+
+ fun setOpacity(value: Int) {
+ defaultStateBitmap.alpha = value
+ pressedOneDirectionStateBitmap.alpha = value
+ pressedTwoDirectionsStateBitmap.alpha = value
+ }
+
+ val bounds: Rect
+ get() = defaultStateBitmap.bounds
+
+ companion object {
+ const val VIRT_AXIS_DEADZONE = 0.5f
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res [Resources] instance.
+ * @param bitmapOuter [Bitmap] which represents the outer non-movable part of the joystick.
+ * @param bitmapInnerDefault [Bitmap] which represents the default inner movable part of the joystick.
+ * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
+ * @param rectOuter [Rect] which represents the outer joystick bounds.
+ * @param rectInner [Rect] which represents the inner joystick bounds.
+ * @param joystickId The ID value what type of joystick this Drawable represents.
+ * @param buttonId The ID value what type of button this Drawable represents.
+ */
+class InputOverlayDrawableJoystick(
+ res: Resources,
+ bitmapOuter: Bitmap,
+ bitmapInnerDefault: Bitmap,
+ bitmapInnerPressed: Bitmap,
+ rectOuter: Rect,
+ rectInner: Rect,
+ val joystickId: Int,
+ val buttonId: Int
+) {
+ // The ID value what motion event is tracking
+ var trackId = -1
+
+ var xAxis = 0f
+ private var yAxis = 0f
+
+ val width: Int
+ val height: Int
+
+ private var opacity: Int = 0
+
+ private var virtBounds: Rect
+ private var origBounds: Rect
+
+ private val outerBitmap: BitmapDrawable
+ private val defaultStateInnerBitmap: BitmapDrawable
+ private val pressedStateInnerBitmap: BitmapDrawable
+
+ private var previousTouchX = 0
+ private var previousTouchY = 0
+ var controlPositionX = 0
+ var controlPositionY = 0
+
+ private val boundsBoxBitmap: BitmapDrawable
+
+ private var pressedState = false
+
+ // TODO: Add button support
+ val buttonStatus: Int
+ get() =
+ NativeLibrary.ButtonState.RELEASED
+ var bounds: Rect
+ get() = outerBitmap.bounds
+ set(bounds) {
+ outerBitmap.bounds = bounds
+ }
+
+ // Nintendo joysticks have y axis inverted
+ val realYAxis: Float
+ get() = -yAxis
+
+ private val currentStateBitmapDrawable: BitmapDrawable
+ get() = if (pressedState) pressedStateInnerBitmap else defaultStateInnerBitmap
+
+ init {
+ outerBitmap = BitmapDrawable(res, bitmapOuter)
+ defaultStateInnerBitmap = BitmapDrawable(res, bitmapInnerDefault)
+ pressedStateInnerBitmap = BitmapDrawable(res, bitmapInnerPressed)
+ boundsBoxBitmap = BitmapDrawable(res, bitmapOuter)
+ width = bitmapOuter.width
+ height = bitmapOuter.height
+ bounds = rectOuter
+ defaultStateInnerBitmap.bounds = rectInner
+ pressedStateInnerBitmap.bounds = rectInner
+ virtBounds = bounds
+ origBounds = outerBitmap.copyBounds()
+ boundsBoxBitmap.alpha = 0
+ boundsBoxBitmap.bounds = virtBounds
+ setInnerBounds()
+ }
+
+ fun draw(canvas: Canvas?) {
+ outerBitmap.draw(canvas!!)
+ currentStateBitmapDrawable.draw(canvas)
+ boundsBoxBitmap.draw(canvas)
+ }
+
+ fun updateStatus(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val xPosition = event.getX(pointerIndex).toInt()
+ val yPosition = event.getY(pointerIndex).toInt()
+ val pointerId = event.getPointerId(pointerIndex)
+ val motionEvent = event.action and MotionEvent.ACTION_MASK
+ val isActionDown =
+ motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+ val isActionUp =
+ motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+
+ if (isActionDown) {
+ if (!bounds.contains(xPosition, yPosition)) {
+ return false
+ }
+ pressedState = true
+ outerBitmap.alpha = 0
+ boundsBoxBitmap.alpha = opacity
+ if (EmulationMenuSettings.joystickRelCenter) {
+ virtBounds.offset(
+ xPosition - virtBounds.centerX(),
+ yPosition - virtBounds.centerY()
+ )
+ }
+ boundsBoxBitmap.bounds = virtBounds
+ trackId = pointerId
+ }
+
+ if (isActionUp) {
+ if (trackId != pointerId) {
+ return false
+ }
+ pressedState = false
+ xAxis = 0.0f
+ yAxis = 0.0f
+ outerBitmap.alpha = opacity
+ boundsBoxBitmap.alpha = 0
+ virtBounds = Rect(
+ origBounds.left,
+ origBounds.top,
+ origBounds.right,
+ origBounds.bottom
+ )
+ bounds = Rect(
+ origBounds.left,
+ origBounds.top,
+ origBounds.right,
+ origBounds.bottom
+ )
+ setInnerBounds()
+ trackId = -1
+ return true
+ }
+
+ if (trackId == -1) return false
+
+ for (i in 0 until event.pointerCount) {
+ if (trackId != event.getPointerId(i)) {
+ continue
+ }
+ var touchX = event.getX(i)
+ var touchY = event.getY(i)
+ var maxY = virtBounds.bottom.toFloat()
+ var maxX = virtBounds.right.toFloat()
+ touchX -= virtBounds.centerX().toFloat()
+ maxX -= virtBounds.centerX().toFloat()
+ touchY -= virtBounds.centerY().toFloat()
+ maxY -= virtBounds.centerY().toFloat()
+ val axisX = touchX / maxX
+ val axisY = touchY / maxY
+ val oldXAxis = xAxis
+ val oldYAxis = yAxis
+
+ // Clamp the circle pad input to a circle
+ val angle = atan2(axisY.toDouble(), axisX.toDouble()).toFloat()
+ var radius = sqrt((axisX * axisX + axisY * axisY).toDouble()).toFloat()
+ if (radius > 1.0f) {
+ radius = 1.0f
+ }
+ xAxis = cos(angle.toDouble()).toFloat() * radius
+ yAxis = sin(angle.toDouble()).toFloat() * radius
+ setInnerBounds()
+ return oldXAxis != xAxis && oldYAxis != yAxis
+ }
+ return false
+ }
+
+ fun onConfigureTouch(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val fingerPositionX = event.getX(pointerIndex).toInt()
+ val fingerPositionY = event.getY(pointerIndex).toInt()
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ controlPositionX = fingerPositionX - (width / 2)
+ controlPositionY = fingerPositionY - (height / 2)
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ controlPositionX += fingerPositionX - previousTouchX
+ controlPositionY += fingerPositionY - previousTouchY
+ bounds = Rect(
+ controlPositionX,
+ controlPositionY,
+ outerBitmap.intrinsicWidth + controlPositionX,
+ outerBitmap.intrinsicHeight + controlPositionY
+ )
+ virtBounds = Rect(
+ controlPositionX,
+ controlPositionY,
+ outerBitmap.intrinsicWidth + controlPositionX,
+ outerBitmap.intrinsicHeight + controlPositionY
+ )
+ setInnerBounds()
+ bounds = Rect(
+ Rect(
+ controlPositionX,
+ controlPositionY,
+ outerBitmap.intrinsicWidth + controlPositionX,
+ outerBitmap.intrinsicHeight + controlPositionY
+ )
+ )
+ previousTouchX = fingerPositionX
+ previousTouchY = fingerPositionY
+ }
+ }
+ origBounds = outerBitmap.copyBounds()
+ return true
+ }
+
+ private fun setInnerBounds() {
+ var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt()
+ var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt()
+ if (x > virtBounds.centerX() + virtBounds.width() / 2) x =
+ virtBounds.centerX() + virtBounds.width() / 2
+ if (x < virtBounds.centerX() - virtBounds.width() / 2) x =
+ virtBounds.centerX() - virtBounds.width() / 2
+ if (y > virtBounds.centerY() + virtBounds.height() / 2) y =
+ virtBounds.centerY() + virtBounds.height() / 2
+ if (y < virtBounds.centerY() - virtBounds.height() / 2) y =
+ virtBounds.centerY() - virtBounds.height() / 2
+ val width = pressedStateInnerBitmap.bounds.width() / 2
+ val height = pressedStateInnerBitmap.bounds.height() / 2
+ defaultStateInnerBitmap.setBounds(
+ x - width,
+ y - height,
+ x + width,
+ y + height
+ )
+ pressedStateInnerBitmap.bounds = defaultStateInnerBitmap.bounds
+ }
+
+ fun setPosition(x: Int, y: Int) {
+ controlPositionX = x
+ controlPositionY = y
+ }
+
+ fun setOpacity(value: Int) {
+ opacity = value
+
+ defaultStateInnerBitmap.alpha = value
+ pressedStateInnerBitmap.alpha = value
+
+ if (trackId == -1) {
+ outerBitmap.alpha = value
+ boundsBoxBitmap.alpha = 0
+ } else {
+ outerBitmap.alpha = 0
+ boundsBoxBitmap.alpha = value
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.color.MaterialColors
+import com.google.android.material.transition.MaterialFadeThrough
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.GameAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
+import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+
+class GamesFragment : Fragment() {
+ private var _binding: FragmentGamesBinding? = null
+ private val binding get() = _binding!!
+
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialFadeThrough()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentGamesBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = true, animated = false)
+
+ binding.gridGames.apply {
+ layoutManager = AutofitGridLayoutManager(
+ requireContext(),
+ requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
+ )
+ adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ }
+
+ binding.swipeRefresh.apply {
+ // Add swipe down to refresh gesture
+ setOnRefreshListener {
+ gamesViewModel.reloadGames(false)
+ }
+
+ // Set theme color to the refresh animation's background
+ setProgressBackgroundColorSchemeColor(
+ MaterialColors.getColor(
+ binding.swipeRefresh,
+ com.google.android.material.R.attr.colorPrimary
+ )
+ )
+ setColorSchemeColors(
+ MaterialColors.getColor(
+ binding.swipeRefresh,
+ com.google.android.material.R.attr.colorOnPrimary
+ )
+ )
+
+ // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn
+ post {
+ if (_binding == null) {
+ return@post
+ }
+ binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!!
+ }
+ }
+
+ gamesViewModel.apply {
+ // Watch for when we get updates to any of our games lists
+ isReloading.observe(viewLifecycleOwner) { isReloading ->
+ binding.swipeRefresh.isRefreshing = isReloading
+ }
+ games.observe(viewLifecycleOwner) {
+ (binding.gridGames.adapter as GameAdapter).submitList(it)
+ if (it.isEmpty()) {
+ binding.noticeText.visibility = View.VISIBLE
+ } else {
+ binding.noticeText.visibility = View.GONE
+ }
+ }
+ shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
+ if (shouldSwapData) {
+ (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value!!)
+ gamesViewModel.setShouldSwapData(false)
+ }
+ }
+
+ // Check if the user reselected the games menu item and then scroll to top of the list
+ shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
+ if (shouldScroll) {
+ scrollToTop()
+ gamesViewModel.setShouldScrollToTop(false)
+ }
+ }
+ }
+
+ setInsets()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun scrollToTop() {
+ if (_binding != null) {
+ binding.gridGames.smoothScrollToPosition(0)
+ }
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
+ val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
+ val spacingNavigationRail =
+ resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
+
+ binding.gridGames.updatePadding(
+ top = barInsets.top + extraListSpacing,
+ bottom = barInsets.bottom + spacingNavigation + extraListSpacing
+ )
+
+ binding.swipeRefresh.setProgressViewEndTarget(
+ false,
+ barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
+ )
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+ val mlpSwipe = binding.swipeRefresh.layoutParams as MarginLayoutParams
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ mlpSwipe.leftMargin = leftInsets + spacingNavigationRail
+ mlpSwipe.rightMargin = rightInsets
+ } else {
+ mlpSwipe.leftMargin = leftInsets
+ mlpSwipe.rightMargin = rightInsets + spacingNavigationRail
+ }
+ binding.swipeRefresh.layoutParams = mlpSwipe
+
+ binding.noticeText.updatePadding(bottom = spacingNavigation)
+
+ windowInsets
+ }
+}
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..041d16f3a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -0,0 +1,528 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.ui.main
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.WindowManager
+import android.view.animation.PathInterpolator
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.setupWithNavController
+import androidx.preference.PreferenceManager
+import com.google.android.material.color.MaterialColors
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.navigation.NavigationBarView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.activities.EmulationActivity
+import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
+import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.*
+import java.io.File
+import java.io.FilenameFilter
+import java.io.IOException
+
+class MainActivity : AppCompatActivity(), ThemeProvider {
+ private lateinit var binding: ActivityMainBinding
+
+ private val homeViewModel: HomeViewModel by viewModels()
+ private val gamesViewModel: GamesViewModel by viewModels()
+ private val settingsViewModel: SettingsViewModel by viewModels()
+
+ override var themeId: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val splashScreen = installSplashScreen()
+ splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
+
+ settingsViewModel.settings.loadSettings()
+
+ ThemeHelper.setTheme(this)
+
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
+
+ window.statusBarColor =
+ ContextCompat.getColor(applicationContext, android.R.color.transparent)
+ window.navigationBarColor =
+ ContextCompat.getColor(applicationContext, android.R.color.transparent)
+
+ binding.statusBarShade.setBackgroundColor(
+ ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.root,
+ com.google.android.material.R.attr.colorSurface
+ ),
+ ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ )
+ if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) {
+ binding.navigationBarShade.setBackgroundColor(
+ ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.root,
+ com.google.android.material.R.attr.colorSurface
+ ),
+ ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ )
+ }
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ setUpNavigation(navHostFragment.navController)
+ (binding.navigationView as NavigationBarView).setOnItemReselectedListener {
+ when (it.itemId) {
+ R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
+ R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
+ R.id.homeSettingsFragment -> SettingsActivity.launch(
+ this,
+ SettingsFile.FILE_NAME_CONFIG,
+ ""
+ )
+ }
+ }
+
+ // Prevents navigation from being drawn for a short time on recreation if set to hidden
+ if (!homeViewModel.navigationVisible.value?.first!!) {
+ binding.navigationView.visibility = View.INVISIBLE
+ binding.statusBarShade.visibility = View.INVISIBLE
+ }
+
+ homeViewModel.navigationVisible.observe(this) {
+ showNavigation(it.first, it.second)
+ }
+ homeViewModel.statusBarShadeVisible.observe(this) { visible ->
+ showStatusBarShade(visible)
+ }
+
+ // Dismiss previous notifications (should not happen unless a crash occurred)
+ EmulationActivity.stopForegroundService(this)
+
+ setInsets()
+ }
+
+ fun finishSetup(navController: NavController) {
+ navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
+ (binding.navigationView as NavigationBarView).setupWithNavController(navController)
+ showNavigation(visible = true, animated = true)
+ }
+
+ private fun setUpNavigation(navController: NavController) {
+ val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
+
+ if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
+ navController.navigate(R.id.firstTimeSetupFragment)
+ homeViewModel.navigatedToSetup = true
+ } else {
+ (binding.navigationView as NavigationBarView).setupWithNavController(navController)
+ }
+ }
+
+ private fun showNavigation(visible: Boolean, animated: Boolean) {
+ if (!animated) {
+ if (visible) {
+ binding.navigationView.visibility = View.VISIBLE
+ } else {
+ binding.navigationView.visibility = View.INVISIBLE
+ }
+ return
+ }
+
+ val smallLayout = resources.getBoolean(R.bool.small_layout)
+ binding.navigationView.animate().apply {
+ if (visible) {
+ binding.navigationView.visibility = View.VISIBLE
+ duration = 300
+ interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
+
+ if (smallLayout) {
+ binding.navigationView.translationY =
+ binding.navigationView.height.toFloat() * 2
+ translationY(0f)
+ } else {
+ if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ binding.navigationView.translationX =
+ binding.navigationView.width.toFloat() * -2
+ translationX(0f)
+ } else {
+ binding.navigationView.translationX =
+ binding.navigationView.width.toFloat() * 2
+ translationX(0f)
+ }
+ }
+ } else {
+ duration = 300
+ interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
+
+ if (smallLayout) {
+ translationY(binding.navigationView.height.toFloat() * 2)
+ } else {
+ if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ translationX(binding.navigationView.width.toFloat() * -2)
+ } else {
+ translationX(binding.navigationView.width.toFloat() * 2)
+ }
+ }
+ }
+ }.withEndAction {
+ if (!visible) {
+ binding.navigationView.visibility = View.INVISIBLE
+ }
+ }.start()
+ }
+
+ private fun showStatusBarShade(visible: Boolean) {
+ binding.statusBarShade.animate().apply {
+ if (visible) {
+ binding.statusBarShade.visibility = View.VISIBLE
+ binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
+ duration = 300
+ translationY(0f)
+ interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
+ } else {
+ duration = 300
+ translationY(binding.navigationView.height.toFloat() * -2)
+ interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
+ }
+ }.withEndAction {
+ if (!visible) {
+ binding.statusBarShade.visibility = View.INVISIBLE
+ }
+ }.start()
+ }
+
+ override fun onResume() {
+ ThemeHelper.setCorrectTheme(this)
+ super.onResume()
+ }
+
+ override fun onDestroy() {
+ EmulationActivity.stopForegroundService(this)
+ super.onDestroy()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams
+ mlpStatusShade.height = insets.top
+ binding.statusBarShade.layoutParams = mlpStatusShade
+
+ // The only situation where we care to have a nav bar shade is when it's at the bottom
+ // of the screen where scrolling list elements can go behind it.
+ val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
+ mlpNavShade.height = insets.bottom
+ binding.navigationBarShade.layoutParams = mlpNavShade
+
+ windowInsets
+ }
+
+ override fun setTheme(resId: Int) {
+ super.setTheme(resId)
+ themeId = resId
+ }
+
+ val getGamesDirectory =
+ registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ // When a new directory is picked, we currently will reset the existing games
+ // database. This effectively means that only one game directory is supported.
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
+ .putString(GameHelper.KEY_GAME_PATH, result.toString())
+ .apply()
+
+ Toast.makeText(
+ applicationContext,
+ R.string.games_dir_selected,
+ Toast.LENGTH_LONG
+ ).show()
+
+ gamesViewModel.reloadGames(true)
+ }
+
+ val getProdKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ if (!FileUtil.hasExtension(result, "keys")) {
+ MessageDialogFragment.newInstance(
+ R.string.reading_keys_failure,
+ R.string.install_prod_keys_failure_extension_description
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ return@registerForActivityResult
+ }
+
+ contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(
+ applicationContext,
+ result,
+ dstPath,
+ "prod.keys"
+ )
+ ) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ gamesViewModel.reloadGames(true)
+ } else {
+ MessageDialogFragment.newInstance(
+ R.string.invalid_keys_error,
+ R.string.install_keys_failure_description,
+ R.string.dumping_keys_quickstart_link
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ }
+ }
+
+ val getFirmware =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val inputZip = contentResolver.openInputStream(result)
+ if (inputZip == null) {
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ return@registerForActivityResult
+ }
+
+ val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
+
+ val firmwarePath =
+ File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
+ val cacheFirmwareDir = File("${cacheDir.path}/registered/")
+
+ val task: () -> Any = {
+ var messageToShow: Any
+ try {
+ FileUtil.unzip(inputZip, cacheFirmwareDir)
+ val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
+ val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
+ messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
+ MessageDialogFragment.newInstance(
+ R.string.firmware_installed_failure,
+ R.string.firmware_installed_failure_description
+ )
+ } else {
+ firmwarePath.deleteRecursively()
+ cacheFirmwareDir.copyRecursively(firmwarePath, true)
+ getString(R.string.save_file_imported_success)
+ }
+ } catch (e: Exception) {
+ messageToShow = getString(R.string.fatal_error)
+ } finally {
+ cacheFirmwareDir.deleteRecursively()
+ }
+ messageToShow
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ this,
+ R.string.firmware_installing,
+ task
+ ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ val getAmiiboKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ if (!FileUtil.hasExtension(result, "bin")) {
+ MessageDialogFragment.newInstance(
+ R.string.reading_keys_failure,
+ R.string.install_amiibo_keys_failure_extension_description
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ return@registerForActivityResult
+ }
+
+ contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(
+ applicationContext,
+ result,
+ dstPath,
+ "key_retail.bin"
+ )
+ ) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ MessageDialogFragment.newInstance(
+ R.string.invalid_keys_error,
+ R.string.install_keys_failure_description,
+ R.string.dumping_keys_quickstart_link
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ }
+ }
+
+ val getDriver =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
+ progressBinding.progressBar.isIndeterminate = true
+ val installationDialog = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.installing_driver)
+ .setView(progressBinding.root)
+ .show()
+
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ // Ignore file exceptions when a user selects an invalid zip
+ try {
+ GpuDriverHelper.installCustomDriver(applicationContext, result)
+ } catch (_: IOException) {
+ }
+
+ withContext(Dispatchers.Main) {
+ installationDialog.dismiss()
+
+ val driverName = GpuDriverHelper.customDriverName
+ if (driverName != null) {
+ Toast.makeText(
+ applicationContext,
+ getString(
+ R.string.select_gpu_driver_install_success,
+ driverName
+ ),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ applicationContext,
+ R.string.select_gpu_driver_error,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+ val installGameUpdate =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) {
+ if (it == null)
+ return@registerForActivityResult
+
+ IndeterminateProgressDialogFragment.newInstance(
+ this@MainActivity,
+ R.string.install_game_content
+ ) {
+ val result = NativeLibrary.installFileToNand(it.toString())
+ lifecycleScope.launch {
+ withContext(Dispatchers.Main) {
+ when (result) {
+ NativeLibrary.InstallFileToNandResult.Success -> {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_game_content_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_game_content_success_overwrite,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
+ MessageDialogFragment.newInstance(
+ R.string.install_game_content_failure,
+ R.string.install_game_content_failure_base
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+
+ NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
+ MessageDialogFragment.newInstance(
+ R.string.install_game_content_failure,
+ R.string.install_game_content_failure_file_extension,
+ R.string.install_game_content_help_link
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+
+ else -> {
+ MessageDialogFragment.newInstance(
+ R.string.install_game_content_failure,
+ R.string.install_game_content_failure_description,
+ R.string.install_game_content_help_link
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ }
+ }
+ }
+ return@newInstance result
+ }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.ui.main
+
+interface ThemeProvider {
+ /**
+ * Provides theme ID by overriding an activity's 'setTheme' method and returning that result
+ */
+ var themeId: Int
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+class BiMap<K, V> {
+ private val forward: MutableMap<K, V> = HashMap()
+ private val backward: MutableMap<V, K> = HashMap()
+
+ @Synchronized
+ fun add(key: K, value: V) {
+ forward[key] = value
+ backward[value] = key
+ }
+
+ @Synchronized
+ fun getForward(key: K): V? {
+ return forward[key]
+ }
+
+ @Synchronized
+ fun getBackward(key: V): K? {
+ return backward[key]
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
+
+/**
+ * Some controllers have incorrect mappings. This class has special-case fixes for them.
+ */
+class ControllerMappingHelper {
+ /**
+ * Some controllers report extra button presses that can be ignored.
+ */
+ fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean {
+ return if (isDualShock4(inputDevice)) {
+ // The two analog triggers generate analog motion events as well as a keycode.
+ // We always prefer to use the analog values, so throw away the button press
+ keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2
+ } else false
+ }
+
+ /**
+ * Scale an axis to be zero-centered with a proper range.
+ */
+ fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float {
+ if (isDualShock4(inputDevice)) {
+ // Android doesn't have correct mappings for this controller's triggers. It reports them
+ // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
+ // Scale them to properly zero-centered with a range of [0.0, 1.0].
+ if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
+ return (value + 1) / 2.0f
+ }
+ } else if (isXboxOneWireless(inputDevice)) {
+ // Same as the DualShock 4, the mappings are missing.
+ if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
+ return (value + 1) / 2.0f
+ }
+ if (axis == MotionEvent.AXIS_GENERIC_1) {
+ // This axis is stuck at ~.5. Ignore it.
+ return 0.0f
+ }
+ } else if (isMogaPro2Hid(inputDevice)) {
+ // This controller has a broken axis that reports a constant value. Ignore it.
+ if (axis == MotionEvent.AXIS_GENERIC_1) {
+ return 0.0f
+ }
+ }
+ return value
+ }
+
+ // Sony DualShock 4 controller
+ private fun isDualShock4(inputDevice: InputDevice): Boolean {
+ return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc
+ }
+
+ // Microsoft Xbox One controller
+ private fun isXboxOneWireless(inputDevice: InputDevice): Boolean {
+ return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0
+ }
+
+ // Moga Pro 2 HID
+ private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean {
+ return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Context
+import org.yuzu.yuzu_emu.NativeLibrary
+import java.io.IOException
+
+object DirectoryInitialization {
+ private var userPath: String? = null
+
+ var areDirectoriesReady: Boolean = false
+
+ fun start(context: Context) {
+ if (!areDirectoriesReady) {
+ initializeInternalStorage(context)
+ NativeLibrary.initializeEmulation()
+ areDirectoriesReady = true
+ }
+ }
+
+ val userDirectory: String?
+ get() {
+ check(areDirectoriesReady) { "Directory initialization is not ready!" }
+ return userPath
+ }
+
+ private fun initializeInternalStorage(context: Context) {
+ try {
+ userPath = context.getExternalFilesDir(null)!!.canonicalPath
+ NativeLibrary.setAppDirectory(userPath!!)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile
+import java.io.File
+import java.util.*
+
+class DocumentsTree {
+ private var root: DocumentsNode? = null
+
+ fun setRoot(rootUri: Uri?) {
+ root = null
+ root = DocumentsNode()
+ root!!.uri = rootUri
+ root!!.isDirectory = true
+ }
+
+ fun openContentUri(filepath: String, openMode: String?): Int {
+ val node = resolvePath(filepath) ?: return -1
+ return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode)
+ }
+
+ fun getFileSize(filepath: String): Long {
+ val node = resolvePath(filepath)
+ return if (node == null || node.isDirectory) {
+ 0
+ } else FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString())
+ }
+
+ fun exists(filepath: String): Boolean {
+ return resolvePath(filepath) != null
+ }
+
+ private fun resolvePath(filepath: String): DocumentsNode? {
+ val tokens = StringTokenizer(filepath, File.separator, false)
+ var iterator = root
+ while (tokens.hasMoreTokens()) {
+ val token = tokens.nextToken()
+ if (token.isEmpty()) continue
+ iterator = find(iterator, token)
+ if (iterator == null) return null
+ }
+ return iterator
+ }
+
+ private fun find(parent: DocumentsNode?, filename: String): DocumentsNode? {
+ if (parent!!.isDirectory && !parent.loaded) {
+ structTree(parent)
+ }
+ return parent.children[filename]
+ }
+
+ /**
+ * Construct current level directory tree
+ * @param parent parent node of this level
+ */
+ private fun structTree(parent: DocumentsNode) {
+ val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!)
+ for (document in documents) {
+ val node = DocumentsNode(document)
+ node.parent = parent
+ parent.children[node.name] = node
+ }
+ parent.loaded = true
+ }
+
+ private class DocumentsNode {
+ var parent: DocumentsNode? = null
+ val children: MutableMap<String?, DocumentsNode> = HashMap()
+ var name: String? = null
+ var uri: Uri? = null
+ var loaded = false
+ var isDirectory = false
+
+ constructor()
+ constructor(document: MinimalDocumentFile) {
+ name = document.filename
+ uri = document.uri
+ isDirectory = document.isDirectory
+ loaded = !isDirectory
+ }
+
+ private constructor(document: DocumentFile, isCreateDir: Boolean) {
+ name = document.name
+ uri = document.uri
+ isDirectory = isCreateDir
+ loaded = true
+ }
+
+ private fun rename(name: String) {
+ if (parent == null) {
+ return
+ }
+ parent!!.children.remove(this.name)
+ this.name = name
+ parent!!.children[name] = this
+ }
+ }
+
+ companion object {
+ fun isNativePath(path: String): Boolean {
+ return if (path.isNotEmpty()) {
+ path[0] == '/'
+ } else false
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+
+object EmulationMenuSettings {
+ private val preferences =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // These must match what is defined in src/core/settings.h
+ const val LayoutOption_Default = 0
+ const val LayoutOption_SingleScreen = 1
+ const val LayoutOption_LargeScreen = 2
+ const val LayoutOption_SideScreen = 3
+ const val LayoutOption_MobilePortrait = 4
+ const val LayoutOption_MobileLandscape = 5
+
+ var joystickRelCenter: Boolean
+ get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, value)
+ .apply()
+ }
+ var dpadSlide: Boolean
+ get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, true)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, value)
+ .apply()
+ }
+ var hapticFeedback: Boolean
+ get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, false)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, value)
+ .apply()
+ }
+
+ var landscapeScreenLayout: Int
+ get() = preferences.getInt(
+ Settings.PREF_MENU_SETTINGS_LANDSCAPE,
+ LayoutOption_MobileLandscape
+ )
+ set(value) {
+ preferences.edit()
+ .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
+ .apply()
+ }
+ var showFps: Boolean
+ get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, value)
+ .apply()
+ }
+ var showOverlay: Boolean
+ get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, true)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, value)
+ .apply()
+ }
+}
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..492b1ad91
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -0,0 +1,350 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.DocumentsContract
+import android.provider.OpenableColumns
+import androidx.documentfile.provider.DocumentFile
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.net.URLDecoder
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+object FileUtil {
+ const val PATH_TREE = "tree"
+ const val DECODE_METHOD = "UTF-8"
+ const val APPLICATION_OCTET_STREAM = "application/octet-stream"
+ const val TEXT_PLAIN = "text/plain"
+
+ /**
+ * Create a file from directory with filename.
+ * @param context Application context
+ * @param directory parent path for file.
+ * @param filename file display name.
+ * @return boolean
+ */
+ fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? {
+ var decodedFilename = filename
+ try {
+ val directoryUri = Uri.parse(directory)
+ val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
+ var mimeType = APPLICATION_OCTET_STREAM
+ if (decodedFilename.endsWith(".txt")) {
+ mimeType = TEXT_PLAIN
+ }
+ val exists = parent.findFile(decodedFilename)
+ return exists ?: parent.createFile(mimeType, decodedFilename)
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot create file, error: " + e.message)
+ }
+ return null
+ }
+
+ /**
+ * Create a directory from directory with filename.
+ * @param context Application context
+ * @param directory parent path for directory.
+ * @param directoryName directory display name.
+ * @return boolean
+ */
+ fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? {
+ var decodedDirectoryName = directoryName
+ try {
+ val directoryUri = Uri.parse(directory)
+ val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
+ val isExist = parent.findFile(decodedDirectoryName)
+ return isExist ?: parent.createDirectory(decodedDirectoryName)
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot create file, error: " + e.message)
+ }
+ return null
+ }
+
+ /**
+ * Open content uri and return file descriptor to JNI.
+ * @param context Application context
+ * @param path Native content uri path
+ * @param openMode will be one of "r", "r", "rw", "wa", "rwa"
+ * @return file descriptor
+ */
+ @JvmStatic
+ fun openContentUri(context: Context, path: String, openMode: String?): Int {
+ try {
+ val uri = Uri.parse(path)
+ val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
+ if (parcelFileDescriptor == null) {
+ Log.error("[FileUtil]: Cannot get the file descriptor from uri: $path")
+ return -1
+ }
+ val fileDescriptor = parcelFileDescriptor.detachFd()
+ parcelFileDescriptor.close()
+ return fileDescriptor
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot open content uri, error: " + e.message)
+ }
+ return -1
+ }
+
+ /**
+ * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
+ * This function will be faster than DoucmentFile.listFiles
+ * @param context Application context
+ * @param uri Directory uri.
+ * @return CheapDocument lists.
+ */
+ fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ )
+ var c: Cursor? = null
+ val results: MutableList<MinimalDocumentFile> = ArrayList()
+ try {
+ val docId: String = if (isRootTreeUri(uri)) {
+ DocumentsContract.getTreeDocumentId(uri)
+ } else {
+ DocumentsContract.getDocumentId(uri)
+ }
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
+ c = resolver.query(childrenUri, columns, null, null, null)
+ while (c!!.moveToNext()) {
+ val documentId = c.getString(0)
+ val documentName = c.getString(1)
+ val documentMimeType = c.getString(2)
+ val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId)
+ val document = MinimalDocumentFile(documentName, documentMimeType, documentUri)
+ results.add(document)
+ }
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot list file error: " + e.message)
+ } finally {
+ closeQuietly(c)
+ }
+ return results.toTypedArray()
+ }
+
+ /**
+ * Check whether given path exists.
+ * @param path Native content uri path
+ * @return bool
+ */
+ fun exists(context: Context, path: String?): Boolean {
+ var c: Cursor? = null
+ try {
+ val mUri = Uri.parse(path)
+ val columns = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
+ c = context.contentResolver.query(mUri, columns, null, null, null)
+ return c!!.count > 0
+ } catch (e: Exception) {
+ Log.info("[FileUtil] Cannot find file from given path, error: " + e.message)
+ } finally {
+ closeQuietly(c)
+ }
+ return false
+ }
+
+ /**
+ * Check whether given path is a directory
+ * @param path content uri path
+ * @return bool
+ */
+ fun isDirectory(context: Context, path: String): Boolean {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ )
+ var isDirectory = false
+ var c: Cursor? = null
+ try {
+ val mUri = Uri.parse(path)
+ c = resolver.query(mUri, columns, null, null, null)
+ c!!.moveToNext()
+ val mimeType = c.getString(0)
+ isDirectory = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot list files, error: " + e.message)
+ } finally {
+ closeQuietly(c)
+ }
+ return isDirectory
+ }
+
+ /**
+ * Get file display name from given path
+ * @param path content uri path
+ * @return String display name
+ */
+ fun getFilename(context: Context, path: String): String {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME
+ )
+ var filename = ""
+ var c: Cursor? = null
+ try {
+ val mUri = Uri.parse(path)
+ c = resolver.query(mUri, columns, null, null, null)
+ c!!.moveToNext()
+ filename = c.getString(0)
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
+ } finally {
+ closeQuietly(c)
+ }
+ return filename
+ }
+
+ fun getFilesName(context: Context, path: String): Array<String> {
+ val uri = Uri.parse(path)
+ val files: MutableList<String> = ArrayList()
+ for (file in listFiles(context, uri)) {
+ files.add(file.filename)
+ }
+ return files.toTypedArray()
+ }
+
+ /**
+ * Get file size from given path.
+ * @param path content uri path
+ * @return long file size
+ */
+ @JvmStatic
+ fun getFileSize(context: Context, path: String): Long {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_SIZE
+ )
+ var size: Long = 0
+ var c: Cursor? = null
+ try {
+ val mUri = Uri.parse(path)
+ c = resolver.query(mUri, columns, null, null, null)
+ c!!.moveToNext()
+ size = c.getLong(0)
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
+ } finally {
+ closeQuietly(c)
+ }
+ return size
+ }
+
+ fun copyUriToInternalStorage(
+ context: Context,
+ sourceUri: Uri?,
+ destinationParentPath: String,
+ destinationFilename: String
+ ): Boolean {
+ var input: InputStream? = null
+ var output: FileOutputStream? = null
+ try {
+ input = context.contentResolver.openInputStream(sourceUri!!)
+ output = FileOutputStream("$destinationParentPath/$destinationFilename")
+ val buffer = ByteArray(1024)
+ var len: Int
+ while (input!!.read(buffer).also { len = it } != -1) {
+ output.write(buffer, 0, len)
+ }
+ output.flush()
+ return true
+ } catch (e: Exception) {
+ Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
+ } finally {
+ if (input != null) {
+ try {
+ input.close()
+ } catch (e: IOException) {
+ Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
+ }
+ }
+ if (output != null) {
+ try {
+ output.close()
+ } catch (e: IOException) {
+ Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
+ }
+ }
+ }
+ return false
+ }
+
+ /**
+ * Extracts the given zip file into the given directory.
+ * @exception IOException if the file was being created outside of the target directory
+ */
+ @Throws(SecurityException::class)
+ fun unzip(zipStream: InputStream, destDir: File): Boolean {
+ ZipInputStream(BufferedInputStream(zipStream)).use { zis ->
+ var entry: ZipEntry? = zis.nextEntry
+ while (entry != null) {
+ val entryName = entry.name
+ val entryFile = File(destDir, entryName)
+ if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
+ throw SecurityException("Entry is outside of the target dir: " + entryFile.name)
+ }
+ if (entry.isDirectory) {
+ entryFile.mkdirs()
+ } else {
+ entryFile.parentFile?.mkdirs()
+ entryFile.createNewFile()
+ entryFile.outputStream().use { fos -> zis.copyTo(fos) }
+ }
+ entry = zis.nextEntry
+ }
+ }
+
+ return true
+ }
+
+ fun isRootTreeUri(uri: Uri): Boolean {
+ val paths = uri.pathSegments
+ return paths.size == 2 && PATH_TREE == paths[0]
+ }
+
+ fun closeQuietly(closeable: AutoCloseable?) {
+ if (closeable != null) {
+ try {
+ closeable.close()
+ } catch (rethrown: RuntimeException) {
+ throw rethrown
+ } catch (ignored: Exception) {
+ }
+ }
+ }
+
+ fun hasExtension(path: String, extension: String): Boolean =
+ path.substring(path.lastIndexOf(".") + 1).contains(extension)
+
+ fun hasExtension(uri: Uri, extension: String): Boolean {
+ val fileName: String?
+ val cursor = YuzuApplication.appContext.contentResolver.query(uri, null, null, null, null)
+ val nameIndex = cursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ cursor?.moveToFirst()
+
+ if (nameIndex == null) {
+ return false
+ }
+
+ fileName = cursor.getString(nameIndex)
+ cursor.close()
+
+ if (fileName == null) {
+ return false
+ }
+ return fileName.substring(fileName.lastIndexOf(".") + 1).contains(extension)
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.activities.EmulationActivity
+
+/**
+ * A service that shows a permanent notification in the background to avoid the app getting
+ * cleared from memory by the system.
+ */
+class ForegroundService : Service() {
+ companion object {
+ const val EMULATION_RUNNING_NOTIFICATION = 0x1000
+
+ const val ACTION_STOP = "stop"
+ }
+
+ private fun showRunningNotification() {
+ // Intent is used to resume emulation if the notification is clicked
+ val contentIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ Intent(this, EmulationActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ val builder =
+ NotificationCompat.Builder(this, getString(R.string.emulation_notification_channel_id))
+ .setSmallIcon(R.drawable.ic_stat_notification_logo)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.emulation_notification_running))
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .setVibrate(null)
+ .setSound(null)
+ .setContentIntent(contentIntent)
+ startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build())
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ showRunningNotification()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+ if (intent.action == ACTION_STOP) {
+ NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelfResult(startId)
+ }
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION)
+ }
+}
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..42b207618
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.SharedPreferences
+import android.net.Uri
+import androidx.preference.PreferenceManager
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.model.Game
+import java.util.*
+
+object GameHelper {
+ const val KEY_GAME_PATH = "game_path"
+ const val KEY_GAMES = "Games"
+
+ private lateinit var preferences: SharedPreferences
+
+ fun getGames(): List<Game> {
+ val games = mutableListOf<Game>()
+ val context = YuzuApplication.appContext
+ val gamesDir =
+ PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
+ val gamesUri = Uri.parse(gamesDir)
+ preferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+ // Ensure keys are loaded so that ROM metadata can be decrypted.
+ NativeLibrary.reloadKeys()
+
+ val children = FileUtil.listFiles(context, gamesUri)
+ for (file in children) {
+ if (!file.isDirectory) {
+ val filename = file.uri.toString()
+ val extensionStart = filename.lastIndexOf('.')
+ if (extensionStart > 0) {
+ val fileExtension = filename.substring(extensionStart)
+
+ // Check that the file has an extension we care about before trying to read out of it.
+ if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
+ games.add(getGame(filename))
+ }
+ }
+ }
+ }
+
+ // Cache list of games found on disk
+ val serializedGames = mutableSetOf<String>()
+ games.forEach {
+ serializedGames.add(Json.encodeToString(it))
+ }
+ preferences.edit()
+ .remove(KEY_GAMES)
+ .putStringSet(KEY_GAMES, serializedGames)
+ .apply()
+
+ return games.toList()
+ }
+
+ private fun getGame(filePath: String): Game {
+ var name = NativeLibrary.getTitle(filePath)
+
+ // If the game's title field is empty, use the filename.
+ if (name.isEmpty()) {
+ name = filePath.substring(filePath.lastIndexOf("/") + 1)
+ }
+ var gameId = NativeLibrary.getGameId(filePath)
+
+ // If the game's ID field is empty, use the filename without extension.
+ if (gameId.isEmpty()) {
+ gameId = filePath.substring(
+ filePath.lastIndexOf("/") + 1,
+ filePath.lastIndexOf(".")
+ )
+ }
+
+ val newGame = Game(
+ name,
+ NativeLibrary.getDescription(filePath).replace("\n", " "),
+ NativeLibrary.getRegions(filePath),
+ filePath,
+ gameId,
+ NativeLibrary.getCompany(filePath),
+ NativeLibrary.isHomebrew(filePath)
+ )
+
+ val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
+ if (addedTime == 0L) {
+ preferences.edit()
+ .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
+ .apply()
+ }
+
+ return newGame
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Context
+import android.net.Uri
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.zip.ZipInputStream
+
+object GpuDriverHelper {
+ private const val META_JSON_FILENAME = "meta.json"
+ private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
+ private var fileRedirectionPath: String? = null
+ private var driverInstallationPath: String? = null
+ private var hookLibPath: String? = null
+
+ @Throws(IOException::class)
+ private fun unzip(zipFilePath: String, destDir: String) {
+ val dir = File(destDir)
+
+ // Create output directory if it doesn't exist
+ if (!dir.exists()) dir.mkdirs()
+
+ // Unpack the files.
+ val inputStream = FileInputStream(zipFilePath)
+ val zis = ZipInputStream(BufferedInputStream(inputStream))
+ val buffer = ByteArray(1024)
+ var ze = zis.nextEntry
+ while (ze != null) {
+ val newFile = File(destDir, ze.name)
+ val canonicalPath = newFile.canonicalPath
+ if (!canonicalPath.startsWith(destDir + ze.name)) {
+ throw SecurityException("Zip file attempted path traversal! " + ze.name)
+ }
+
+ newFile.parentFile!!.mkdirs()
+ val fos = FileOutputStream(newFile)
+ var len: Int
+ while (zis.read(buffer).also { len = it } > 0) {
+ fos.write(buffer, 0, len)
+ }
+ fos.close()
+ zis.closeEntry()
+ ze = zis.nextEntry
+ }
+ zis.closeEntry()
+ }
+
+ fun initializeDriverParameters(context: Context) {
+ try {
+ // Initialize the file redirection directory.
+ fileRedirectionPath =
+ context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
+
+ // Initialize the driver installation directory.
+ driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+
+ // Initialize directories.
+ initializeDirectories()
+
+ // Initialize hook libraries directory.
+ hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
+
+ // Initialize GPU driver.
+ NativeLibrary.initializeGpuDriver(
+ hookLibPath,
+ driverInstallationPath,
+ customDriverLibraryName,
+ fileRedirectionPath
+ )
+ }
+
+ fun installDefaultDriver(context: Context) {
+ // Removing the installed driver will result in the backend using the default system driver.
+ val driverInstallationDir = File(driverInstallationPath!!)
+ deleteRecursive(driverInstallationDir)
+ initializeDriverParameters(context)
+ }
+
+ fun installCustomDriver(context: Context, driverPathUri: Uri?) {
+ // Revert to system default in the event the specified driver is bad.
+ installDefaultDriver(context)
+
+ // Ensure we have directories.
+ initializeDirectories()
+
+ // Copy the zip file URI into our private storage.
+ copyUriToInternalStorage(
+ context,
+ driverPathUri,
+ driverInstallationPath!!,
+ DRIVER_INTERNAL_FILENAME
+ )
+
+ // Unzip the driver.
+ try {
+ unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
+ } catch (e: SecurityException) {
+ return
+ }
+
+ // Initialize the driver parameters.
+ initializeDriverParameters(context)
+ }
+
+ // Parse the custom driver metadata to retrieve the name.
+ val customDriverName: String?
+ get() {
+ val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
+ return metadata.name
+ }
+
+ // Parse the custom driver metadata to retrieve the library name.
+ private val customDriverLibraryName: String?
+ get() {
+ // Parse the custom driver metadata to retrieve the library name.
+ val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
+ return metadata.libraryName
+ }
+
+ private fun initializeDirectories() {
+ // Ensure the file redirection directory exists.
+ val fileRedirectionDir = File(fileRedirectionPath!!)
+ if (!fileRedirectionDir.exists()) {
+ fileRedirectionDir.mkdirs()
+ }
+ // Ensure the driver installation directory exists.
+ val driverInstallationDir = File(driverInstallationPath!!)
+ if (!driverInstallationDir.exists()) {
+ driverInstallationDir.mkdirs()
+ }
+ }
+
+ private fun deleteRecursive(fileOrDirectory: File) {
+ if (fileOrDirectory.isDirectory) {
+ for (child in fileOrDirectory.listFiles()!!) {
+ deleteRecursive(child)
+ }
+ }
+ fileOrDirectory.delete()
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Paths
+
+class GpuDriverMetadata(metadataFilePath: String) {
+ var name: String? = null
+ var description: String? = null
+ var author: String? = null
+ var vendor: String? = null
+ var driverVersion: String? = null
+ var minApi = 0
+ var libraryName: String? = null
+
+ init {
+ try {
+ val json = JSONObject(getStringFromFile(metadataFilePath))
+ name = json.getString("name")
+ description = json.getString("description")
+ author = json.getString("author")
+ vendor = json.getString("vendor")
+ driverVersion = json.getString("driverVersion")
+ minApi = json.getInt("minApi")
+ libraryName = json.getString("libraryName")
+ } catch (e: JSONException) {
+ // JSON is malformed, ignore and treat as unsupported metadata.
+ } catch (e: IOException) {
+ // File is inaccessible, ignore and treat as unsupported metadata.
+ }
+ }
+
+ companion object {
+ @Throws(IOException::class)
+ private fun getStringFromFile(filePath: String): String {
+ val path = Paths.get(filePath)
+ val bytes = Files.readAllBytes(path)
+ return String(bytes, StandardCharsets.UTF_8)
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.view.KeyEvent
+import android.view.MotionEvent
+import org.yuzu.yuzu_emu.NativeLibrary
+import kotlin.math.sqrt
+
+class InputHandler {
+ fun initialize() {
+ // Connect first controller
+ NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
+ }
+
+ fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ val button: Int = when (event.device.vendorId) {
+ 0x045E -> getInputXboxButtonKey(event.keyCode)
+ 0x054C -> getInputDS5ButtonKey(event.keyCode)
+ 0x057E -> getInputJoyconButtonKey(event.keyCode)
+ 0x1532 -> getInputRazerButtonKey(event.keyCode)
+ else -> getInputGenericButtonKey(event.keyCode)
+ }
+
+ val action = when (event.action) {
+ KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED
+ KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
+ else -> return false
+ }
+
+ // Ignore invalid buttons
+ if (button < 0) {
+ return false
+ }
+
+ return NativeLibrary.onGamePadButtonEvent(
+ getPlayerNumber(event.device.controllerNumber),
+ button,
+ action
+ )
+ }
+
+ fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
+ val device = event.device
+ // Check every axis input available on the controller
+ for (range in device.motionRanges) {
+ val axis = range.axis
+ when (device.vendorId) {
+ 0x045E -> setGenericAxisInput(event, axis)
+ 0x054C -> setGenericAxisInput(event, axis)
+ 0x057E -> setJoyconAxisInput(event, axis)
+ 0x1532 -> setRazerAxisInput(event, axis)
+ else -> setGenericAxisInput(event, axis)
+ }
+ }
+
+ return true
+ }
+
+ private fun getPlayerNumber(index: Int): Int {
+ // TODO: Joycons are handled as different controllers. Find a way to merge them.
+ return when (index) {
+ 2 -> NativeLibrary.Player2Device
+ 3 -> NativeLibrary.Player3Device
+ 4 -> NativeLibrary.Player4Device
+ 5 -> NativeLibrary.Player5Device
+ 6 -> NativeLibrary.Player6Device
+ 7 -> NativeLibrary.Player7Device
+ 8 -> NativeLibrary.Player8Device
+ else -> if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
+ }
+ }
+
+ private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
+ // Calculate vector size
+ val r2 = xAxis * xAxis + yAxis * yAxis
+ var r = sqrt(r2.toDouble()).toFloat()
+
+ // Adjust range of joystick
+ val deadzone = 0.15f
+ var x = xAxis
+ var y = yAxis
+
+ if (r > deadzone) {
+ val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
+ x *= deadzoneFactor
+ y *= deadzoneFactor
+ r *= deadzoneFactor
+ } else {
+ x = 0.0f
+ y = 0.0f
+ }
+
+ // Normalize joystick
+ if (r > 1.0f) {
+ x /= r
+ y /= r
+ }
+
+ NativeLibrary.onGamePadJoystickEvent(
+ playerNumber,
+ index,
+ x,
+ -y
+ )
+ }
+
+ private fun getAxisToButton(axis: Float): Int {
+ return if (axis > 0.5f) NativeLibrary.ButtonState.PRESSED else NativeLibrary.ButtonState.RELEASED
+ }
+
+ private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) {
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.DPAD_UP,
+ getAxisToButton(-yAxis)
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.DPAD_DOWN,
+ getAxisToButton(yAxis)
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.DPAD_LEFT,
+ getAxisToButton(-xAxis)
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.DPAD_RIGHT,
+ getAxisToButton(xAxis)
+ )
+ }
+
+ private fun getInputDS5ButtonKey(key: Int): Int {
+ // The missing ds5 buttons are axis
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun getInputJoyconButtonKey(key: Int): Int {
+ // Joycon support is half dead. A lot of buttons can't be mapped
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
+ KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
+ KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
+ KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
+ KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun getInputXboxButtonKey(key: Int): Int {
+ // The missing xbox buttons are axis
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun getInputRazerButtonKey(key: Int): Int {
+ // The missing xbox buttons are axis
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun getInputGenericButtonKey(key: Int): Int {
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
+ KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
+ KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
+ KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
+ KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
+ val playerNumber = getPlayerNumber(event.device.controllerNumber)
+
+ when (axis) {
+ MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_L,
+ event.getAxisValue(MotionEvent.AXIS_X),
+ event.getAxisValue(MotionEvent.AXIS_Y)
+ )
+ MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_R,
+ event.getAxisValue(MotionEvent.AXIS_RX),
+ event.getAxisValue(MotionEvent.AXIS_RY)
+ )
+ MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_R,
+ event.getAxisValue(MotionEvent.AXIS_Z),
+ event.getAxisValue(MotionEvent.AXIS_RZ)
+ )
+ MotionEvent.AXIS_LTRIGGER ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZL,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
+ )
+ MotionEvent.AXIS_BRAKE ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZL,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
+ )
+ MotionEvent.AXIS_RTRIGGER ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZR,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
+ )
+ MotionEvent.AXIS_GAS ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZR,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
+ )
+ MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
+ setAxisDpadState(
+ playerNumber,
+ event.getAxisValue(MotionEvent.AXIS_HAT_X),
+ event.getAxisValue(MotionEvent.AXIS_HAT_Y)
+ )
+ }
+ }
+
+
+ private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
+ // Joycon support is half dead. Right joystick doesn't work
+ val playerNumber = getPlayerNumber(event.device.controllerNumber)
+
+ when (axis) {
+ MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_L,
+ event.getAxisValue(MotionEvent.AXIS_X),
+ event.getAxisValue(MotionEvent.AXIS_Y)
+ )
+ MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_R,
+ event.getAxisValue(MotionEvent.AXIS_Z),
+ event.getAxisValue(MotionEvent.AXIS_RZ)
+ )
+ MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_R,
+ event.getAxisValue(MotionEvent.AXIS_RX),
+ event.getAxisValue(MotionEvent.AXIS_RY)
+ )
+ }
+ }
+
+ private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
+ val playerNumber = getPlayerNumber(event.device.controllerNumber)
+
+ when (axis) {
+ MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_L,
+ event.getAxisValue(MotionEvent.AXIS_X),
+ event.getAxisValue(MotionEvent.AXIS_Y)
+ )
+ MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
+ setStickState(
+ playerNumber,
+ NativeLibrary.StickType.STICK_R,
+ event.getAxisValue(MotionEvent.AXIS_Z),
+ event.getAxisValue(MotionEvent.AXIS_RZ)
+ )
+ MotionEvent.AXIS_BRAKE ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZL,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
+ )
+ MotionEvent.AXIS_GAS ->
+ NativeLibrary.onGamePadButtonEvent(
+ playerNumber,
+ NativeLibrary.ButtonType.TRIGGER_ZR,
+ getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
+ )
+ MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
+ setAxisDpadState(
+ playerNumber,
+ event.getAxisValue(MotionEvent.AXIS_HAT_X),
+ event.getAxisValue(MotionEvent.AXIS_HAT_Y)
+ )
+ }
+ }
+
+
+} \ 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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.graphics.Rect
+
+object InsetsHelper {
+ const val THREE_BUTTON_NAVIGATION = 0
+ const val TWO_BUTTON_NAVIGATION = 1
+ const val GESTURE_NAVIGATION = 2
+
+ @SuppressLint("DiscouragedApi")
+ fun getSystemGestureType(context: Context): Int {
+ val resources = context.resources
+ val resourceId =
+ resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
+ return if (resourceId != 0) {
+ resources.getInteger(resourceId)
+ } else 0
+ }
+
+ fun getBottomPaddingRequired(activity: Activity): Int {
+ val visibleFrame = Rect()
+ activity.window.decorView.getWindowVisibleDisplayFrame(visibleFrame)
+ return visibleFrame.bottom - visibleFrame.top - activity.resources.displayMetrics.heightPixels
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.util.Log
+import org.yuzu.yuzu_emu.BuildConfig
+
+/**
+ * Contains methods that call through to [android.util.Log], but
+ * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
+ * levels in release builds.
+ */
+object Log {
+ private const val TAG = "Yuzu Frontend"
+
+ fun verbose(message: String) {
+ if (BuildConfig.DEBUG) {
+ Log.v(TAG, message)
+ }
+ }
+
+ fun debug(message: String) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, message)
+ }
+ }
+
+ fun info(message: String) {
+ Log.i(TAG, message)
+ }
+
+ fun warning(message: String) {
+ Log.w(TAG, message)
+ }
+
+ fun error(message: String) {
+ Log.e(TAG, message)
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentFilter
+import android.nfc.NfcAdapter
+import android.nfc.Tag
+import android.nfc.tech.NfcA
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import org.yuzu.yuzu_emu.NativeLibrary
+import java.io.IOException
+
+class NfcReader(private val activity: Activity) {
+ private var nfcAdapter: NfcAdapter? = null
+ private var pendingIntent: PendingIntent? = null
+
+ fun initialize() {
+ nfcAdapter = NfcAdapter.getDefaultAdapter(activity) ?: return
+
+ pendingIntent = PendingIntent.getActivity(
+ activity,
+ 0, Intent(activity, activity.javaClass),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ else PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val tagDetected = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
+ tagDetected.addCategory(Intent.CATEGORY_DEFAULT)
+ }
+
+ fun startScanning() {
+ nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, null, null)
+ }
+
+ fun stopScanning() {
+ nfcAdapter?.disableForegroundDispatch(activity)
+ }
+
+ fun onNewIntent(intent: Intent) {
+ val action = intent.action
+ if (NfcAdapter.ACTION_TAG_DISCOVERED != action
+ && NfcAdapter.ACTION_TECH_DISCOVERED != action
+ && NfcAdapter.ACTION_NDEF_DISCOVERED != action
+ ) {
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val tag =
+ intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) ?: return
+ readTagData(tag)
+ return
+ }
+
+ val tag =
+ intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
+ readTagData(tag)
+ }
+
+ private fun readTagData(tag: Tag) {
+ if (!tag.techList.contains("android.nfc.tech.NfcA")) {
+ return
+ }
+
+ val amiibo = NfcA.get(tag) ?: return
+ amiibo.connect()
+
+ val tagData = ntag215ReadAll(amiibo) ?: return
+ NativeLibrary.onReadNfcTag(tagData)
+
+ nfcAdapter?.ignore(
+ tag,
+ 1000,
+ { NativeLibrary.onRemoveNfcTag() },
+ Handler(Looper.getMainLooper())
+ )
+ }
+
+ private fun ntag215ReadAll(amiibo: NfcA): ByteArray? {
+ val bufferSize = amiibo.maxTransceiveLength;
+ val tagSize = 0x21C
+ val pageSize = 4
+ val lastPage = tagSize / pageSize - 1
+ val tagData = ByteArray(tagSize)
+
+ // We need to read the ntag in steps otherwise we overflow the buffer
+ for (i in 0..tagSize step bufferSize - 1) {
+ val dataStart = i / pageSize
+ var dataEnd = (i + bufferSize) / pageSize
+
+ if (dataEnd > lastPage) {
+ dataEnd = lastPage
+ }
+
+ try {
+ val data = ntag215FastRead(amiibo, dataStart, dataEnd - 1)
+ System.arraycopy(data, 0, tagData, i, (dataEnd - dataStart) * pageSize)
+ } catch (e: IOException) {
+ return null;
+ }
+ }
+ return tagData
+ }
+
+ private fun ntag215Read(amiibo: NfcA, page: Int): ByteArray? {
+ return amiibo.transceive(
+ byteArrayOf(
+ 0x30.toByte(),
+ (page and 0xFF).toByte()
+ )
+ )
+ }
+
+ private fun ntag215FastRead(amiibo: NfcA, start: Int, end: Int): ByteArray? {
+ return amiibo.transceive(
+ byteArrayOf(
+ 0x3A.toByte(),
+ (start and 0xFF).toByte(),
+ (end and 0xFF).toByte()
+ )
+ )
+ }
+
+ private fun ntag215PWrite(
+ amiibo: NfcA,
+ page: Int,
+ data1: Int,
+ data2: Int,
+ data3: Int,
+ data4: Int
+ ): ByteArray? {
+ return amiibo.transceive(
+ byteArrayOf(
+ 0xA2.toByte(),
+ (page and 0xFF).toByte(),
+ (data1 and 0xFF).toByte(),
+ (data2 and 0xFF).toByte(),
+ (data3 and 0xFF).toByte(),
+ (data4 and 0xFF).toByte()
+ )
+ )
+ }
+
+ private fun ntag215PwdAuth(
+ amiibo: NfcA,
+ data1: Int,
+ data2: Int,
+ data3: Int,
+ data4: Int
+ ): ByteArray? {
+ return amiibo.transceive(
+ byteArrayOf(
+ 0x1B.toByte(),
+ (data1 and 0xFF).toByte(),
+ (data2 and 0xFF).toByte(),
+ (data3 and 0xFF).toByte(),
+ (data4 and 0xFF).toByte()
+ )
+ )
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import java.io.Serializable
+
+object SerializableHelper {
+ inline fun <reified T : Serializable> Bundle.serializable(key: String): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ getSerializable(key, T::class.java)
+ else
+ getSerializable(key) as? T
+ }
+
+ inline fun <reified T : Serializable> Intent.serializable(key: String): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ getSerializableExtra(key, T::class.java)
+ else
+ getSerializableExtra(key) as? T
+ }
+
+ inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ getParcelable(key, T::class.java)
+ else
+ getParcelable(key) as? T
+ }
+
+ inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ getParcelableExtra(key, T::class.java)
+ else
+ getParcelableExtra(key) as? T
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.app.Activity
+import android.content.res.Configuration
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.content.ContextCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.ui.main.ThemeProvider
+import kotlin.math.roundToInt
+
+object ThemeHelper {
+ const val SYSTEM_BAR_ALPHA = 0.9f
+
+ private const val DEFAULT = 0
+ private const val MATERIAL_YOU = 1
+
+ fun setTheme(activity: AppCompatActivity) {
+ val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ setThemeMode(activity)
+ when (preferences.getInt(Settings.PREF_THEME, 0)) {
+ DEFAULT -> activity.setTheme(R.style.Theme_Yuzu_Main)
+ MATERIAL_YOU -> activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
+ }
+
+ // Using a specific night mode check because this could apply incorrectly when using the
+ // light app mode, dark system mode, and black backgrounds. Launching the settings activity
+ // will then show light mode colors/navigation bars but with black backgrounds.
+ if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+ && isNightMode(activity)
+ ) {
+ activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark)
+ }
+ }
+
+ @ColorInt
+ fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
+ return Color.argb(
+ (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color),
+ Color.green(color), Color.blue(color)
+ )
+ }
+
+ fun setCorrectTheme(activity: AppCompatActivity) {
+ val currentTheme = (activity as ThemeProvider).themeId
+ setTheme(activity)
+ if (currentTheme != (activity as ThemeProvider).themeId) {
+ activity.recreate()
+ }
+ }
+
+ fun setThemeMode(activity: AppCompatActivity) {
+ val themeMode = PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
+ .getInt(Settings.PREF_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ activity.delegate.localNightMode = themeMode
+ val windowController = WindowCompat.getInsetsController(
+ activity.window,
+ activity.window.decorView
+ )
+ when (themeMode) {
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> when (isNightMode(activity)) {
+ false -> setLightModeSystemBars(windowController)
+ true -> setDarkModeSystemBars(windowController)
+ }
+ AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
+ AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
+ }
+ }
+
+ private fun isNightMode(activity: AppCompatActivity): Boolean {
+ return when (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+ Configuration.UI_MODE_NIGHT_NO -> false
+ Configuration.UI_MODE_NIGHT_YES -> true
+ else -> false
+ }
+ }
+
+ private fun setLightModeSystemBars(windowController: WindowInsetsControllerCompat) {
+ windowController.isAppearanceLightStatusBars = true
+ windowController.isAppearanceLightNavigationBars = true
+ }
+
+ private fun setDarkModeSystemBars(windowController: WindowInsetsControllerCompat) {
+ windowController.isAppearanceLightStatusBars = false
+ windowController.isAppearanceLightNavigationBars = false
+ }
+}
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..d89a89983
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Rational
+import android.view.SurfaceView
+import kotlin.math.roundToInt
+
+class FixedRatioSurfaceView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : SurfaceView(context, attrs, defStyleAttr) {
+ private var aspectRatio: Float = 0f // (width / height), 0f is a special value for stretch
+
+ /**
+ * Sets the desired aspect ratio for this view
+ * @param ratio the ratio to force the view to, or null to stretch to fit
+ */
+ fun setAspectRatio(ratio: Rational?) {
+ aspectRatio = ratio?.toFloat() ?: 0f
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = MeasureSpec.getSize(widthMeasureSpec)
+ val height = MeasureSpec.getSize(heightMeasureSpec)
+ if (aspectRatio != 0f) {
+ val newWidth: Int
+ val newHeight: Int
+ if (height * aspectRatio < width) {
+ newWidth = (height * aspectRatio).roundToInt()
+ newHeight = height
+ } else {
+ newWidth = width
+ newHeight = (width / aspectRatio).roundToInt()
+ }
+ val left = (width - newWidth) / 2;
+ val top = (height - newHeight) / 2;
+ setLeftTopRightBottom(left, top, left + newWidth, top + newHeight)
+ } else {
+ setLeftTopRightBottom(0, 0, width, height)
+ }
+ }
+}
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 @@
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+add_library(yuzu-android SHARED
+ android_common/android_common.cpp
+ android_common/android_common.h
+ applets/software_keyboard.cpp
+ applets/software_keyboard.h
+ config.cpp
+ config.h
+ default_ini.h
+ emu_window/emu_window.cpp
+ emu_window/emu_window.h
+ id_cache.cpp
+ id_cache.h
+ native.cpp
+ native.h
+)
+
+set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
+
+target_link_libraries(yuzu-android PRIVATE audio_core common core input_common)
+target_link_libraries(yuzu-android PRIVATE android camera2ndk EGL glad inih jnigraphics log)
+if (ARCHITECTURE_arm64)
+ target_link_libraries(yuzu-android PRIVATE adrenotools)
+endif()
+
+set(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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "jni/android_common/android_common.h"
+
+#include <string>
+#include <string_view>
+
+#include <jni.h>
+
+#include "common/string_util.h"
+
+std::string GetJString(JNIEnv* env, jstring jstr) {
+ if (!jstr) {
+ return {};
+ }
+
+ const jchar* jchars = env->GetStringChars(jstr, nullptr);
+ const jsize length = env->GetStringLength(jstr);
+ const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
+ const std::string converted_string = Common::UTF16ToUTF8(string_view);
+ env->ReleaseStringChars(jstr, jchars);
+
+ return converted_string;
+}
+
+jstring ToJString(JNIEnv* env, std::string_view str) {
+ const std::u16string converted_string = Common::UTF8ToUTF16(str);
+ return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
+ static_cast<jint>(converted_string.size()));
+}
+
+jstring ToJString(JNIEnv* env, std::u16string_view str) {
+ return ToJString(env, Common::UTF16ToUTF8(str));
+}
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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+
+#include <jni.h>
+
+std::string GetJString(JNIEnv* env, jstring jstr);
+jstring ToJString(JNIEnv* env, std::string_view str);
+jstring 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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <map>
+#include <thread>
+
+#include <jni.h>
+
+#include "common/logging/log.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "jni/android_common/android_common.h"
+#include "jni/applets/software_keyboard.h"
+#include "jni/id_cache.h"
+
+static jclass s_software_keyboard_class;
+static jclass s_keyboard_config_class;
+static jclass s_keyboard_data_class;
+static jmethodID s_swkbd_execute_normal;
+static jmethodID s_swkbd_execute_inline;
+
+namespace SoftwareKeyboard {
+
+static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
+ JNIEnv* env = IDCache::GetEnvForThread();
+ jobject object = env->AllocObject(s_keyboard_config_class);
+
+ env->SetObjectField(object,
+ env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"),
+ ToJString(env, config.ok_text));
+ env->SetObjectField(
+ object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"),
+ ToJString(env, config.header_text));
+ env->SetObjectField(object,
+ env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"),
+ ToJString(env, config.sub_text));
+ env->SetObjectField(
+ object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"),
+ ToJString(env, config.guide_text));
+ env->SetObjectField(
+ object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"),
+ ToJString(env, config.initial_text));
+ env->SetShortField(object,
+ env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"),
+ static_cast<jshort>(config.left_optional_symbol_key));
+ env->SetShortField(object,
+ env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"),
+ static_cast<jshort>(config.right_optional_symbol_key));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
+ static_cast<jint>(config.max_text_length));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"),
+ static_cast<jint>(config.min_text_length));
+ env->SetIntField(object,
+ env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"),
+ static_cast<jint>(config.initial_cursor_position));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"),
+ static_cast<jint>(config.type));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"),
+ static_cast<jint>(config.password_mode));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"),
+ static_cast<jint>(config.text_draw_type));
+ env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"),
+ static_cast<jint>(config.key_disable_flags.raw));
+ env->SetBooleanField(object,
+ env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"),
+ static_cast<jboolean>(config.use_blur_background));
+ env->SetBooleanField(object,
+ env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"),
+ static_cast<jboolean>(config.enable_backspace_button));
+ env->SetBooleanField(object,
+ env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"),
+ static_cast<jboolean>(config.enable_return_button));
+ env->SetBooleanField(object,
+ env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"),
+ static_cast<jboolean>(config.disable_cancel_button));
+
+ return object;
+}
+
+AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) {
+ JNIEnv* env = IDCache::GetEnvForThread();
+ const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
+ object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
+ return ResultData{GetJString(env, string),
+ static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
+ object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
+}
+
+AndroidKeyboard::~AndroidKeyboard() = default;
+
+void AndroidKeyboard::InitializeKeyboard(
+ bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters,
+ SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) {
+ if (is_inline) {
+ LOG_WARNING(
+ Frontend,
+ "(STUBBED) called, backend requested to initialize the inline software keyboard.");
+
+ submit_inline_callback = std::move(submit_inline_callback_);
+ } else {
+ LOG_WARNING(
+ Frontend,
+ "(STUBBED) called, backend requested to initialize the normal software keyboard.");
+
+ submit_normal_callback = std::move(submit_normal_callback_);
+ }
+
+ parameters = std::move(initialize_parameters);
+
+ LOG_INFO(Frontend,
+ "\nKeyboardInitializeParameters:"
+ "\nok_text={}"
+ "\nheader_text={}"
+ "\nsub_text={}"
+ "\nguide_text={}"
+ "\ninitial_text={}"
+ "\nmax_text_length={}"
+ "\nmin_text_length={}"
+ "\ninitial_cursor_position={}"
+ "\ntype={}"
+ "\npassword_mode={}"
+ "\ntext_draw_type={}"
+ "\nkey_disable_flags={}"
+ "\nuse_blur_background={}"
+ "\nenable_backspace_button={}"
+ "\nenable_return_button={}"
+ "\ndisable_cancel_button={}",
+ Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text),
+ Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text),
+ Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length,
+ parameters.min_text_length, parameters.initial_cursor_position, parameters.type,
+ parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw,
+ parameters.use_blur_background, parameters.enable_backspace_button,
+ parameters.enable_return_button, parameters.disable_cancel_button);
+}
+
+void AndroidKeyboard::ShowNormalKeyboard() const {
+ LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard.");
+
+ ResultData data{};
+
+ // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
+ std::thread([&] {
+ data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod(
+ s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters)));
+ }).join();
+
+ SubmitNormalText(data);
+}
+
+void AndroidKeyboard::ShowTextCheckDialog(
+ Service::AM::Applets::SwkbdTextCheckResult text_check_result,
+ std::u16string text_check_message) const {
+ LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
+}
+
+void AndroidKeyboard::ShowInlineKeyboard(
+ Core::Frontend::InlineAppearParameters appear_parameters) const {
+ LOG_WARNING(Frontend,
+ "(STUBBED) called, backend requested to show the inline software keyboard.");
+
+ LOG_INFO(Frontend,
+ "\nInlineAppearParameters:"
+ "\nmax_text_length={}"
+ "\nmin_text_length={}"
+ "\nkey_top_scale_x={}"
+ "\nkey_top_scale_y={}"
+ "\nkey_top_translate_x={}"
+ "\nkey_top_translate_y={}"
+ "\ntype={}"
+ "\nkey_disable_flags={}"
+ "\nkey_top_as_floating={}"
+ "\nenable_backspace_button={}"
+ "\nenable_return_button={}"
+ "\ndisable_cancel_button={}",
+ appear_parameters.max_text_length, appear_parameters.min_text_length,
+ appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y,
+ appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y,
+ appear_parameters.type, appear_parameters.key_disable_flags.raw,
+ appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button,
+ appear_parameters.enable_return_button, appear_parameters.disable_cancel_button);
+
+ // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
+ m_is_inline_active = true;
+ std::thread([&] {
+ IDCache::GetEnvForThread()->CallStaticVoidMethod(
+ s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters));
+ }).join();
+}
+
+void AndroidKeyboard::HideInlineKeyboard() const {
+ LOG_WARNING(Frontend,
+ "(STUBBED) called, backend requested to hide the inline software keyboard.");
+}
+
+void AndroidKeyboard::InlineTextChanged(
+ Core::Frontend::InlineTextParameters text_parameters) const {
+ LOG_WARNING(Frontend,
+ "(STUBBED) called, backend requested to change the inline keyboard text.");
+
+ LOG_INFO(Frontend,
+ "\nInlineTextParameters:"
+ "\ninput_text={}"
+ "\ncursor_position={}",
+ Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
+
+ submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
+ text_parameters.input_text, text_parameters.cursor_position);
+}
+
+void AndroidKeyboard::ExitKeyboard() const {
+ LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard.");
+}
+
+void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
+ if (!m_is_inline_active) {
+ return;
+ }
+
+ m_current_text += submitted_text;
+
+ submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
+ m_current_text.size());
+}
+
+void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
+ static constexpr int KEYCODE_BACK = 4;
+ static constexpr int KEYCODE_ENTER = 66;
+ static constexpr int KEYCODE_DEL = 67;
+
+ if (!m_is_inline_active) {
+ return;
+ }
+
+ switch (key_code) {
+ case KEYCODE_BACK:
+ case KEYCODE_ENTER:
+ m_is_inline_active = false;
+ submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
+ static_cast<s32>(m_current_text.size()));
+ break;
+ case KEYCODE_DEL:
+ m_current_text.pop_back();
+ submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
+ m_current_text.size());
+ break;
+ }
+}
+
+void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
+ submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true);
+}
+
+void InitJNI(JNIEnv* env) {
+ s_software_keyboard_class = reinterpret_cast<jclass>(
+ env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard")));
+ s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
+ env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig")));
+ s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
+ env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardData")));
+
+ s_swkbd_execute_normal = env->GetStaticMethodID(
+ s_software_keyboard_class, "executeNormal",
+ "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
+ "applets/keyboard/SoftwareKeyboard$KeyboardData;");
+ s_swkbd_execute_inline = env->GetStaticMethodID(
+ s_software_keyboard_class, "executeInline",
+ "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)V");
+}
+
+void CleanupJNI(JNIEnv* env) {
+ env->DeleteGlobalRef(s_software_keyboard_class);
+ env->DeleteGlobalRef(s_keyboard_config_class);
+ env->DeleteGlobalRef(s_keyboard_data_class);
+}
+
+} // 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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <jni.h>
+
+#include "core/frontend/applets/software_keyboard.h"
+
+namespace SoftwareKeyboard {
+
+class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet {
+public:
+ ~AndroidKeyboard() override;
+
+ void Close() const override {
+ ExitKeyboard();
+ }
+
+ void InitializeKeyboard(bool is_inline,
+ Core::Frontend::KeyboardInitializeParameters initialize_parameters,
+ SubmitNormalCallback submit_normal_callback_,
+ SubmitInlineCallback submit_inline_callback_) override;
+
+ void ShowNormalKeyboard() const override;
+
+ void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
+ std::u16string text_check_message) const override;
+
+ void ShowInlineKeyboard(
+ Core::Frontend::InlineAppearParameters appear_parameters) const override;
+
+ void HideInlineKeyboard() const override;
+
+ void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
+
+ void ExitKeyboard() const override;
+
+ void SubmitInlineKeyboardText(std::u16string submitted_text);
+
+ void SubmitInlineKeyboardInput(int key_code);
+
+private:
+ struct ResultData {
+ static ResultData CreateFromFrontend(jobject object);
+
+ std::string text;
+ Service::AM::Applets::SwkbdResult result{};
+ };
+
+ void SubmitNormalText(const ResultData& result) const;
+
+ Core::Frontend::KeyboardInitializeParameters parameters{};
+
+ mutable SubmitNormalCallback submit_normal_callback;
+ mutable SubmitInlineCallback submit_inline_callback;
+
+private:
+ mutable bool m_is_inline_active{};
+ std::u16string m_current_text;
+};
+
+// Should be called in JNI_Load
+void InitJNI(JNIEnv* env);
+
+// Should be called in JNI_Unload
+void CleanupJNI(JNIEnv* env);
+
+} // namespace SoftwareKeyboard
+
+// Native function calls
+extern "C" {
+JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(
+ JNIEnv* env, jclass clazz, jstring text);
+
+JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(
+ JNIEnv* env, jclass clazz, jstring text);
+}
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..43e8aa72a
--- /dev/null
+++ b/src/android/app/src/main/jni/config.cpp
@@ -0,0 +1,301 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <memory>
+#include <optional>
+#include <sstream>
+
+#include <INIReader.h>
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "core/hle/service/acc/profile_manager.h"
+#include "input_common/main.h"
+#include "jni/config.h"
+#include "jni/default_ini.h"
+
+namespace FS = Common::FS;
+
+Config::Config(std::optional<std::filesystem::path> config_path)
+ : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")},
+ config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
+ Reload();
+}
+
+Config::~Config() = default;
+
+bool Config::LoadINI(const std::string& default_contents, bool retry) {
+ const auto config_loc_str = FS::PathToUTF8String(config_loc);
+ if (config->ParseError() < 0) {
+ if (retry) {
+ LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...",
+ config_loc_str);
+
+ void(FS::CreateParentDir(config_loc));
+ void(FS::WriteStringToFile(config_loc, FS::FileType::TextFile, default_contents));
+
+ config = std::make_unique<INIReader>(config_loc_str);
+
+ return LoadINI(default_contents, false);
+ }
+ LOG_ERROR(Config, "Failed.");
+ return false;
+ }
+ LOG_INFO(Config, "Successfully loaded {}", config_loc_str);
+ return true;
+}
+
+template <>
+void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
+ std::string setting_value = config->Get(group, setting.GetLabel(), setting.GetDefault());
+ if (setting_value.empty()) {
+ setting_value = setting.GetDefault();
+ }
+ setting = std::move(setting_value);
+}
+
+template <>
+void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) {
+ setting = config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
+}
+
+template <typename Type, bool ranged>
+void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
+ setting = static_cast<Type>(
+ config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
+}
+
+void Config::ReadValues() {
+ ReadSetting("ControlsGeneral", Settings::values.mouse_enabled);
+ ReadSetting("ControlsGeneral", Settings::values.touch_device);
+ ReadSetting("ControlsGeneral", Settings::values.keyboard_enabled);
+ ReadSetting("ControlsGeneral", Settings::values.debug_pad_enabled);
+ ReadSetting("ControlsGeneral", Settings::values.vibration_enabled);
+ ReadSetting("ControlsGeneral", Settings::values.enable_accurate_vibrations);
+ ReadSetting("ControlsGeneral", Settings::values.motion_enabled);
+ Settings::values.touchscreen.enabled =
+ config->GetBoolean("ControlsGeneral", "touch_enabled", true);
+ Settings::values.touchscreen.rotation_angle =
+ config->GetInteger("ControlsGeneral", "touch_angle", 0);
+ Settings::values.touchscreen.diameter_x =
+ config->GetInteger("ControlsGeneral", "touch_diameter_x", 15);
+ Settings::values.touchscreen.diameter_y =
+ config->GetInteger("ControlsGeneral", "touch_diameter_y", 15);
+
+ int num_touch_from_button_maps =
+ config->GetInteger("ControlsGeneral", "touch_from_button_map", 0);
+ if (num_touch_from_button_maps > 0) {
+ for (int i = 0; i < num_touch_from_button_maps; ++i) {
+ Settings::TouchFromButtonMap map;
+ map.name = config->Get("ControlsGeneral",
+ std::string("touch_from_button_maps_") + std::to_string(i) +
+ std::string("_name"),
+ "default");
+ const int num_touch_maps = config->GetInteger(
+ "ControlsGeneral",
+ std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
+ 0);
+ map.buttons.reserve(num_touch_maps);
+
+ for (int j = 0; j < num_touch_maps; ++j) {
+ std::string touch_mapping =
+ config->Get("ControlsGeneral",
+ std::string("touch_from_button_maps_") + std::to_string(i) +
+ std::string("_bind_") + std::to_string(j),
+ "");
+ map.buttons.emplace_back(std::move(touch_mapping));
+ }
+
+ Settings::values.touch_from_button_maps.emplace_back(std::move(map));
+ }
+ } else {
+ Settings::values.touch_from_button_maps.emplace_back(
+ Settings::TouchFromButtonMap{"default", {}});
+ num_touch_from_button_maps = 1;
+ }
+ Settings::values.touch_from_button_map_index = std::clamp(
+ Settings::values.touch_from_button_map_index.GetValue(), 0, num_touch_from_button_maps - 1);
+
+ ReadSetting("ControlsGeneral", Settings::values.udp_input_servers);
+
+ // Data Storage
+ ReadSetting("Data Storage", Settings::values.use_virtual_sd);
+ FS::SetYuzuPath(FS::YuzuPath::NANDDir,
+ config->Get("Data Storage", "nand_directory",
+ FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
+ FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
+ config->Get("Data Storage", "sdmc_directory",
+ FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
+ FS::SetYuzuPath(FS::YuzuPath::LoadDir,
+ config->Get("Data Storage", "load_directory",
+ FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
+ FS::SetYuzuPath(FS::YuzuPath::DumpDir,
+ config->Get("Data Storage", "dump_directory",
+ FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
+ ReadSetting("Data Storage", Settings::values.gamecard_inserted);
+ ReadSetting("Data Storage", Settings::values.gamecard_current_game);
+ ReadSetting("Data Storage", Settings::values.gamecard_path);
+
+ // System
+ ReadSetting("System", Settings::values.current_user);
+ Settings::values.current_user = std::clamp<int>(Settings::values.current_user.GetValue(), 0,
+ Service::Account::MAX_USERS - 1);
+
+ // Disable docked mode by default on Android
+ Settings::values.use_docked_mode = config->GetBoolean("System", "use_docked_mode", false);
+
+ const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false);
+ if (rng_seed_enabled) {
+ Settings::values.rng_seed.SetValue(config->GetInteger("System", "rng_seed", 0));
+ } else {
+ Settings::values.rng_seed.SetValue(std::nullopt);
+ }
+
+ const auto custom_rtc_enabled = config->GetBoolean("System", "custom_rtc_enabled", false);
+ if (custom_rtc_enabled) {
+ Settings::values.custom_rtc = config->GetInteger("System", "custom_rtc", 0);
+ } else {
+ Settings::values.custom_rtc = std::nullopt;
+ }
+
+ ReadSetting("System", Settings::values.language_index);
+ ReadSetting("System", Settings::values.region_index);
+ ReadSetting("System", Settings::values.time_zone_index);
+ ReadSetting("System", Settings::values.sound_index);
+
+ // Core
+ ReadSetting("Core", Settings::values.use_multi_core);
+ ReadSetting("Core", Settings::values.use_unsafe_extended_memory_layout);
+
+ // Cpu
+ ReadSetting("Cpu", Settings::values.cpu_accuracy);
+ ReadSetting("Cpu", Settings::values.cpu_debug_mode);
+ ReadSetting("Cpu", Settings::values.cpuopt_page_tables);
+ ReadSetting("Cpu", Settings::values.cpuopt_block_linking);
+ ReadSetting("Cpu", Settings::values.cpuopt_return_stack_buffer);
+ ReadSetting("Cpu", Settings::values.cpuopt_fast_dispatcher);
+ ReadSetting("Cpu", Settings::values.cpuopt_context_elimination);
+ ReadSetting("Cpu", Settings::values.cpuopt_const_prop);
+ ReadSetting("Cpu", Settings::values.cpuopt_misc_ir);
+ ReadSetting("Cpu", Settings::values.cpuopt_reduce_misalign_checks);
+ ReadSetting("Cpu", Settings::values.cpuopt_fastmem);
+ ReadSetting("Cpu", Settings::values.cpuopt_fastmem_exclusives);
+ ReadSetting("Cpu", Settings::values.cpuopt_recompile_exclusives);
+ ReadSetting("Cpu", Settings::values.cpuopt_ignore_memory_aborts);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_unfuse_fma);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_reduce_fp_error);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_standard_fpcr);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_inaccurate_nan);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_fastmem_check);
+ ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_global_monitor);
+
+ // Renderer
+ ReadSetting("Renderer", Settings::values.renderer_backend);
+ ReadSetting("Renderer", Settings::values.renderer_debug);
+ ReadSetting("Renderer", Settings::values.renderer_shader_feedback);
+ ReadSetting("Renderer", Settings::values.enable_nsight_aftermath);
+ ReadSetting("Renderer", Settings::values.disable_shader_loop_safety_checks);
+ ReadSetting("Renderer", Settings::values.vulkan_device);
+
+ ReadSetting("Renderer", Settings::values.resolution_setup);
+ ReadSetting("Renderer", Settings::values.scaling_filter);
+ ReadSetting("Renderer", Settings::values.fsr_sharpening_slider);
+ ReadSetting("Renderer", Settings::values.anti_aliasing);
+ ReadSetting("Renderer", Settings::values.fullscreen_mode);
+ ReadSetting("Renderer", Settings::values.aspect_ratio);
+ ReadSetting("Renderer", Settings::values.max_anisotropy);
+ ReadSetting("Renderer", Settings::values.use_speed_limit);
+ ReadSetting("Renderer", Settings::values.speed_limit);
+ ReadSetting("Renderer", Settings::values.use_disk_shader_cache);
+ ReadSetting("Renderer", Settings::values.use_asynchronous_gpu_emulation);
+ ReadSetting("Renderer", Settings::values.vsync_mode);
+ ReadSetting("Renderer", Settings::values.shader_backend);
+ ReadSetting("Renderer", Settings::values.use_asynchronous_shaders);
+ ReadSetting("Renderer", Settings::values.nvdec_emulation);
+ ReadSetting("Renderer", Settings::values.use_fast_gpu_time);
+ ReadSetting("Renderer", Settings::values.use_vulkan_driver_pipeline_cache);
+
+ ReadSetting("Renderer", Settings::values.bg_red);
+ ReadSetting("Renderer", Settings::values.bg_green);
+ ReadSetting("Renderer", Settings::values.bg_blue);
+
+ // Use GPU accuracy normal by default on Android
+ Settings::values.gpu_accuracy = static_cast<Settings::GPUAccuracy>(config->GetInteger(
+ "Renderer", "gpu_accuracy", static_cast<u32>(Settings::GPUAccuracy::Normal)));
+
+ // Use GPU default anisotropic filtering on Android
+ Settings::values.max_anisotropy = config->GetInteger("Renderer", "max_anisotropy", 1);
+
+ // Disable ASTC compute by default on Android
+ Settings::values.accelerate_astc = config->GetBoolean("Renderer", "accelerate_astc", false);
+
+ // Enable asynchronous presentation by default on Android
+ Settings::values.async_presentation =
+ config->GetBoolean("Renderer", "async_presentation", true);
+
+ // Disable force_max_clock by default on Android
+ Settings::values.renderer_force_max_clock =
+ config->GetBoolean("Renderer", "force_max_clock", false);
+
+ // Disable use_reactive_flushing by default on Android
+ Settings::values.use_reactive_flushing =
+ config->GetBoolean("Renderer", "use_reactive_flushing", false);
+
+ // Audio
+ ReadSetting("Audio", Settings::values.sink_id);
+ ReadSetting("Audio", Settings::values.audio_output_device_id);
+ ReadSetting("Audio", Settings::values.volume);
+
+ // Miscellaneous
+ // log_filter has a different default here than from common
+ Settings::values.log_filter = "*:Info";
+ ReadSetting("Miscellaneous", Settings::values.use_dev_keys);
+
+ // Debugging
+ Settings::values.record_frame_times =
+ config->GetBoolean("Debugging", "record_frame_times", false);
+ ReadSetting("Debugging", Settings::values.dump_exefs);
+ ReadSetting("Debugging", Settings::values.dump_nso);
+ ReadSetting("Debugging", Settings::values.enable_fs_access_log);
+ ReadSetting("Debugging", Settings::values.reporting_services);
+ ReadSetting("Debugging", Settings::values.quest_flag);
+ ReadSetting("Debugging", Settings::values.use_debug_asserts);
+ ReadSetting("Debugging", Settings::values.use_auto_stub);
+ ReadSetting("Debugging", Settings::values.disable_macro_jit);
+ ReadSetting("Debugging", Settings::values.disable_macro_hle);
+ ReadSetting("Debugging", Settings::values.use_gdbstub);
+ ReadSetting("Debugging", Settings::values.gdbstub_port);
+
+ const auto title_list = config->Get("AddOns", "title_ids", "");
+ std::stringstream ss(title_list);
+ std::string line;
+ while (std::getline(ss, line, '|')) {
+ const auto title_id = std::stoul(line, nullptr, 16);
+ const auto disabled_list = config->Get("AddOns", "disabled_" + line, "");
+
+ std::stringstream inner_ss(disabled_list);
+ std::string inner_line;
+ std::vector<std::string> out;
+ while (std::getline(inner_ss, inner_line, '|')) {
+ out.push_back(inner_line);
+ }
+
+ Settings::values.disabled_addons.insert_or_assign(title_id, out);
+ }
+
+ // Web Service
+ ReadSetting("WebService", Settings::values.enable_telemetry);
+ ReadSetting("WebService", Settings::values.web_api_url);
+ ReadSetting("WebService", Settings::values.yuzu_username);
+ ReadSetting("WebService", Settings::values.yuzu_token);
+
+ // Network
+ ReadSetting("Network", Settings::values.network_interface);
+}
+
+void Config::Reload() {
+ LoadINI(DefaultINI::android_config_file);
+ ReadValues();
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "common/settings.h"
+
+class INIReader;
+
+class Config {
+ std::filesystem::path config_loc;
+ std::unique_ptr<INIReader> config;
+
+ bool LoadINI(const std::string& default_contents = "", bool retry = true);
+ void ReadValues();
+
+public:
+ explicit Config(std::optional<std::filesystem::path> config_path = std::nullopt);
+ ~Config();
+
+ void Reload();
+
+private:
+ /**
+ * Applies a value read from the sdl2_config to a Setting.
+ *
+ * @param group The name of the INI group
+ * @param setting The yuzu setting to modify
+ */
+ template <typename Type, bool ranged>
+ void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
+};
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..d81422a74
--- /dev/null
+++ b/src/android/app/src/main/jni/default_ini.h
@@ -0,0 +1,511 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+namespace DefaultINI {
+
+const char* android_config_file = R"(
+
+[ControlsP0]
+# The input devices and parameters for each Switch native input
+# The config section determines the player number where the config will be applied on. For example "ControlsP0", "ControlsP1", ...
+# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..."
+# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values
+
+# Indicates if this player should be connected at boot
+connected=
+
+# for button input, the following devices are available:
+# - "keyboard" (default) for keyboard input. Required parameters:
+# - "code": the code of the key to bind
+# - "sdl" for joystick input using SDL. Required parameters:
+# - "guid": SDL identification GUID of the joystick
+# - "port": the index of the joystick to bind
+# - "button"(optional): the index of the button to bind
+# - "hat"(optional): the index of the hat to bind as direction buttons
+# - "axis"(optional): the index of the axis to bind
+# - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right"
+# - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is
+# triggered if the axis value crosses
+# - "direction"(only used for axis): "+" means the button is triggered when the axis value
+# is greater than the threshold; "-" means the button is triggered when the axis value
+# is smaller than the threshold
+button_a=
+button_b=
+button_x=
+button_y=
+button_lstick=
+button_rstick=
+button_l=
+button_r=
+button_zl=
+button_zr=
+button_plus=
+button_minus=
+button_dleft=
+button_dup=
+button_dright=
+button_ddown=
+button_lstick_left=
+button_lstick_up=
+button_lstick_right=
+button_lstick_down=
+button_sl=
+button_sr=
+button_home=
+button_screenshot=
+
+# for analog input, the following devices are available:
+# - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters:
+# - "up", "down", "left", "right": sub-devices for each direction.
+# Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00"
+# - "modifier": sub-devices as a modifier.
+# - "modifier_scale": a float number representing the applied modifier scale to the analog input.
+# Must be in range of 0.0-1.0. Defaults to 0.5
+# - "sdl" for joystick input using SDL. Required parameters:
+# - "guid": SDL identification GUID of the joystick
+# - "port": the index of the joystick to bind
+# - "axis_x": the index of the axis to bind as x-axis (default to 0)
+# - "axis_y": the index of the axis to bind as y-axis (default to 1)
+lstick=
+rstick=
+
+# for motion input, the following devices are available:
+# - "keyboard" (default) for emulating random motion input from buttons. Required parameters:
+# - "code": the code of the key to bind
+# - "sdl" for motion input using SDL. Required parameters:
+# - "guid": SDL identification GUID of the joystick
+# - "port": the index of the joystick to bind
+# - "motion": the index of the motion sensor to bind
+# - "cemuhookudp" for motion input using Cemu Hook protocol. Required parameters:
+# - "guid": the IP address of the cemu hook server encoded to a hex string. for example 192.168.0.1 = "c0a80001"
+# - "port": the port of the cemu hook server
+# - "pad": the index of the joystick
+# - "motion": the index of the motion sensor of the joystick to bind
+motionleft=
+motionright=
+
+[ControlsGeneral]
+# To use the debug_pad, prepend `debug_pad_` before each button setting above.
+# i.e. debug_pad_button_a=
+
+# Enable debug pad inputs to the guest
+# 0 (default): Disabled, 1: Enabled
+debug_pad_enabled =
+
+# Whether to enable or disable vibration
+# 0: Disabled, 1 (default): Enabled
+vibration_enabled=
+
+# Whether to enable or disable accurate vibrations
+# 0 (default): Disabled, 1: Enabled
+enable_accurate_vibrations=
+
+# Enables controller motion inputs
+# 0: Disabled, 1 (default): Enabled
+motion_enabled =
+
+# Defines the udp device's touch screen coordinate system for cemuhookudp devices
+# - "min_x", "min_y", "max_x", "max_y"
+touch_device=
+
+# for mapping buttons to touch inputs.
+#touch_from_button_map=1
+#touch_from_button_maps_0_name=default
+#touch_from_button_maps_0_count=2
+#touch_from_button_maps_0_bind_0=foo
+#touch_from_button_maps_0_bind_1=bar
+# etc.
+
+# List of Cemuhook UDP servers, delimited by ','.
+# Default: 127.0.0.1:26760
+# Example: 127.0.0.1:26760,123.4.5.67:26761
+udp_input_servers =
+
+# Enable controlling an axis via a mouse input.
+# 0 (default): Off, 1: On
+mouse_panning =
+
+# Set mouse sensitivity.
+# Default: 1.0
+mouse_panning_sensitivity =
+
+# Emulate an analog control stick from keyboard inputs.
+# 0 (default): Disabled, 1: Enabled
+emulate_analog_keyboard =
+
+# Enable mouse inputs to the guest
+# 0 (default): Disabled, 1: Enabled
+mouse_enabled =
+
+# Enable keyboard inputs to the guest
+# 0 (default): Disabled, 1: Enabled
+keyboard_enabled =
+
+[Core]
+# Whether to use multi-core for CPU emulation
+# 0: Disabled, 1 (default): Enabled
+use_multi_core =
+
+# Enable unsafe extended guest system memory layout (8GB DRAM)
+# 0 (default): Disabled, 1: Enabled
+use_unsafe_extended_memory_layout =
+
+[Cpu]
+# Adjusts various optimizations.
+# Auto-select mode enables choice unsafe optimizations.
+# Accurate enables only safe optimizations.
+# Unsafe allows any unsafe optimizations.
+# 0 (default): Auto-select, 1: Accurate, 2: Enable unsafe optimizations
+cpu_accuracy =
+
+# Allow disabling safe optimizations.
+# 0 (default): Disabled, 1: Enabled
+cpu_debug_mode =
+
+# Enable inline page tables optimization (faster guest memory access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_page_tables =
+
+# Enable block linking CPU optimization (reduce block dispatcher use during predictable jumps)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_block_linking =
+
+# Enable return stack buffer CPU optimization (reduce block dispatcher use during predictable returns)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_return_stack_buffer =
+
+# Enable fast dispatcher CPU optimization (use a two-tiered dispatcher architecture)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_fast_dispatcher =
+
+# Enable context elimination CPU Optimization (reduce host memory use for guest context)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_context_elimination =
+
+# Enable constant propagation CPU optimization (basic IR optimization)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_const_prop =
+
+# Enable miscellaneous CPU optimizations (basic IR optimization)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_misc_ir =
+
+# Enable reduction of memory misalignment checks (reduce memory fallbacks for misaligned access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_reduce_misalign_checks =
+
+# Enable Host MMU Emulation (faster guest memory access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_fastmem =
+
+# Enable Host MMU Emulation for exclusive memory instructions (faster guest memory access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_fastmem_exclusives =
+
+# Enable fallback on failure of fastmem of exclusive memory instructions (faster guest memory access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_recompile_exclusives =
+
+# Enable optimization to ignore invalid memory accesses (faster guest memory access)
+# 0: Disabled, 1 (default): Enabled
+cpuopt_ignore_memory_aborts =
+
+# Enable unfuse FMA (improve performance on CPUs without FMA)
+# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_unfuse_fma =
+
+# Enable faster FRSQRTE and FRECPE
+# Only enabled if cpu_accuracy is set to Unsafe.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_reduce_fp_error =
+
+# Enable faster ASIMD instructions (32 bits only)
+# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_ignore_standard_fpcr =
+
+# Enable inaccurate NaN handling
+# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_inaccurate_nan =
+
+# Disable address space checks (64 bits only)
+# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_fastmem_check =
+
+# Enable faster exclusive instructions
+# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select.
+# 0: Disabled, 1 (default): Enabled
+cpuopt_unsafe_ignore_global_monitor =
+
+[Renderer]
+# Which backend API to use.
+# 0: OpenGL (unsupported), 1 (default): Vulkan, 2: Null
+backend =
+
+# Whether to enable asynchronous presentation (Vulkan only)
+# 0: Off, 1 (default): On
+async_presentation =
+
+# Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).
+# 0 (default): Disabled, 1: Enabled
+force_max_clock =
+
+# Enable graphics API debugging mode.
+# 0 (default): Disabled, 1: Enabled
+debug =
+
+# Enable shader feedback.
+# 0 (default): Disabled, 1: Enabled
+renderer_shader_feedback =
+
+# Enable Nsight Aftermath crash dumps
+# 0 (default): Disabled, 1: Enabled
+nsight_aftermath =
+
+# Disable shader loop safety checks, executing the shader without loop logic changes
+# 0 (default): Disabled, 1: Enabled
+disable_shader_loop_safety_checks =
+
+# Which Vulkan physical device to use (defaults to 0)
+vulkan_device =
+
+# 0: 0.5x (360p/540p) [EXPERIMENTAL]
+# 1: 0.75x (540p/810p) [EXPERIMENTAL]
+# 2 (default): 1x (720p/1080p)
+# 3: 2x (1440p/2160p)
+# 4: 3x (2160p/3240p)
+# 5: 4x (2880p/4320p)
+# 6: 5x (3600p/5400p)
+# 7: 6x (4320p/6480p)
+resolution_setup =
+
+# Pixel filter to use when up- or down-sampling rendered frames.
+# 0: Nearest Neighbor
+# 1 (default): Bilinear
+# 2: Bicubic
+# 3: Gaussian
+# 4: ScaleForce
+# 5: AMD FidelityFX™️ Super Resolution [Vulkan Only]
+scaling_filter =
+
+# Anti-Aliasing (AA)
+# 0 (default): None, 1: FXAA
+anti_aliasing =
+
+# Whether to use fullscreen or borderless window mode
+# 0 (Windows default): Borderless window, 1 (All other default): Exclusive fullscreen
+fullscreen_mode =
+
+# Aspect ratio
+# 0: Default (16:9), 1: Force 4:3, 2: Force 21:9, 3: Force 16:10, 4: Stretch to Window
+aspect_ratio =
+
+# Anisotropic filtering
+# 0: Default, 1: 2x, 2: 4x, 3: 8x, 4: 16x
+max_anisotropy =
+
+# Whether to enable VSync or not.
+# OpenGL: Values other than 0 enable VSync
+# Vulkan: FIFO is selected if the requested mode is not supported by the driver.
+# FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen refresh rate.
+# FIFO Relaxed is similar to FIFO but allows tearing as it recovers from a slow down.
+# Mailbox can have lower latency than FIFO and does not tear but may drop frames.
+# Immediate (no synchronization) just presents whatever is available and can exhibit tearing.
+# 0: Immediate (Off), 1 (Default): Mailbox (On), 2: FIFO, 3: FIFO Relaxed
+use_vsync =
+
+# Selects the OpenGL shader backend. NV_gpu_program5 is required for GLASM. If NV_gpu_program5 is
+# not available and GLASM is selected, GLSL will be used.
+# 0: GLSL, 1 (default): GLASM, 2: SPIR-V
+shader_backend =
+
+# Whether to allow asynchronous shader building.
+# 0 (default): Off, 1: On
+use_asynchronous_shaders =
+
+# Uses reactive flushing instead of predictive flushing. Allowing a more accurate syncing of memory.
+# 0 (default): Off, 1: On
+use_reactive_flushing =
+
+# NVDEC emulation.
+# 0: Disabled, 1: CPU Decoding, 2 (default): GPU Decoding
+nvdec_emulation =
+
+# Accelerate ASTC texture decoding.
+# 0 (default): Off, 1: On
+accelerate_astc =
+
+# Turns on the speed limiter, which will limit the emulation speed to the desired speed limit value
+# 0: Off, 1: On (default)
+use_speed_limit =
+
+# Limits the speed of the game to run no faster than this value as a percentage of target speed
+# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
+speed_limit =
+
+# Whether to use disk based shader cache
+# 0: Off, 1 (default): On
+use_disk_shader_cache =
+
+# Which gpu accuracy level to use
+# 0 (default): Normal, 1: High, 2: Extreme (Very slow)
+gpu_accuracy =
+
+# Whether to use asynchronous GPU emulation
+# 0 : Off (slow), 1 (default): On (fast)
+use_asynchronous_gpu_emulation =
+
+# Inform the guest that GPU operations completed more quickly than they did.
+# 0: Off, 1 (default): On
+use_fast_gpu_time =
+
+# Force unmodified buffers to be flushed, which can cost performance.
+# 0: Off (default), 1: On
+use_pessimistic_flushes =
+
+# Whether to use garbage collection or not for GPU caches.
+# 0 (default): Off, 1: On
+use_caches_gc =
+
+# The clear color for the renderer. What shows up on the sides of the bottom screen.
+# Must be in range of 0-255. Defaults to 0 for all.
+bg_red =
+bg_blue =
+bg_green =
+
+[Audio]
+# Which audio output engine to use.
+# auto (default): Auto-select
+# cubeb: Cubeb audio engine (if available)
+# sdl2: SDL2 audio engine (if available)
+# null: No audio output
+output_engine =
+
+# Which audio device to use.
+# auto (default): Auto-select
+output_device =
+
+# Output volume.
+# 100 (default): 100%, 0; mute
+volume =
+
+[Data Storage]
+# Whether to create a virtual SD card.
+# 1: Yes, 0 (default): No
+use_virtual_sd =
+
+# Whether or not to enable gamecard emulation
+# 1: Yes, 0 (default): No
+gamecard_inserted =
+
+# Whether or not the gamecard should be emulated as the current game
+# If 'gamecard_inserted' is 0 this setting is irrelevant
+# 1: Yes, 0 (default): No
+gamecard_current_game =
+
+# Path to an XCI file to use as the gamecard
+# If 'gamecard_inserted' is 0 this setting is irrelevant
+# If 'gamecard_current_game' is 1 this setting is irrelevant
+gamecard_path =
+
+[System]
+# Whether the system is docked
+# 1 (default): Yes, 0: No
+use_docked_mode =
+
+# Sets the seed for the RNG generator built into the switch
+# rng_seed will be ignored and randomly generated if rng_seed_enabled is false
+rng_seed_enabled =
+rng_seed =
+
+# Sets the current time (in seconds since 12:00 AM Jan 1, 1970) that will be used by the time service
+# This will auto-increment, with the time set being the time the game is started
+# This override will only occur if custom_rtc_enabled is true, otherwise the current time is used
+custom_rtc_enabled =
+custom_rtc =
+
+# Sets the systems language index
+# 0: Japanese, 1: English (default), 2: French, 3: German, 4: Italian, 5: Spanish, 6: Chinese,
+# 7: Korean, 8: Dutch, 9: Portuguese, 10: Russian, 11: Taiwanese, 12: British English, 13: Canadian French,
+# 14: Latin American Spanish, 15: Simplified Chinese, 16: Traditional Chinese, 17: Brazilian Portuguese
+language_index =
+
+# The system region that yuzu will use during emulation
+# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan
+region_index =
+
+# The system time zone that yuzu will use during emulation
+# 0: Auto-select (default), 1: Default (system archive value), Others: Index for specified time zone
+time_zone_index =
+
+# Sets the sound output mode.
+# 0: Mono, 1 (default): Stereo, 2: Surround
+sound_index =
+
+[Miscellaneous]
+# A filter which removes logs below a certain logging level.
+# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical
+log_filter = *:Trace
+
+# Use developer keys
+# 0 (default): Disabled, 1: Enabled
+use_dev_keys =
+
+[Debugging]
+# Record frame time data, can be found in the log directory. Boolean value
+record_frame_times =
+# Determines whether or not yuzu will dump the ExeFS of all games it attempts to load while loading them
+dump_exefs=false
+# Determines whether or not yuzu will dump all NSOs it attempts to load while loading them
+dump_nso=false
+# Determines whether or not yuzu will save the filesystem access log.
+enable_fs_access_log=false
+# Enables verbose reporting services
+reporting_services =
+# Determines whether or not yuzu will report to the game that the emulated console is in Kiosk Mode
+# false: Retail/Normal Mode (default), true: Kiosk Mode
+quest_flag =
+# Determines whether debug asserts should be enabled, which will throw an exception on asserts.
+# false: Disabled (default), true: Enabled
+use_debug_asserts =
+# Determines whether unimplemented HLE service calls should be automatically stubbed.
+# false: Disabled (default), true: Enabled
+use_auto_stub =
+# Enables/Disables the macro JIT compiler
+disable_macro_jit=false
+# Determines whether to enable the GDB stub and wait for the debugger to attach before running.
+# false: Disabled (default), true: Enabled
+use_gdbstub=false
+# The port to use for the GDB server, if it is enabled.
+gdbstub_port=6543
+
+[WebService]
+# Whether or not to enable telemetry
+# 0: No, 1 (default): Yes
+enable_telemetry =
+# URL for Web API
+web_api_url = https://api.yuzu-emu.org
+# Username and token for yuzu Web Service
+# See https://profile.yuzu-emu.org/ for more info
+yuzu_username =
+yuzu_token =
+
+[Network]
+# Name of the network interface device to use with yuzu LAN play.
+# e.g. On *nix: 'enp7s0', 'wlp6s0u1u3u3', 'lo'
+# e.g. On Windows: 'Ethernet', 'Wi-Fi'
+network_interface =
+
+[AddOns]
+# Used to disable add-ons
+# List of title IDs of games that will have add-ons disabled (separated by '|'):
+title_ids =
+# 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 '|')
+# e.x. disabled_0100000000010000 = Update|DLC <- disables Updates and DLC on Super Mario Odyssey
+)";
+} // 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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <android/native_window_jni.h>
+
+#include "common/logging/log.h"
+#include "input_common/drivers/touch_screen.h"
+#include "input_common/drivers/virtual_amiibo.h"
+#include "input_common/drivers/virtual_gamepad.h"
+#include "input_common/main.h"
+#include "jni/emu_window/emu_window.h"
+
+void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
+ window_info.render_surface = reinterpret_cast<void*>(surface);
+}
+
+void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
+ const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
+ m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
+}
+
+void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
+ const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
+ m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
+}
+
+void EmuWindow_Android::OnTouchReleased(int id) {
+ m_input_subsystem->GetTouchScreen()->TouchReleased(id);
+}
+
+void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
+ m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
+}
+
+void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
+ m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
+}
+
+void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
+ float gyro_y, float gyro_z, float accel_x,
+ float accel_y, float accel_z) {
+ m_input_subsystem->GetVirtualGamepad()->SetMotionState(
+ player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
+}
+
+void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
+ m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
+}
+
+void EmuWindow_Android::OnRemoveNfcTag() {
+ m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
+}
+
+EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem,
+ ANativeWindow* surface,
+ std::shared_ptr<Common::DynamicLibrary> driver_library)
+ : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} {
+ LOG_INFO(Frontend, "initializing");
+
+ if (!surface) {
+ LOG_CRITICAL(Frontend, "surface is nullptr");
+ return;
+ }
+
+ m_window_width = ANativeWindow_getWidth(surface);
+ m_window_height = ANativeWindow_getHeight(surface);
+
+ // Ensures that we emulate with the correct aspect ratio.
+ UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
+
+ window_info.type = Core::Frontend::WindowSystemType::Android;
+ window_info.render_surface = reinterpret_cast<void*>(surface);
+
+ m_input_subsystem->Initialize();
+}
+
+EmuWindow_Android::~EmuWindow_Android() {
+ m_input_subsystem->Shutdown();
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <span>
+
+#include "core/frontend/emu_window.h"
+#include "core/frontend/graphics_context.h"
+#include "input_common/main.h"
+
+struct ANativeWindow;
+
+class GraphicsContext_Android final : public Core::Frontend::GraphicsContext {
+public:
+ explicit GraphicsContext_Android(std::shared_ptr<Common::DynamicLibrary> driver_library)
+ : m_driver_library{driver_library} {}
+
+ ~GraphicsContext_Android() = default;
+
+ std::shared_ptr<Common::DynamicLibrary> GetDriverLibrary() override {
+ return m_driver_library;
+ }
+
+private:
+ std::shared_ptr<Common::DynamicLibrary> m_driver_library;
+};
+
+class EmuWindow_Android final : public Core::Frontend::EmuWindow {
+
+public:
+ EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface,
+ std::shared_ptr<Common::DynamicLibrary> driver_library);
+
+ ~EmuWindow_Android();
+
+ void OnSurfaceChanged(ANativeWindow* surface);
+ void OnTouchPressed(int id, float x, float y);
+ void OnTouchMoved(int id, float x, float y);
+ void OnTouchReleased(int id);
+ void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
+ void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
+ void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
+ float gyro_z, float accel_x, float accel_y, float accel_z);
+ void OnReadNfcTag(std::span<u8> data);
+ void OnRemoveNfcTag();
+ void OnFrameDisplayed() override {}
+
+ std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
+ return {std::make_unique<GraphicsContext_Android>(m_driver_library)};
+ }
+ bool IsShown() const override {
+ return true;
+ };
+
+private:
+ InputCommon::InputSubsystem* m_input_subsystem{};
+
+ float m_window_width{};
+ float m_window_height{};
+
+ std::shared_ptr<Common::DynamicLibrary> m_driver_library;
+};
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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <jni.h>
+
+#include "common/assert.h"
+#include "common/fs/fs_android.h"
+#include "jni/applets/software_keyboard.h"
+#include "jni/id_cache.h"
+#include "video_core/rasterizer_interface.h"
+
+static JavaVM* s_java_vm;
+static jclass s_native_library_class;
+static jclass s_disk_cache_progress_class;
+static jclass s_load_callback_stage_class;
+static jmethodID s_exit_emulation_activity;
+static jmethodID s_disk_cache_load_progress;
+
+static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
+
+namespace IDCache {
+
+JNIEnv* GetEnvForThread() {
+ thread_local static struct OwnedEnv {
+ OwnedEnv() {
+ status = s_java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+ if (status == JNI_EDETACHED)
+ s_java_vm->AttachCurrentThread(&env, nullptr);
+ }
+
+ ~OwnedEnv() {
+ if (status == JNI_EDETACHED)
+ s_java_vm->DetachCurrentThread();
+ }
+
+ int status;
+ JNIEnv* env = nullptr;
+ } owned;
+ return owned.env;
+}
+
+jclass GetNativeLibraryClass() {
+ return s_native_library_class;
+}
+
+jclass GetDiskCacheProgressClass() {
+ return s_disk_cache_progress_class;
+}
+
+jclass GetDiskCacheLoadCallbackStageClass() {
+ return s_load_callback_stage_class;
+}
+
+jmethodID GetExitEmulationActivity() {
+ return s_exit_emulation_activity;
+}
+
+jmethodID GetDiskCacheLoadProgress() {
+ return s_disk_cache_load_progress;
+}
+
+} // namespace IDCache
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+ s_java_vm = vm;
+
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
+ return JNI_ERR;
+
+ // Initialize Java classes
+ const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
+ s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
+ s_disk_cache_progress_class = reinterpret_cast<jclass>(env->NewGlobalRef(
+ env->FindClass("org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress")));
+ s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
+ "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
+
+ // Initialize methods
+ s_exit_emulation_activity =
+ env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
+ s_disk_cache_load_progress =
+ env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
+
+ // Initialize Android Storage
+ Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
+
+ // Initialize applets
+ SoftwareKeyboard::InitJNI(env);
+
+ return JNI_VERSION;
+}
+
+void JNI_OnUnload(JavaVM* vm, void* reserved) {
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
+ return;
+ }
+
+ // UnInitialize Android Storage
+ Common::FS::Android::UnRegisterCallbacks();
+ env->DeleteGlobalRef(s_native_library_class);
+ env->DeleteGlobalRef(s_disk_cache_progress_class);
+ env->DeleteGlobalRef(s_load_callback_stage_class);
+
+ // UnInitialize applets
+ SoftwareKeyboard::CleanupJNI(env);
+}
+
+#ifdef __cplusplus
+}
+#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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <jni.h>
+
+#include "video_core/rasterizer_interface.h"
+
+namespace IDCache {
+
+JNIEnv* GetEnvForThread();
+jclass GetNativeLibraryClass();
+jclass GetDiskCacheProgressClass();
+jclass GetDiskCacheLoadCallbackStageClass();
+jmethodID GetExitEmulationActivity();
+jmethodID GetDiskCacheLoadProgress();
+
+} // 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..4091c23d1
--- /dev/null
+++ b/src/android/app/src/main/jni/native.cpp
@@ -0,0 +1,850 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <codecvt>
+#include <locale>
+#include <string>
+#include <string_view>
+#include <dlfcn.h>
+
+#ifdef ARCHITECTURE_arm64
+#include <adrenotools/driver.h>
+#endif
+
+#include <android/api-level.h>
+#include <android/native_window_jni.h>
+#include <core/loader/nro.h>
+
+#include "common/detached_tasks.h"
+#include "common/dynamic_library.h"
+#include "common/fs/path_util.h"
+#include "common/logging/backend.h"
+#include "common/logging/log.h"
+#include "common/microprofile.h"
+#include "common/scm_rev.h"
+#include "common/scope_exit.h"
+#include "common/settings.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/cpu_manager.h"
+#include "core/crypto/key_manager.h"
+#include "core/file_sys/card_image.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/file_sys/submission_package.h"
+#include "core/file_sys/vfs.h"
+#include "core/file_sys/vfs_real.h"
+#include "core/frontend/applets/cabinet.h"
+#include "core/frontend/applets/controller.h"
+#include "core/frontend/applets/error.h"
+#include "core/frontend/applets/general_frontend.h"
+#include "core/frontend/applets/mii_edit.h"
+#include "core/frontend/applets/profile_select.h"
+#include "core/frontend/applets/software_keyboard.h"
+#include "core/frontend/applets/web_browser.h"
+#include "core/hid/emulated_controller.h"
+#include "core/hid/hid_core.h"
+#include "core/hid/hid_types.h"
+#include "core/hle/service/acc/profile_manager.h"
+#include "core/hle/service/am/applet_ae.h"
+#include "core/hle/service/am/applet_oe.h"
+#include "core/hle/service/am/applets/applets.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/loader.h"
+#include "core/perf_stats.h"
+#include "jni/android_common/android_common.h"
+#include "jni/applets/software_keyboard.h"
+#include "jni/config.h"
+#include "jni/emu_window/emu_window.h"
+#include "jni/id_cache.h"
+#include "video_core/rasterizer_interface.h"
+#include "video_core/renderer_base.h"
+
+namespace {
+
+class EmulationSession final {
+public:
+ EmulationSession() {
+ m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
+ }
+
+ ~EmulationSession() = default;
+
+ static EmulationSession& GetInstance() {
+ return s_instance;
+ }
+
+ const Core::System& System() const {
+ return m_system;
+ }
+
+ Core::System& System() {
+ return m_system;
+ }
+
+ const EmuWindow_Android& Window() const {
+ return *m_window;
+ }
+
+ EmuWindow_Android& Window() {
+ return *m_window;
+ }
+
+ ANativeWindow* NativeWindow() const {
+ return m_native_window;
+ }
+
+ void SetNativeWindow(ANativeWindow* native_window) {
+ m_native_window = native_window;
+ }
+
+ int InstallFileToNand(std::string filename) {
+ const auto copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
+ std::size_t block_size) {
+ if (src == nullptr || dest == nullptr) {
+ return false;
+ }
+ if (!dest->Resize(src->GetSize())) {
+ return false;
+ }
+
+ using namespace Common::Literals;
+ std::vector<u8> buffer(1_MiB);
+
+ for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+ const auto read = src->Read(buffer.data(), buffer.size(), i);
+ dest->Write(buffer.data(), read, i);
+ }
+ return true;
+ };
+
+ enum InstallResult {
+ Success = 0,
+ SuccessFileOverwritten = 1,
+ InstallError = 2,
+ ErrorBaseGame = 3,
+ ErrorFilenameExtension = 4,
+ };
+
+ m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+ m_system.GetFileSystemController().CreateFactories(*m_vfs);
+
+ std::shared_ptr<FileSys::NSP> nsp;
+ if (filename.ends_with("nsp")) {
+ nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
+ if (nsp->IsExtractedType()) {
+ return InstallError;
+ }
+ } else if (filename.ends_with("xci")) {
+ const auto xci =
+ std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
+ nsp = xci->GetSecurePartitionNSP();
+ } else {
+ return ErrorFilenameExtension;
+ }
+
+ if (!nsp) {
+ return InstallError;
+ }
+
+ if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+ return InstallError;
+ }
+
+ const auto res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(
+ *nsp, true, copy_func);
+
+ switch (res) {
+ case FileSys::InstallResult::Success:
+ return Success;
+ case FileSys::InstallResult::OverwriteExisting:
+ return SuccessFileOverwritten;
+ case FileSys::InstallResult::ErrorBaseInstall:
+ return ErrorBaseGame;
+ default:
+ return InstallError;
+ }
+ }
+
+ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
+ const std::string& custom_driver_name,
+ const std::string& file_redirect_dir) {
+#ifdef ARCHITECTURE_arm64
+ void* handle{};
+ const char* file_redirect_dir_{};
+ int featureFlags{};
+
+ // Enable driver file redirection when renderer debugging is enabled.
+ if (Settings::values.renderer_debug && file_redirect_dir.size()) {
+ featureFlags |= ADRENOTOOLS_DRIVER_FILE_REDIRECT;
+ file_redirect_dir_ = file_redirect_dir.c_str();
+ }
+
+ // Try to load a custom driver.
+ if (custom_driver_name.size()) {
+ handle = adrenotools_open_libvulkan(
+ RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
+ custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
+ }
+
+ // Try to load the system driver.
+ if (!handle) {
+ handle =
+ adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
+ nullptr, nullptr, file_redirect_dir_, nullptr);
+ }
+
+ m_vulkan_library = std::make_shared<Common::DynamicLibrary>(handle);
+#endif
+ }
+
+ bool IsRunning() const {
+ std::scoped_lock lock(m_mutex);
+ return m_is_running;
+ }
+
+ const Core::PerfStatsResults& PerfStats() const {
+ std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
+ return m_perf_stats;
+ }
+
+ void SurfaceChanged() {
+ if (!IsRunning()) {
+ return;
+ }
+ m_window->OnSurfaceChanged(m_native_window);
+ m_system.Renderer().NotifySurfaceChanged();
+ }
+
+ Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
+ std::scoped_lock lock(m_mutex);
+
+ // Loads the configuration.
+ Config{};
+
+ // Create the render window.
+ m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window,
+ m_vulkan_library);
+
+ m_system.SetFilesystem(m_vfs);
+
+ // Initialize system.
+ auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
+ m_software_keyboard = android_keyboard.get();
+ m_system.SetShuttingDown(false);
+ m_system.ApplySettings();
+ m_system.HIDCore().ReloadInputDevices();
+ m_system.SetAppletFrontendSet({
+ nullptr, // Amiibo Settings
+ nullptr, // Controller Selector
+ nullptr, // Error Display
+ nullptr, // Mii Editor
+ nullptr, // Parental Controls
+ nullptr, // Photo Viewer
+ nullptr, // Profile Selector
+ std::move(android_keyboard), // Software Keyboard
+ nullptr, // Web Browser
+ });
+ m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+ m_system.GetFileSystemController().CreateFactories(*m_vfs);
+
+ // Initialize account manager
+ m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
+
+ // Load the ROM.
+ m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
+ if (m_load_result != Core::SystemResultStatus::Success) {
+ return m_load_result;
+ }
+
+ // Complete initialization.
+ m_system.GPU().Start();
+ m_system.GetCpuManager().OnGpuReady();
+ m_system.RegisterExitCallback([&] { HaltEmulation(); });
+
+ return Core::SystemResultStatus::Success;
+ }
+
+ void ShutdownEmulation() {
+ std::scoped_lock lock(m_mutex);
+
+ m_is_running = false;
+
+ // Unload user input.
+ m_system.HIDCore().UnloadInputDevices();
+
+ // Shutdown the main emulated process
+ if (m_load_result == Core::SystemResultStatus::Success) {
+ m_system.DetachDebugger();
+ m_system.ShutdownMainProcess();
+ m_detached_tasks.WaitForAllTasks();
+ m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
+ }
+
+ // Tear down the render window.
+ m_window.reset();
+ }
+
+ void PauseEmulation() {
+ std::scoped_lock lock(m_mutex);
+ m_system.Pause();
+ }
+
+ void UnPauseEmulation() {
+ std::scoped_lock lock(m_mutex);
+ m_system.Run();
+ }
+
+ void HaltEmulation() {
+ std::scoped_lock lock(m_mutex);
+ m_is_running = false;
+ m_cv.notify_one();
+ }
+
+ void RunEmulation() {
+ {
+ std::scoped_lock lock(m_mutex);
+ m_is_running = true;
+ }
+
+ // Load the disk shader cache.
+ if (Settings::values.use_disk_shader_cache.GetValue()) {
+ LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
+ m_system.Renderer().ReadRasterizer()->LoadDiskResources(
+ m_system.GetApplicationProcessProgramID(), std::stop_token{},
+ LoadDiskCacheProgress);
+ LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
+ }
+
+ void(m_system.Run());
+
+ if (m_system.DebuggerEnabled()) {
+ m_system.InitializeDebugger();
+ }
+
+ while (true) {
+ {
+ std::unique_lock lock(m_mutex);
+ if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
+ [&]() { return !m_is_running; })) {
+ // Emulation halted.
+ break;
+ }
+ }
+ {
+ // Refresh performance stats.
+ std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
+ m_perf_stats = m_system.GetAndResetPerfStats();
+ }
+ }
+ }
+
+ std::string GetRomTitle(const std::string& path) {
+ return GetRomMetadata(path).title;
+ }
+
+ std::vector<u8> GetRomIcon(const std::string& path) {
+ return GetRomMetadata(path).icon;
+ }
+
+ bool GetIsHomebrew(const std::string& path) {
+ return GetRomMetadata(path).isHomebrew;
+ }
+
+ void ResetRomMetadata() {
+ m_rom_metadata_cache.clear();
+ }
+
+ bool IsHandheldOnly() {
+ const auto npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
+
+ if (npad_style_set.fullkey == 1) {
+ return false;
+ }
+
+ if (npad_style_set.handheld == 0) {
+ return false;
+ }
+
+ return !Settings::values.use_docked_mode.GetValue();
+ }
+
+ void SetDeviceType(int index, int type) {
+ auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
+ controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
+ }
+
+ void OnGamepadConnectEvent(int index) {
+ auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
+
+ // Ensure that player1 is configured correctly and handheld disconnected
+ if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
+ auto handheld =
+ m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
+
+ if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
+ handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
+ controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
+ handheld->Disconnect();
+ }
+ }
+
+ // Ensure that handheld is configured correctly and player 1 disconnected
+ if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
+ auto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
+
+ if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
+ player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
+ controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
+ player1->Disconnect();
+ }
+ }
+
+ if (!controller->IsConnected()) {
+ controller->Connect();
+ }
+ }
+
+ void OnGamepadDisconnectEvent(int index) {
+ auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
+ controller->Disconnect();
+ }
+
+ SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() {
+ return m_software_keyboard;
+ }
+
+private:
+ struct RomMetadata {
+ std::string title;
+ std::vector<u8> icon;
+ bool isHomebrew;
+ };
+
+ RomMetadata GetRomMetadata(const std::string& path) {
+ if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
+ return search->second;
+ }
+
+ return CacheRomMetadata(path);
+ }
+
+ RomMetadata CacheRomMetadata(const std::string& path) {
+ const auto file = Core::GetGameFileFromPath(m_vfs, path);
+ auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
+
+ RomMetadata entry;
+ loader->ReadTitle(entry.title);
+ loader->ReadIcon(entry.icon);
+ if (loader->GetFileType() == Loader::FileType::NRO) {
+ auto loader_nro = dynamic_cast<Loader::AppLoader_NRO*>(loader.get());
+ entry.isHomebrew = loader_nro->IsHomebrew();
+ } else {
+ entry.isHomebrew = false;
+ }
+
+ m_rom_metadata_cache[path] = entry;
+
+ return entry;
+ }
+
+private:
+ static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
+ JNIEnv* env = IDCache::GetEnvForThread();
+ env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
+ IDCache::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
+ static_cast<jint>(progress), static_cast<jint>(max));
+ }
+
+private:
+ static EmulationSession s_instance;
+
+ // Frontend management
+ std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
+
+ // Window management
+ std::unique_ptr<EmuWindow_Android> m_window;
+ ANativeWindow* m_native_window{};
+
+ // Core emulation
+ Core::System m_system;
+ InputCommon::InputSubsystem m_input_subsystem;
+ Common::DetachedTasks m_detached_tasks;
+ Core::PerfStatsResults m_perf_stats{};
+ std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
+ Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
+ bool m_is_running{};
+ SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
+ std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
+
+ // GPU driver parameters
+ std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
+
+ // Synchronization
+ std::condition_variable_any m_cv;
+ mutable std::mutex m_perf_stats_mutex;
+ mutable std::mutex m_mutex;
+};
+
+/*static*/ EmulationSession EmulationSession::s_instance;
+
+} // Anonymous namespace
+
+static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
+ Common::Log::Initialize();
+ Common::Log::SetColorConsoleBackendEnabled(true);
+ Common::Log::Start();
+
+ MicroProfileOnThreadCreate("EmuThread");
+ SCOPE_EXIT({ MicroProfileShutdown(); });
+
+ LOG_INFO(Frontend, "starting");
+
+ if (filepath.empty()) {
+ LOG_CRITICAL(Frontend, "failed to load: filepath empty!");
+ return Core::SystemResultStatus::ErrorLoader;
+ }
+
+ SCOPE_EXIT({ EmulationSession::GetInstance().ShutdownEmulation(); });
+
+ const auto result = EmulationSession::GetInstance().InitializeEmulation(filepath);
+ if (result != Core::SystemResultStatus::Success) {
+ return result;
+ }
+
+ EmulationSession::GetInstance().RunEmulation();
+
+ return Core::SystemResultStatus::Success;
+}
+
+extern "C" {
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jobject surf) {
+ EmulationSession::GetInstance().SetNativeWindow(ANativeWindow_fromSurface(env, surf));
+ EmulationSession::GetInstance().SurfaceChanged();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ ANativeWindow_release(EmulationSession::GetInstance().NativeWindow());
+ EmulationSession::GetInstance().SetNativeWindow(nullptr);
+ EmulationSession::GetInstance().SurfaceChanged();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_directory) {
+ Common::FS::SetAppDirectory(GetJString(env, j_directory));
+}
+
+int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_file) {
+ return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file));
+}
+
+void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(
+ JNIEnv* env, [[maybe_unused]] jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir,
+ jstring custom_driver_name, jstring file_redirect_dir) {
+ EmulationSession::GetInstance().InitializeGpuDriver(
+ GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir),
+ GetJString(env, custom_driver_name), GetJString(env, file_redirect_dir));
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ Core::Crypto::KeyManager::Instance().ReloadKeys();
+ return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ EmulationSession::GetInstance().UnPauseEmulation();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ EmulationSession::GetInstance().PauseEmulation();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ EmulationSession::GetInstance().HaltEmulation();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_resetRomMetadata([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ EmulationSession::GetInstance().ResetRomMetadata();
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ return EmulationSession::GetInstance().IsHandheldOnly();
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jint j_device, jint j_type) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jint j_device) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
+ }
+ return static_cast<jboolean>(true);
+}
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jint j_device,
+ jint j_button, jint action) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ // Ensure gamepad is connected
+ EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
+ EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
+ action != 0);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jint j_device, jint stick_id,
+ jfloat x, jfloat y) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device,
+ jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, jfloat gyro_z, jfloat accel_x,
+ jfloat accel_y, jfloat accel_z) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
+ j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jbyteArray j_data) {
+ jboolean isCopy{false};
+ std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
+ static_cast<size_t>(env->GetArrayLength(j_data)));
+
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnReadNfcTag(data);
+ }
+ return static_cast<jboolean>(true);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnRemoveNfcTag();
+ }
+ return static_cast<jboolean>(true);
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz, jint id,
+ jfloat x, jfloat y) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
+ }
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz, jint id,
+ jfloat x, jfloat y) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
+ }
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz, jint id) {
+ if (EmulationSession::GetInstance().IsRunning()) {
+ EmulationSession::GetInstance().Window().OnTouchReleased(id);
+ }
+}
+
+jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_filename) {
+ auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
+ jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
+ env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
+ reinterpret_cast<jbyte*>(icon_data.data()));
+ return icon;
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_filename) {
+ auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
+ return env->NewStringUTF(title.c_str());
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDescription([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_filename) {
+ return j_filename;
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_filename) {
+ return j_filename;
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_filename) {
+ return env->NewStringUTF("");
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_filename) {
+ return env->NewStringUTF("");
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_filename) {
+ return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename));
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation
+ [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) {
+ // Create the default config.ini.
+ Config{};
+ // Initialize the emulated system.
+ EmulationSession::GetInstance().System().Initialize();
+}
+
+jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ return {};
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_file,
+ [[maybe_unused]] jstring j_savestate, [[maybe_unused]] jboolean j_delete_savestate) {}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ Config{};
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserSetting([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_game_id, jstring j_section,
+ jstring j_key) {
+ std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
+ std::string_view section = env->GetStringUTFChars(j_section, 0);
+ std::string_view key = env->GetStringUTFChars(j_key, 0);
+
+ env->ReleaseStringUTFChars(j_game_id, game_id.data());
+ env->ReleaseStringUTFChars(j_section, section.data());
+ env->ReleaseStringUTFChars(j_key, key.data());
+
+ return env->NewStringUTF("");
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_setUserSetting([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_game_id, jstring j_section,
+ jstring j_key, jstring j_value) {
+ std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
+ std::string_view section = env->GetStringUTFChars(j_section, 0);
+ std::string_view key = env->GetStringUTFChars(j_key, 0);
+ std::string_view value = env->GetStringUTFChars(j_value, 0);
+
+ env->ReleaseStringUTFChars(j_game_id, game_id.data());
+ env->ReleaseStringUTFChars(j_section, section.data());
+ env->ReleaseStringUTFChars(j_key, key.data());
+ env->ReleaseStringUTFChars(j_value, value.data());
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_game_id) {
+ std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
+
+ env->ReleaseStringUTFChars(j_game_id, game_id.data());
+}
+
+jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ jdoubleArray j_stats = env->NewDoubleArray(4);
+
+ if (EmulationSession::GetInstance().IsRunning()) {
+ const auto results = EmulationSession::GetInstance().PerfStats();
+
+ // Converting the structure into an array makes it easier to pass it to the frontend
+ double stats[4] = {results.system_fps, results.average_game_fps, results.frametime,
+ results.emulation_speed};
+
+ env->SetDoubleArrayRegion(j_stats, 0, 4, stats);
+ }
+
+ return j_stats;
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) {}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ jstring j_path) {
+ const std::string path = GetJString(env, j_path);
+
+ const Core::SystemResultStatus result{RunEmulation(path)};
+ if (result != Core::SystemResultStatus::Success) {
+ env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
+ IDCache::GetExitEmulationActivity(), static_cast<int>(result));
+ }
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ LOG_INFO(Frontend, "yuzu Version: {}-{}", Common::g_scm_branch, Common::g_scm_desc);
+ LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardText(JNIEnv* env, jclass clazz,
+ jstring j_text) {
+ const std::u16string input = Common::UTF8ToUTF16(GetJString(env, j_text));
+ EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env, jclass clazz,
+ jint j_key_code) {
+ EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
+}
+
+} // 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 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <jni.h>
+
+// Function calls from the Java side
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env,
+ jclass clazz,
+ jstring j_device,
+ jstring j_type);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(
+ JNIEnv* env, jclass clazz, jstring j_device);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(
+ JNIEnv* env, jclass clazz, jstring j_device);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
+ JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEvent(
+ JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent(
+ JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env,
+ jclass clazz,
+ jbyteArray j_data);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
+ jclass clazz,
+ jfloat x, jfloat y,
+ jboolean pressed);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
+ jfloat x, jfloat y);
+
+JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
+ jclass clazz,
+ jstring j_file);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
+ jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(JNIEnv* env,
+ jclass clazz,
+ jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz,
+ jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
+ jclass clazz,
+ jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
+ jclass clazz,
+ jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
+ jclass clazz,
+ jstring j_directory);
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_NativeLibrary_Java_org_yuzu_yuzu_1emu_NativeLibrary_InitializeGpuDriver(
+ JNIEnv* env, jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir,
+ jstring custom_driver_name, jstring file_redirect_dir);
+
+JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
+ JNIEnv* env, jclass clazz, jstring path_);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
+ jclass clazz,
+ jstring path);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitializeEmulation(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
+ jclass clazz);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
+ jboolean enable);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
+ JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2(
+ JNIEnv* env, jclass clazz, jstring j_path);
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z(
+ JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
+ jclass clazz,
+ jobject surf);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
+ jstring j_game_id);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
+ JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
+ jstring j_value);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting(
+ JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
+
+JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
+ jclass clazz);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(
+ JNIEnv* env, jclass clazz, jstring j_text);
+
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(
+ JNIEnv* env, jclass clazz, jint j_key_code);
+
+#ifdef __cplusplus
+}
+#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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="125"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0" />
+
+ <translate
+ android:duration="125"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromXDelta="0"
+ android:toXDelta="-75" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1" />
+
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromXDelta="-200"
+ android:toXDelta="0" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="125"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0" />
+
+ <translate
+ android:duration="125"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromXDelta="0"
+ android:toXDelta="75" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1" />
+
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromXDelta="200"
+ android:toXDelta="0" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <objectAnimator
+ android:propertyName="translationX"
+ android:valueType="floatType"
+ android:valueFrom="-1280dp"
+ android:valueTo="0"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:duration="300"/>
+
+ <objectAnimator
+ android:propertyName="alpha"
+ android:valueType="floatType"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:interpolator/accelerate_quad"
+ android:duration="300"/>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- This animation is used ONLY when a submenu is replaced. -->
+ <objectAnimator
+ android:propertyName="translationX"
+ android:valueType="floatType"
+ android:valueFrom="0"
+ android:valueTo="-1280dp"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:duration="200"/>
+
+ <objectAnimator
+ android:propertyName="alpha"
+ android:valueType="floatType"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:duration="200"/>
+
+</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-xhdpi/tv_banner.png b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png
new file mode 100644
index 000000000..20c770591
--- /dev/null
+++ b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.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 @@
+<vector android:alpha="0.6" android:height="221.78dp"
+ android:viewportHeight="221.78" android:viewportWidth="221.78"
+ android:width="221.78dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.75"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="110.89" android:centerY="110.89"
+ android:gradientRadius="110.89" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <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"/>
+ <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"/>
+ <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"/>
+ <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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="221.78dp"
+ android:viewportHeight="221.78" android:viewportWidth="221.78"
+ android:width="221.78dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="110.89" android:endY="-38.27"
+ android:startX="110.89" android:startY="183.51" android:type="linear">
+ <item android:color="#7F000000" android:offset="0"/>
+ <item android:color="#BA000000" android:offset="0.43"/>
+ <item android:color="#FF000000" android:offset="0.5"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.1" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.1" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.1" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="221.78dp"
+ android:viewportHeight="221.78" android:viewportWidth="221.78"
+ android:width="221.78dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="31.24" android:endY="31.24"
+ android:startX="188.07" android:startY="188.07" android:type="linear">
+ <item android:color="#7F000000" android:offset="0"/>
+ <item android:color="#BA000000" android:offset="0.43"/>
+ <item android:color="#FF000000" android:offset="0.5"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.1" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.1" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="51.8" android:centerY="51.8"
+ android:gradientRadius="51.8" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.6" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="51.8" android:centerY="51.8"
+ android:gradientRadius="51.8" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.6" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="70.55dp"
+ android:viewportHeight="70.55" android:viewportWidth="70.55"
+ android:width="70.55dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.75"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="35.27" android:centerY="35.27"
+ android:gradientRadius="35.27" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="70.55dp"
+ android:viewportHeight="70.55" android:viewportWidth="70.55"
+ android:width="70.55dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="69.95dp"
+ android:viewportHeight="69.95" android:viewportWidth="69.95"
+ android:width="69.95dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.75"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="34.97" android:centerY="34.97"
+ android:gradientRadius="34.97" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"
+ android:strokeAlpha="0.75" android:strokeColor="#000" android:strokeWidth="2.5"/>
+</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 @@
+<vector android:alpha="0.6" android:height="69.95dp"
+ android:viewportHeight="69.95" android:viewportWidth="69.95"
+ android:width="69.95dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"
+ android:strokeAlpha="0.75" android:strokeColor="#fff" android:strokeWidth="2.5"/>
+</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 @@
+<vector android:alpha="0.6" android:height="69.95dp"
+ android:viewportHeight="69.95" android:viewportWidth="69.95"
+ android:width="69.95dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.75"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="34.97" android:centerY="34.97"
+ android:gradientRadius="34.97" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"
+ android:strokeAlpha="0.75" android:strokeColor="#000" android:strokeWidth="2.5"/>
+</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 @@
+<vector android:alpha="0.6" android:height="69.95dp"
+ android:viewportHeight="69.95" android:viewportWidth="69.95"
+ android:width="69.95dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"
+ android:strokeAlpha="0.75" android:strokeColor="#fff" android:strokeWidth="2.5"/>
+</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 @@
+<vector android:alpha="0.6" android:height="70dp"
+ android:viewportHeight="70" android:viewportWidth="70"
+ android:width="70dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.75"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="35" android:centerY="35"
+ android:gradientRadius="42.51" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="70dp"
+ android:viewportHeight="70" android:viewportWidth="70"
+ android:width="70dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="51.8" android:centerY="51.8"
+ android:gradientRadius="51.8" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.6" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="51.8" android:centerY="51.8"
+ android:gradientRadius="51.8" android:type="radial">
+ <item android:color="#FFC3C4C5" android:offset="0.58"/>
+ <item android:color="#FFC6C6C6" android:offset="0.84"/>
+ <item android:color="#FFC7C7C7" android:offset="0.88"/>
+ <item android:color="#FFC2C2C2" android:offset="0.91"/>
+ <item android:color="#FFB5B5B5" android:offset="0.94"/>
+ <item android:color="#FF9E9E9E" android:offset="0.98"/>
+ <item android:color="#FF8F8F8F" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.6" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="103.61dp"
+ android:viewportHeight="103.61" android:viewportWidth="103.61"
+ android:width="103.61dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,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" />
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="200dp"
+ android:height="200dp"
+ android:viewportWidth="256"
+ android:viewportHeight="256">
+ <path
+ android:fillColor="#5865F2"
+ android:fillType="nonZero"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ 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"/>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="98dp"
+ android:height="96dp"
+ android:viewportWidth="98"
+ android:viewportHeight="96">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:fillType="evenOdd"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="512dp"
+ android:height="512dp"
+ android:viewportWidth="512"
+ android:viewportHeight="512">
+ <group>
+ <clip-path
+ android:pathData="M0,0h512v512h-512z"/>
+ <path
+ android:pathData="M0,0h512v512h-512z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M0,0h512v512h-512z"
+ android:fillColor="#1C1C1C"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M183.86,97.29L177.93,92.92H189.79L183.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M399.86,97.29L393.93,92.92H405.79L399.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M327.86,199.29L321.93,194.92H333.79L327.86,199.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M471.86,97.29L465.93,92.92H477.79L471.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M327.86,97.29L321.93,92.92H333.79L327.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M255.86,97.29L249.93,92.92H261.79L255.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M39.86,97.29L33.93,92.92H45.79L39.86,97.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M38.86,400.29L32.93,395.92H44.79L38.86,400.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M399.86,299.29L393.93,294.92H405.79L399.86,299.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M471.86,299.29L465.93,294.92H477.79L471.86,299.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M111.86,299.29L105.93,294.92H117.79L111.86,299.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M327.86,299.29L321.93,294.92H333.79L327.86,299.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M39.86,299.29L33.93,294.92H45.79L39.86,299.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M110.86,400.29L104.93,395.92H116.79L110.86,400.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M183.86,198.29L177.93,193.92H189.79L183.86,198.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M471.86,198.29L465.93,193.92H477.79L471.86,198.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M39.86,198.29L33.93,193.92H45.79L39.86,198.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M255.86,198.29L249.93,193.92H261.79L255.86,198.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M471.86,400.29L465.93,395.92H477.79L471.86,400.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M327.86,400.29L321.93,395.92H333.79L327.86,400.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M38.86,501.29L32.93,496.92H44.79L38.86,501.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M471.86,501.29L465.93,496.92H477.79L471.86,501.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M327.86,501.29L321.93,496.92H333.79L327.86,501.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M183.86,501.29L177.93,496.92H189.79L183.86,501.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M255.86,501.29L249.93,496.92H261.79L255.86,501.29Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <group>
+ <clip-path
+ android:pathData="M80,8h64v192h-64z"/>
+ <path
+ android:pathData="M112.06,8H144.11V200H112.06C94.32,200 80,185.68 80,167.96V40.04C80,22.31 94.32,8 112.06,8Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ </group>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <group>
+ <clip-path
+ android:pathData="M368,311h64v192h-64z"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ android:strokeWidth="1"
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:strokeColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ </group>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1A1A1A"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1A1A1A"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1A1A1A"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1A1A1A"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ android:pathData="M380.46,155.54L377.68,151.3H378.96L380.98,154.54L383.05,151.3H384.27L381.49,155.54V158.7H380.49V155.54H380.46Z"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#1C1C1C"
+ android:fillAlpha="0.7"/>
+ <path
+ 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"
+ android:strokeAlpha="0.7"
+ android:fillColor="#282828"
+ android:fillAlpha="0.7"/>
+ </group>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_icon_bg" />
+ <foreground android:drawable="@drawable/ic_yuzu" />
+ <monochrome android:drawable="@drawable/ic_yuzu" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ 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"/>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" />
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M8,5v14l11,-7z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ 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"/>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="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" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_system_update_alt.xml b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml
new file mode 100644
index 000000000..0f6adfdb8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M140,800q-24,0 -42,-18t-18,-42v-520q0,-24 18,-42t42,-18h250v60L140,220v520h680v-520L570,220v-60h250q24,0 42,18t18,42v520q0,24 -18,42t-42,18L140,800ZM480,615L280,415l43,-43 127,127v-339h60v339l127,-127 43,43 -200,200Z"/>
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="200dp"
+ android:height="200dp"
+ android:viewportWidth="500"
+ android:viewportHeight="500">
+ <path
+ android:fillColor="#FF3C28"
+ android:fillType="nonZero"
+ 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"
+ android:strokeWidth="1.46"
+ android:strokeColor="#00000000"
+ android:strokeLineCap="butt"
+ android:strokeLineJoin="miter" />
+ <path
+ android:fillColor="#0AB9E6"
+ android:fillType="nonZero"
+ 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"
+ android:strokeWidth="1.46"
+ android:strokeColor="#00000000"
+ android:strokeLineCap="butt"
+ android:strokeLineJoin="miter" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="155.3dp"
+ android:height="172.55dp"
+ android:viewportWidth="155.3"
+ android:viewportHeight="172.55">
+ <path
+ android:fillColor="#FF3C28"
+ 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" />
+ <path
+ android:fillColor="#0AB9E6"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="340.97dp"
+ android:height="389.85dp"
+ android:viewportWidth="340.97"
+ android:viewportHeight="389.85">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="#ff3c28"
+ 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" />
+ <path
+ android:fillColor="#0ab9e6"
+ 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" />
+</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 @@
+<vector android:alpha="0.6" android:height="161.61dp"
+ android:viewportHeight="161.61" android:viewportWidth="161.61"
+ android:width="161.61dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.8"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="68.25" android:centerY="57.75"
+ android:gradientRadius="122.17" android:type="radial">
+ <item android:color="#FFD9D9D9" android:offset="0.44"/>
+ <item android:color="#FF141414" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.8"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="80.49" android:centerY="60.19"
+ android:gradientRadius="88.23" android:type="radial">
+ <item android:color="#FFBABABA" android:offset="0.15"/>
+ <item android:color="#FF9E9E9E" android:offset="0.46"/>
+ <item android:color="#FF868686" android:offset="0.63"/>
+ <item android:color="#FF575757" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.8"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="80.8" android:centerY="80.8"
+ android:gradientRadius="97.63" android:type="radial">
+ <item android:color="#FFC2C2C3" android:offset="0.04"/>
+ <item android:color="#FFC0C0C1" android:offset="0.35"/>
+ <item android:color="#FFB9B9BA" android:offset="0.47"/>
+ <item android:color="#FFADADAE" android:offset="0.56"/>
+ <item android:color="#FF9C9C9D" android:offset="0.63"/>
+ <item android:color="#FF868687" android:offset="0.69"/>
+ <item android:color="#FF6A6A6B" android:offset="0.74"/>
+ <item android:color="#FF4A4A4A" android:offset="0.79"/>
+ <item android:color="#FF252525" android:offset="0.83"/>
+ <item android:color="#FF000000" android:offset="0.87"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+</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 @@
+<vector android:alpha="0.6" android:height="161.73dp"
+ android:viewportHeight="161.73" android:viewportWidth="161.73"
+ android:width="161.73dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#FF000000"
+ 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"/>
+ <path android:fillAlpha="0.5" android:fillColor="#FF000000"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="265.64dp"
+ android:viewportHeight="265.64" android:viewportWidth="265.64"
+ android:width="265.64dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.8"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="132.82" android:centerY="132.82"
+ android:gradientRadius="195.71" android:type="radial">
+ <item android:color="#00000000" android:offset="0"/>
+ <item android:color="#14161515" android:offset="0.27"/>
+ <item android:color="#30333031" android:offset="0.42"/>
+ <item android:color="#42393738" android:offset="0.45"/>
+ <item android:color="#754B494A" android:offset="0.51"/>
+ <item android:color="#C6676666" android:offset="0.59"/>
+ <item android:color="#FF7A7A7A" android:offset="0.63"/>
+ <item android:color="#FF787878" android:offset="0.99"/>
+ <item android:color="#FF787878" android:offset="0.99"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.6"
+ 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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:centerX="132.82" android:centerY="132.7"
+ android:gradientRadius="141.24" android:type="radial">
+ <item android:color="#FF969696" android:offset="0"/>
+ <item android:color="#FF949494" android:offset="0.8"/>
+ <item android:color="#FF8D8D8D" android:offset="0.84"/>
+ <item android:color="#FF828282" android:offset="0.87"/>
+ <item android:color="#FF717171" android:offset="0.9"/>
+ <item android:color="#FF5B5B5B" android:offset="0.94"/>
+ <item android:color="#FF404040" android:offset="0.98"/>
+ <item android:color="#FF303030" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="244.91" android:endY="49.29"
+ android:startX="0" android:startY="49.29" android:type="linear">
+ <item android:color="#FFC3C4C5" android:offset="0"/>
+ <item android:color="#FFC4C5C5" android:offset="0.05"/>
+ <item android:color="#FFC7C7C7" android:offset="0.47"/>
+ <item android:color="#F7C4C4C4" android:offset="0.54"/>
+ <item android:color="#E5BABABA" android:offset="0.65"/>
+ <item android:color="#C6ABABAB" android:offset="0.77"/>
+ <item android:color="#9E969696" android:offset="0.91"/>
+ <item android:color="#7F878787" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ android:pathData="M106.15,20h7.57V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ android:pathData="M106.15,20h7.57V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:type="linear"
+ android:angle="45"
+ android:startColor="@color/yuzu_ea_background_start"
+ android:endColor="@color/yuzu_ea_background_end" />
+ <corners android:radius="12dp" />
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="0" android:endY="49.29"
+ android:startX="244.91" android:startY="49.29" android:type="linear">
+ <item android:color="#FFC3C4C5" android:offset="0"/>
+ <item android:color="#FFC4C5C5" android:offset="0.05"/>
+ <item android:color="#FFC7C7C7" android:offset="0.47"/>
+ <item android:color="#F7C4C4C4" android:offset="0.54"/>
+ <item android:color="#E5BABABA" android:offset="0.65"/>
+ <item android:color="#C6ABABAB" android:offset="0.77"/>
+ <item android:color="#9E969696" android:offset="0.91"/>
+ <item android:color="#7F878787" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_cartridge_outline" android:state_checked="false"/>
+ <item android:drawable="@drawable/ic_cartridge" android:state_checked="true"/>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_settings_outline" android:state_checked="false"/>
+ <item android:drawable="@drawable/ic_settings" android:state_checked="true"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="244.91" android:endY="49.29"
+ android:startX="0" android:startY="49.29" android:type="linear">
+ <item android:color="#FFC3C4C5" android:offset="0"/>
+ <item android:color="#FFC4C5C5" android:offset="0.05"/>
+ <item android:color="#FFC7C7C7" android:offset="0.47"/>
+ <item android:color="#F7C4C4C4" android:offset="0.54"/>
+ <item android:color="#E5BABABA" android:offset="0.65"/>
+ <item android:color="#C6ABABAB" android:offset="0.77"/>
+ <item android:color="#9E969696" android:offset="0.91"/>
+ <item android:color="#7F878787" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ android:pathData="M132.19,20h7.56V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ android:pathData="M132.19,20h7.56V72.24h25v6.35h-32.6Z" android:strokeAlpha="0.75"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.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">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="0" android:endY="49.29"
+ android:startX="244.91" android:startY="49.29" android:type="linear">
+ <item android:color="#FFC3C4C5" android:offset="0"/>
+ <item android:color="#FFC4C5C5" android:offset="0.05"/>
+ <item android:color="#FFC7C7C7" android:offset="0.47"/>
+ <item android:color="#F7C4C4C4" android:offset="0.54"/>
+ <item android:color="#E5BABABA" android:offset="0.65"/>
+ <item android:color="#C6ABABAB" android:offset="0.77"/>
+ <item android:color="#9E969696" android:offset="0.91"/>
+ <item android:color="#7F878787" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#FF000000"
+ 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"/>
+</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 @@
+<vector android:alpha="0.6" android:height="98.58dp"
+ android:viewportHeight="98.58" android:viewportWidth="244.91"
+ android:width="244.91dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0.5" android:fillColor="#151515"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+ <path android:fillAlpha="0.75" android:fillColor="#fff"
+ 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"/>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/coordinator_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/home_navigation"
+ tools:layout="@layout/fragment_games" />
+
+ <com.google.android.material.navigationrail.NavigationRailView
+ android:id="@+id/navigation_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:labelVisibilityMode="selected"
+ app:menu="@menu/menu_navigation"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/status_bar_shade"
+ android:layout_width="0dp"
+ android:layout_height="1px"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <View
+ android:id="@+id/navigation_bar_shade"
+ android:layout_width="0dp"
+ android:layout_height="1px"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/setup_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/viewPager2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/next"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_back"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/back"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="260dp"
+ android:layout_height="260dp"
+ android:layout_gravity="center" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.DisplaySmall"
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnSurface"
+ android:textStyle="bold"
+ tools:text="@string/welcome" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:id="@+id/text_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingHorizontal="32dp"
+ android:textAlignment="center"
+ android:textSize="26sp"
+ app:lineHeight="40sp"
+ tools:text="@string/welcome_description" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_action"
+ android:layout_width="wrap_content"
+ android:layout_height="56dp"
+ android:layout_marginTop="32dp"
+ android:textSize="20sp"
+ app:iconSize="24sp"
+ app:iconGravity="end"
+ tools:text="Get started" />
+
+ </LinearLayout>
+
+</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 @@
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/frame_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:keepScreenOn="true">
+
+ <FrameLayout
+ android:id="@+id/frame_emulation_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/coordinator_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/home_navigation"
+ tools:layout="@layout/fragment_games" />
+
+ <com.google.android.material.bottomnavigation.BottomNavigationView
+ android:id="@+id/navigation_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:menu="@menu/menu_navigation"
+ app:labelVisibilityMode="selected"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/status_bar_shade"
+ android:layout_width="0dp"
+ android:layout_height="1px"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <View
+ android:id="@+id/navigation_bar_shade"
+ android:layout_width="0dp"
+ android:layout_height="1px"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:id="@+id/coordinator_main"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ app:elevation="0dp">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ style="?attr/collapsingToolbarLayoutMediumStyle"
+ android:id="@+id/toolbar_settings_layout"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_settings"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:layout_collapseMode="pin" />
+
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:id="@+id/frame_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="12dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <View
+ android:id="@+id/navigation_bar_shade"
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_gravity="bottom|center_horizontal" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.card.MaterialCardView
+ style="?attr/materialCardViewElevatedStyle"
+ android:id="@+id/card_game"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:clipToPadding="false"
+ android:focusable="true"
+ android:transitionName="card_game"
+ android:layout_gravity="center"
+ app:cardElevation="0dp"
+ app:cardCornerRadius="12dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="6dp">
+
+ <com.google.android.material.card.MaterialCardView
+ style="?attr/materialCardViewElevatedStyle"
+ android:id="@+id/card_game_art"
+ android:layout_width="150dp"
+ android:layout_height="150dp"
+ app:cardCornerRadius="4dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ImageView
+ android:id="@+id/image_game_screen"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:src="@drawable/default_icon" />
+
+ </com.google.android.material.card.MaterialCardView>
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:id="@+id/text_game_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAlignment="center"
+ android:textSize="14sp"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:ellipsize="none"
+ android:requiresFadingEdge="horizontal"
+ app:layout_constraintEnd_toEndOf="@+id/card_game_art"
+ app:layout_constraintStart_toStartOf="@+id/card_game_art"
+ app:layout_constraintTop_toBottomOf="@+id/card_game_art"
+ tools:text="The Legend of Zelda: Skyward Sword" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/materialCardViewFilledStyle"
+ android:id="@+id/option_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="12dp"
+ android:layout_marginHorizontal="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:backgroundTint="?attr/colorSurfaceVariant"
+ android:clickable="true"
+ android:focusable="true">
+
+ <LinearLayout
+ android:id="@+id/option_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/option_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginStart="24dp"
+ android:layout_gravity="center_vertical"
+ app:tint="?attr/colorOnSurface" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="10dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:id="@+id/option_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textStyle="bold"
+ android:textSize="16sp"
+ tools:text="@string/install_prod_keys" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodySmall"
+ android:id="@+id/option_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textSize="14sp"
+ android:layout_marginTop="5dp"
+ tools:text="@string/install_prod_keys_description" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/edit_text_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="24dp"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="none" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="16dp">
+
+ <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"/>
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.HeadlineLarge"
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ tools:text="@string/license_adreno_tools" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:id="@+id/text_link"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_marginTop="16dp"
+ android:autoLink="all"
+ tools:text="@string/license_adreno_tools_link" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:id="@+id/text_copyright"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_marginTop="16dp"
+ android:textStyle="bold"
+ tools:text="@string/license_adreno_tools_copyright" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:id="@+id/text_license"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginVertical="16dp"
+ android:autoLink="all"
+ tools:text="@string/license_adreno_tools_text" />
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/input_scale_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/emulation_control_scale"
+ android:textAlignment="viewStart"
+ android:textSize="16sp"
+ app:layout_constraintStart_toStartOf="@+id/input_scale_slider"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/input_scale_slider"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/input_scale_name" />
+
+ <TextView
+ android:id="@+id/input_scale_value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ app:layout_constraintBottom_toTopOf="@+id/input_scale_slider"
+ app:layout_constraintEnd_toEndOf="@+id/input_scale_slider"
+ tools:text="100%" />
+
+ <TextView
+ android:id="@+id/input_opacity_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/emulation_control_opacity"
+ android:textAlignment="viewStart"
+ android:textSize="16sp"
+ app:layout_constraintStart_toStartOf="@+id/input_opacity_slider"
+ app:layout_constraintTop_toBottomOf="@+id/input_scale_slider" />
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/input_opacity_slider"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/input_opacity_name" />
+
+ <TextView
+ android:id="@+id/input_opacity_value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ app:layout_constraintBottom_toTopOf="@+id/input_opacity_slider"
+ app:layout_constraintEnd_toEndOf="@+id/input_opacity_slider"
+ tools:text="100%" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical">
+
+ <com.google.android.material.progressindicator.LinearProgressIndicator
+ android:id="@+id/progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="24dp"
+ app:trackCornerRadius="4dp" />
+
+ <TextView
+ android:id="@+id/progress_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="24dp"
+ android:layout_marginRight="24dp"
+ android:layout_marginBottom="24dp"
+ android:gravity="end" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/text_value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="@dimen/spacing_medlarge"
+ android:layout_marginTop="@dimen/spacing_medlarge"
+ tools:text="75" />
+
+ <TextView
+ android:id="@+id/text_units"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/text_value"
+ android:layout_toEndOf="@+id/text_value"
+ tools:text="%" />
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/slider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_below="@+id/text_value"
+ android:layout_marginBottom="@dimen/spacing_medlarge"
+ android:layout_marginLeft="@dimen/spacing_large"
+ android:layout_marginRight="@dimen/spacing_large" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/coordinator_about"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_about"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_about"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:title="@string/about"
+ app:navigationIcon="@drawable/ic_back" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:id="@+id/scroll_about"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"
+ android:fadeScrollbars="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <LinearLayout
+ android:id="@+id/content_about"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/image_logo"
+ android:layout_width="250dp"
+ android:layout_height="250dp"
+ android:layout_marginTop="20dp"
+ android:layout_gravity="center_horizontal"
+ android:src="@drawable/ic_yuzu_title" />
+
+ <com.google.android.material.divider.MaterialDivider
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp"
+ android:layout_marginTop="28dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="16dp"
+ android:paddingHorizontal="16dp"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:textAlignment="viewStart"
+ android:text="@string/about" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="6dp"
+ android:textAlignment="viewStart"
+ android:text="@string/about_app_description" />
+
+ </LinearLayout>
+
+ <com.google.android.material.divider.MaterialDivider
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp" />
+
+ <LinearLayout
+ android:id="@+id/button_contributors"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="16dp"
+ android:paddingHorizontal="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:textAlignment="viewStart"
+ android:text="@string/contributors" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="6dp"
+ android:textAlignment="viewStart"
+ android:text="@string/contributors_description" />
+
+ </LinearLayout>
+
+ <com.google.android.material.divider.MaterialDivider
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp" />
+
+ <LinearLayout
+ android:id="@+id/button_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="16dp"
+ android:paddingHorizontal="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:textAlignment="viewStart"
+ android:text="@string/licenses" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="6dp"
+ android:textAlignment="viewStart"
+ android:text="@string/licenses_description" />
+
+ </LinearLayout>
+
+ <com.google.android.material.divider.MaterialDivider
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp" />
+
+ <LinearLayout
+ android:id="@+id/button_build_hash"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="16dp"
+ android:paddingHorizontal="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:textAlignment="viewStart"
+ android:text="@string/build" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/text_build_hash"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="6dp"
+ android:textAlignment="viewStart"
+ tools:text="abc123" />
+
+ </LinearLayout>
+
+ <com.google.android.material.divider.MaterialDivider
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="16dp"
+ android:layout_marginHorizontal="40dp">
+
+ <Button
+ style="?attr/materialIconButtonStyle"
+ android:id="@+id/button_discord"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ app:icon="@drawable/ic_discord"
+ app:iconTint="?attr/colorOnSurface"
+ app:iconSize="24dp"
+ app:iconGravity="textEnd" />
+
+ <Button
+ style="?attr/materialIconButtonStyle"
+ android:id="@+id/button_website"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ app:icon="@drawable/ic_website"
+ app:iconTint="?attr/colorOnSurface"
+ app:iconSize="24dp"
+ app:iconGravity="textEnd" />
+
+ <Button
+ android:id="@+id/button_github"
+ style="?attr/materialIconButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ app:icon="@drawable/ic_github"
+ app:iconTint="?attr/colorOnSurface"
+ app:iconSize="24dp"
+ app:iconGravity="textEnd" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_about"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_ea"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_about"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/early_access" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:id="@+id/scroll_ea"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingBottom="20dp"
+ android:scrollbars="vertical"
+ android:fadeScrollbars="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <LinearLayout
+ android:id="@+id/card_ea"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginVertical="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:background="@drawable/premium_background"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginHorizontal="20dp"
+ android:text="@string/early_access_benefits"
+ android:textAlignment="center"
+ android:textStyle="bold" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/cutting_edge_features"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/early_access_updates"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/no_manual_installation"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/prioritized_support"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/helping_game_preservation"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginHorizontal="20dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_check_circle"
+ app:tint="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="20dp"
+ android:text="@string/our_eternal_gratitude"
+ android:textAlignment="viewStart"
+ android:layout_gravity="start|center_vertical" />
+
+ </LinearLayout>
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/are_you_interested"
+ android:layout_marginTop="80dp"
+ android:layout_marginHorizontal="20dp"
+ android:textStyle="bold"
+ android:textAlignment="center" />
+
+ <com.google.android.material.card.MaterialCardView
+ style="?attr/materialCardViewFilledStyle"
+ android:id="@+id/get_early_access_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginHorizontal="20dp"
+ android:layout_marginBottom="28dp"
+ android:background="?attr/selectableItemBackground"
+ android:backgroundTint="@android:color/black">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/get_early_access"
+ android:layout_marginHorizontal="20dp"
+ android:layout_marginVertical="8dp"
+ android:textColor="@android:color/white"
+ android:textStyle="bold"
+ android:textAlignment="center" />
+
+ </com.google.android.material.card.MaterialCardView>
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
+</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 @@
+<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:keepScreenOn="true"
+ tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment"
+ tools:openDrawer="start">
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- This is what everything is rendered to during emulation -->
+ <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
+ android:id="@+id/surface_emulation"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:focusable="false"
+ android:focusableInTouchMode="false" />
+
+ <FrameLayout
+ android:id="@+id/overlay_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="bottom">
+
+ <!-- This is the onscreen input overlay -->
+ <org.yuzu.yuzu_emu.overlay.InputOverlay
+ android:id="@+id/surface_input_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <TextView
+ android:id="@+id/show_fps_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:clickable="false"
+ android:focusable="false"
+ android:shadowColor="@android:color/black"
+ android:textColor="@android:color/white"
+ android:textSize="12sp"
+ tools:ignore="RtlHardcoded" />
+
+ <Button
+ style="@style/Widget.Material3.Button.ElevatedButton"
+ android:id="@+id/done_control_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/emulation_done"
+ android:visibility="gone" />
+ </FrameLayout>
+
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+ <com.google.android.material.navigation.NavigationView
+ android:id="@+id/in_game_menu"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start|bottom"
+ app:headerLayout="@layout/header_in_game"
+ app:menu="@menu/menu_in_game" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/swipe_refresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:clipToPadding="false">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/notice_text"
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:padding="@dimen/spacing_large"
+ android:text="@string/empty_gamelist"
+ android:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/grid_games"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ tools:listitem="@layout/card_game" />
+
+ </RelativeLayout>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/scroll_view_settings"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:scrollbars="vertical"
+ android:fadeScrollbars="false"
+ android:clipToPadding="false">
+
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:id="@+id/linear_layout_settings"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="?attr/colorSurface">
+
+ <ImageView
+ android:id="@+id/logo_image"
+ android:layout_width="128dp"
+ android:layout_height="128dp"
+ android:layout_margin="64dp"
+ android:layout_gravity="center_horizontal"
+ android:src="@drawable/ic_yuzu_full" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/home_settings_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </androidx.appcompat.widget.LinearLayoutCompat>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:title="@string/licenses"
+ app:navigationIcon="@drawable/ic_back" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/constraint_search"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:clipToPadding="false">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/divider">
+
+ <LinearLayout
+ android:id="@+id/no_results_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/icon_no_results"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ android:src="@drawable/ic_search" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/notice_text"
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:text="@string/search_and_filter_games"
+ tools:visibility="visible" />
+
+ </LinearLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/grid_games_search"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
+
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/frame_search"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:layout_marginHorizontal="20dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/search_background"
+ style="?attr/materialCardViewFilledStyle"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ app:cardCornerRadius="28dp">
+
+ <LinearLayout
+ android:id="@+id/search_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="56dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="28dp"
+ android:layout_height="28dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="24dp"
+ android:src="@drawable/ic_search"
+ app:tint="?attr/colorOnSurfaceVariant" />
+
+ <EditText
+ android:id="@+id/search_text"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:hint="@string/home_search_games"
+ android:inputType="text"
+ android:maxLines="1"
+ android:imeOptions="flagNoFullscreen" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/clear_button"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical|end"
+ android:layout_marginEnd="24dp"
+ android:background="?attr/selectableItemBackground"
+ android:src="@drawable/ic_clear"
+ android:visibility="invisible"
+ app:tint="?attr/colorOnSurfaceVariant"
+ tools:visibility="visible" />
+
+ </com.google.android.material.card.MaterialCardView>
+
+ </FrameLayout>
+
+ <HorizontalScrollView
+ android:id="@+id/horizontalScrollView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:scrollbars="none"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/frame_search">
+
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingVertical="4dp"
+ app:chipSpacingHorizontal="12dp"
+ app:singleLine="true"
+ app:singleSelection="true">
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_recently_played"
+ style="@style/Widget.Material3.Chip.Suggestion.Elevated"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/search_recently_played"
+ app:chipCornerRadius="28dp" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_recently_added"
+ style="@style/Widget.Material3.Chip.Suggestion.Elevated"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/search_recently_added"
+ app:chipCornerRadius="28dp" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_retail"
+ style="@style/Widget.Material3.Chip.Suggestion.Elevated"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/search_retail"
+ app:chipCornerRadius="28dp" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_homebrew"
+ style="@style/Widget.Material3.Chip.Suggestion.Elevated"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/search_homebrew"
+ app:chipCornerRadius="28dp" />
+
+ </com.google.android.material.chip.ChipGroup>
+
+ </HorizontalScrollView>
+
+ <com.google.android.material.divider.MaterialDivider
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="20dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/horizontalScrollView" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_settings"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:clipToPadding="false" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/setup_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/viewPager2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:clipToPadding="false"
+ android:layout_marginBottom="16dp"
+ app:layout_constraintBottom_toTopOf="@+id/button_next"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="12dp"
+ android:text="@string/next"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_back"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="12dp"
+ android:text="@string/back"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.textview.MaterialTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/text_game_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:textAppearance="?attr/textAppearanceHeadlineMedium"
+ android:textColor="?attr/colorOnSurface"
+ android:textAlignment="viewStart"
+ tools:text="Super Mario 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:minHeight="72dp"
+ android:padding="@dimen/spacing_large">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.HeadlineMedium"
+ android:id="@+id/text_setting_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:textSize="16sp"
+ android:textAlignment="viewStart"
+ app:lineHeight="28dp"
+ tools:text="Setting Name" />
+
+ <TextView
+ style="@style/TextAppearance.Material3.BodySmall"
+ android:id="@+id/text_setting_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignStart="@+id/text_setting_name"
+ android:layout_below="@+id/text_setting_name"
+ android:layout_marginTop="@dimen/spacing_small"
+ android:visibility="visible"
+ android:textAlignment="viewStart"
+ tools:text="@string/app_disclaimer" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:minHeight="72dp"
+ android:paddingStart="@dimen/spacing_large"
+ android:paddingEnd="24dp"
+ android:paddingVertical="@dimen/spacing_large">
+
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:id="@+id/switch_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodySmall"
+ android:id="@+id/text_setting_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignStart="@+id/text_setting_name"
+ android:layout_below="@+id/text_setting_name"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_small"
+ android:layout_toStartOf="@+id/switch_widget"
+ android:textAlignment="viewStart"
+ tools:text="@string/frame_limit_enable_description" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.HeadlineMedium"
+ android:id="@+id/text_setting_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_toStartOf="@+id/switch_widget"
+ android:textSize="16sp"
+ android:textAlignment="viewStart"
+ app:lineHeight="28dp"
+ tools:text="@string/frame_limit_enable" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:paddingVertical="4dp"
+ android:paddingHorizontal="@dimen/spacing_large">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleSmall"
+ android:id="@+id/text_header_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:textColor="?attr/colorPrimary"
+ android:textAlignment="viewStart"
+ android:textStyle="bold"
+ tools:text="CPU Settings" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="64dp"
+ android:layout_marginBottom="32dp"
+ app:layout_constraintBottom_toTopOf="@+id/text_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHeight_max="220dp"
+ app:layout_constraintHeight_min="110dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="spread"
+ app:layout_constraintWidth_max="220dp"
+ app:layout_constraintWidth_min="110dp"
+ app:layout_constraintVertical_weight="3" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/text_title"
+ style="@style/TextAppearance.Material3.DisplayMedium"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnSurface"
+ android:textStyle="bold"
+ app:layout_constraintBottom_toTopOf="@+id/text_description"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/icon"
+ app:layout_constraintVertical_weight="1.3"
+ tools:text="@string/welcome" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/text_description"
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:textAlignment="center"
+ android:textSize="26sp"
+ android:paddingHorizontal="16dp"
+ app:layout_constraintBottom_toTopOf="@+id/button_action"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/text_title"
+ app:layout_constraintVertical_weight="2"
+ app:lineHeight="40sp"
+ tools:text="@string/welcome_description" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_action"
+ android:layout_width="wrap_content"
+ android:layout_height="56dp"
+ android:textSize="20sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="48dp"
+ app:iconGravity="end"
+ app:iconSize="24sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/text_description"
+ tools:text="Get started" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/homeSettingsFragment"
+ android:icon="@drawable/selector_settings"
+ android:title="@string/home_settings" />
+
+ <item
+ android:id="@+id/searchFragment"
+ android:icon="@drawable/ic_search"
+ android:title="@string/home_search" />
+
+ <item
+ android:id="@+id/gamesFragment"
+ android:icon="@drawable/selector_cartridge"
+ android:title="@string/home_games" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_pause_emulation"
+ android:icon="@drawable/ic_pause"
+ android:title="@string/emulation_pause" />
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/ic_settings"
+ android:title="@string/preferences_settings" />
+
+ <item
+ android:id="@+id/menu_overlay_controls"
+ android:icon="@drawable/ic_controller"
+ android:title="@string/emulation_input_overlay" />
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/ic_exit"
+ android:title="@string/emulation_exit" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/gamesFragment"
+ android:icon="@drawable/selector_cartridge"
+ android:title="@string/home_games" />
+
+ <item
+ android:id="@+id/searchFragment"
+ android:icon="@drawable/ic_search"
+ android:title="@string/home_search" />
+
+ <item
+ android:id="@+id/homeSettingsFragment"
+ android:icon="@drawable/selector_settings"
+ android:title="@string/home_settings" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_toggle_fps"
+ android:title="@string/emulation_fps_counter"
+ android:checkable="true" />
+
+ <item
+ android:id="@+id/menu_edit_overlay"
+ android:title="@string/emulation_touch_overlay_edit" />
+
+ <item
+ android:id="@+id/menu_adjust_overlay"
+ android:title="@string/emulation_control_adjust" />
+
+ <item
+ android:id="@+id/menu_toggle_controls"
+ android:title="@string/emulation_toggle_controls" />
+
+ <item
+ android:id="@+id/menu_show_overlay"
+ android:title="@string/emulation_show_overlay"
+ android:checkable="true" />
+
+ <item
+ android:id="@+id/menu_rel_stick_center"
+ android:title="@string/emulation_rel_stick_center"
+ android:checkable="true" />
+
+ <item
+ android:id="@+id/menu_dpad_slide"
+ android:title="@string/emulation_dpad_slide"
+ android:checkable="true" />
+
+ <item
+ android:id="@+id/menu_haptics"
+ android:title="@string/emulation_haptics"
+ android:checkable="true" />
+
+ <item
+ android:id="@+id/menu_reset_overlay"
+ android:title="@string/emulation_touch_overlay_reset" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/home_navigation"
+ app:startDestination="@id/gamesFragment">
+
+ <fragment
+ android:id="@+id/gamesFragment"
+ android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
+ android:label="PlatformGamesFragment" />
+
+ <fragment
+ android:id="@+id/homeSettingsFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.HomeSettingsFragment"
+ android:label="HomeSettingsFragment" >
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_aboutFragment"
+ app:destination="@id/aboutFragment" />
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment"
+ app:destination="@id/earlyAccessFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/firstTimeSetupFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.SetupFragment"
+ android:label="FirstTimeSetupFragment" >
+ <action
+ android:id="@+id/action_firstTimeSetupFragment_to_gamesFragment"
+ app:destination="@id/gamesFragment"
+ app:popUpTo="@id/firstTimeSetupFragment"
+ app:popUpToInclusive="true" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/searchFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
+ android:label="SearchFragment" />
+
+ <fragment
+ android:id="@+id/aboutFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"
+ android:label="AboutFragment" >
+ <action
+ android:id="@+id/action_aboutFragment_to_licensesFragment"
+ app:destination="@id/licensesFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/earlyAccessFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.EarlyAccessFragment"
+ android:label="EarlyAccessFragment" />
+
+ <fragment
+ android:id="@+id/licensesFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment"
+ android:label="LicensesFragment" />
+
+</navigation>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..969223ef8
--- /dev/null
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,332 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Diese Software kann Spiele für die Nintendo Switch abspielen. Keine Spiele oder Spielekeys sind enthalten.&lt;br /&gt;&lt;br /&gt;Bevor du beginnst, bitte halte deine <![CDATA[<b> prod.keys </b>]]> auf deinem Gerät bereit. .&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Mehr Infos</a>]]></string>
+ <string name="emulation_notification_channel_name">Emulation ist aktiv</string>
+ <string name="emulation_notification_channel_description">Zeigt eine dauerhafte Benachrichtigung an, wenn die Emulation läuft.</string>
+ <string name="emulation_notification_running">yuzu läuft</string>
+ <string name="notice_notification_channel_name">Hinweise und Fehler</string>
+ <string name="notice_notification_channel_description">Zeigt Benachrichtigungen an, wenn etwas schief läuft.</string>
+ <string name="notification_permission_not_granted">Berechtigung für Benachrichtigungen nicht erlaubt!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Willkommen!</string>
+ <string name="welcome_description">Erfahre wie man &lt;b>yuzu&lt;/b> einrichtet und beginne mit der Emulation.</string>
+ <string name="get_started">Erste Schritte</string>
+ <string name="keys">Schlüssel</string>
+ <string name="keys_description">Wähle deine &lt;b>prod.keys&lt;/b> Datei mit dem Button unten aus.</string>
+ <string name="select_keys">Schlüssel auswählen</string>
+ <string name="games">Spiele</string>
+ <string name="games_description">Wähle mit dem Knopf unten den &lt;b>Spiele&lt;/b>-Ordner aus.</string>
+ <string name="done">Fertig</string>
+ <string name="done_description">Wir können loslegen.\nViel Spaß!</string>
+ <string name="text_continue">Fortsetzen</string>
+ <string name="next">Weiter</string>
+ <string name="back">Zurück</string>
+ <string name="add_games">Spiele hinzufügen</string>
+ <string name="add_games_description">Spieleverzeichnis auswählen</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Spiele</string>
+ <string name="home_search">Suche</string>
+ <string name="home_settings">Einstellungen</string>
+ <string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
+ <string name="search_and_filter_games">Spiele suchen und filtern</string>
+ <string name="select_games_folder">Spieleverzeichnis auswählen</string>
+ <string name="select_games_folder_description">Erlaubt yuzu die Spieleliste zu füllen</string>
+ <string name="add_games_warning">Auswahl des Spieleverzeichnisses überspringen?</string>
+ <string name="add_games_warning_description">Spiele werden in der Spieleliste nicht angezeigt, wenn kein Ordner ausgewählt ist.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Spiele suchen</string>
+ <string name="games_dir_selected">Spieleverzeichnis ausgewählt</string>
+ <string name="install_prod_keys">prod.keys installieren</string>
+ <string name="install_prod_keys_description">Zum Entschlüsseln von Spielen benötigt</string>
+ <string name="install_prod_keys_warning">Hinzufügen der Schlüssel überspringen?</string>
+ <string name="install_prod_keys_warning_description">Für die Emulation von Spielen sind gültige Schlüssel erforderlich. Wenn du fortfährst, funktionieren nur Homebrew-Anwendungen.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Benachrichtigungen</string>
+ <string name="notifications_description">Erteile mit dem Knopf unten die Berechtigung, Benachrichtigungen zu senden.</string>
+ <string name="give_permission">Berechtigung erteilen</string>
+ <string name="notification_warning_description">yuzu wird dich nicht über wichtige Informationen benachrichtigen können.</string>
+ <string name="permission_denied">Zugriff verweigert</string>
+ <string name="permission_denied_description">Du hast diese Berechtigung zu oft verweigert und musst sie nun manuell in den Systemeinstellungen erteilen.</string>
+ <string name="about">Über</string>
+ <string name="about_description">Build-Version, Credits und mehr</string>
+ <string name="warning_help">Hilfe</string>
+ <string name="warning_skip">Überspringen</string>
+ <string name="warning_cancel">Abbrechen</string>
+ <string name="install_amiibo_keys">Amiibo-Schlüssel installieren</string>
+ <string name="install_amiibo_keys_description">Benötigt um Amiibos im Spiel zu verwenden</string>
+ <string name="invalid_keys_file">Ungültige Schlüsseldatei ausgewählt</string>
+ <string name="install_keys_success">Schlüssel erfolgreich installiert</string>
+ <string name="reading_keys_failure">Fehler beim Lesen der Schlüssel</string>
+ <string name="invalid_keys_error">Ungültige Schlüssel</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_gpu_driver">GPU-Treiber installieren</string>
+ <string name="install_gpu_driver_description">Alternative Treiber für eventuell bessere Leistung oder Genauigkeit installieren</string>
+ <string name="advanced_settings">Erweiterte Einstellungen</string>
+ <string name="settings_description">Emulatoreinstellungen konfigurieren</string>
+ <string name="search_recently_played">Kürzlich gespielt</string>
+ <string name="search_recently_added">Kürzlich hinzugefügt</string>
+ <string name="search_retail">Spiele</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">yuzu-Ordner öffnen</string>
+ <string name="open_user_folder_description">yuzu\'s interne Dateien verwalten</string>
+ <string name="theme_and_color_description">Das Aussehen der App ändern</string>
+ <string name="no_file_manager">Kein Dateimanager gefunden</string>
+ <string name="notification_no_directory_link">yuzu-Verzeichnis konnte nicht geöffnet werden</string>
+ <string name="notification_no_directory_link_description">Bitte suche den Benutzerordner manuell über die Seitenleiste des Dateimanagers.</string>
+ <string name="manage_save_data">Speicherdaten verwalten</string>
+ <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string>
+ <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string>
+ <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string>
+ <string name="save_file_imported_success">Erfolgreich importiert</string>
+ <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string>
+ <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string>
+ <string name="import_saves">Importieren</string>
+ <string name="export_saves">Exportieren</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia ist nicht real</string>
+ <string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
+ <string name="about_app_description">Ein quelloffener Switch-Emulator</string>
+ <string name="contributors">Beitragende</string>
+ <string name="contributors_description">Gemacht mit \u2764 vom yuzu Team</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Build</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Early Access</string>
+ <string name="get_early_access">Early Access bekommen</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Neueste Features, frühzeitiger Zugriff auf Updates und mehr</string>
+ <string name="early_access_benefits">Early Access Vorteile</string>
+ <string name="cutting_edge_features">Neueste Features</string>
+ <string name="early_access_updates">Früherer Zugriff auf Updates</string>
+ <string name="no_manual_installation">Keine manuelle Installation</string>
+ <string name="prioritized_support">Priorisierte Unterstützung</string>
+ <string name="our_eternal_gratitude">Unsere ewige Dankbarkeit</string>
+ <string name="are_you_interested">Bist du interessiert?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Geschwindigkeitsbegrenzung aktivieren</string>
+ <string name="frame_limit_enable_description">Wenn aktiviert, wird die Emulationsgeschwindigkeit auf einen Prozentsatz der normalen Geschwindigkeit begrenzt.</string>
+ <string name="frame_limit_slider">Geschwindkeitsbegrenzung in Prozent</string>
+ <string name="frame_limit_slider_description">Legt den Prozentsatz der Bergrenzung der Emulationsgeschwindigkeit fest. Mit dem Standardwert von 100% wird die Emulation auf die normale Geschwindigkeit begrenzt. Höhere oder niedrigere Werte erhöhen oder verringern die Geschwindigkeitsbegrenzung.</string>
+ <string name="cpu_accuracy">CPU-Genauigkeit</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Dock-Modus</string>
+ <string name="use_docked_mode_description">Emuliert im Dock-Modus, was die Auflösung verbessert, aber die Leistung senkt.</string>
+ <string name="emulated_region">Emulierte Region</string>
+ <string name="emulated_language">Emulierte Sprache</string>
+ <string name="select_rtc_date">RTC-Datum auswählen</string>
+ <string name="select_rtc_time">RTC-Zeit auswählen</string>
+ <string name="use_custom_rtc">Benutzerdefinierte RTC aktivieren</string>
+ <string name="use_custom_rtc_description">Mit dieser Einstellung kann eine benutzerdefinierte Echtzeituhr unabhängig von der aktuellen Systemzeit verwendet werden.</string>
+ <string name="set_custom_rtc">Benutzerdefinierte RTC einstellen</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Genauigkeitsstufe</string>
+ <string name="renderer_resolution">Auflösung</string>
+ <string name="renderer_vsync">VSync-Modus</string>
+ <string name="renderer_aspect_ratio">Seitenverhältnis</string>
+ <string name="renderer_scaling_filter">Fensteranpassungsfilter</string>
+ <string name="renderer_anti_aliasing">Kantenglättungs-Methode</string>
+ <string name="renderer_force_max_clock">Maximale Taktfrequenz erzwingen (nur Adreno)</string>
+ <string name="renderer_force_max_clock_description">Erzwingt den Betrieb der GPU mit der maximal möglichen Taktfrequenz (Temperaturbeschränkungen werden weiterhin angewendet).</string>
+ <string name="renderer_asynchronous_shaders">Asynchrone Shader nutzen</string>
+ <string name="renderer_asynchronous_shaders_description">Kompiliert Shader asynchron, was Ruckler reduziert, aber zu Glitches führen kann.</string>
+ <string name="renderer_debug">Grafik-Debugging aktivieren</string>
+ <string name="renderer_debug_description">Wenn aktiviert, schaltet die Grafik-API in einen langsameren Debugging-Modus.</string>
+ <string name="use_disk_shader_cache">Nutze Festplatten-Shader-Cache</string>
+ <string name="use_disk_shader_cache_description">Ruckeln wird durch das Speichern und Laden von generierten Shadern auf der Festplatte reduziert.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Lautstärke</string>
+ <string name="audio_volume_description">Legt die Lautstärke der Audioausgabe fest.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Standard</string>
+ <string name="ini_saved">Einstellungen gespeichert</string>
+ <string name="gameid_saved">Einstellungen für %1$s gespeichert</string>
+ <string name="error_saving">Fehler beim Speichern von %1$s.ini: %2$s</string>
+ <string name="loading">Lädt...</string>
+ <string name="reset_setting_confirmation">Möchtest du diese Einstellung auf den Standardwert zurücksetzen?</string>
+ <string name="reset_to_default">Auf Standard zurücksetzen</string>
+ <string name="reset_all_settings">Alle Einstellungen zurücksetzen?</string>
+ <string name="reset_all_settings_description">Alle erweiterten Einstellungen werden auf ihren Standardwert zurückgesetzt. Dies kann nicht rückgängig gemacht werden.</string>
+ <string name="settings_reset">Einstellungen zurückgesetzt</string>
+ <string name="close">Schließen</string>
+ <string name="learn_more">Mehr erfahren</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">GPU-Treiber auswählen</string>
+ <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>
+ <string name="select_gpu_driver_install">Installieren</string>
+ <string name="select_gpu_driver_default">Standard</string>
+ <string name="select_gpu_driver_install_success">%s wurde installiert</string>
+ <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string>
+ <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>
+ <string name="system_gpu_driver">System GPU-Treiber</string>
+ <string name="installing_driver">Treiber wird installiert...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Einstellungen</string>
+ <string name="preferences_general">Allgemein</string>
+ <string name="preferences_system">System</string>
+ <string name="preferences_graphics">Grafik</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Theme und Farbe</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Das ROM ist verschlüsselt</string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Bitte stelle sicher dass die <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> Datei installiert ist, damit Spiele entschlüsselt werden können.]]></string>
+ <string name="loader_error_video_core">Bei der Initialisierung des Videokerns ist ein Fehler aufgetreten</string>
+ <string name="loader_error_video_core_description">Dies wird normalerweise durch einen inkompatiblen GPU-Treiber verursacht. Die Installation eines passenden GPU-Treibers kann dieses Problem beheben.</string>
+ <string name="loader_error_invalid_format">ROM konnte nicht geladen werden</string>
+ <string name="loader_error_file_not_found">ROM-Datei existiert nicht</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Emulation beenden</string>
+ <string name="emulation_done">Fertig</string>
+ <string name="emulation_fps_counter">FPS Zähler</string>
+ <string name="emulation_toggle_controls">Steuerung umschalten</string>
+ <string name="emulation_rel_stick_center">Relative Stick-Mitte</string>
+ <string name="emulation_dpad_slide">DPad Slide</string>
+ <string name="emulation_haptics">Haptik</string>
+ <string name="emulation_show_overlay">Overlay anzeigen</string>
+ <string name="emulation_toggle_all">Alle umschalten</string>
+ <string name="emulation_control_adjust">Overlay anpassen</string>
+ <string name="emulation_control_scale">Größe</string>
+ <string name="emulation_control_opacity">Transparenz</string>
+ <string name="emulation_touch_overlay_reset">Overlay zurücksetzen</string>
+ <string name="emulation_touch_overlay_edit">Overlay bearbeiten</string>
+ <string name="emulation_pause">Emulation pausieren</string>
+ <string name="emulation_unpause">Emulation fortsetzen</string>
+ <string name="emulation_input_overlay">Overlay-Optionen</string>
+ <string name="emulation_game_loading">Spiel lädt…</string>
+
+ <string name="load_settings">Lädt Einstellungen...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Software-Tastatur</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abbrechen</string>
+ <string name="continue_button">Fortsetzen</string>
+ <string name="system_archive_not_found">Systemarchiv nicht gefunden</string>
+ <string name="system_archive_general">Ein System-Archiv</string>
+ <string name="save_load_error">Speicher-/Ladefehler</string>
+ <string name="fatal_error">Schwerwiegender Fehler</string>
+ <string name="fatal_error_message">Ein schwerwiegender Fehler ist aufgetreten. Einzelheiten wurden im Log protokolliert.\nDas Fortsetzen der Emulation kann zu Abstürzen und Bugs führen.</string>
+ <string name="performance_warning">Das Deaktivieren dieser Einstellung führt zu erheblichen Leistungsverlusten! Für ein optimales Erlebnis wird empfohlen, sie aktiviert zu lassen.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japan</string>
+ <string name="region_usa">USA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Australien</string>
+ <string name="region_china">China</string>
+ <string name="region_korea">Korea</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japanisch (日本語)</string>
+ <string name="language_english">Englisch</string>
+ <string name="language_french">Französisch (Français)</string>
+ <string name="langauge_german">Deutsch (German)</string>
+ <string name="language_italian">Italienisch (Italiano)</string>
+ <string name="language_spanish">Spanisch (Español)</string>
+ <string name="language_chinese">Chinesisch (简体中文)</string>
+ <string name="language_korean">Koreanisch (한국어)</string>
+ <string name="language_dutch">Niederländisch (Nederlands)</string>
+ <string name="language_portuguese">Portugiesisch (Português)</string>
+ <string name="language_russian">Russisch (Русский)</string>
+ <string name="language_taiwanese">Taiwanesisch (台湾)</string>
+ <string name="language_british_english">Britisches Englisch</string>
+ <string name="language_canadian_french">Kanadisches Französisch (Français canadien)</string>
+ <string name="language_latin_american_spanish">Lateinamerikanisches Spanisch (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Vereinfachtes Chinesisch (简体中文)</string>
+ <string name="language_traditional_chinese">Traditionelles Chinesisch (正體中文)</string>
+ <string name="language_brazilian_portuguese">Brasilianisches Portugiesisch (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Keiner</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Hoch</string>
+ <string name="renderer_accuracy_extreme">Extrem (Langsam)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Langsam)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Langsam)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Langsam)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Direkt (Aus)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (An)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Nächste-Nachbarn</string>
+ <string name="scaling_filter_bilinear">Bilinear</string>
+ <string name="scaling_filter_bicubic">Bikubisch</string>
+ <string name="scaling_filter_gaussian">Gaussian</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Keiner</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Standard (16:9)</string>
+ <string name="ratio_force_four_three">4:3 erzwingen</string>
+ <string name="ratio_force_twenty_one_nine">21:9 erzwingen</string>
+ <string name="ratio_force_sixteen_ten">Erzwinge 16:10</string>
+ <string name="ratio_stretch">Auf Fenster anpassen</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Akkurat</string>
+ <string name="cpu_accuracy_unsafe">Unsicher</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Langsam)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">Steuerkreuz</string>
+ <string name="gamepad_left_stick">Linker Analogstick</string>
+ <string name="gamepad_right_stick">Rechter Analogstick</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Screenshot</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Shader werden vorbereitet</string>
+ <string name="building_shaders">Shader werden erstellt</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">App-Theme ändern</string>
+ <string name="theme_default">Standard</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Theme-Modus ändern</string>
+ <string name="theme_mode_follow_system">System folgen</string>
+ <string name="theme_mode_light">Hell</string>
+ <string name="theme_mode_dark">Dunkel</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Schwarze Hintergünde verwenden</string>
+ <string name="use_black_backgrounds_description">Bei Verwendung des dunklen Themes, schwarze Hintergründe verwenden.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..986e80e50
--- /dev/null
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Este software ejecuta juegos para la videoconsola Nintendo Switch. Los videojuegos o keys no vienen incluidos.&lt;br /&gt;&lt;br /&gt;Antes de empezar, por favor, localice el archivo <![CDATA[<b> prod.keys </b>]]>en el almacenamiento de su dispositivo..&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Saber más</a>]]></string>
+ <string name="emulation_notification_channel_name">Emulación activa</string>
+ <string name="emulation_notification_channel_description">Muestra una notificación persistente cuando la emulación está activa.</string>
+ <string name="emulation_notification_running">yuzu esta ejecutándose</string>
+ <string name="notice_notification_channel_name">Avisos y errores</string>
+ <string name="notice_notification_channel_description">Mostrar notificaciones cuándo algo vaya mal.</string>
+ <string name="notification_permission_not_granted">¡Permisos de notificación no concedidos!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">¡Bienvenido!</string>
+ <string name="welcome_description">Aprende cómo configurar &lt;b>yuzu&lt;/b> y avanza a la emulación.</string>
+ <string name="get_started">Empezar</string>
+ <string name="keys">Claves</string>
+ <string name="keys_description">Selecciona el archivo &lt;b>prod.keys&lt;/b> utilizando el botón de abajo.</string>
+ <string name="select_keys">Seleccionar las claves</string>
+ <string name="games">Juegos</string>
+ <string name="games_description">Selecciona la carpeta &lt;b>Games&lt;/b> utilizando el botón de abajo</string>
+ <string name="done">Hecho</string>
+ <string name="done_description">Todo listo.\n¡Disfrute de sus juegos!</string>
+ <string name="text_continue">Continuar</string>
+ <string name="next">Siguiente</string>
+ <string name="back">Atrás</string>
+ <string name="add_games">Añadir Juegos</string>
+ <string name="add_games_description">Selecciona la carpeta de juegos</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Juegos</string>
+ <string name="home_search">Buscar</string>
+ <string name="home_settings">Ajustes</string>
+ <string name="empty_gamelist">No se ha encontrado ningún archivo o aún no se ha seleccionado ningún directorio de juegos.</string>
+ <string name="search_and_filter_games">Busca y filtra juegos</string>
+ <string name="select_games_folder">Seleccionar carpeta de juegos</string>
+ <string name="select_games_folder_description">Permite que yuzu llene la lista de juegos</string>
+ <string name="add_games_warning">¿Omitir la selección de la carpeta de juegos?</string>
+ <string name="add_games_warning_description">No se mostrará ningún juego si no se ha seleccionado una carpeta de juegos.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Buscar Juegos</string>
+ <string name="games_dir_selected">Directorio de juegos seleccionado</string>
+ <string name="install_prod_keys">Instalar prod.keys</string>
+ <string name="install_prod_keys_description">Requerido para descifrar juegos</string>
+ <string name="install_prod_keys_warning">¿Omitir agregar claves?</string>
+ <string name="install_prod_keys_warning_description">Se requieren claves válidas para emular juegos. Solo las aplicaciones homebrew funcionarán si continúas.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Notificaciones</string>
+ <string name="notifications_description">Otorgue el permiso de notificación con el botón de abajo.</string>
+ <string name="give_permission">Conceder permiso</string>
+ <string name="notification_warning">¿Omitir conceder el permiso de notificación?</string>
+ <string name="notification_warning_description">yuzu no podrá notificarte información importante.</string>
+ <string name="permission_denied">Permiso denegado</string>
+ <string name="permission_denied_description">Negó este permiso demasiadas veces y ahora debe otorgarlo manualmente en la configuración del sistema.</string>
+ <string name="about">Acerca de</string>
+ <string name="about_description">Versión, créditos y más</string>
+ <string name="warning_help">Ayuda</string>
+ <string name="warning_skip">Siguiente</string>
+ <string name="warning_cancel">Cancelar</string>
+ <string name="install_amiibo_keys">Instalar clave de Amiiboo</string>
+ <string name="install_amiibo_keys_description">Necesario para usar Amiibo en el juego</string>
+ <string name="invalid_keys_file">Archivo de claves inválido seleccionado</string>
+ <string name="install_keys_success">Claves instaladas correctamente</string>
+ <string name="reading_keys_failure">Error al leer las claves de cifrado</string>
+ <string name="invalid_keys_error">Claves de cifrado no válidas</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">El archivo seleccionado es incorrecto o está corrupto. Vuelva a redumpear sus claves.</string>
+ <string name="install_gpu_driver">Instalar driver de GPU</string>
+ <string name="install_gpu_driver_description">Instale drivers alternativos para obtener un rendimiento o una precisión potencialmente mejores</string>
+ <string name="advanced_settings">Opciones avanzadas</string>
+ <string name="settings_description">Configurar las opciones del emulador</string>
+ <string name="search_recently_played">Jugado recientemente</string>
+ <string name="search_recently_added">Añadido recientemente</string>
+ <string name="search_retail">Juegos</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Abrir la carpeta de yuzu</string>
+ <string name="open_user_folder_description">Administrar los archivos internos de yuzu</string>
+ <string name="theme_and_color_description">Modificar la apariencia de la aplicación</string>
+ <string name="no_file_manager">Explorador de archivos no encontrado</string>
+ <string name="notification_no_directory_link">No se pudo abrir la carpeta yuzu</string>
+ <string name="notification_no_directory_link_description">Por favor, busque la carpeta user con el panel lateral del explorador de archivos de forma manual.</string>
+ <string name="manage_save_data">Administrar datos de guardado</string>
+ <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string>
+ <string name="import_export_saves_description">Importar o exportar archivos de guardado</string>
+ <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string>
+ <string name="save_file_imported_success">Importado correctamente</string>
+ <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string>
+ <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string>
+ <string name="import_saves">Importar</string>
+ <string name="export_saves">Exportar</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia no es real</string>
+ <string name="copied_to_clipboard">Copiado al portapapeles</string>
+ <string name="about_app_description">Un emulador de Switch de código abierto</string>
+ <string name="contributors">Contribuidores</string>
+ <string name="contributors_description">Hecho con \u2764 del equipo yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Versión</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Early Access</string>
+ <string name="get_early_access">Conseguir Early Access</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Funciones de vanguardia, acceso anticipado a actualizaciones y más</string>
+ <string name="early_access_benefits">Beneficios Early Access</string>
+ <string name="cutting_edge_features">Características de vanguardia</string>
+ <string name="early_access_updates">Acceso anticipado a las actualizaciones</string>
+ <string name="no_manual_installation">Sin instalación manual</string>
+ <string name="prioritized_support">Soporte prioritario</string>
+ <string name="helping_game_preservation">Ayudarás a la preservación de juegos</string>
+ <string name="our_eternal_gratitude">Nuestra eterna gratitud</string>
+ <string name="are_you_interested">¿Estás interesado?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Activar limite de velocidad</string>
+ <string name="frame_limit_enable_description">Cuando está habilitado, la velocidad de emulación se limitará a un porcentaje específico de la velocidad normal.</string>
+ <string name="frame_limit_slider">Limitar porcentaje de velocidad</string>
+ <string name="frame_limit_slider_description">Especifica el porcentaje para limitar la velocidad de emulación. Con el valor predeterminado del 100 %, la emulación se limitará a la velocidad normal. Valores más altos o más bajos aumentarán o disminuirán el límite de velocidad.</string>
+ <string name="cpu_accuracy">Precisión de CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Modo sobremesa</string>
+ <string name="use_docked_mode_description">Emula en modo sobremesa, lo que aumenta la resolución perjudicando el rendimiento.</string>
+ <string name="emulated_region">Región emulada</string>
+ <string name="emulated_language">Idioma emulado</string>
+ <string name="select_rtc_date">Seleccionar Fecha RTC</string>
+ <string name="select_rtc_time">Seleccionar Tiempo RTC</string>
+ <string name="use_custom_rtc">Habilitar RTC Personalizado</string>
+ <string name="use_custom_rtc_description">Esta configuración le permite configurar un reloj de tiempo real personalizado diferente a la hora actual de su sistema</string>
+ <string name="set_custom_rtc">Establecer RTC Personalizado</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Nivel de precisión</string>
+ <string name="renderer_resolution">Resolución</string>
+ <string name="renderer_vsync">Modo VSync</string>
+ <string name="renderer_aspect_ratio">Relación de aspecto</string>
+ <string name="renderer_scaling_filter">Filtro de adaptación de ventana</string>
+ <string name="renderer_anti_aliasing">Metodo Anti Aliasing</string>
+ <string name="renderer_force_max_clock">Forzar velocidad al máximo (solo Adreno)</string>
+ <string name="renderer_force_max_clock_description">Fuerza a la GPU a ejecutarse a la velocidad máxima de reloj posible (se seguirán aplicando restricciones térmicas).</string>
+ <string name="renderer_asynchronous_shaders">Usar shaders asíncronos</string>
+ <string name="renderer_asynchronous_shaders_description">Compila shaders de forma asincrónica, lo que reducirá los parones pero puede introducir fallos.</string>
+ <string name="renderer_debug">Habilitar la depuración de gráficos</string>
+ <string name="renderer_debug_description">Cuando esté marcado, la API de gráficos entra en un modo de depuración más lento.</string>
+ <string name="use_disk_shader_cache">Usar caché de shaders en disco</string>
+ <string name="use_disk_shader_cache_description">Reduzca los parones almacenando y cargando shaders generados en el disco.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volumen</string>
+ <string name="audio_volume_description">Especifica el volumen de la salida de audio.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Predeterminado</string>
+ <string name="ini_saved">Configuración guardada</string>
+ <string name="gameid_saved">Configuración guardada para %1$s</string>
+ <string name="error_saving">Error guardando %1$s.ini: %2$s</string>
+ <string name="loading">Cargando...</string>
+ <string name="reset_setting_confirmation">¿Desea restablecer esta configuración a su valor predeterminado?</string>
+ <string name="reset_to_default">Restablecer a predeterminado</string>
+ <string name="reset_all_settings">¿Restablecer todas las configuraciones?</string>
+ <string name="reset_all_settings_description">Todas las configuraciones avanzadas se restablecerán a su configuración predeterminada. Esto no se puede deshacer.</string>
+ <string name="settings_reset">Reiniciar la configuracion</string>
+ <string name="close">Cerrar</string>
+ <string name="learn_more">Más información</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Seleccionar driver GPU</string>
+ <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>
+ <string name="select_gpu_driver_install">Instalar</string>
+ <string name="select_gpu_driver_default">Predeterminado</string>
+ <string name="select_gpu_driver_install_success">Instalado %s</string>
+ <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string>
+ <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>
+ <string name="system_gpu_driver">Driver GPU del sistema</string>
+ <string name="installing_driver">Instalando driver...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Ajustes</string>
+ <string name="preferences_general">General</string>
+ <string name="preferences_system">Sistema</string>
+ <string name="preferences_graphics">Gráficos</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Tema y color</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Su ROM está encriptada</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Por favor, siga las guías para redumpear <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">cartuchos de juegos</a> o <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">titulos instalados</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Por favor, compruebe que su archivo <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> está instalado, para que los juegos sean descifrados.]]></string>
+ <string name="loader_error_video_core">Ocurrió un error al inicializar el núcleo de video, posiblemente debido a una incompatibilidad con el driver seleccionado</string>
+ <string name="loader_error_video_core_description">Esto suele deberse a un driver de GPU incompatible. La instalación de un controlador de GPU personalizado puede resolver este problema.</string>
+ <string name="loader_error_invalid_format">No se pudo cargar la ROM</string>
+ <string name="loader_error_file_not_found">Archivo ROM no existe</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Salir de la emulación</string>
+ <string name="emulation_done">Hecho</string>
+ <string name="emulation_fps_counter">Contador de FPS</string>
+ <string name="emulation_toggle_controls">Alternar Controles</string>
+ <string name="emulation_rel_stick_center">Centro Relativo del Stick</string>
+ <string name="emulation_dpad_slide">Deslizamiento de la Cruceta</string>
+ <string name="emulation_haptics">Hápticos</string>
+ <string name="emulation_show_overlay">Mostrar pantalla</string>
+ <string name="emulation_toggle_all">Alternar Todo</string>
+ <string name="emulation_control_adjust">Ajustar pantalla</string>
+ <string name="emulation_control_scale">Escala</string>
+ <string name="emulation_control_opacity">Opacidad</string>
+ <string name="emulation_touch_overlay_reset">Reiniciar pantalla</string>
+ <string name="emulation_touch_overlay_edit">Editar pantalla</string>
+ <string name="emulation_pause">Pausar Emulación</string>
+ <string name="emulation_unpause">Reanudar Emulación</string>
+ <string name="emulation_input_overlay">Opciones de pantalla </string>
+ <string name="emulation_game_loading">Cargando juego...</string>
+
+ <string name="load_settings">Cargando configuración...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Software del teclado</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abortar</string>
+ <string name="continue_button">Continuar</string>
+ <string name="system_archive_not_found">Archivo del sistema no encontrado</string>
+ <string name="system_archive_not_found_message">%s no se ha encontrado. Vacíe los archivos de su sistema.\nContinuar con la emulación puede provocar bloqueos y errores.</string>
+ <string name="system_archive_general">Un archivo del sistema</string>
+ <string name="save_load_error">Error de Guardado/Carga</string>
+ <string name="fatal_error">Error fatal</string>
+ <string name="fatal_error_message">Ocurrió un error fatal. Consulte el registro para obtener más detalles.\nContinuar con la emulación puede provocar bloqueos y errores.</string>
+ <string name="performance_warning">¡Desactivar esta configuración reducirá significativamente el rendimiento de la emulación! Para obtener la mejor experiencia, se recomienda dejar esta configuración habilitada.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japón</string>
+ <string name="region_usa">EEUU</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Australia</string>
+ <string name="region_china">China</string>
+ <string name="region_korea">Corea</string>
+ <string name="region_taiwan">Taiwán</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japonés (日本語)</string>
+ <string name="language_english">Inglés (English)</string>
+ <string name="language_french">Francés (Français)</string>
+ <string name="langauge_german">Alemán (deutsch)</string>
+ <string name="language_italian">Italiano (Italiano)</string>
+ <string name="language_spanish">Español (Español)</string>
+ <string name="language_chinese">Chino (简体中文)</string>
+ <string name="language_korean">Coreano (한국어)</string>
+ <string name="language_dutch">Holandés (nederlands)</string>
+ <string name="language_portuguese">Portugués (Português)</string>
+ <string name="language_russian">Ruso (Русский)</string>
+ <string name="language_taiwanese">Taiwanés (台湾)</string>
+ <string name="language_british_english">Inglés británico</string>
+ <string name="language_canadian_french">Francés Canadiense (Français canadien)</string>
+ <string name="language_latin_american_spanish">Español Latinoamericano (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Chino Simplificado (简体中文)</string>
+ <string name="language_traditional_chinese">Chino tradicional (正體中文)</string>
+ <string name="language_brazilian_portuguese">Portugués Brasileño (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Ninguno</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Alto</string>
+ <string name="renderer_accuracy_extreme">Extremo (Lento)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">x1 (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Lento)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Lento)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Lento)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Inmediata (Desactivado)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (Activado)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relajado</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Vecino más próximo</string>
+ <string name="scaling_filter_bilinear">Bilineal</string>
+ <string name="scaling_filter_bicubic">Bicúbico</string>
+ <string name="scaling_filter_gaussian">Gaussiano</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolución</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Ninguno</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Predeterminado (16:9)</string>
+ <string name="ratio_force_four_three">Forzar 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Forzar 21:9</string>
+ <string name="ratio_force_sixteen_ten">Forzar 16:10</string>
+ <string name="ratio_stretch">Ajustar a la ventana</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Preciso</string>
+ <string name="cpu_accuracy_unsafe">Impreciso</string>
+ <string name="cpu_accuracy_paranoid">Paranoico (Lento)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">Cruceta</string>
+ <string name="gamepad_left_stick">Palanca izquierda</string>
+ <string name="gamepad_right_stick">Palanca derecha</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Captura de pantalla</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Preparando shaders</string>
+ <string name="building_shaders">Construyendo shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Cambiar Tema</string>
+ <string name="theme_default">Predeterminado</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Cambiar modo del tema</string>
+ <string name="theme_mode_follow_system">Igual al sistema</string>
+ <string name="theme_mode_light">Claro</string>
+ <string name="theme_mode_dark">Oscuro</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Usar Fondos Negros</string>
+ <string name="use_black_backgrounds_description">Cuando utilice el modo oscuro, aplique fondos negros.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..14a9b2d5c
--- /dev/null
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Ce logiciel exécutera des jeux pour la console de jeu Nintendo Switch. Aucun jeux ou clés n\'est inclus.&lt;br /&gt;&lt;br /&gt;Avant de commencer, veuillez localiser votre fichier <![CDATA[<b> prod.keys </b>]]> sur le stockage de votre appareil.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">En savoir plus</a>]]></string>
+ <string name="emulation_notification_channel_name">L\'émulation est active</string>
+ <string name="emulation_notification_channel_description">Affiche une notification persistante lorsque l\'émulation est en cours d\'exécution.</string>
+ <string name="emulation_notification_running">yuzu est en cours d\'exécution</string>
+ <string name="notice_notification_channel_name">Avis et erreurs</string>
+ <string name="notice_notification_channel_description">Affiche des notifications en cas de problème.</string>
+ <string name="notification_permission_not_granted">Permission de notification non accordée !</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Bienvenue !</string>
+ <string name="welcome_description">Apprenez à configurer &lt;b>yuzu&lt;/b> et passez à l\'émulation.</string>
+ <string name="get_started">Commencer</string>
+ <string name="keys">Clés</string>
+ <string name="keys_description">Sélectionnez votre fichier &lt;b>prod.keys&lt;/b> avec le bouton ci-dessous.</string>
+ <string name="select_keys">Sélectionner les clés</string>
+ <string name="games">Jeux</string>
+ <string name="games_description">Sélectionnez votre dossier &lt;b>de Jeux&lt;/b> avec le bouton ci-dessous.</string>
+ <string name="done">Terminé</string>
+ <string name="done_description">Vous êtes prêt.\nProfitez de vos jeux !</string>
+ <string name="text_continue">Continuer</string>
+ <string name="next">Suivant</string>
+ <string name="back">Retour</string>
+ <string name="add_games">Ajouter des jeux</string>
+ <string name="add_games_description">Sélectionner votre dossier de jeux</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Jeux</string>
+ <string name="home_search">Rechercher</string>
+ <string name="home_settings">Paramètres</string>
+ <string name="empty_gamelist">Aucun fichier n\'a été trouvé ou aucun répertoire de jeu n\'a encore été sélectionné.</string>
+ <string name="search_and_filter_games">Rechercher et filtrer les jeux</string>
+ <string name="select_games_folder">Sélectionner le dossier de jeux</string>
+ <string name="select_games_folder_description">Permet à yuzu de remplir la liste des jeux</string>
+ <string name="add_games_warning">Ne pas sélectionner le dossier des jeux ?</string>
+ <string name="add_games_warning_description">Les jeux ne seront pas affichés dans la liste des jeux si aucun dossier n\'est sélectionné.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Rechercher des jeux</string>
+ <string name="games_dir_selected">Répertoire de jeux sélectionné</string>
+ <string name="install_prod_keys">Installer prod.keys</string>
+ <string name="install_prod_keys_description">Nécessaire pour décrypter les jeux commerciaux.</string>
+ <string name="install_prod_keys_warning">Sauter l\'ajout des clés ?</string>
+ <string name="install_prod_keys_warning_description">Des clés valides sont nécessaires pour émuler des jeux commerciaux. Seules les applications homebrew fonctionneront si vous continuez.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Notifications</string>
+ <string name="notifications_description">Accordez l\'autorisation de notification avec le bouton ci-dessous.</string>
+ <string name="give_permission">Donner la permission</string>
+ <string name="notification_warning">Ne pas accorder la permission de notification ?</string>
+ <string name="notification_warning_description">yuzu ne pourra pas vous communiquer d\'informations importantes.</string>
+ <string name="permission_denied">Permission refusée</string>
+ <string name="permission_denied_description">Vous avez refusé cette permission trop de fois et vous devez maintenant l\'accorder manuellement dans les paramètres système.</string>
+ <string name="about">À propos</string>
+ <string name="about_description">Numéro de build, crédits et plus encore</string>
+ <string name="warning_help">Aide</string>
+ <string name="warning_skip">Sauter</string>
+ <string name="warning_cancel">Annuler</string>
+ <string name="install_amiibo_keys">Installer les clés Amiibo</string>
+ <string name="install_amiibo_keys_description">Nécessaire pour utiliser les Amiibo en jeu</string>
+ <string name="invalid_keys_file">Fichier de clés sélectionné invalide</string>
+ <string name="install_keys_success">Clés installées avec succès</string>
+ <string name="reading_keys_failure">Erreur lors de la lecture des clés de chiffrement</string>
+ <string name="invalid_keys_error">Clés de chiffrement invalides</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Le fichier sélectionné est incorrect ou corrompu. Veuillez dumper à nouveau vos clés.</string>
+ <string name="install_gpu_driver">Installer le pilote du GPU</string>
+ <string name="install_gpu_driver_description">Installez des pilotes alternatifs pour des performances ou une précision potentiellement meilleures</string>
+ <string name="advanced_settings">Paramètres avancés</string>
+ <string name="settings_description">Configurer les paramètres de l\'émulateur</string>
+ <string name="search_recently_played">Joué récemment</string>
+ <string name="search_recently_added">Ajouté récemment</string>
+ <string name="search_retail">Commercial</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Ouvrir le dossier de yuzu</string>
+ <string name="open_user_folder_description">Gérer les fichiers internes de yuzu</string>
+ <string name="theme_and_color_description">Modifier l\'apparence de l\'application</string>
+ <string name="no_file_manager">Aucun gestionnaire de fichiers trouvé</string>
+ <string name="notification_no_directory_link">Impossible d\'ouvrir le répertoire de yuzu</string>
+ <string name="notification_no_directory_link_description">Veuillez localiser manuellement le dossier utilisateur avec le panneau latéral du gestionnaire de fichiers.</string>
+ <string name="manage_save_data">Gérer les données de sauvegarde</string>
+ <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string>
+ <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string>
+ <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string>
+ <string name="save_file_imported_success">Importé avec succès</string>
+ <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string>
+ <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string>
+ <string name="import_saves">Importer</string>
+ <string name="export_saves">Exporter</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia n\'est pas réel</string>
+ <string name="copied_to_clipboard">Copié dans le presse-papier</string>
+ <string name="about_app_description">Un émulateur Switch open source</string>
+ <string name="contributors">Contributeurs</string>
+ <string name="contributors_description">Fait avec \u2764 de l\'équipe yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Build</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Early Access</string>
+ <string name="get_early_access">Obtenir l\'Early Access</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Fonctionnalités de pointe, accès anticipé aux mises à jour, et plus encore</string>
+ <string name="early_access_benefits">Avantages de l\'Early Access</string>
+ <string name="cutting_edge_features">Fonctionnalités de pointe</string>
+ <string name="early_access_updates">Accès anticipé aux mises à jour</string>
+ <string name="no_manual_installation">Pas d\'installation manuelle</string>
+ <string name="prioritized_support">Assistance prioritaire</string>
+ <string name="helping_game_preservation">Contribuer à la préservation des jeux</string>
+ <string name="our_eternal_gratitude">Notre gratitude éternelle</string>
+ <string name="are_you_interested">Es tu intéressé ?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Activer la vitesse limite</string>
+ <string name="frame_limit_enable_description">Lorsqu\'elle est activée, la vitesse d\'émulation sera limitée à un pourcentage spécifié de la vitesse normale.</string>
+ <string name="frame_limit_slider">Limite en pourcentage de vitesse</string>
+ <string name="frame_limit_slider_description">Spécifie le pourcentage pour limiter la vitesse d\'émulation. Avec la valeur par défaut de 100%, l\'émulation sera limitée à la vitesse normale. Des valeurs supérieures ou inférieures augmenteront ou diminueront la limite de vitesse.</string>
+ <string name="cpu_accuracy">Précision du CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Mode TV</string>
+ <string name="use_docked_mode_description">Émuler en mode TV augmente la résolution au détriment des performances.</string>
+ <string name="emulated_region">Région émulée</string>
+ <string name="emulated_language">Langue émulée</string>
+ <string name="select_rtc_date">Sélectionner la date RTC</string>
+ <string name="select_rtc_time">Sélectionner l\'heure RTC</string>
+ <string name="use_custom_rtc">Activer l\'horloge RTC personnalisée</string>
+ <string name="use_custom_rtc_description">Ce paramètre vous permet de définir une horloge en temps réel personnalisée distincte de l\'heure actuelle de votre système.</string>
+ <string name="set_custom_rtc">Définir l\'horloge RTC personnalisée</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Niveau de précision</string>
+ <string name="renderer_resolution">Résolution</string>
+ <string name="renderer_vsync">Mode VSync</string>
+ <string name="renderer_aspect_ratio">Format</string>
+ <string name="renderer_scaling_filter">Filtre de fenêtre adaptatif</string>
+ <string name="renderer_anti_aliasing">Méthode d\'anticrénelage :</string>
+ <string name="renderer_force_max_clock">Forcer la fréquence d\'horloge maximale (Adreno uniquement)</string>
+ <string name="renderer_force_max_clock_description">Force le GPU à fonctionner au maximum d\'horloges possibles (les contraintes thermiques seront toujours appliquées).</string>
+ <string name="renderer_asynchronous_shaders">Utiliser les shaders asynchrones</string>
+ <string name="renderer_asynchronous_shaders_description">Compile les shaders de manière asynchrone, ce qui réduira les saccades mais peut entraîner des problèmes visuels.</string>
+ <string name="renderer_debug">Activer le débogage des graphismes</string>
+ <string name="renderer_debug_description">Lorsque cette case est cochée, l\'API graphique entre dans un mode de débogage plus lent.</string>
+ <string name="use_disk_shader_cache">Utiliser les shader cache de disque</string>
+ <string name="use_disk_shader_cache_description">Réduire les saccades en stockant et en chargeant les shaders générés sur le disque.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volume</string>
+ <string name="audio_volume_description">Spécifie le volume de la sortie audio.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Défaut</string>
+ <string name="ini_saved">Paramètres enregistrés</string>
+ <string name="gameid_saved">Paramètres enregistrés pour %1$s</string>
+ <string name="error_saving">Erreur lors de l\'enregistrement de %1$s.ini: %2$s</string>
+ <string name="loading">Chargement...</string>
+ <string name="reset_setting_confirmation">Voulez-vous réinitialiser ce paramètre à sa valeur par défaut ?</string>
+ <string name="reset_to_default">Réinitialiser par défaut</string>
+ <string name="reset_all_settings">Réinitialiser tous les réglages ?</string>
+ <string name="reset_all_settings_description">Tous les paramètres avancés seront réinitialisés à leur configuration par défaut. Ça ne peut pas être annulé.</string>
+ <string name="settings_reset">Paramètres réinitialisés</string>
+ <string name="close">Fermer</string>
+ <string name="learn_more">Plus d\'informations</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Sélectionner le pilote du GPU</string>
+ <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>
+ <string name="select_gpu_driver_install">Installer</string>
+ <string name="select_gpu_driver_default">Défaut</string>
+ <string name="select_gpu_driver_install_success">%s Installé</string>
+ <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string>
+ <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>
+ <string name="system_gpu_driver">Pilote du GPU du système</string>
+ <string name="installing_driver">Installation du pilote...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Paramètres</string>
+ <string name="preferences_general">Général</string>
+ <string name="preferences_system">Système</string>
+ <string name="preferences_graphics">Vidéo</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Thème et couleur</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Votre ROM est cryptée</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Veuillez suivre les guides pour redumper vos <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">cartouches de jeu</a> ou <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">titres installés</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Veuillez vous assurer que votre fichier <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> est installé pour que les jeux puissent être déchiffrés.]]></string>
+ <string name="loader_error_video_core">Une erreur s\'est produite lors de l\'initialisation du noyau vidéo</string>
+ <string name="loader_error_video_core_description">Cela est généralement dû à un pilote du GPU incompatible. L\'installation d\'un pilote du GPU personnalisé peut résoudre ce problème.</string>
+ <string name="loader_error_invalid_format">Impossible de charger la ROM</string>
+ <string name="loader_error_file_not_found">Le fichier ROM n\'existe pas</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Quitter l\'émulation</string>
+ <string name="emulation_done">Terminé</string>
+ <string name="emulation_fps_counter">Compteur FPS</string>
+ <string name="emulation_toggle_controls">Activer/Désactiver les contrôles</string>
+ <string name="emulation_rel_stick_center">Centre du stick relatif</string>
+ <string name="emulation_dpad_slide">Glissement du DPad</string>
+ <string name="emulation_haptics">Haptique</string>
+ <string name="emulation_show_overlay">Afficher l\'overlay</string>
+ <string name="emulation_toggle_all">Tout basculer</string>
+ <string name="emulation_control_adjust">Ajuster l\'overlay</string>
+ <string name="emulation_control_scale">Échelle</string>
+ <string name="emulation_control_opacity">Opacité</string>
+ <string name="emulation_touch_overlay_reset">Réinitialiser l\'overlay</string>
+ <string name="emulation_touch_overlay_edit">Modifier l\'overlay</string>
+ <string name="emulation_pause">Mettre en pause l\'émulation</string>
+ <string name="emulation_unpause">Reprendre l\'émulation</string>
+ <string name="emulation_input_overlay">Options de l\'overlay</string>
+ <string name="emulation_game_loading">Chargement du jeu...</string>
+
+ <string name="load_settings">Chargement des paramètres…</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Clavier virtuel</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abandonner</string>
+ <string name="continue_button">Continuer</string>
+ <string name="system_archive_not_found">Archive système introuvable</string>
+ <string name="system_archive_not_found_message">%s est manquant. Veuillez dumper vos archives système.\nContinuer peut entraîner des plantages et des bogues.</string>
+ <string name="system_archive_general">Une archive système</string>
+ <string name="save_load_error">Erreur de sauvegarde/chargement</string>
+ <string name="fatal_error">Erreur fatale</string>
+ <string name="fatal_error_message">Une erreur fatale s\'est produite. Consultez les logs pour plus de détails.\nContinuer l\'émulation peut entraîner des plantages et des bogues.</string>
+ <string name="performance_warning">La désactivation de ce paramètre réduira considérablement les performances d\'émulation ! Pour une expérience optimale, il est recommandé de laisser ce paramètre activé.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japon</string>
+ <string name="region_usa">É.-U.A.</string>
+ <string name="region_europe">Europe</string>
+ <string name="region_australia">Australie</string>
+ <string name="region_china">Chine</string>
+ <string name="region_korea">Corée</string>
+ <string name="region_taiwan">Taïwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japonais (日本語)</string>
+ <string name="language_english">Anglais</string>
+ <string name="language_french">Français (Français)</string>
+ <string name="langauge_german">Allemand (Deutsch)</string>
+ <string name="language_italian">Italien (Italiano)</string>
+ <string name="language_spanish">Espagnol (Español)</string>
+ <string name="language_chinese">Chinois (简体中文)</string>
+ <string name="language_korean">Coréen (한국어)</string>
+ <string name="language_dutch">Néerlandais (Nederlands)</string>
+ <string name="language_portuguese">Portugais (Português)</string>
+ <string name="language_russian">Russe (Русский)</string>
+ <string name="language_taiwanese">Taïwanais (台湾)</string>
+ <string name="language_british_english">Anglais Britannique</string>
+ <string name="language_canadian_french">Français canadien (Français canadien)</string>
+ <string name="language_latin_american_spanish">Espagnol latino-américain (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Chinois simplifié (简体中文)</string>
+ <string name="language_traditional_chinese">Chinois Traditionnel (正體中文)</string>
+ <string name="language_brazilian_portuguese">Portugais brésilien (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Aucune</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Haut</string>
+ <string name="renderer_accuracy_extreme">Extrême (Lent)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Lent)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Lent)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Lent)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Immédiat (Désactivé)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (Activé)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO souple</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Plus proche voisin</string>
+ <string name="scaling_filter_bilinear">Bilinéaire</string>
+ <string name="scaling_filter_bicubic">Bicubique</string>
+ <string name="scaling_filter_gaussian">Gaussien</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Aucune</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Par défaut (16:9)</string>
+ <string name="ratio_force_four_three">Forcer le 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Forcer le 21:9</string>
+ <string name="ratio_force_sixteen_ten">Forcer le 16:10</string>
+ <string name="ratio_stretch">Étirer à la fenêtre</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Précis</string>
+ <string name="cpu_accuracy_unsafe">Risqué</string>
+ <string name="cpu_accuracy_paranoid">Paranoïaque (Lent)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">Pavé directionnel</string>
+ <string name="gamepad_left_stick">Stick Gauche</string>
+ <string name="gamepad_right_stick">Stick Droit</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Capture d\'écran</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Préparation des shaders</string>
+ <string name="building_shaders">Compilation des shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Changer le thème de l\'application</string>
+ <string name="theme_default">Défaut</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Changer le mode de thème</string>
+ <string name="theme_mode_follow_system">Automatique</string>
+ <string name="theme_mode_light">Lumineux</string>
+ <string name="theme_mode_dark">Sombre</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Utiliser des arrière-plans noirs</string>
+ <string name="use_black_backgrounds_description">Lorsque vous utilisez le thème sombre, appliquer des arrière-plans noirs.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..47a4cfa31
--- /dev/null
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Questo software permette di giocare ai giochi della console Nintendo Switch. Nessun gioco o chiave è inclusa.&lt;br /&gt;&lt;br /&gt;Prima di iniziare, perfavore individua il file <![CDATA[<b>prod.keys </b>]]> nella memoria del tuo dispositivo.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Scopri di più</a>]]></string>
+ <string name="emulation_notification_channel_name">L\'emulatore è attivo</string>
+ <string name="emulation_notification_channel_description">Mostra una notifica persistente quando l\'emulatore è in esecuzione.</string>
+ <string name="emulation_notification_running">yuzu è in esecuzione</string>
+ <string name="notice_notification_channel_name">Avvisi ed errori</string>
+ <string name="notice_notification_channel_description">Mostra le notifiche quando qualcosa va storto.</string>
+ <string name="notification_permission_not_granted">Autorizzazione di notifica non concessa!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Benvenuto!</string>
+ <string name="welcome_description">Scopri come configurare &lt;b>yuzu&lt;/b> e passare all\'emulazione.</string>
+ <string name="get_started">Iniziare</string>
+ <string name="keys">Pulsanti</string>
+ <string name="keys_description">Seleziona il tuo file &lt;b>prod.keys&lt;/b> con il pulsante in basso.</string>
+ <string name="select_keys">Selezione Pulsanti</string>
+ <string name="games">Giochi</string>
+ <string name="games_description">Seleziona la cartella &lt;b>Games&lt;/b> con il pulsante in basso.</string>
+ <string name="done">Fatto</string>
+ <string name="done_description">È tutto pronto.\nDivertiti a giocare!</string>
+ <string name="text_continue">Continua</string>
+ <string name="next">Successivo</string>
+ <string name="back">Indietro</string>
+ <string name="add_games">Aggiungi giochi</string>
+ <string name="add_games_description">Seleziona la cartella dei giochi</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Giochi</string>
+ <string name="home_search">Cerca</string>
+ <string name="home_settings">Impostazioni</string>
+ <string name="empty_gamelist">Non sono stati trovati file o non è stata ancora selezionata alcuna directory di gioco.</string>
+ <string name="search_and_filter_games">Cerca e filtra i giochi</string>
+ <string name="select_games_folder">Seleziona la cartella di gioco</string>
+ <string name="select_games_folder_description">Consente a yuzu di popolare l\'elenco dei giochi</string>
+ <string name="add_games_warning">Saltare la selezione della cartella dei giochi?</string>
+ <string name="add_games_warning_description">I giochi non saranno mostrati nella lista dei giochi se una cartella non è selezionata.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Cerca giochi</string>
+ <string name="games_dir_selected">Cartella dei giochi selezionata</string>
+ <string name="install_prod_keys">Installa prod.keys</string>
+ <string name="install_prod_keys_description">Necessario per decrittografare i giochi</string>
+ <string name="install_prod_keys_warning">Saltare l\'aggiunta delle chiavi?</string>
+ <string name="install_prod_keys_warning_description">Sono necessarie delle chiavi valide per emulare i giochi. Se continui, funzioneranno solo le app homebrew.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Notifiche</string>
+ <string name="notifications_description">Concedi l\'autorizzazione alle notifiche con il pulsante in basso.</string>
+ <string name="give_permission">Concedere l\'autorizzazione</string>
+ <string name="notification_warning">Saltare la concessione dell\'autorizzazione alle notifiche?</string>
+ <string name="notification_warning_description">yuzu non sarà in grado di notificarti informazioni importanti.</string>
+ <string name="permission_denied">Permesso negato</string>
+ <string name="permission_denied_description">Hai negato l\'autorizzazione troppe volte ed ora devi concederla manualmente nelle impostazioni di sistema.</string>
+ <string name="about">Informazioni</string>
+ <string name="about_description">Versione build, crediti ed altro</string>
+ <string name="warning_help">Aiuto</string>
+ <string name="warning_skip">Salta</string>
+ <string name="warning_cancel">Annulla</string>
+ <string name="install_amiibo_keys">Installa le chiavi degli Amiibo</string>
+ <string name="install_amiibo_keys_description">Necessario per usare gli Amiibo in gioco</string>
+ <string name="invalid_keys_file">Selezionate chiavi non valide</string>
+ <string name="install_keys_success">Chiavi installate correttamente</string>
+ <string name="reading_keys_failure">Errore durante la lettura delle chiavi di crittografia</string>
+ <string name="invalid_keys_error">Chiavi di crittografia non valide</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Il file selezionato è incorretto o corrotto. Per favore riesegui il dump delle tue chiavi.</string>
+ <string name="install_gpu_driver">Installa i driver GPU</string>
+ <string name="install_gpu_driver_description">Installa driver alternativi per potenziali prestazioni migliori o accuratezza.</string>
+ <string name="advanced_settings">Impostazioni avanzate</string>
+ <string name="settings_description">Configura le impostazioni dell\'emulatore</string>
+ <string name="search_recently_played">Giocato recentemente</string>
+ <string name="search_recently_added">Aggiunto recentemente</string>
+ <string name="search_retail">Rivenditore</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Apri la cartella di yuzu</string>
+ <string name="open_user_folder_description">Gestisci i file interni di yuzu</string>
+ <string name="theme_and_color_description">Modifica l\'aspetto dell\'app</string>
+ <string name="no_file_manager">Nessun file manager trovato</string>
+ <string name="notification_no_directory_link">Impossibile aprire la cartella di yuzu</string>
+ <string name="notification_no_directory_link_description">Per favore individua la cartella dell\'utente manualmente con il pannello laterale del file manager.</string>
+ <string name="manage_save_data">Gestisci i salvataggi</string>
+ <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string>
+ <string name="import_export_saves_description">Importa o esporta i salvataggi</string>
+ <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string>
+ <string name="save_file_imported_success">Importato con successo</string>
+ <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string>
+ <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string>
+ <string name="import_saves">Importa</string>
+ <string name="export_saves">Esporta</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia non è reale</string>
+ <string name="copied_to_clipboard">Copiato negli appunti</string>
+ <string name="about_app_description">Un emulatore della Switch open-source.</string>
+ <string name="contributors">Collaboratori</string>
+ <string name="contributors_description">Realizzato con \u2764 dal team yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Compilazione</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Accesso Anticipato</string>
+ <string name="get_early_access">Ottieni l\'accesso anticipato</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Funzionalità all\'avanguardia, aggiornamenti in anticipo e altro</string>
+ <string name="early_access_benefits">Vantaggi dell\'accesso anticipato</string>
+ <string name="cutting_edge_features">Funzionalità all\'avanguardia</string>
+ <string name="early_access_updates">Accesso anticipato agli aggiornamenti</string>
+ <string name="no_manual_installation">Non installare manualmente.</string>
+ <string name="prioritized_support">Supporto prioritario</string>
+ <string name="helping_game_preservation">Aiuta a preservare il gioco</string>
+ <string name="our_eternal_gratitude">La nostra gratitudine eterna</string>
+ <string name="are_you_interested">Sei interessato?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Abilita il limite di velocità</string>
+ <string name="frame_limit_enable_description">Quando abilitato, la velocità di emulazione verrà limitata a una specifica percentuale della velocità normale.</string>
+ <string name="frame_limit_slider">Limite velocità percentuale</string>
+ <string name="frame_limit_slider_description">Specifica la percentuale del limite della velocità di emulazione. Con quella preimpostata al 100% l\'emulazione verrà limitata alla velocità normale. Valori più alti o bassi aumenteranno o diminuiranno il limite di velocità.</string>
+ <string name="cpu_accuracy">Accuratezza della CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Modalità docked</string>
+ <string name="use_docked_mode_description">Emula in modalità docked, questo aumenta la risoluzione a spese delle performance.</string>
+ <string name="emulated_region">Regione emulata</string>
+ <string name="emulated_language">Lingua emulata</string>
+ <string name="select_rtc_date">Seleziona la data dall\'orologio in tempo reale</string>
+ <string name="select_rtc_time">Seleziona il tempo dall\'orologio in tempo reale</string>
+ <string name="use_custom_rtc">Abilità l\'orologio in tempo reale personalizzato</string>
+ <string name="use_custom_rtc_description">Questa impostazione ti permette di impostare un orologio in tempo reale personalizzato separato da quello del tuo sistema corrente.</string>
+ <string name="set_custom_rtc">Imposta l\'orologio in tempo reale personalizzato</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Livello di accuratezza</string>
+ <string name="renderer_resolution">Risoluzione</string>
+ <string name="renderer_vsync">Modalità VSync</string>
+ <string name="renderer_aspect_ratio">Rapporto d\'aspetto</string>
+ <string name="renderer_scaling_filter">Filtro di adattamento alla finestra</string>
+ <string name="renderer_anti_aliasing">Metodo di anti-aliasing</string>
+ <string name="renderer_force_max_clock">Forza clock massimi (solo Adreno)</string>
+ <string name="renderer_force_max_clock_description">Forza la GPU a girare col massimo clock possibile (i vincoli alla temperatura saranno comunque applicati)</string>
+ <string name="renderer_asynchronous_shaders">Usa shaders asincrone</string>
+ <string name="renderer_asynchronous_shaders_description">Compila le shaders asincronamente, questo riduce lo shutter ma potrebbe introdurre dei glitch. </string>
+ <string name="renderer_debug">Abilità il debug grafico</string>
+ <string name="renderer_debug_description">Quando l\'opzione è selezionata, l\'API grafica entra in una modalità di debug più lenta</string>
+ <string name="use_disk_shader_cache">Usa cache shader su disco</string>
+ <string name="use_disk_shader_cache_description">Riduce lo stuttering salvando e caricando le shader generate sul disco.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volume</string>
+ <string name="audio_volume_description">Specifica il volume dell\'audio in uscita.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Predefinito</string>
+ <string name="ini_saved">Impostazioni salvate</string>
+ <string name="gameid_saved">Impostazioni salvate per %1$s</string>
+ <string name="error_saving">Errore nel salvare %1$s.ini %2$s</string>
+ <string name="loading">Caricamento…</string>
+ <string name="reset_setting_confirmation">Vuoi ripristinare queste impostazioni al loro valore originale?</string>
+ <string name="reset_to_default">Riportare alle impostazioni originali</string>
+ <string name="reset_all_settings">Resettare tutte le impostazioni?</string>
+ <string name="reset_all_settings_description">Tutte le Impostazioni Avanzate saranno ripristinate a quelle originali. Questa operazione non è reversibile</string>
+ <string name="settings_reset">Reimposta le impostazioni</string>
+ <string name="close">Chiudi</string>
+ <string name="learn_more">Per saperne di più</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Seleziona il driver della GPU</string>
+ <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>
+ <string name="select_gpu_driver_install">Installa</string>
+ <string name="select_gpu_driver_default">Predefinito</string>
+ <string name="select_gpu_driver_install_success">Installato%s</string>
+ <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string>
+ <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>
+ <string name="system_gpu_driver">Driver GPU del sistema</string>
+ <string name="installing_driver">Installando i driver...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Impostazioni</string>
+ <string name="preferences_general">Generali</string>
+ <string name="preferences_system">Sistema</string>
+ <string name="preferences_graphics">Grafica</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Tema e colori</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">La tua ROM è criptata</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Per favore segui la guida per eseguire il dump della <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">cartuccia di gioco</a> o i <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">titoli installati</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Per favore assicurati che il file <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> sia installato in modo che i giochi possano essere decrittati.]]></string>
+ <string name="loader_error_video_core">È stato riscontrato un errore nell\'inizializzazione del core video</string>
+ <string name="loader_error_video_core_description">Questo è causato solitamente dal driver incompatibile di una GPU. L\'installazione di driver GPU personalizzati potrebbe risolvere questo problema.</string>
+ <string name="loader_error_invalid_format">Impossibile caricare la ROM</string>
+ <string name="loader_error_file_not_found">Il file della ROM non esiste</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Uscire dall\'emulazione</string>
+ <string name="emulation_done">Fatto</string>
+ <string name="emulation_fps_counter">Contatore degli FPS</string>
+ <string name="emulation_toggle_controls">Controlli a interruttore</string>
+ <string name="emulation_rel_stick_center">Centro relativo degli Stick</string>
+ <string name="emulation_dpad_slide">Slittamento del Pad Direzionale</string>
+ <string name="emulation_haptics">Aptico</string>
+ <string name="emulation_show_overlay">Mostra Overlay</string>
+ <string name="emulation_toggle_all">Attiva/disattiva tutto</string>
+ <string name="emulation_control_adjust">Aggiusta Overlay</string>
+ <string name="emulation_control_scale">Scala</string>
+ <string name="emulation_control_opacity">Opacità</string>
+ <string name="emulation_touch_overlay_reset">Reimposta Overlay</string>
+ <string name="emulation_touch_overlay_edit">Modifica Overlay</string>
+ <string name="emulation_pause">Metti in pausa l\'emulazione</string>
+ <string name="emulation_unpause">Riprendi Emulazione</string>
+ <string name="emulation_input_overlay">Impostazioni Overlay</string>
+ <string name="emulation_game_loading">Caricamento del gioco...</string>
+
+ <string name="load_settings">Caricamento delle impostazioni...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Tastiera software</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Interrompi</string>
+ <string name="continue_button">Continua</string>
+ <string name="system_archive_not_found">Archivio di sistema non trovato</string>
+ <string name="system_archive_not_found_message">%s è mancante. Per favore esegui il dump degli archivi del tuo sistema.\nContinuare ad emulare potrebbe portare bug o causare crash.</string>
+ <string name="system_archive_general">Un archivio di sistema</string>
+ <string name="save_load_error">Errore di salvataggio/caricamento</string>
+ <string name="fatal_error">Errore Fatale</string>
+ <string name="fatal_error_message">Un errore fatale è accaduto. Controlla i log per i dettagli.\nContinuare ad emulare potrebbe portare bug o causare crash.</string>
+ <string name="performance_warning">Disattivare questa impostazione può ridurre significativamente le performance di emulazione! Per una migliore esperienza, è consigliato lasciare questa impostazione attivata.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Giappone</string>
+ <string name="region_usa">USA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Australia</string>
+ <string name="region_china">Cina</string>
+ <string name="region_korea">Corea</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Giapponese (日本語)</string>
+ <string name="language_english">Inglese (English)</string>
+ <string name="language_french">Francese (Français)</string>
+ <string name="langauge_german">Tedesco (Deutsch)</string>
+ <string name="language_italian">Italiano (Italiano)</string>
+ <string name="language_spanish">Spagnolo (Español)</string>
+ <string name="language_chinese">Cinese (简体中文)</string>
+ <string name="language_korean">Coreano (한국어)</string>
+ <string name="language_dutch">Olandese (Nederlands)</string>
+ <string name="language_portuguese">Portoghese (Português)</string>
+ <string name="language_russian">Russo (Русский)</string>
+ <string name="language_taiwanese">Taiwanese (台湾)</string>
+ <string name="language_british_english">Inglese britannico</string>
+ <string name="language_canadian_french">Francese Canadese (Français canadien)</string>
+ <string name="language_latin_american_spanish">Spagnolo Latino Americano (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Cinese Semplificato (简体中文)</string>
+ <string name="language_traditional_chinese">Cinese tradizionale (正體中文)</string>
+ <string name="language_brazilian_portuguese">Portoghese (Português)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Nessuna</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normale</string>
+ <string name="renderer_accuracy_high">Alta</string>
+ <string name="renderer_accuracy_extreme">Estrema (Lenta)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Slow)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Slow)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Slow)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Immediato (Off)</string>
+ <string name="renderer_vsync_mailbox">Cassella postale</string>
+ <string name="renderer_vsync_fifo">FIFO (On)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Rilassato</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Nearest neighbor</string>
+ <string name="scaling_filter_bilinear">Bilineare</string>
+ <string name="scaling_filter_bicubic">Bicubico</string>
+ <string name="scaling_filter_gaussian">Gaussiano</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™️ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Nessuna</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Predefinito (16:9)</string>
+ <string name="ratio_force_four_three">Forza 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Forza 21:9</string>
+ <string name="ratio_force_sixteen_ten">Forza 16:10</string>
+ <string name="ratio_stretch">Allunga a finestra</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Accurata</string>
+ <string name="cpu_accuracy_unsafe">Non sicura</string>
+ <string name="cpu_accuracy_paranoid">Paranoico (Lento)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-Pad</string>
+ <string name="gamepad_left_stick">Levetta sinistra</string>
+ <string name="gamepad_right_stick">Levetta destra</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Screenshot</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Preparazione degli shaders</string>
+ <string name="building_shaders">Costruendo gli shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Cambia il tema dell\'app</string>
+ <string name="theme_default">Predefinito</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Cambia la modalità del tema</string>
+ <string name="theme_mode_follow_system">Segue il Sistema</string>
+ <string name="theme_mode_light">Chiaro</string>
+ <string name="theme_mode_dark">Scuro</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Usa sfondi neri</string>
+ <string name="use_black_backgrounds_description">Quando utilizzi il tema scuro, applica sfondi neri.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 000000000..46eda9ef7
--- /dev/null
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,335 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">このソフトウェアは、Nintendo Switch用のゲームを実行します。 ゲームソフトやキーは含まれません。&lt;br /&gt;&lt;br /&gt;事前に、 <![CDATA[<b> prod.keys </b>]]> ファイルをデバイスのストレージに配置しておいてください。&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">詳細</a>]]></string>
+ <string name="emulation_notification_channel_name">エミュレーションが有効です</string>
+ <string name="emulation_notification_channel_description">エミュレーションの実行中に常設通知を表示します。</string>
+ <string name="emulation_notification_running">yuzu は実行中です</string>
+ <string name="notice_notification_channel_description">問題が発生したときに通知を表示します。</string>
+ <string name="notification_permission_not_granted">通知が許可されていません!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">ようこそ!</string>
+ <string name="welcome_description">&lt;b>yuzu&lt;/b> のセットアップ方法を学び、エミュレーションに飛び込みましょう。</string>
+ <string name="get_started">はじめる</string>
+ <string name="keys">キー</string>
+ <string name="keys_description">下のボタンから &lt;b>prod.keys&lt;/b> ファイルを選択してください。</string>
+ <string name="select_keys">キーを選択</string>
+ <string name="games">ゲーム</string>
+ <string name="games_description">下のボタンから&lt;b>ゲーム&lt;/b>があるフォルダを選択してください。</string>
+ <string name="done">完了</string>
+ <string name="done_description">準備が完了しました。\nゲームをお楽しみください!</string>
+ <string name="text_continue">続行</string>
+ <string name="next">次へ</string>
+ <string name="back">戻る</string>
+ <string name="add_games">ゲームを追加</string>
+ <string name="add_games_description">ゲームフォルダを選択</string>
+
+ <!-- Home strings -->
+ <string name="home_games">ゲーム</string>
+ <string name="home_search">検索</string>
+ <string name="home_settings">設定</string>
+ <string name="empty_gamelist">ファイルが見つからないか、ゲームディレクトリがまだ選択されていません。</string>
+ <string name="search_and_filter_games">ゲームの検索と絞り込み</string>
+ <string name="select_games_folder">ゲームフォルダを選択</string>
+ <string name="select_games_folder_description">yuzu がゲームリストに追加できるようにします</string>
+ <string name="add_games_warning">ゲームフォルダの選択をスキップしますか?</string>
+ <string name="add_games_warning_description">フォルダを選択しない場合、ゲームはゲームリストに表示されません。</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">ゲームを検索</string>
+ <string name="games_dir_selected">ゲームディレクトリが選択されました</string>
+ <string name="install_prod_keys">prod.keys をインストール</string>
+ <string name="install_prod_keys_description">ゲームの復号化に必要</string>
+ <string name="install_prod_keys_warning">キーの追加をスキップしますか?</string>
+ <string name="install_prod_keys_warning_description">製品版ゲームのエミュレーションには、有効なキーが必要です。続行すると自作アプリしか機能しません。</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">通知</string>
+ <string name="notifications_description">下のボタンで通知の権限を許可してください。</string>
+ <string name="give_permission">許可</string>
+ <string name="notification_warning">通知の許可をスキップしますか?</string>
+ <string name="notification_warning_description">yuzuは重要なお知らせを通知できません。</string>
+ <string name="permission_denied">権限が拒否されました</string>
+ <string name="permission_denied_description">この権限を複数回拒否したため、システム設定で手動で許可する必要があります。</string>
+ <string name="about">情報</string>
+ <string name="about_description">ビルドバージョン、クレジットなど</string>
+ <string name="warning_help">ヘルプ</string>
+ <string name="warning_skip">スキップ</string>
+ <string name="warning_cancel">キャンセル</string>
+ <string name="install_amiibo_keys">Amiibo キーをインストール</string>
+ <string name="install_amiibo_keys_description">ゲーム内での Amiibo の使用に必要</string>
+ <string name="invalid_keys_file">無効なキーファイルが選択されました</string>
+ <string name="install_keys_success">正常にインストールされました</string>
+ <string name="reading_keys_failure">暗号化キーの読み取りエラー</string>
+ <string name="invalid_keys_error">暗号化キーが無効です</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">選択されたファイルが不正または破損しています。キーを再ダンプしてください。</string>
+ <string name="install_gpu_driver">GPUドライバーをインストール</string>
+ <string name="install_gpu_driver_description">代替ドライバーをインストールしてパフォーマンスや精度を向上させます</string>
+ <string name="advanced_settings">高度な設定</string>
+ <string name="settings_description">エミュレーターの設定を構成します</string>
+ <string name="search_recently_played">最近プレイした</string>
+ <string name="search_recently_added">最近追加された</string>
+ <string name="search_retail">製品版</string>
+ <string name="search_homebrew">自作</string>
+ <string name="open_user_folder">yuzu フォルダを開く</string>
+ <string name="open_user_folder_description">yuzu内部のファイルを管理します</string>
+ <string name="theme_and_color_description">アプリの見た目を変更</string>
+ <string name="no_file_manager">ファイルマネージャーが見つかりませんでした</string>
+ <string name="notification_no_directory_link">yuzuのディレクトリを開けません</string>
+ <string name="notification_no_directory_link_description">ファイルマネージャのサイドパネルでユーザーフォルダを手動で探してください。</string>
+ <string name="manage_save_data">セーブデータを管理</string>
+ <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string>
+ <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string>
+ <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string>
+ <string name="save_file_imported_success">インポートが完了しました</string>
+ <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string>
+ <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string>
+ <string name="import_saves">インポート</string>
+ <string name="export_saves">エクスポート</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">ガイアは実在しない</string>
+ <string name="copied_to_clipboard">クリップボードにコピーしました</string>
+ <string name="about_app_description">オープンソースのSwitchエミュレータ</string>
+ <string name="contributors">貢献者</string>
+ <string name="contributors_description">yuzuチームの\u2764で作られた</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">ビルド</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">早期アクセス</string>
+ <string name="get_early_access">早期アクセスを手に入れる</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">最先端の機能、アップデートの早期アクセスなど</string>
+ <string name="early_access_benefits">早期アクセスのメリット</string>
+ <string name="cutting_edge_features">最先端の機能</string>
+ <string name="early_access_updates">アップデートの早期アクセス</string>
+ <string name="no_manual_installation">手動インストールが不要</string>
+ <string name="prioritized_support">優先的なサポート</string>
+ <string name="helping_game_preservation">ゲームの保存に貢献</string>
+ <string name="our_eternal_gratitude">私たちの永遠の感謝</string>
+ <string name="are_you_interested">興味がありますか?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">速度制限を有効化</string>
+ <string name="frame_limit_enable_description">有効にすると、エミュレーション速度が任意の割合に制限されます。</string>
+ <string name="frame_limit_slider">エミュレーション速度の制限</string>
+ <string name="frame_limit_slider_description">エミュレーション速度を制限する割合を指定します。デフォルトの100%では、エミュレーションは通常の速度に制限されます。値が高いまたは低いほど、速度制限が増加または減少します。</string>
+ <string name="cpu_accuracy">CPU精度</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">TVモード</string>
+ <string name="use_docked_mode_description">TVモードでエミュレートします。パフォーマンスが犠牲になりますが、解像度が向上します。</string>
+ <string name="emulated_region">地域</string>
+ <string name="emulated_language">言語</string>
+ <string name="select_rtc_date">RTCの日付を選択</string>
+ <string name="select_rtc_time">RTCの時刻を選択</string>
+ <string name="use_custom_rtc">カスタムRTC</string>
+ <string name="use_custom_rtc_description">現在のシステム時間とは別にカスタムのリアルタイムクロックを設定できます。</string>
+ <string name="set_custom_rtc">カスタムRTCを設定</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">精度</string>
+ <string name="renderer_resolution">解像度</string>
+ <string name="renderer_vsync">垂直同期モード</string>
+ <string name="renderer_aspect_ratio">アスペクト比</string>
+ <string name="renderer_scaling_filter">ウィンドウ適応フィルター</string>
+ <string name="renderer_anti_aliasing">アンチエイリアス方式</string>
+ <string name="renderer_force_max_clock">最大クロックを強制 (Adrenoのみ)</string>
+ <string name="renderer_force_max_clock_description">GPUを可能な限り最大クロックで動作させます (過熱制限は引き続き適用されます)。</string>
+ <string name="renderer_asynchronous_shaders">非同期シェーダー</string>
+ <string name="renderer_asynchronous_shaders_description">シェーダーを非同期でコンパイルします。コマ落ちが軽減されますが、不具合が発生する可能性があります。</string>
+ <string name="renderer_debug">グラフィックデバッグ</string>
+ <string name="renderer_debug_description">オンにすると、グラフィックAPI は低速のデバッグモードに入ります。</string>
+ <string name="use_disk_shader_cache">シェーダーキャッシュを使用</string>
+ <string name="use_disk_shader_cache_description">生成したシェーダーをディスクに保存して読み込むことで、コマ落ちを軽減します。</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">音量</string>
+ <string name="audio_volume_description">オーディオ出力の音量を指定します</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">デフォルト</string>
+ <string name="ini_saved">設定を保存しました</string>
+ <string name="gameid_saved">%1$sの設定を保存しました</string>
+ <string name="error_saving">%1$s.ini の保存エラー: %2$s</string>
+ <string name="loading">読み込み中…</string>
+ <string name="reset_setting_confirmation">この設定を初期値にリセットしますか?</string>
+ <string name="reset_to_default">初期設定に戻す</string>
+ <string name="reset_all_settings">すべての設定をリセットしますか?</string>
+ <string name="reset_all_settings_description">すべての詳細設定が初期設定に戻されます。この操作は元に戻せません。</string>
+ <string name="settings_reset">設定をリセットしました</string>
+ <string name="close">閉じる</string>
+ <string name="learn_more">詳細情報</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">GPUドライバを選択</string>
+ <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>
+ <string name="select_gpu_driver_install">インストール</string>
+ <string name="select_gpu_driver_default">デフォルト</string>
+ <string name="select_gpu_driver_install_success">%s をインストールしました</string>
+ <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string>
+ <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>
+ <string name="system_gpu_driver">システムのGPUドライバ</string>
+ <string name="installing_driver">インストール中…</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">設定</string>
+ <string name="preferences_general">全般</string>
+ <string name="preferences_system">システム</string>
+ <string name="preferences_graphics">グラフィック</string>
+ <string name="preferences_audio">サウンド</string>
+ <string name="preferences_theme">テーマと色</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">ROMが暗号化されています</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">ゲームカートリッジ</a>や<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">インストール済みのタイトル</a>を再度ダンプするためのガイドに従ってください。]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[ゲームを復号化するために <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> ファイルがインストールされていることを確認してください。]]></string>
+ <string name="loader_error_video_core">ビデオコアの初期化中にエラーが発生しました</string>
+ <string name="loader_error_video_core_description">これは通常、互換性のないGPUドライバーが原因で発生します。 カスタムGPUドライバーをインストールすると、問題が解決する可能性があります。</string>
+ <string name="loader_error_invalid_format">ROMの読み込みに失敗しました</string>
+ <string name="loader_error_file_not_found">ROMファイルが存在しません</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">エミュレーションを終了</string>
+ <string name="emulation_done">完了</string>
+ <string name="emulation_fps_counter">FPSカウンター</string>
+ <string name="emulation_toggle_controls">コントロールを切り替え</string>
+ <string name="emulation_dpad_slide">十字キーのスライド操作</string>
+ <string name="emulation_haptics">振動</string>
+ <string name="emulation_show_overlay">オーバーレイを表示</string>
+ <string name="emulation_toggle_all">すべて選択</string>
+ <string name="emulation_control_adjust">オーバーレイを調整</string>
+ <string name="emulation_control_scale">大きさ</string>
+ <string name="emulation_control_opacity">不透明度</string>
+ <string name="emulation_touch_overlay_reset">リセット</string>
+ <string name="emulation_touch_overlay_edit">オーバーレイを編集</string>
+ <string name="emulation_pause">エミュレーションを一時停止</string>
+ <string name="emulation_unpause">エミュレーションを再開</string>
+ <string name="emulation_input_overlay">オーバーレイオプション</string>
+ <string name="emulation_game_loading">ロード中…</string>
+
+ <string name="load_settings">設定をロード中…</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">ソフトウェアキーボード</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">中断</string>
+ <string name="continue_button">続行</string>
+ <string name="system_archive_not_found">システムアーカイブが見つかりません</string>
+ <string name="system_archive_not_found_message">%s が見つかりません。システムアーカイブをダンプしてください。\nエミュレーションを続行すると、クラッシュやバグが発生する可能性があります。</string>
+ <string name="system_archive_general">システムアーカイブ</string>
+ <string name="save_load_error">セーブ/ロード エラー</string>
+ <string name="fatal_error">致命的なエラー</string>
+ <string name="fatal_error_message">致命的なエラーが発生しました。詳細はログを確認してください。\nエミュレーションを続行するとクラッシュやバグが発生する可能性があります。</string>
+ <string name="performance_warning">この設定をオフにすると、エミュレーションのパフォーマンスが著しく低下します!最高の体験を得るためには、この設定を有効にしておくことをお勧めします。</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">日本</string>
+ <string name="region_usa">アメリカ</string>
+ <string name="region_europe">ヨーロッパ</string>
+ <string name="region_australia">オーストラリア</string>
+ <string name="region_china">中国</string>
+ <string name="region_korea">韓国</string>
+ <string name="region_taiwan">台湾</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">日本語</string>
+ <string name="language_english">英語</string>
+ <string name="language_french">フランス語 (Français)</string>
+ <string name="langauge_german">ドイツ語 (Deutsch)</string>
+ <string name="language_italian">イタリア語 (Italiano)</string>
+ <string name="language_spanish">スペイン語 (Español)</string>
+ <string name="language_chinese">中国語 (简体中文)</string>
+ <string name="language_korean">韓国語 (한국어)</string>
+ <string name="language_dutch">オランダ語 (Nederlands)</string>
+ <string name="language_portuguese">ポルトガル語 (Português)</string>
+ <string name="language_russian">ロシア語 (Русский)</string>
+ <string name="language_taiwanese">台湾語 (台湾)</string>
+ <string name="language_british_english">イギリス英語</string>
+ <string name="language_canadian_french">フランス語(カナダ) (Français canadien)</string>
+ <string name="language_latin_american_spanish">スペイン語(ラテンアメリカ) (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">中国語 (简体中文)</string>
+ <string name="language_traditional_chinese">繁体字中国語 (正體中文)</string>
+ <string name="language_brazilian_portuguese">ポルトガル語(ブラジル) (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">なし</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">標準</string>
+ <string name="renderer_accuracy_high">高い</string>
+ <string name="renderer_accuracy_extreme">最高 (低速)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (低速)</string>
+ <string name="resolution_three">3X (2160p/3240p) (低速)</string>
+ <string name="resolution_four">4X (2880p/4320p) (低速)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Immediate (オフ)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (オン)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Nearest Neighbor</string>
+ <string name="scaling_filter_bilinear">Bilinear</string>
+ <string name="scaling_filter_bicubic">Bicubic</string>
+ <string name="scaling_filter_gaussian">Gaussian</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">なし</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">デフォルト (16:9)</string>
+ <string name="ratio_force_four_three">強制 4:3</string>
+ <string name="ratio_force_twenty_one_nine">強制 21:9</string>
+ <string name="ratio_force_sixteen_ten">強制 16:10</string>
+ <string name="ratio_stretch">ウィンドウに合わせる</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">正確</string>
+ <string name="cpu_accuracy_unsafe">不安定</string>
+ <string name="cpu_accuracy_paranoid">パラノイド (低速)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">方向ボタン</string>
+ <string name="gamepad_left_stick">Lスティック</string>
+ <string name="gamepad_right_stick">Rスティック</string>
+ <string name="gamepad_home">HOMEボタン</string>
+ <string name="gamepad_screenshot">スクリーンショット</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">シェーダーを準備しています</string>
+ <string name="building_shaders">シェーダーを構築しています</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">アプリのテーマ</string>
+ <string name="theme_default">デフォルト</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">テーマモード</string>
+ <string name="theme_mode_follow_system">システムに従う</string>
+ <string name="theme_mode_light">ライト</string>
+ <string name="theme_mode_dark">ダーク</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">黒色の背景を使用</string>
+ <string name="use_black_backgrounds_description">ダークテーマの使用時は、黒色の背景を有効にしてください。</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
new file mode 100644
index 000000000..5da80ab4b
--- /dev/null
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">이 소프트웨어는 닌텐도 스위치 게임 콘솔용 게임을 실행합니다. 게임 타이틀이나 keys는 포함되어 있지 않습니다.&lt;br /&gt;&lt;br /&gt;시작하기 전에 장치 저장소에서 <![CDATA[<b> prod.keys </b>]]> 파일을 찾아주세요.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">자세히 알아보기</a>]]></string>
+ <string name="emulation_notification_channel_name">에뮬레이션이 활성화됨</string>
+ <string name="emulation_notification_channel_description">에뮬레이션이 실행 중일 때 영구 알림을 표시합니다.</string>
+ <string name="emulation_notification_running">yuzu가 실행 중입니다.</string>
+ <string name="notice_notification_channel_name">알림 및 오류</string>
+ <string name="notice_notification_channel_description">문제가 발생하면 알림을 표시합니다.</string>
+ <string name="notification_permission_not_granted">알림 권한이 부여되지 않았습니다!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">환영합니다!</string>
+ <string name="welcome_description">&lt;b>yuzu&lt;/b> 를 설정하고 에뮬레이션으로 이동하는 방법을 알아보세요.</string>
+ <string name="get_started">시작하기</string>
+ <string name="keys">Keys</string>
+ <string name="keys_description">아래 버튼을 사용하여 &lt;b>prod.keys&lt;/b> 파일을 선택합니다.</string>
+ <string name="select_keys">keys 선택</string>
+ <string name="games">게임</string>
+ <string name="games_description">아래 버튼으로 &lt;b>게임&lt;/b> 폴더를 선택합니다.</string>
+ <string name="done">완료</string>
+ <string name="done_description">모든 준비가 완료되었습니다.\n게임을 즐기세요!</string>
+ <string name="text_continue">계속</string>
+ <string name="next">다음</string>
+ <string name="back">뒤로</string>
+ <string name="add_games">게임 추가</string>
+ <string name="add_games_description">게임 폴더 선택</string>
+
+ <!-- Home strings -->
+ <string name="home_games">게임</string>
+ <string name="home_search">검색</string>
+ <string name="home_settings">설정</string>
+ <string name="empty_gamelist">파일을 찾을 수 없거나 아직 게임 디렉토리를 선택하지 않았습니다.</string>
+ <string name="search_and_filter_games">게임 검색 및 필터링</string>
+ <string name="select_games_folder">게임 폴더 선택</string>
+ <string name="select_games_folder_description">yuzu가 게임 목록을 채울 수 있도록 허용</string>
+ <string name="add_games_warning">게임 폴더 선택을 건너뛰겠습니까?</string>
+ <string name="add_games_warning_description">폴더를 선택하지 않으면 게임 목록에 게임이 표시되지 않습니다.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">게임 검색</string>
+ <string name="games_dir_selected">게임 디렉터리 선택</string>
+ <string name="install_prod_keys">prod.keys 설치</string>
+ <string name="install_prod_keys_description">판매용 게임 암호 해독에 요구</string>
+ <string name="install_prod_keys_warning">keys 추가를 건너뛰겠습니까?</string>
+ <string name="install_prod_keys_warning_description">정품 게임을 에뮬레이트하려면 유효한 keys가 필요합니다. 계속하면 자체 제작 앱만 작동합니다.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">알림</string>
+ <string name="notifications_description">아래 버튼으로 알림 권한을 부여합니다.</string>
+ <string name="give_permission">권한 부여</string>
+ <string name="notification_warning">알림 권한 부여를 건너뛰겠습니까?</string>
+ <string name="notification_warning_description">yuzu는 중요한 정보를 알려드리지 않습니다.</string>
+ <string name="permission_denied">권한 거부됨</string>
+ <string name="permission_denied_description">이 권한을 너무 많이 거부했으므로 이제 시스템 설정에서 수동으로 권한을 부여해야 합니다.</string>
+ <string name="about">정보</string>
+ <string name="about_description">빌드 버전, 크레딧 등</string>
+ <string name="warning_help">도움말</string>
+ <string name="warning_skip">건너뛰기</string>
+ <string name="warning_cancel">취소</string>
+ <string name="install_amiibo_keys">Amiibo keys 설치</string>
+ <string name="install_amiibo_keys_description">게임에서 아미보 사용 시 필요</string>
+ <string name="invalid_keys_file">잘못된 keys 파일 선택</string>
+ <string name="install_keys_success">keys가 성공적으로 설치됨</string>
+ <string name="reading_keys_failure">암호화 keys 읽기 오류</string>
+ <string name="invalid_keys_error">잘못된 암호화 keys</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">선택한 파일이 잘못되었거나 손상되었습니다. keys를 다시 덤프하세요.</string>
+ <string name="install_gpu_driver">GPU 드라이버 설치</string>
+ <string name="install_gpu_driver_description">잠재적으로 더 나은 성능 또는 정확성을 위해 대체 드라이버를 설치하세요.</string>
+ <string name="advanced_settings">고급 설정</string>
+ <string name="settings_description">에뮬레이터 설정 구성</string>
+ <string name="search_recently_played">최근 플레이한 게임</string>
+ <string name="search_recently_added">최근 추가한 게임</string>
+ <string name="search_retail">판매용</string>
+ <string name="search_homebrew">홈브류</string>
+ <string name="open_user_folder">yuzu 폴더 열기</string>
+ <string name="open_user_folder_description">yuzu의 내부 파일 관리</string>
+ <string name="theme_and_color_description">앱 모양 수정</string>
+ <string name="no_file_manager">파일 관리자를 찾을 수 없음</string>
+ <string name="notification_no_directory_link">yuzu 디렉토리를 열 수 없음</string>
+ <string name="notification_no_directory_link_description">파일 관리자의 사이드 패널에서 사용자 폴더를 수동으로 찾아주세요.</string>
+ <string name="manage_save_data">저장 데이터 관리</string>
+ <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string>
+ <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string>
+ <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string>
+ <string name="save_file_imported_success">가져오기 성공</string>
+ <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string>
+ <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string>
+ <string name="import_saves">가져오기</string>
+ <string name="export_saves">내보내기</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">가이아는 진짜가 아님</string>
+ <string name="copied_to_clipboard">클립보드에 복사</string>
+ <string name="about_app_description">오픈 소스 스위치 에뮬레이터</string>
+ <string name="contributors">기여자</string>
+ <string name="contributors_description">yuzu 팀의 \u2764로 제작</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">빌드</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">미리 체험하기</string>
+ <string name="get_early_access">미리 체험하기 신청</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">최첨단 기능, 미리 체험하기 업데이트 등</string>
+ <string name="early_access_benefits">미리 체험하기 혜택</string>
+ <string name="cutting_edge_features">최첨단 기능</string>
+ <string name="early_access_updates">미리 체험하기 업데이트</string>
+ <string name="no_manual_installation">수동 설치 불필요</string>
+ <string name="prioritized_support">우선 지원</string>
+ <string name="helping_game_preservation">게임 보존 도움주기</string>
+ <string name="our_eternal_gratitude">영원한 감사의 마음을 전합니다</string>
+ <string name="are_you_interested">관심 있으세요?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">제한 속도 활성화</string>
+ <string name="frame_limit_enable_description">활성화하면 에뮬레이션 속도가 정상 속도의 지정된 비율로 제한됩니다.</string>
+ <string name="frame_limit_slider">속도 제한 비율</string>
+ <string name="frame_limit_slider_description">에뮬레이션 속도를 제한할 비율을 지정합니다. 기본값인 100%로 설정하면 에뮬레이션이 정상 속도로 제한됩니다. 값이 높거나 낮으면 속도 제한이 증가하거나 감소합니다.</string>
+ <string name="cpu_accuracy">CPU 정확도</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">도킹 모드</string>
+ <string name="use_docked_mode_description">도킹 모드에서 에뮬레이션하면 성능이 저하되는 대신 해상도가 향상됩니다.</string>
+ <string name="emulated_region">에뮬레이트된 지역</string>
+ <string name="emulated_language">에뮬레이트된 언어</string>
+ <string name="select_rtc_date">RTC 날짜 선택</string>
+ <string name="select_rtc_time">RTC 시간 선택</string>
+ <string name="use_custom_rtc">커스텀 RTC 활성화</string>
+ <string name="use_custom_rtc_description">이 설정을 사용하면 현재 시스템 시간과 별도로 사용자 지정 실시간 시계를 설정할 수 있음</string>
+ <string name="set_custom_rtc">커스텀 RTC 설정</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">정확도 수준</string>
+ <string name="renderer_resolution">해상도</string>
+ <string name="renderer_vsync">수직동기화 모드</string>
+ <string name="renderer_aspect_ratio">화면비</string>
+ <string name="renderer_scaling_filter">창 적응 필터</string>
+ <string name="renderer_anti_aliasing">안티-에일리어싱 방법</string>
+ <string name="renderer_force_max_clock">최대 클럭 강제 설정 (아드레노만 해당)</string>
+ <string name="renderer_force_max_clock_description">GPU가 가능한 최대 클럭으로 실행되도록 강제합니다 (열 제약 조건은 여전히 적용됩니다).</string>
+ <string name="renderer_asynchronous_shaders">비동기 셰이더 사용</string>
+ <string name="renderer_asynchronous_shaders_description">셰이더를 비동기식으로 컴파일하므로 끊김 현상이 줄어들지만 글리치가 발생할 수 있습니다.</string>
+ <string name="renderer_debug">그래픽 디버깅 활성화</string>
+ <string name="renderer_debug_description">이 옵션을 선택하면 그래픽 API가 느린 디버깅 모드로 전환됩니다.</string>
+ <string name="use_disk_shader_cache">디스크 셰이더 캐시 사용</string>
+ <string name="use_disk_shader_cache_description">생성된 셰이더를 디스크에 저장하고 불러오기하여 끊김 현상을 줄입니다.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">볼륨</string>
+ <string name="audio_volume_description">오디오 출력의 볼륨을 지정합니다.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">기본값</string>
+ <string name="ini_saved">저장된 설정</string>
+ <string name="gameid_saved">%1$s를 위해 저장된 설정</string>
+ <string name="error_saving">%1$s.ini 저장 중 오류: %2$s</string>
+ <string name="loading">불러오기 중...</string>
+ <string name="reset_setting_confirmation">이 설정을 기본값으로 되돌리겠습니까?</string>
+ <string name="reset_to_default">기본값으로 재설정</string>
+ <string name="reset_all_settings">모든 설정을 초기화하겠습니까?</string>
+ <string name="reset_all_settings_description">모든 고급 설정이 기본 구성으로 재설정됩니다. 이 설정은 되돌릴 수 없습니다.</string>
+ <string name="settings_reset">설정 초기화</string>
+ <string name="close">닫기</string>
+ <string name="learn_more">자세히 알아보기</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">GPU 드라이버 선택</string>
+ <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>
+ <string name="select_gpu_driver_install">설치</string>
+ <string name="select_gpu_driver_default">기본값</string>
+ <string name="select_gpu_driver_install_success">설치된 %s</string>
+ <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string>
+ <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>
+ <string name="system_gpu_driver">시스템 GPU 드라이버</string>
+ <string name="installing_driver">드라이버 설치 중...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">설정</string>
+ <string name="preferences_general">일반</string>
+ <string name="preferences_system">시스템</string>
+ <string name="preferences_graphics">그래픽</string>
+ <string name="preferences_audio">오디오</string>
+ <string name="preferences_theme">테마 및 색상</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">롬이 암호화되었음</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[가이드에 따라 <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">게임 카트리지</a> 또는 <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">설치된 타이틀</a>를 다시 덤프하세요.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[P게임을 해독할 수 있도록 <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> 파일이 설치되어 있는지 확인하세요.]]></string>
+ <string name="loader_error_video_core">비디오 코어를 초기화하는 동안 오류 발생</string>
+ <string name="loader_error_video_core_description">이 문제는 일반적으로 호환되지 않는 GPU 드라이버로 인해 발생합니다. 사용자 지정 GPU 드라이버를 설치하면 이 문제가 해결될 수 있습니다.</string>
+ <string name="loader_error_invalid_format">롬을 불러올 수 없음</string>
+ <string name="loader_error_file_not_found">롬 파일이 존재하지 않음</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">에뮬레이션 종료</string>
+ <string name="emulation_done">완료</string>
+ <string name="emulation_fps_counter">FPS 카운터</string>
+ <string name="emulation_toggle_controls">토글 제어</string>
+ <string name="emulation_rel_stick_center">상대 스틱 센터</string>
+ <string name="emulation_dpad_slide">십자패드 슬라이드</string>
+ <string name="emulation_haptics">햅틱</string>
+ <string name="emulation_show_overlay">오버레이 표시</string>
+ <string name="emulation_toggle_all">모두 토글</string>
+ <string name="emulation_control_adjust">오버레이 조정</string>
+ <string name="emulation_control_scale">스케일</string>
+ <string name="emulation_control_opacity">불투명도</string>
+ <string name="emulation_touch_overlay_reset">오버레이 재설정</string>
+ <string name="emulation_touch_overlay_edit">오버레이 편집</string>
+ <string name="emulation_pause">에뮬레이션 일시 중지</string>
+ <string name="emulation_unpause">에뮬레이션 일시 중지 해제</string>
+ <string name="emulation_input_overlay">오버레이 옵션</string>
+ <string name="emulation_game_loading">게임 불러오기 중...</string>
+
+ <string name="load_settings">설정 불러오기 중...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">가상 키보드</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">정보</string>
+ <string name="continue_button">계속</string>
+ <string name="system_archive_not_found">시스템 아카이브를 찾을 수 없음</string>
+ <string name="system_archive_not_found_message">%s가 누락되었습니다. 시스템 아카이브를 덤프하세요.\n에뮬레이션을 계속하면 충돌 및 버그가 발생할 수 있습니다.</string>
+ <string name="system_archive_general">시스템 아카이브</string>
+ <string name="save_load_error">저장하기/불러오기 오류</string>
+ <string name="fatal_error">치명적인 오류</string>
+ <string name="fatal_error_message">치명적인 오류가 발생했습니다. 자세한 내용은 로그를 확인하십시오.\n에뮬레이션을 계속하면 충돌 및 버그가 발생할 수 있습니다.</string>
+ <string name="performance_warning">이 설정을 끄면 에뮬레이션 성능이 크게 저하됩니다! 최상의 환경을 위해 이 설정을 활성화된 상태로 두는 것이 좋습니다.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">일본</string>
+ <string name="region_usa">미국</string>
+ <string name="region_europe">유럽</string>
+ <string name="region_australia">호주</string>
+ <string name="region_china">중국</string>
+ <string name="region_korea">대한민국</string>
+ <string name="region_taiwan">타이완</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">일본어 (日本語)</string>
+ <string name="language_english">영어 (English)</string>
+ <string name="language_french">프랑스어 (Français)</string>
+ <string name="langauge_german">독일어(Deutsch)</string>
+ <string name="language_italian">이탈리아어 (Italiano)</string>
+ <string name="language_spanish">스페인어 (Español)</string>
+ <string name="language_chinese">중국어 (简体中文)</string>
+ <string name="language_korean">한국어 (Korean)</string>
+ <string name="language_dutch">네덜란드어 (Nederlands)</string>
+ <string name="language_portuguese">포르투갈어 (Português)</string>
+ <string name="language_russian">러시아어 (Русский)</string>
+ <string name="language_taiwanese">대만어 (台湾)</string>
+ <string name="language_british_english">영어 (British English)</string>
+ <string name="language_canadian_french">캐나다 프랑스어 (Français canadien)</string>
+ <string name="language_latin_american_spanish">라틴 아메리카 스페인어 (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">중국어 간체 (简体中文)</string>
+ <string name="language_traditional_chinese">중국어 번체 (正體中文)</string>
+ <string name="language_brazilian_portuguese">브라질 포르투갈어 (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">불칸</string>
+ <string name="renderer_none">없음</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">보통</string>
+ <string name="renderer_accuracy_high">높음</string>
+ <string name="renderer_accuracy_extreme">극한 (느림)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (느림)</string>
+ <string name="resolution_three">3X (2160p/3240p) (느림)</string>
+ <string name="resolution_four">4X (2880p/4320p) (느림)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">즉시 (끔)</string>
+ <string name="renderer_vsync_mailbox">메일박스</string>
+ <string name="renderer_vsync_fifo">FIFO (켬)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO 릴랙스</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">가장 가까운 이웃</string>
+ <string name="scaling_filter_bilinear">이중선형</string>
+ <string name="scaling_filter_bicubic">고등차수보간</string>
+ <string name="scaling_filter_gaussian">가우시안</string>
+ <string name="scaling_filter_scale_force">스케일포스</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ 초고해상도</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">없음</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">기본 (16:9)</string>
+ <string name="ratio_force_four_three">강제 4:3</string>
+ <string name="ratio_force_twenty_one_nine">강제 21:9</string>
+ <string name="ratio_force_sixteen_ten">강제 16:10</string>
+ <string name="ratio_stretch">창에 맞게 늘림</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">정확함</string>
+ <string name="cpu_accuracy_unsafe">안전하지 않음</string>
+ <string name="cpu_accuracy_paranoid">편집증 (느림)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">십자패드</string>
+ <string name="gamepad_left_stick">L 스틱</string>
+ <string name="gamepad_right_stick">R 스틱</string>
+ <string name="gamepad_home">홈</string>
+ <string name="gamepad_screenshot">스크린샷</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">셰이더 준비하기</string>
+ <string name="building_shaders">셰이더 빌드 중</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">앱 테마 변경</string>
+ <string name="theme_default">기본값</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">테마 모드 변경</string>
+ <string name="theme_mode_follow_system">팔로우 시스템</string>
+ <string name="theme_mode_light">밝음</string>
+ <string name="theme_mode_dark">어두움</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">검은색 배경 사용</string>
+ <string name="use_black_backgrounds_description">어두운 테마를 사용할 때는 검은색 배경을 적용합니다.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml
new file mode 100644
index 000000000..3e1f9bce5
--- /dev/null
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Denne programvaren vil kjøre spill for Nintendo Switch-spillkonsollen. Ingen spilltitler eller nøkler er inkludert.&lt;br /&gt;&lt;br /&gt;Før du begynner, må du finne <![CDATA[<b> prod.keys </b>]]> filen din på enhetslagringen.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Lær mer</a>]]></string>
+ <string name="emulation_notification_channel_name">Emulering er aktiv</string>
+ <string name="emulation_notification_channel_description">Viser et vedvarende varsel når emuleringen kjører.</string>
+ <string name="emulation_notification_running">Yuzu kjører</string>
+ <string name="notice_notification_channel_name">Merknader og feil</string>
+ <string name="notice_notification_channel_description">Viser varsler når noe går galt.</string>
+ <string name="notification_permission_not_granted">Varslingstillatelse ikke gitt!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Velkommen!</string>
+ <string name="welcome_description">Lær å sette opp &lt;b>yuzu&lt;/b> og hopp inn i emulering.</string>
+ <string name="get_started">Kom i gang</string>
+ <string name="keys">Nøkler</string>
+ <string name="keys_description">Velg din &lt;b>prod.keys&lt;/b> fil ved å bruke knappen under.</string>
+ <string name="select_keys">Velg nøkler</string>
+ <string name="games">Spill</string>
+ <string name="games_description">Velg din &lt;b>Spill&lt;/b> mappe ved å bruke knappen under.</string>
+ <string name="done">Ferdig</string>
+ <string name="done_description">Nå er du klar.\nGled deg til å spille!</string>
+ <string name="text_continue">Fortsett</string>
+ <string name="next">Neste</string>
+ <string name="back">Tilbake</string>
+ <string name="add_games">Legg til spill</string>
+ <string name="add_games_description">Velg din spillmappe</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Spill</string>
+ <string name="home_search">Søk</string>
+ <string name="home_settings">Innstillinger</string>
+ <string name="empty_gamelist">Ingen filer ble funnet eller ingen spillkatalog er valgt ennå.</string>
+ <string name="search_and_filter_games">Søk og filtrer spill</string>
+ <string name="select_games_folder">Velg spillmappe</string>
+ <string name="select_games_folder_description">Gjør det mulig for yuzu å fylle ut spillelisten.</string>
+ <string name="add_games_warning">Hoppe over valg av spillmappe?</string>
+ <string name="add_games_warning_description">Spill vises ikke i Spill-listen hvis en mappe ikke er valgt.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Søk i spill</string>
+ <string name="games_dir_selected">Spillkatalogen er valgt</string>
+ <string name="install_prod_keys">Installer prod.keys</string>
+ <string name="install_prod_keys_description">Nødvendig for å dekryptere spill</string>
+ <string name="install_prod_keys_warning">Hoppe over å legge til nøkler?</string>
+ <string name="install_prod_keys_warning_description">Gyldige nøkler er påkrevd for å emulere spill. Bare hjemmebryggede apper vil fungere hvis du fortsetter.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Varsler</string>
+ <string name="notifications_description">Gi varslingstillatelse med knappen nedenfor.</string>
+ <string name="give_permission">Gi tillatelse</string>
+ <string name="notification_warning">Hoppe over å gi tillatelse til varsling?</string>
+ <string name="notification_warning_description">yuzu vil ikke kunne varsle deg om viktig informasjon.</string>
+ <string name="permission_denied">Tillatelse avslått</string>
+ <string name="permission_denied_description">Du har nektet denne tillatelsen for mange ganger, og nå må du gi den manuelt i systeminnstillingene.</string>
+ <string name="about">Om</string>
+ <string name="about_description">Byggeversjon, kildehenvisninger og mer</string>
+ <string name="warning_help">Hjelp</string>
+ <string name="warning_skip">Hopp over</string>
+ <string name="warning_cancel">Avbryt</string>
+ <string name="install_amiibo_keys">Installer Amiibo-nøkler</string>
+ <string name="install_amiibo_keys_description">Kreves for å bruke Amiibo i spillet</string>
+ <string name="invalid_keys_file">Ugyldig nøkkelfil valgt</string>
+ <string name="install_keys_success">Nøkler vellykket installert</string>
+ <string name="reading_keys_failure">Feil ved lesing av krypteringsnøkler</string>
+ <string name="invalid_keys_error">Ugyldige krypteringsnøkler</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Den valgte filen er feil eller ødelagt. Vennligst dump nøklene på nytt.</string>
+ <string name="install_gpu_driver">Installer GPU-driver</string>
+ <string name="install_gpu_driver_description">Installer alternative drivere for potensielt bedre ytelse eller nøyaktighet.</string>
+ <string name="advanced_settings">Avanserte innstillinger</string>
+ <string name="settings_description">Konfigurere emulatorinnstillinger</string>
+ <string name="search_recently_played">Nylig spilt</string>
+ <string name="search_recently_added">Nylig lagt til</string>
+ <string name="search_retail">Butikkhandel</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Åpne yuzu-mappen</string>
+ <string name="open_user_folder_description">Administrere yuzus interne filer</string>
+ <string name="theme_and_color_description">Endre appens utseende</string>
+ <string name="no_file_manager">Ingen filbehandler funnet</string>
+ <string name="notification_no_directory_link">Kunne ikke åpne yuzu-katalogen</string>
+ <string name="notification_no_directory_link_description">Finn brukermappen manuelt med filbehandlingens sidepanel.</string>
+ <string name="manage_save_data">Administrere lagringsdata</string>
+ <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string>
+ <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string>
+ <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string>
+ <string name="save_file_imported_success">Vellykket import</string>
+ <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string>
+ <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string>
+ <string name="import_saves">Importer</string>
+ <string name="export_saves">Eksporter</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia er ikke ekte</string>
+ <string name="copied_to_clipboard">Kopiert til utklippstavlen</string>
+ <string name="about_app_description">En Switch-emulator med åpen kildekode</string>
+ <string name="contributors">Bidragsytere</string>
+ <string name="contributors_description">Laget med \u2764 fra yuzu-teamet</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Bygg</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Tidlig tilgang</string>
+ <string name="get_early_access">Få tidlig tilgang</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Banebrytende funksjoner, tidlig tilgang til oppdateringer og mye mer.</string>
+ <string name="early_access_benefits">Fordeler ved tidlig tilgang</string>
+ <string name="cutting_edge_features">Avanserte funksjoner</string>
+ <string name="early_access_updates">Tidlig tilgang til oppdateringer</string>
+ <string name="no_manual_installation">Ingen manuell installasjon</string>
+ <string name="prioritized_support">Prioritert støtte</string>
+ <string name="helping_game_preservation">Bidra til bevaring av spill</string>
+ <string name="our_eternal_gratitude">Vår evige takknemlighet</string>
+ <string name="are_you_interested">Er du interessert?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Aktiver hastighetsbegrensning</string>
+ <string name="frame_limit_enable_description">Når aktivert, begrenses emuleringshastigheten til en angitt prosentandel av normal hastighet.</string>
+ <string name="frame_limit_slider">Hastighetsbegrensning i prosent</string>
+ <string name="frame_limit_slider_description">Angir prosentandelen som skal begrense emuleringshastigheten. Med standardverdien 100 % vil emuleringen være begrenset til normal hastighet. Høyere eller lavere verdier vil øke eller redusere hastighetsbegrensningen.</string>
+ <string name="cpu_accuracy">CPU-nøyaktighet</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Dokket modus</string>
+ <string name="use_docked_mode_description">Emulerer i dokket modus, noe som øker oppløsningen på bekostning av ytelsen.</string>
+ <string name="emulated_region">Emulert region</string>
+ <string name="emulated_language">Emulert språk</string>
+ <string name="select_rtc_date">Velg RTC-dato</string>
+ <string name="select_rtc_time">Velg RTC-tid</string>
+ <string name="use_custom_rtc">Aktiver egendefinert RTC</string>
+ <string name="use_custom_rtc_description">Med denne innstillingen kan du stille inn en egendefinert sanntidsklokke som er atskilt fra gjeldende systemtid.</string>
+ <string name="set_custom_rtc">Angi egendefinert RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Nøyaktighetsnivå</string>
+ <string name="renderer_resolution">Oppløsning</string>
+ <string name="renderer_vsync">VSync-modus</string>
+ <string name="renderer_aspect_ratio">Størrelsesforhold</string>
+ <string name="renderer_scaling_filter">Filter for vindustilpasning</string>
+ <string name="renderer_anti_aliasing">Anti-Aliasing-metode</string>
+ <string name="renderer_force_max_clock">Tving fram maksimal klokkefrekvens (kun Adreno)</string>
+ <string name="renderer_force_max_clock_description">Tvinger GPU-en til å kjøre med maksimal klokkefrekvens (termiske begrensninger vil fortsatt gjelde).</string>
+ <string name="renderer_asynchronous_shaders">Bruk asynkrone shaders</string>
+ <string name="renderer_asynchronous_shaders_description">Kompilerer shaders asynkront, noe som reduserer hakkingen, men kan føre til feil.</string>
+ <string name="renderer_debug">Aktiver feilsøking av grafikk</string>
+ <string name="renderer_debug_description">Når dette er merket av, går grafikk-API-et inn i en langsommere feilsøkingsmodus.</string>
+ <string name="use_disk_shader_cache">Bruk disk shader-cache</string>
+ <string name="use_disk_shader_cache_description">Reduser hakking ved å lagre og laste inn genererte shaders på disken.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volum</string>
+ <string name="audio_volume_description">Angir volumet på lydutgangen.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Standard</string>
+ <string name="ini_saved">Lagrede innstillinger</string>
+ <string name="gameid_saved">Lagrede innstillinger for %1$s</string>
+ <string name="error_saving">Feil ved lagring av %1$s.ini: %2$s</string>
+ <string name="loading">Lastes inn...</string>
+ <string name="reset_setting_confirmation">Vil du tilbakestille denne innstillingen til standardverdien?</string>
+ <string name="reset_to_default">Tilbakestill til standardinnstillingene</string>
+ <string name="reset_all_settings">Tilbakestille alle innstillinger?</string>
+ <string name="reset_all_settings_description">Alle avanserte innstillinger tilbakestilles til standardkonfigurasjonen. Dette kan ikke angres.</string>
+ <string name="settings_reset">Tilbakestilling av innstillinger</string>
+ <string name="close">Lukk</string>
+ <string name="learn_more">Lær Mer</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Velg GPU-driver</string>
+ <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>
+ <string name="select_gpu_driver_install">Installer</string>
+ <string name="select_gpu_driver_default">Standard</string>
+ <string name="select_gpu_driver_install_success">Installert %s</string>
+ <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string>
+ <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>
+ <string name="system_gpu_driver">Systemets GPU-driver</string>
+ <string name="installing_driver">Installerer driver...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Innstillinger</string>
+ <string name="preferences_general">Generelt</string>
+ <string name="preferences_system">System</string>
+ <string name="preferences_graphics">Grafikk</string>
+ <string name="preferences_audio">Lyd</string>
+ <string name="preferences_theme">Tema og farge</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">ROM-en din er kryptert</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Følg veiledningene for å redumpe dine <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">spillkassetter</a> eller <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">installerte titler</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Vennligst sørg for at <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> filen er installert slik at spillene kan dekrypteres.]]></string>
+ <string name="loader_error_video_core">Det oppstod en feil ved initialisering av videokjernen</string>
+ <string name="loader_error_video_core_description">Dette skyldes vanligvis en inkompatibel GPU-driver. Installering av en tilpasset GPU-driver kan løse problemet.</string>
+ <string name="loader_error_invalid_format">Kunne ikke laste inn ROM</string>
+ <string name="loader_error_file_not_found">ROM-filen finnes ikke</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Avslutt emulering</string>
+ <string name="emulation_done">Ferdig</string>
+ <string name="emulation_fps_counter">FPS-teller</string>
+ <string name="emulation_toggle_controls">Veksle kontroller</string>
+ <string name="emulation_rel_stick_center">Relativt senter for stikken</string>
+ <string name="emulation_dpad_slide">DPad-skyveplate</string>
+ <string name="emulation_haptics">Haptikk</string>
+ <string name="emulation_show_overlay">Vis overlegg</string>
+ <string name="emulation_toggle_all">Slå av alt</string>
+ <string name="emulation_control_adjust">Juster overlegg</string>
+ <string name="emulation_control_scale">Skaler</string>
+ <string name="emulation_control_opacity">Gjennomsiktighet</string>
+ <string name="emulation_touch_overlay_reset">Tilbakestill overlegg</string>
+ <string name="emulation_touch_overlay_edit">Rediger overlegg</string>
+ <string name="emulation_pause">Pause Emulering</string>
+ <string name="emulation_unpause">Opphev pausing av emulering</string>
+ <string name="emulation_input_overlay">Alternativer for overlegg</string>
+ <string name="emulation_game_loading">Spillet lastes inn...</string>
+
+ <string name="load_settings">Laster inn innstillinger...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Programvare Tastatur</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Avbryt</string>
+ <string name="continue_button">Fortsett</string>
+ <string name="system_archive_not_found">System Arkiv Ikke Funnet</string>
+ <string name="system_archive_not_found_message">%s mangler. Dump systemarkivene dine.\nFortsatt emulering kan føre til krasj og feil.</string>
+ <string name="system_archive_general">Et systemarkiv</string>
+ <string name="save_load_error">Feil ved lagring/innlasting</string>
+ <string name="fatal_error">Fatal Feil</string>
+ <string name="fatal_error_message">Det oppstod en fatal feil. Sjekk loggen for mer informasjon.\nFortsatt emulering kan føre til krasj og feil.</string>
+ <string name="performance_warning">Hvis du slår av denne innstillingen, reduseres emuleringsytelsen betydelig! Vi anbefaler at du lar denne innstillingen være aktivert for å få den beste opplevelsen.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japan</string>
+ <string name="region_usa">USA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Australia</string>
+ <string name="region_china">Kina</string>
+ <string name="region_korea">Korea</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japansk (日本語)</string>
+ <string name="language_english">Engelsk</string>
+ <string name="language_french">Fransk (Français)</string>
+ <string name="langauge_german">Tysk (Deutsch)</string>
+ <string name="language_italian">Italiensk (Italiano)</string>
+ <string name="language_spanish">Spansk (Español)</string>
+ <string name="language_chinese">Kinesisk (简体中文)</string>
+ <string name="language_korean">Koreansk (한국어)</string>
+ <string name="language_dutch">Nederlandsk (Nederlands)</string>
+ <string name="language_portuguese">Portugisisk (Português)</string>
+ <string name="language_russian">Russisk (Русский)</string>
+ <string name="language_taiwanese">Taiwansk (台湾)</string>
+ <string name="language_british_english">Britisk Engelsk</string>
+ <string name="language_canadian_french">Kanadisk fransk (Français canadien)</string>
+ <string name="language_latin_american_spanish">Latinamerikansk spansk (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Forenklet kinesisk (简体中文)</string>
+ <string name="language_traditional_chinese">Tradisjonell Kinesisk (正體中文)</string>
+ <string name="language_brazilian_portuguese">Brasiliansk portugisisk (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Ingen</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Høy</string>
+ <string name="renderer_accuracy_extreme">Ekstrem (Treg)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Slow)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Slow)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Slow)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Umiddelbar (av)</string>
+ <string name="renderer_vsync_mailbox">Postkasse</string>
+ <string name="renderer_vsync_fifo">FIFO (På)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO avslappet</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Nærmeste nabo</string>
+ <string name="scaling_filter_bilinear">Bilineær</string>
+ <string name="scaling_filter_bicubic">Bikubisk</string>
+ <string name="scaling_filter_gaussian">Gaussisk</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Ingen</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Standard (16:9)</string>
+ <string name="ratio_force_four_three">Tving 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Tving 21:9</string>
+ <string name="ratio_force_sixteen_ten">Tving 16:10</string>
+ <string name="ratio_stretch">Strekk til Vindu</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Nøyaktig</string>
+ <string name="cpu_accuracy_unsafe">Utrygt</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Langsom)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-Pad</string>
+ <string name="gamepad_left_stick">Venstre Pinne</string>
+ <string name="gamepad_right_stick">Høyre Pinne</string>
+ <string name="gamepad_home">Hjem</string>
+ <string name="gamepad_screenshot">Skjermbilde</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Forberedelse av shaders</string>
+ <string name="building_shaders">Bygging av shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Endre appens tema</string>
+ <string name="theme_default">Standard</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Endre temamodus</string>
+ <string name="theme_mode_follow_system">Følg systemet</string>
+ <string name="theme_mode_light">Lys</string>
+ <string name="theme_mode_dark">Mørk</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Bruk svart bakgrunn</string>
+ <string name="use_black_backgrounds_description">Bruk svart bakgrunn når du bruker det mørke temaet.</string>
+
+</resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.Yuzu.Main.MaterialYou" parent="Theme.Yuzu.Main">
+ <item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
+ <item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item>
+ <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
+ <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item>
+ <item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item>
+ <item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item>
+ <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item>
+ <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item>
+ <item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item>
+ <item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item>
+ <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item>
+ <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item>
+ <item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_background</item>
+ <item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
+ <item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
+ <item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item>
+ <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
+ <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
+ <item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
+ <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
+ <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_surface_variant</item>
+ <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
+
+ <item name="materialAlertDialogTheme">@style/ThemeOverlay.Material3.MaterialAlertDialog</item>
+ </style>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="ThemeOverlay.Yuzu.Dark" parent="">
+ <item name="colorSurface">@android:color/black</item>
+ <item name="android:colorBackground">@android:color/black</item>
+ </style>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <color name="yuzu_primary">#A7DDEC</color>
+ <color name="yuzu_onPrimary">#003399</color>
+ <color name="yuzu_primaryContainer">#31323F</color>
+ <color name="yuzu_onPrimaryContainer">#D1E4FF</color>
+ <color name="yuzu_secondary">#BAC8DB</color>
+ <color name="yuzu_onSecondary">#253140</color>
+ <color name="yuzu_secondaryContainer">#3B4858</color>
+ <color name="yuzu_onSecondaryContainer">#D6E4F7</color>
+ <color name="yuzu_tertiary">#D6BEE5</color>
+ <color name="yuzu_onTertiary">#3A2948</color>
+ <color name="yuzu_tertiaryContainer">#524060</color>
+ <color name="yuzu_onTertiaryContainer">#F2DAFF</color>
+ <color name="yuzu_error">#FFB4AB</color>
+ <color name="yuzu_errorContainer">#93000A</color>
+ <color name="yuzu_onError">#690005</color>
+ <color name="yuzu_onErrorContainer">#FFDAD6</color>
+ <color name="yuzu_background">#1A1C1E</color>
+ <color name="yuzu_onBackground">#E2E2E6</color>
+ <color name="yuzu_surface">#1B1B1D</color>
+ <color name="yuzu_onSurface">#E2E2E6</color>
+ <color name="yuzu_surfaceVariant">#26282C</color>
+ <color name="yuzu_onSurfaceVariant">#C3C7CF</color>
+ <color name="yuzu_outline">#8C9199</color>
+ <color name="yuzu_inverseOnSurface">#1A1C1E</color>
+ <color name="yuzu_inverseSurface">#E2E2E6</color>
+ <color name="yuzu_inversePrimary">#0062A2</color>
+ <color name="yuzu_shadow">#000000</color>
+ <color name="yuzu_surfaceTint">#9DCAFF</color>
+ <color name="yuzu_outlineVariant">#42474E</color>
+
+ <color name="yuzu_ea_background_start">#840099</color>
+ <color name="yuzu_ea_background_end">#005AE1</color>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 000000000..1cd1a8f87
--- /dev/null
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">To oprogramowanie umożliwia uruchomienie gier z konsoli Nintendo Switch. Nie zawiera gier ani wymaganych kluczy.&lt;br /&gt;&lt;br /&gt;Zanim zaczniesz, wybierz plik kluczy <![CDATA[<b> prod.keys </b>]]> z katalogu w pamięci masowej.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Dowiedz się więcej</a>]]></string>
+ <string name="emulation_notification_channel_name">Emulacja jest uruchomiona</string>
+ <string name="emulation_notification_channel_description">Pokaż trwałe powiadomienie gdy emulacja jest uruchomiona.</string>
+ <string name="emulation_notification_running">yuzu jest uruchomiony</string>
+ <string name="notice_notification_channel_name">Powiadomienia błędy</string>
+ <string name="notice_notification_channel_description">Pokaż powiadomienie gdy coś pójdzie źle</string>
+ <string name="notification_permission_not_granted">Nie zezwolono na powiadomienia!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Witaj!</string>
+ <string name="welcome_description">Zobacz jak skonfigurować &lt;b>yuzu&lt;/b> i wskocz w świat emulacji.</string>
+ <string name="get_started">Zaczynamy</string>
+ <string name="keys">Klucze</string>
+ <string name="keys_description">Wybierz swoje klucze &lt;b>prod.keys&lt;/b> za pomocą przycisku poniżej.</string>
+ <string name="select_keys">Wybierz klucze</string>
+ <string name="games">Gry</string>
+ <string name="games_description">Wybierz katalog z grami &lt;b>Games&lt;/b> za pomocą przycisku poniżej.</string>
+ <string name="done">Gotowe</string>
+ <string name="done_description">Wszystko skonfigurowane.\n Miłego grania!</string>
+ <string name="text_continue">Kontynuuj</string>
+ <string name="next">Dalej</string>
+ <string name="back">Wstecz</string>
+ <string name="add_games">Dodaj gry</string>
+ <string name="add_games_description">Wybierz folder zawierający Twoje gry</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Gry</string>
+ <string name="home_search">Szukaj</string>
+ <string name="home_settings">Ustawienia</string>
+ <string name="empty_gamelist">Nie znaleziono plików, lub nie wybrano jeszcze katalogu zawierającego gry.</string>
+ <string name="search_and_filter_games">Szukaj i filtruj gry</string>
+ <string name="select_games_folder">Wybierz folder z grami</string>
+ <string name="select_games_folder_description">Pozwala yuzu wygenerować listę gier</string>
+ <string name="add_games_warning">Pominąć wybór folderu z grami?</string>
+ <string name="add_games_warning_description">Aby pokazać listę gier wybierz katalog zawierający gry.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Szukaj gier</string>
+ <string name="games_dir_selected">Wybrano katalog gier</string>
+ <string name="install_prod_keys">Instaluj klucze prod.keys</string>
+ <string name="install_prod_keys_description">Wymagane aby poprawnie odczytać sklepowe gry</string>
+ <string name="install_prod_keys_warning">Pominąć dodawanie kluczy?</string>
+ <string name="install_prod_keys_warning_description">Poprawne klucze są wymagane aby emulować sklepowe gry. Jeśli przejdziesz dalej, jedynie homebrew będą działać.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Powiadomienia</string>
+ <string name="notifications_description">Nadaj uprawnienia dostępu do powiadomień. </string>
+ <string name="give_permission">Nadaj uprawnienia</string>
+ <string name="notification_warning">Pominąć nadanie uprawnień powiadomień?</string>
+ <string name="notification_warning_description">yuzu nie będzie mógł powiadamiać Cię o ważnych informacjach.</string>
+ <string name="permission_denied">Odmowa dostępu</string>
+ <string name="permission_denied_description">Odmówiłeś dostępu do powiadomień zbyt wiele razy, teraz musisz przyznać je w ustawieniach systemowych Androida.</string>
+ <string name="about">O aplikacji</string>
+ <string name="about_description">Wersja, podziękowania i więcej</string>
+ <string name="warning_help">Pomoc</string>
+ <string name="warning_skip">Pomiń</string>
+ <string name="warning_cancel">Anuluj</string>
+ <string name="install_amiibo_keys">Zainstaluj klucze Amiibo</string>
+ <string name="install_amiibo_keys_description">Wymagane aby korzystać z Amiibo w grze</string>
+ <string name="invalid_keys_file">Wybrano niepoprawne klucze</string>
+ <string name="install_keys_success">Klucze zainstalowane pomyślnie</string>
+ <string name="reading_keys_failure">Błąd podczas odczytu kluczy</string>
+ <string name="invalid_keys_error">Niepoprawne klucze</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Wybrany plik jest niepoprawny lub uszkodzony. Zrzuć ponownie swoje klucze.</string>
+ <string name="install_gpu_driver">Zainstaluj sterownik GPU</string>
+ <string name="install_gpu_driver_description">Użyj alternatywnych sterowników aby potencjalnie zwiększyć wydajność i naprawić błędy</string>
+ <string name="advanced_settings">Ustawienia zaawansowane</string>
+ <string name="settings_description">Skonfiguruj ustawienia emulatora</string>
+ <string name="search_recently_played">Ostatnio grane</string>
+ <string name="search_recently_added">Ostatnio dodane</string>
+ <string name="search_retail">Sklepowe</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Otwórz folder yuzu</string>
+ <string name="open_user_folder_description">Zarządzaj plikami emulatora</string>
+ <string name="theme_and_color_description">Personalizuj wygląd aplikacji</string>
+ <string name="no_file_manager">Nie znaleziono menedżera plików</string>
+ <string name="notification_no_directory_link">Nie można otworzyć folderu emulatora</string>
+ <string name="notification_no_directory_link_description">Proszę wybrać ręcznie folder z pomocą panelu bocznego menedżera plików.</string>
+ <string name="manage_save_data">Zarządzaj plikami zapisów gier</string>
+ <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string>
+ <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string>
+ <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string>
+ <string name="save_file_imported_success">Zaimportowano pomyślnie</string>
+ <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string>
+ <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string>
+ <string name="import_saves">Importuj</string>
+ <string name="export_saves">Eksportuj</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia isn\'t real</string>
+ <string name="copied_to_clipboard">Skopiowano do schowka</string>
+ <string name="about_app_description">Otwarto-źródłowy emulator konsoli Switch</string>
+ <string name="contributors">Współtwórcy</string>
+ <string name="contributors_description">Stworzone z \u2764 przez zespół yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Wersja</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Wczesny dostęp</string>
+ <string name="get_early_access">Uzyskaj wczesny dostęp</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Nowe funkcje, szybszy dostęp do aktualizacji i nie tylko</string>
+ <string name="early_access_benefits">Korzyści z wcześniejszego dostępu</string>
+ <string name="cutting_edge_features">Nowatorskie funkcje</string>
+ <string name="early_access_updates">Częste aktualizacje</string>
+ <string name="no_manual_installation">Automatyczne aktualizacje</string>
+ <string name="prioritized_support">Priorytetowe wsparcie</string>
+ <string name="helping_game_preservation">Pomoc w problemach z grami</string>
+ <string name="our_eternal_gratitude">Nasza wdzięczność</string>
+ <string name="are_you_interested">Jesteś zainteresowany?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Włącz limit szybkości emulacji</string>
+ <string name="frame_limit_enable_description">Włącz, aby ustawić procentowy limit szybkości emulacji</string>
+ <string name="frame_limit_slider">Procentowy limit szybkości emulacji</string>
+ <string name="frame_limit_slider_description">Określa limit szybkości emulacji gier. Domyślna wartość 100% oznacza normalną szybkość z jaką działa gra. Wartości niższe lub wyższe zmniejszą lub zwiększą limit szybkości.</string>
+ <string name="cpu_accuracy">Dokładność procesora CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Tryb zadokowany</string>
+ <string name="use_docked_mode_description">Emulacja w trybie stacji dokującej, zwiększa rozdzielczość kosztem wydajności.</string>
+ <string name="emulated_region">Region emulacji</string>
+ <string name="emulated_language">Język emulacji</string>
+ <string name="select_rtc_date">Ustaw datę RTC</string>
+ <string name="select_rtc_time">Ustaw czas RTC</string>
+ <string name="use_custom_rtc">Włącz niestandardowy zegar RTC</string>
+ <string name="use_custom_rtc_description">Ta opcja pozwala na wybranie własnych ustawień czasu używanych w czasie emulacji, innych niż czas systemu Android.</string>
+ <string name="set_custom_rtc">Ustaw niestandardowy czas RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">Interfejs graficzny</string>
+ <string name="renderer_accuracy">Poziom precyzji emulacji</string>
+ <string name="renderer_resolution">Rozdzielczość</string>
+ <string name="renderer_vsync">Synchronizacja pionowa VSync</string>
+ <string name="renderer_aspect_ratio">Proporcje ekranu</string>
+ <string name="renderer_scaling_filter">Filtr adaptacji rozdzielczości</string>
+ <string name="renderer_anti_aliasing">Metoda wygładzania krawędzi</string>
+ <string name="renderer_force_max_clock">Maksymalne taktowanie GPU (układy Adreno)</string>
+ <string name="renderer_force_max_clock_description">Wymusza uruchomienie maksymalnego taktowania układu graficznego (zabezpieczenia termiczne będą dalej aktywne).</string>
+ <string name="renderer_asynchronous_shaders">Wyłącz synchronizację shaderów</string>
+ <string name="renderer_asynchronous_shaders_description">Kompiluj oświetlenie bez synchronizacji, poprawi wydajność ale może powodować błędy.</string>
+ <string name="renderer_debug">Włącz debugowanie grafiki</string>
+ <string name="renderer_debug_description">Kiedy włączone, interfejs graficzny korzysta z wolnego trybu debugowania błędów.</string>
+ <string name="use_disk_shader_cache">Użyj pamięci podręcznej shaderów na dysku</string>
+ <string name="use_disk_shader_cache_description">Zmniejsza przycięcia przez przechowywanie gotowych wygenerowanych plików oświetlenia w pamięci urządzenia.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Głośność</string>
+ <string name="audio_volume_description">Ustala poziom głośności wyjścia dźwięku.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Domyślne</string>
+ <string name="ini_saved">Ustawienia zapisane</string>
+ <string name="gameid_saved">Ustawienia zapisane w %1$s</string>
+ <string name="error_saving">Błąd zapisu %1$s.ini: %2$s</string>
+ <string name="loading">Wczytywanie...</string>
+ <string name="reset_setting_confirmation">Przywrócić wartość tego ustawienia do wartości domyślnej?</string>
+ <string name="reset_to_default">Przywróć ustawienia domyślne</string>
+ <string name="reset_all_settings">Przywrócić WSZYSTKIE ustawienia?</string>
+ <string name="reset_all_settings_description">Wszystkie zaawansowane opcje zostaną przywrócone do wartości domyślnych. Czynności nie będzie można cofnąć.</string>
+ <string name="settings_reset">Reset ustawień</string>
+ <string name="close">Zamknij</string>
+ <string name="learn_more">Dowiedz się więcej</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Wybierz sterownik GPU </string>
+ <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>
+ <string name="select_gpu_driver_install">Zainstaluj</string>
+ <string name="select_gpu_driver_default">Domyślne</string>
+ <string name="select_gpu_driver_install_success">Zainstalowano %s</string>
+ <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string>
+ <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>
+ <string name="system_gpu_driver">Systemowy sterownik GPU</string>
+ <string name="installing_driver">Instalowanie sterownika...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Ustawienia</string>
+ <string name="preferences_general">Ogólne</string>
+ <string name="preferences_system">System</string>
+ <string name="preferences_graphics">Grafika</string>
+ <string name="preferences_audio">Dźwięk</string>
+ <string name="preferences_theme">Motyw i kolor</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Twój ROM jest zakodowany</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Użyj przewodnika aby wykonać zrzuty <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">kardridży</a> lub <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">zainstalowanych gier</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Upewnij się że plik kluczy <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> jest zainstalowany aby gry mogły zostać odczytane.]]></string>
+ <string name="loader_error_video_core">Błąd inicjacji podsystemu graficznego</string>
+ <string name="loader_error_video_core_description">Zazwyczaj spowodowane niekompatybilnym sterownikiem GPU, instalacja niestandardowego sterownika może rozwiązać ten problem.</string>
+ <string name="loader_error_invalid_format">Nie można wczytać pliku ROM</string>
+ <string name="loader_error_file_not_found">Plik ROM nie istnieje</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Zakończ emulację</string>
+ <string name="emulation_done">Gotowe</string>
+ <string name="emulation_fps_counter">Licznik FPS</string>
+ <string name="emulation_toggle_controls">Wybierz przyciski</string>
+ <string name="emulation_rel_stick_center">Wycentruj gałki</string>
+ <string name="emulation_dpad_slide">Ruchomy DPad</string>
+ <string name="emulation_haptics">Wibracje haptyczne</string>
+ <string name="emulation_show_overlay">Pokaż przyciski</string>
+ <string name="emulation_toggle_all">Zaznacz wszystkie</string>
+ <string name="emulation_control_adjust">Dostosuj nakładkę</string>
+ <string name="emulation_control_scale">Skala</string>
+ <string name="emulation_control_opacity">Przeźroczystość</string>
+ <string name="emulation_touch_overlay_reset">Resetuj</string>
+ <string name="emulation_touch_overlay_edit">Edytuj nakładkę</string>
+ <string name="emulation_pause">Wstrzymaj emulację</string>
+ <string name="emulation_unpause">Wznów emulację</string>
+ <string name="emulation_input_overlay">Opcje nakładki</string>
+ <string name="emulation_game_loading">Wczytywanie gry...</string>
+
+ <string name="load_settings">Wczytywanie ustawień...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Klawiatura systemowa</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Przerwij</string>
+ <string name="continue_button">Kontynuuj</string>
+ <string name="system_archive_not_found">Archiwum systemu nie znalezione.</string>
+ <string name="system_archive_not_found_message">%s nieznaleziony. Proszę wykonać zrzut archiwum systemu.\nKontynuowanie może powodować błędy lub przerwanie emulacji.</string>
+ <string name="system_archive_general">Archiwum systemu</string>
+ <string name="save_load_error">Błąd odczytu/zapisu</string>
+ <string name="fatal_error">Błąd krytyczny</string>
+ <string name="fatal_error_message">Wystąpił błąd krytyczny. Szczegóły znajdziesz w pliku log.\nKontynuowanie może spowodować błędy lub przerwanie emulacji. </string>
+ <string name="performance_warning">Wyłączenie tej opcji znacząco ograniczy wydajność! Dla najlepszego doświadczenia, zaleca się zostawienie tej opcji włączonej.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japonia</string>
+ <string name="region_usa">USA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Australia</string>
+ <string name="region_china">Chiny</string>
+ <string name="region_korea">Korea</string>
+ <string name="region_taiwan">Tajwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japoński (日本語)</string>
+ <string name="language_english">Angielski</string>
+ <string name="language_french">Francuski (Francja)</string>
+ <string name="langauge_german">Niemiecki (Niemcy)</string>
+ <string name="language_italian">Włoski (Włochy)</string>
+ <string name="language_spanish">Hiszpański (Hiszpania)</string>
+ <string name="language_chinese">Chiński (简体中文)</string>
+ <string name="language_korean">Koreański (한국어)</string>
+ <string name="language_dutch">Duński (Holandia)</string>
+ <string name="language_portuguese">Portugalski (Portugalia)</string>
+ <string name="language_russian">Rosyjski (Русский)</string>
+ <string name="language_taiwanese">Tajwański (台湾)</string>
+ <string name="language_british_english">Angielski Brytyjski</string>
+ <string name="language_canadian_french">Francuski (Kanada)</string>
+ <string name="language_latin_american_spanish">Hiszpański (Ameryka Latynoska)</string>
+ <string name="language_simplified_chinese">Chiński uproszczony (简体中文)</string>
+ <string name="language_traditional_chinese">Chiński tradycyjny (正體中文)</string>
+ <string name="language_brazilian_portuguese">Portugalski (Brazylia)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Żadny</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normalny</string>
+ <string name="renderer_accuracy_high">Wysoki</string>
+ <string name="renderer_accuracy_extreme">Ekstremalny (Wolny)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Wolno)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Wolno)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Wolno)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Natychmiastowa (Wyłączona)</string>
+ <string name="renderer_vsync_mailbox">Skrzynka pocztowa</string>
+ <string name="renderer_vsync_fifo">FIFO (Włączona)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaks</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Najbliższy sąsiadujący</string>
+ <string name="scaling_filter_bilinear">Bilinearny</string>
+ <string name="scaling_filter_bicubic">Bikubiczny</string>
+ <string name="scaling_filter_gaussian">Kulisty</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Żadna (wyłączony)</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Domyślne (16:9)</string>
+ <string name="ratio_force_four_three">Wymuś 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Wymuś 21:9</string>
+ <string name="ratio_force_sixteen_ten">Wymuś 16:10</string>
+ <string name="ratio_stretch">Rozciągnij do Okna</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Dokładny</string>
+ <string name="cpu_accuracy_unsafe">Niebezpieczny</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Wolny)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-Pad</string>
+ <string name="gamepad_left_stick">Lewa gałka</string>
+ <string name="gamepad_right_stick">Prawa gałka</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Zrzut ekranu</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Przygotowanie shaderów</string>
+ <string name="building_shaders">Budowanie shaderów</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Zmień motyw aplikacji</string>
+ <string name="theme_default">Domyślny</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Zmiana trybu motywu</string>
+ <string name="theme_mode_follow_system">Podążaj za systemowym</string>
+ <string name="theme_mode_light">Jasny</string>
+ <string name="theme_mode_dark">Ciemny</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Używaj czarnego tła</string>
+ <string name="use_black_backgrounds_description">Kiedy używany ciemny motyw, tła zostają zastąpione czernią.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 000000000..35197c280
--- /dev/null
+++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Este software corre jogos para a consola Nintendo Switch. Não estão incluídas nem jogos ou chaves. &lt;br /&gt;&lt;br /&gt;Antes de começares, por favor localiza o ficheiro <![CDATA[1 prod.keys 1]]> no armazenamento do teu dispositivo.&lt;br /&gt;&lt;br /&gt;<![CDATA[2Learn more2]]></string>
+ <string name="emulation_notification_channel_name">Emulação está Ativa</string>
+ <string name="emulation_notification_channel_description">Mostra uma notificação permanente enquanto a emulação está a correr.</string>
+ <string name="emulation_notification_running">Yuzu está em execução </string>
+ <string name="notice_notification_channel_name">Notificações e erros</string>
+ <string name="notice_notification_channel_description">Mostra notificações quendo algo corre mal.</string>
+ <string name="notification_permission_not_granted">Permissões de notificação não permitidas </string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Bemvindo! </string>
+ <string name="welcome_description">Aprende como configurar &lt;b>yuzu&lt;/b> e arranca a emulação.</string>
+ <string name="get_started">Começa</string>
+ <string name="keys">Chaves</string>
+ <string name="keys_description">Seleciona o teu ficheiro &lt;b>prod.keys&lt;/b> com o botão abaixo.</string>
+ <string name="select_keys">Seleciona as Chaves</string>
+ <string name="games">Jogos</string>
+ <string name="games_description">Seleciona a tua pasta &lt;b>Games&lt;/b> com o botão abaixo.</string>
+ <string name="done">Feito</string>
+ <string name="done_description">Tudo pronto.\nDisfruta dos teus jogos!</string>
+ <string name="text_continue">Continuar</string>
+ <string name="next">Próximo</string>
+ <string name="back">Voltar</string>
+ <string name="add_games">Adiciona Jogos</string>
+ <string name="add_games_description">Seleciona a tua pasta de Jogos</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Jogos</string>
+ <string name="home_search">Pesquisar</string>
+ <string name="home_settings">Configurações</string>
+ <string name="empty_gamelist">Não foram encontrados jogos ou a pasta de Jogos ainda não foi definida. </string>
+ <string name="search_and_filter_games">Procura e filtra jogos.</string>
+ <string name="select_games_folder">Seleciona a pasta de jogos.</string>
+ <string name="select_games_folder_description">Permite que o Yuzu preencha a lista de jogos</string>
+ <string name="add_games_warning">Ignorar a seleção da pasta de jogos?</string>
+ <string name="add_games_warning_description">Os jogos não serão exibidos na lista de jogos se uma pasta não estiver selecionada.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Procurar Jogos</string>
+ <string name="games_dir_selected">Pasta de Jogos selecionada</string>
+ <string name="install_prod_keys">Instala prod.keys</string>
+ <string name="install_prod_keys_description">Necessário para desencriptar jogos comerciais</string>
+ <string name="install_prod_keys_warning">Ignorar a adição de chaves?</string>
+ <string name="install_prod_keys_warning_description">São necessárias chaves válidas para emular jogos comerciais. Somente aplicativos homebrew funcionarão se você continuar.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#Guia de introdução</string>
+ <string name="notifications">Notificações</string>
+ <string name="notifications_description">Conceda a permissão de notificação com o botão abaixo.</string>
+ <string name="give_permission">Conceda permissão</string>
+ <string name="notification_warning">Saltar a concessão da permissão de notificação?</string>
+ <string name="notification_warning_description">Yuzu não conseguirá te notificar de informações importantes. </string>
+ <string name="permission_denied">Permissão negada</string>
+ <string name="permission_denied_description">Você negou essa permissão muitas vezes e agora precisa concedê-la manualmente nas configurações do sistema.</string>
+ <string name="about">Sobre</string>
+ <string name="about_description">Versão de compilação, créditos e mais</string>
+ <string name="warning_help">Ajuda</string>
+ <string name="warning_skip">Saltar</string>
+ <string name="warning_cancel">Cancelar</string>
+ <string name="install_amiibo_keys">Instala chaves Amiibo</string>
+ <string name="install_amiibo_keys_description">Necessário para usares Amiibo no jogo</string>
+ <string name="invalid_keys_file">Ficheiro de chaves inválido</string>
+ <string name="install_keys_success">Chaves instaladas com sucesso</string>
+ <string name="reading_keys_failure">Erro ao ler chaves de encriptação</string>
+ <string name="invalid_keys_error">Chaves de encriptação inválidas</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">O ficheiro selecionado está corrompido. Por favor recarrega as tuas chaves.</string>
+ <string name="install_gpu_driver">Instala driver para GPU</string>
+ <string name="install_gpu_driver_description">Instala drivers alternativos para desempenho ou precisão potencialmente melhores</string>
+ <string name="advanced_settings">Definições avançadas</string>
+ <string name="settings_description">Configura definições do emulador</string>
+ <string name="search_recently_played">Jogos recentes</string>
+ <string name="search_recently_added">Adicionados recentemente</string>
+ <string name="search_retail">Jogos comerciais</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Abre a pasta Yuzu</string>
+ <string name="open_user_folder_description">Gere os ficheiro internos do Yuzu</string>
+ <string name="theme_and_color_description">Modifica a aparência da App</string>
+ <string name="no_file_manager">Nenhum gestor de ficheiros encontrado</string>
+ <string name="notification_no_directory_link">Impossível abrir pasta Yuzu</string>
+ <string name="notification_no_directory_link_description">Localiza a pasta de utilizador manualmente com o painel lateral do gestor de ficheiros.</string>
+ <string name="manage_save_data">Gerir dados guardados</string>
+ <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
+ <string name="import_export_saves_description">Importa ou exporta dados guardados</string>
+ <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
+ <string name="save_file_imported_success">Importado com sucesso</string>
+ <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
+ <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string>
+ <string name="import_saves">Importar</string>
+ <string name="export_saves">Exportar</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia não é real</string>
+ <string name="copied_to_clipboard">Copiado para a área de transferência</string>
+ <string name="about_app_description">Um emulador Switch de código aberto</string>
+ <string name="contributors">Contribuidores</string>
+ <string name="contributors_description">Feito com \u2764 da equipa do Yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Versão</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Acesso antecipado</string>
+ <string name="get_early_access">Obtém Acesso Antecipado</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Recursos de ponta, acesso antecipado a atualizações e muito mais</string>
+ <string name="early_access_benefits">Benefícios do Acesso Antecipado</string>
+ <string name="cutting_edge_features">Recursos de ponta</string>
+ <string name="early_access_updates">Acesso antecipado a atualizações</string>
+ <string name="no_manual_installation">Sem instalação manual</string>
+ <string name="prioritized_support">Suporte prioritário</string>
+ <string name="helping_game_preservation">Ajuda na preservação dos jogos</string>
+ <string name="our_eternal_gratitude">A nossa eterna gratidão</string>
+ <string name="are_you_interested">Estás interessado?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Ativar limite de velocidade</string>
+ <string name="frame_limit_enable_description">Quando ativada, a velocidade da emulação será limitada à percentagem definida da velocidade normal.</string>
+ <string name="frame_limit_slider">Percentagem do limite de velocidade</string>
+ <string name="frame_limit_slider_description">Especifica o limite da percentagem da velocidade da emulação. Com a velocidade por defeito a 100% a emulação será limitada à velocidade normal. Valores maiores ou menores aumentarão ou diminuirão o limite de velocidade.</string>
+ <string name="cpu_accuracy">Precisão do CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Modo ancorado</string>
+ <string name="use_docked_mode_description">Emula em modo ancorado, que aumenta a resolução ás custas da performance.</string>
+ <string name="emulated_region">Região da emulação</string>
+ <string name="emulated_language">Idioma da emulação</string>
+ <string name="select_rtc_date">Seleciona a data RTC</string>
+ <string name="select_rtc_time">Seleciona a hora RTC</string>
+ <string name="use_custom_rtc">Ativa RTC personalizado</string>
+ <string name="use_custom_rtc_description">Esta configuração permite definir um RTC personalizado diferente da hora atual do sistema</string>
+ <string name="set_custom_rtc">Define RTC personalizado</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Nível de precisão</string>
+ <string name="renderer_resolution">Resolução</string>
+ <string name="renderer_vsync">Modo VSync</string>
+ <string name="renderer_aspect_ratio">Proporção do ecrã</string>
+ <string name="renderer_scaling_filter">Filtro de Adaptação da Janela</string>
+ <string name="renderer_anti_aliasing">Método de Anti-Aliasing </string>
+ <string name="renderer_force_max_clock">Força velocidade máxima (Adreno only)</string>
+ <string name="renderer_force_max_clock_description">Força o GPU a correr à velocidade máxima (restrições térmicas serão aplicadas)</string>
+ <string name="renderer_asynchronous_shaders">Usa shaders assíncronos </string>
+ <string name="renderer_asynchronous_shaders_description">Compila shaders assincronamente, que aumentará a fluidez, mas poderá causar falhas.</string>
+ <string name="renderer_debug">Ativar depuração de gráficos</string>
+ <string name="renderer_debug_description">Quando selecionado, a API gráfica entra num modo de depuração mais lento.</string>
+ <string name="use_disk_shader_cache">Usar cache de shaders em disco</string>
+ <string name="use_disk_shader_cache_description">Aumenta a fluidez ao guardar e carregar shaders gerados para o armazenamento.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volume</string>
+ <string name="audio_volume_description">Especifica o volume de saída.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Padrão</string>
+ <string name="ini_saved">Definições guardadas</string>
+ <string name="gameid_saved">Definições guardadas para %1$s</string>
+ <string name="error_saving">Erro ao guardar %1$s.ini: %2$s</string>
+ <string name="loading">A carregar...</string>
+ <string name="reset_setting_confirmation">Queres reverter esta definição para os valores padrão?</string>
+ <string name="reset_to_default">Reverter para padrão</string>
+ <string name="reset_all_settings">Redefinir todas as definições?</string>
+ <string name="reset_all_settings_description">Todas as definições avançadas serão redefinidas para as definições padrão. Isto não pode ser revertido.</string>
+ <string name="settings_reset">Redefinir definições</string>
+ <string name="close">Fechar</string>
+ <string name="learn_more">Saiba mais</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Seleciona a driver para o GPU</string>
+ <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
+ <string name="select_gpu_driver_install">Instalar</string>
+ <string name="select_gpu_driver_default">Padrão</string>
+ <string name="select_gpu_driver_install_success">Instalado%s</string>
+ <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
+ <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
+ <string name="system_gpu_driver">Driver do GPU padrão</string>
+ <string name="installing_driver">A instalar o Driver...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Configurações</string>
+ <string name="preferences_general">Geral</string>
+ <string name="preferences_system">Sistema</string>
+ <string name="preferences_graphics">Gráficos</string>
+ <string name="preferences_audio">Áudio</string>
+ <string name="preferences_theme">Cor e tema.</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">A tua ROM está encriptada</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Por favor segue os guias para fazer redump das tuas<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">Cartidges de Jogo</a> or <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">Jogos Instalados</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Por favor confirma que o teu ficheiro <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> está instalado para que os jogos possam ser desencriptados.]]></string>
+ <string name="loader_error_video_core">Ocorreu um erro ao iniciar o núcleo de vídeo.</string>
+ <string name="loader_error_video_core_description">Isto é normalmente causado por um driver de GPU incompatível. Instalar um driver GPU pode resolver este problema.</string>
+ <string name="loader_error_invalid_format">Impossível carregar a tua ROM</string>
+ <string name="loader_error_file_not_found">O ficheiro da ROM não existe</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Sair da emulação</string>
+ <string name="emulation_done">Feito</string>
+ <string name="emulation_fps_counter">Contador de FPS</string>
+ <string name="emulation_toggle_controls">Alterar Controlos</string>
+ <string name="emulation_rel_stick_center">Centro do Analógico Relativo</string>
+ <string name="emulation_dpad_slide">Deslizar do DPad</string>
+ <string name="emulation_haptics">Hápticos </string>
+ <string name="emulation_show_overlay">Mostrar sobreposição </string>
+ <string name="emulation_toggle_all">Alterar todos</string>
+ <string name="emulation_control_adjust">Ajustar a sobreposição </string>
+ <string name="emulation_control_scale">Escala</string>
+ <string name="emulation_control_opacity">Opacidade</string>
+ <string name="emulation_touch_overlay_reset">Redefinir Sobreposição </string>
+ <string name="emulation_touch_overlay_edit">Editar sobreposição </string>
+ <string name="emulation_pause">Pausa emulação</string>
+ <string name="emulation_unpause">Retomar emulação</string>
+ <string name="emulation_input_overlay">Opções de sobreposição </string>
+ <string name="emulation_game_loading">Jogo a carregar...</string>
+
+ <string name="load_settings">Configurações a carregar...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Teclado de software</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abortar</string>
+ <string name="continue_button">Continuar</string>
+ <string name="system_archive_not_found">Arquivo do sistema não encontrado</string>
+ <string name="system_archive_not_found_message">%s está em falta. Por favor apaga os teus ficheiros de sistema.\nContinuar a emulação pode causar erros.</string>
+ <string name="system_archive_general">Um arquivo do sistema</string>
+ <string name="save_load_error">Erro Guardar/Carregar</string>
+ <string name="fatal_error">Erro fatal</string>
+ <string name="fatal_error_message">Ocorreu um erro fatal. Verifica o teu registro para detalhes. \nContinuar a emulação pode causar erros.</string>
+ <string name="performance_warning">Desligar esta configuração irá reduzir a performance da emulação significantemente! Para a melhor experiência é recomendado que deixes esta configuração ativada.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japão</string>
+ <string name="region_usa">EUA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Austrália</string>
+ <string name="region_china">China</string>
+ <string name="region_korea">Coréia</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japônes (日本語)</string>
+ <string name="language_english">Português do Brasil</string>
+ <string name="language_french">Francês (Français)</string>
+ <string name="langauge_german">Alemão (Deutsch)</string>
+ <string name="language_italian">Italiano (Italiano)</string>
+ <string name="language_spanish">Espanhol (Español)</string>
+ <string name="language_chinese">Mandarim (简体中文)</string>
+ <string name="language_korean">Coreano (한국어)</string>
+ <string name="language_dutch">Holandês (Nederlands)</string>
+ <string name="language_portuguese">Português (Português)</string>
+ <string name="language_russian">Russo (Русский)</string>
+ <string name="language_taiwanese">Taiwanês (台湾)</string>
+ <string name="language_british_english">Inglês britânico (British English)</string>
+ <string name="language_canadian_french">Fracês Canadiano (Français canadien)</string>
+ <string name="language_latin_american_spanish">Espanhol da América Latina (Español latino-americano)</string>
+ <string name="language_simplified_chinese">Chinês Simplificado (简体中文)</string>
+ <string name="language_traditional_chinese">Chinês tradicional (正體中文)</string>
+ <string name="language_brazilian_portuguese">Português do Brasil (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulcano</string>
+ <string name="renderer_none">Nenhum</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Alto</string>
+ <string name="renderer_accuracy_extreme">Estremo (Lento)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Slow)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Lento)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Lento)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Imediato (Desligado)</string>
+ <string name="renderer_vsync_mailbox">Caixa de entrada</string>
+ <string name="renderer_vsync_fifo">FIFO (Ligado)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxado </string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Vizinho mais próximo</string>
+ <string name="scaling_filter_bilinear">Bilinear</string>
+ <string name="scaling_filter_bicubic">Bicúbico</string>
+ <string name="scaling_filter_gaussian">Gaussiano</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Nenhum</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Padrão (16:9)</string>
+ <string name="ratio_force_four_three">Forçar 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Forçar 21:9</string>
+ <string name="ratio_force_sixteen_ten">Forçar 16:10</string>
+ <string name="ratio_stretch">Esticar para a janela</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Preciso</string>
+ <string name="cpu_accuracy_unsafe">Não seguro</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Lento)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-pad</string>
+ <string name="gamepad_left_stick">Analógico esquerdo</string>
+ <string name="gamepad_right_stick">Analógico direito</string>
+ <string name="gamepad_home">Botão Home</string>
+ <string name="gamepad_screenshot">Captura de ecrã</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">A preparar shaders</string>
+ <string name="building_shaders">A criar shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Muda o Tema da App</string>
+ <string name="theme_default">Padrão</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Altera o Modo do Tema</string>
+ <string name="theme_mode_follow_system">Igual ao Sistema</string>
+ <string name="theme_mode_light">Claro</string>
+ <string name="theme_mode_dark">Escuro</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Usa Fundos Negros</string>
+ <string name="use_black_backgrounds_description">Quando usar tema escuro, aplicar fundos escuros</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 000000000..8761e2374
--- /dev/null
+++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Este software corre jogos para a consola Nintendo Switch. Não estão incluídas nem jogos ou chaves. &lt;br /&gt;&lt;br /&gt;Antes de começares, por favor localiza o ficheiro <![CDATA[1 prod.keys 1]]> no armazenamento do teu dispositivo.&lt;br /&gt;&lt;br /&gt;<![CDATA[2Learn more2]]></string>
+ <string name="emulation_notification_channel_name">Emulação está Ativa</string>
+ <string name="emulation_notification_channel_description">Mostra uma notificação permanente enquanto a emulação está a correr.</string>
+ <string name="emulation_notification_running">Yuzu está em execução </string>
+ <string name="notice_notification_channel_name">Notificações e erros</string>
+ <string name="notice_notification_channel_description">Mostra notificações quendo algo corre mal.</string>
+ <string name="notification_permission_not_granted">Permissões de notificação não permitidas </string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Benvindo! </string>
+ <string name="welcome_description">Aprende como configurar &lt;b>yuzu&lt;/b> e arranca a emulação.</string>
+ <string name="get_started">Começa</string>
+ <string name="keys">Chaves</string>
+ <string name="keys_description">Seleciona o teu ficheiro &lt;b>prod.keys&lt;/b> com o botão abaixo.</string>
+ <string name="select_keys">Seleciona as Chaves</string>
+ <string name="games">Jogos</string>
+ <string name="games_description">Seleciona a tua pasta &lt;b>Games&lt;/b> com o botão abaixo.</string>
+ <string name="done">Feito</string>
+ <string name="done_description">Tudo pronto.\nDisfruta dos teus jogos!</string>
+ <string name="text_continue">Continuar</string>
+ <string name="next">Próximo</string>
+ <string name="back">Voltar</string>
+ <string name="add_games">Adiciona Jogos</string>
+ <string name="add_games_description">Seleciona a tua pasta de Jogos</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Jogos</string>
+ <string name="home_search">Pesquisar</string>
+ <string name="home_settings">Configurações</string>
+ <string name="empty_gamelist">Não foram encontrados jogos ou a pasta de Jogos ainda não foi definida. </string>
+ <string name="search_and_filter_games">Procura e filtra jogos.</string>
+ <string name="select_games_folder">Seleciona a pasta de jogos.</string>
+ <string name="select_games_folder_description">Permite que o Yuzu preencha a lista de jogos</string>
+ <string name="add_games_warning">Ignorar a seleção da pasta de jogos?</string>
+ <string name="add_games_warning_description">Os jogos não serão exibidos na lista de jogos se uma pasta não estiver selecionada.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Procurar Jogos</string>
+ <string name="games_dir_selected">Pasta de Jogos selecionada</string>
+ <string name="install_prod_keys">Instala prod.keys</string>
+ <string name="install_prod_keys_description">Necessário para desencriptar jogos comerciais</string>
+ <string name="install_prod_keys_warning">Ignorar a adição de chaves?</string>
+ <string name="install_prod_keys_warning_description">São necessárias chaves válidas para emular jogos comerciais. Somente aplicativos homebrew funcionarão se você continuar.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Notificações</string>
+ <string name="notifications_description">Conceda a permissão de notificação com o botão abaixo.</string>
+ <string name="give_permission">Conceda permissão</string>
+ <string name="notification_warning">Saltar a concessão da permissão de notificação?</string>
+ <string name="notification_warning_description">Yuzu não conseguirá te notificar de informações importantes. </string>
+ <string name="permission_denied">Permissão negada</string>
+ <string name="permission_denied_description">Você negou essa permissão muitas vezes e agora precisa concedê-la manualmente nas configurações do sistema.</string>
+ <string name="about">Sobre</string>
+ <string name="about_description">Versão de compilação, créditos e mais</string>
+ <string name="warning_help">Ajuda</string>
+ <string name="warning_skip">Saltar</string>
+ <string name="warning_cancel">Cancelar</string>
+ <string name="install_amiibo_keys">Instala chaves Amiibo</string>
+ <string name="install_amiibo_keys_description">Necessário para usares Amiibo no jogo</string>
+ <string name="invalid_keys_file">Ficheiro de chaves inválido</string>
+ <string name="install_keys_success">Chaves instaladas com sucesso</string>
+ <string name="reading_keys_failure">Erro ao ler chaves de encriptação</string>
+ <string name="invalid_keys_error">Chaves de encriptação inválidas</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">O ficheiro selecionado está corrompido. Por favor recarrega as tuas chaves.</string>
+ <string name="install_gpu_driver">Instala driver para GPU</string>
+ <string name="install_gpu_driver_description">Instala drivers alternativos para desempenho ou precisão potencialmente melhores</string>
+ <string name="advanced_settings">Configurações avançadas</string>
+ <string name="settings_description">Configura configurações do emulador</string>
+ <string name="search_recently_played">Jogos recentes</string>
+ <string name="search_recently_added">Adicionados recentemente</string>
+ <string name="search_retail">Jogos comerciais</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Abre a pasta Yuzu</string>
+ <string name="open_user_folder_description">Gere os ficheiro internos do Yuzu</string>
+ <string name="theme_and_color_description">Modifica a aparência da App</string>
+ <string name="no_file_manager">Nenhum gestor de ficheiros encontrado</string>
+ <string name="notification_no_directory_link">Impossível abrir pasta Yuzu</string>
+ <string name="notification_no_directory_link_description">Localiza a pasta de utilizador manualmente com o painel lateral do gestor de ficheiros.</string>
+ <string name="manage_save_data">Gerir dados guardados</string>
+ <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
+ <string name="import_export_saves_description">Importa ou exporta dados guardados</string>
+ <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
+ <string name="save_file_imported_success">Importado com sucesso</string>
+ <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
+ <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string>
+ <string name="import_saves">Importar</string>
+ <string name="export_saves">Exportar</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia não é real</string>
+ <string name="copied_to_clipboard">Copiado para a área de transferência</string>
+ <string name="about_app_description">Um emulador Switch de código aberto</string>
+ <string name="contributors">Contribuidores</string>
+ <string name="contributors_description">Feito com \u2764 da equipa do Yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Versão</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Acesso antecipado</string>
+ <string name="get_early_access">Obtém Acesso Antecipado</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Recursos de ponta, acesso antecipado a atualizações e muito mais</string>
+ <string name="early_access_benefits">Benefícios do Acesso Antecipado</string>
+ <string name="cutting_edge_features">Recursos de ponta</string>
+ <string name="early_access_updates">Acesso antecipado a atualizações</string>
+ <string name="no_manual_installation">Sem instalação manual</string>
+ <string name="prioritized_support">Suporte prioritário</string>
+ <string name="helping_game_preservation">Ajuda na preservação dos jogos</string>
+ <string name="our_eternal_gratitude">A nossa eterna gratidão</string>
+ <string name="are_you_interested">Estás interessado?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Ativar limite de velocidade</string>
+ <string name="frame_limit_enable_description">Quando ativada, a velocidade da emulação será limitada à percentagem definida da velocidade normal.</string>
+ <string name="frame_limit_slider">Percentagem do limite de velocidade</string>
+ <string name="frame_limit_slider_description">Especifica o limite da percentagem da velocidade da emulação. Com a velocidade por defeito a 100% a emulação será limitada à velocidade normal. Valores maiores ou menores aumentarão ou diminuirão o limite de velocidade.</string>
+ <string name="cpu_accuracy">Precisão do CPU</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Modo ancorado</string>
+ <string name="use_docked_mode_description">Emula em modo ancorado, que aumenta a resolução ás custas da performance.</string>
+ <string name="emulated_region">Região da emulação</string>
+ <string name="emulated_language">Idioma da emulação</string>
+ <string name="select_rtc_date">Seleciona a data RTC</string>
+ <string name="select_rtc_time">Seleciona a hora RTC</string>
+ <string name="use_custom_rtc">Ativa RTC personalizado</string>
+ <string name="use_custom_rtc_description">Esta configuração permite definir um RTC personalizado diferente da hora atual do sistema</string>
+ <string name="set_custom_rtc">Define RTC personalizado</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Nível de precisão</string>
+ <string name="renderer_resolution">Resolução</string>
+ <string name="renderer_vsync">Modo VSync</string>
+ <string name="renderer_aspect_ratio">Proporção do ecrã</string>
+ <string name="renderer_scaling_filter">Filtro de Adaptação da Janela</string>
+ <string name="renderer_anti_aliasing">Método de Anti-Aliasing </string>
+ <string name="renderer_force_max_clock">Força velocidade máxima (Adreno only)</string>
+ <string name="renderer_force_max_clock_description">Força o GPU a correr à velocidade máxima (restrições térmicas serão aplicadas)</string>
+ <string name="renderer_asynchronous_shaders">Usa shaders assíncronos </string>
+ <string name="renderer_asynchronous_shaders_description">Compila shaders assincronamente, que aumentará a fluidez, mas poderá causar falhas.</string>
+ <string name="renderer_debug">Ativar depuração de gráficos</string>
+ <string name="renderer_debug_description">Quando selecionado, a API gráfica entra num modo de depuração mais lento.</string>
+ <string name="use_disk_shader_cache">Usar cache do disk shader</string>
+ <string name="use_disk_shader_cache_description">Aumenta a fluidez ao guardar e carregar shaders gerados para o armazenamento.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volume</string>
+ <string name="audio_volume_description">Especifica o volume de saída.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Padrão</string>
+ <string name="ini_saved">Configurações guardadas</string>
+ <string name="gameid_saved">Configurações guardadas para %1$s</string>
+ <string name="error_saving">Erro ao guardar %1$s.ini: %2$s</string>
+ <string name="loading">A carregar...</string>
+ <string name="reset_setting_confirmation">Queres reverter esta definição para os valores padrão?</string>
+ <string name="reset_to_default">Reverter para padrão</string>
+ <string name="reset_all_settings">Redefinir todas as configurações?</string>
+ <string name="reset_all_settings_description">Todas as configurações avançadas serão redefinidas para as definições padrão. Isto não pode ser revertido.</string>
+ <string name="settings_reset">Redefinir configurações </string>
+ <string name="close">Fechar</string>
+ <string name="learn_more">Saber Mais</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Seleciona a driver para o GPU</string>
+ <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
+ <string name="select_gpu_driver_install">Instalar</string>
+ <string name="select_gpu_driver_default">Padrão</string>
+ <string name="select_gpu_driver_install_success">Instalado%s</string>
+ <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
+ <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
+ <string name="system_gpu_driver">Driver do GPU padrão</string>
+ <string name="installing_driver">A instalar o Driver...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Configurações</string>
+ <string name="preferences_general">Geral</string>
+ <string name="preferences_system">Sistema</string>
+ <string name="preferences_graphics">Gráficos</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Cor e tema.</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">A tua ROM está encriptada</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Por favor segue os guias para fazer redump das tuas<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">Cartidges de Jogo</a> or <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">Jogos Instalados</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Por favor confirma que o teu ficheiro <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> está instalado para que os jogos possam ser desencriptados.]]></string>
+ <string name="loader_error_video_core">Ocorreu um erro ao iniciar o núcleo de vídeo.</string>
+ <string name="loader_error_video_core_description">Isto é normalmente causado por um driver de GPU incompatível. Instalar um driver GPU pode resolver este problema.</string>
+ <string name="loader_error_invalid_format">Impossível carregar a tua ROM</string>
+ <string name="loader_error_file_not_found">O ficheiro da ROM não existe</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Sair da emulação</string>
+ <string name="emulation_done">Feito</string>
+ <string name="emulation_fps_counter">Contador de FPS</string>
+ <string name="emulation_toggle_controls">Alterar Controlos</string>
+ <string name="emulation_rel_stick_center">Centro do Analógico Relativo</string>
+ <string name="emulation_dpad_slide">Deslizar do DPad</string>
+ <string name="emulation_haptics">Hápticos </string>
+ <string name="emulation_show_overlay">Mostrar sobreposição </string>
+ <string name="emulation_toggle_all">Alterar todos</string>
+ <string name="emulation_control_adjust">Ajustar a sobreposição </string>
+ <string name="emulation_control_scale">Escala</string>
+ <string name="emulation_control_opacity">Opacidade</string>
+ <string name="emulation_touch_overlay_reset">Redefinir Sobreposição </string>
+ <string name="emulation_touch_overlay_edit">Editar sobreposição </string>
+ <string name="emulation_pause">Pausa emulação</string>
+ <string name="emulation_unpause">Retomar emulação</string>
+ <string name="emulation_input_overlay">Opções de sobreposição </string>
+ <string name="emulation_game_loading">Jogo a carregar...</string>
+
+ <string name="load_settings">Configurações a carregar...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Teclado de Software</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abortar</string>
+ <string name="continue_button">Continuar</string>
+ <string name="system_archive_not_found">Arquivo do Sistema Não Encontrado</string>
+ <string name="system_archive_not_found_message">%s está em falta. Por favor apaga os teus ficheiros de sistema.\nContinuar a emulação pode causar erros.</string>
+ <string name="system_archive_general">Um arquivo do sistema</string>
+ <string name="save_load_error">Erro Guardar/Carregar</string>
+ <string name="fatal_error">Erro fatal</string>
+ <string name="fatal_error_message">Ocorreu um erro fatal. Verifica o teu registro para detalhes. \nContinuar a emulação pode causar erros.</string>
+ <string name="performance_warning">Desligar esta configuração irá reduzir a performance da emulação significantemente! Para a melhor experiência é recomendado que deixes esta configuração ativada.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japão</string>
+ <string name="region_usa">EUA</string>
+ <string name="region_europe">Europa</string>
+ <string name="region_australia">Austrália</string>
+ <string name="region_china">China</string>
+ <string name="region_korea">Coreia</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japonês (日本語)</string>
+ <string name="language_english">Inglês</string>
+ <string name="language_french">Francês (Français)</string>
+ <string name="langauge_german">Alemão (Deutsch)</string>
+ <string name="language_italian">Italiano (Italiano)</string>
+ <string name="language_spanish">Espanhol (Español)</string>
+ <string name="language_chinese">Chinês simplificado (简体中文)</string>
+ <string name="language_korean">Coreano (한국어)</string>
+ <string name="language_dutch">Holandês (Nederlands)</string>
+ <string name="language_portuguese">Português (Português)</string>
+ <string name="language_russian">Russo (Русский)</string>
+ <string name="language_taiwanese">Taiwanês (台湾)</string>
+ <string name="language_british_english">Inglês Britânico</string>
+ <string name="language_canadian_french">Fracês Canadiano (Français canadien)</string>
+ <string name="language_latin_american_spanish">Espanhol da América Latina (Español latino-americano)</string>
+ <string name="language_simplified_chinese">Chinês Simplificado (简体中文)</string>
+ <string name="language_traditional_chinese">Chinês Tradicional (正 體 中文)</string>
+ <string name="language_brazilian_portuguese">Português do Brasil (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulcano</string>
+ <string name="renderer_none">Nenhum</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">Alto</string>
+ <string name="renderer_accuracy_extreme">Estremo (Lento)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Lento)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Lento)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Lento)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Imediato (Desligado)</string>
+ <string name="renderer_vsync_mailbox">Caixa de entrada</string>
+ <string name="renderer_vsync_fifo">FIFO (Ligado)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxado </string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Vizinho mais próximo</string>
+ <string name="scaling_filter_bilinear">Bilinear</string>
+ <string name="scaling_filter_bicubic">Bicúbico</string>
+ <string name="scaling_filter_gaussian">Gaussiano</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Nenhum</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Padrão (16:9)</string>
+ <string name="ratio_force_four_three">Forçar 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Forçar 21:9</string>
+ <string name="ratio_force_sixteen_ten">Forçar 16:10</string>
+ <string name="ratio_stretch">Esticar à Janela</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Preciso</string>
+ <string name="cpu_accuracy_unsafe">Inseguro</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Lento)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-Pad</string>
+ <string name="gamepad_left_stick">Analógico Esquerdo</string>
+ <string name="gamepad_right_stick">Analógico Direito</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Captura de ecrã</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">A preparar shaders</string>
+ <string name="building_shaders">A criar shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Muda o Tema da App</string>
+ <string name="theme_default">Padrão</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Altera o Modo do Tema</string>
+ <string name="theme_mode_follow_system">Igual ao Sistema</string>
+ <string name="theme_mode_light">Claro</string>
+ <string name="theme_mode_dark">Escuro</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Usa Fundos Escuros</string>
+ <string name="use_black_backgrounds_description">Quando usar tema escuro, aplicar fundos escuros</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..0fb4908f7
--- /dev/null
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Это программное обеспечение позволяет запускать игры для игровой консоли Nintendo Switch. Мы не предоставляем сами игры или ключи.&lt;br /&gt;&lt;br /&gt;Перед началом работы найдите файл <![CDATA[<b> prod.keys </b>]]> в хранилище устройства..&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Узнать больше</a>]]></string>
+ <string name="emulation_notification_channel_name">Эмуляция активна</string>
+ <string name="emulation_notification_channel_description">Показывает постоянное уведомление, когда запущена эмуляция.</string>
+ <string name="emulation_notification_running">yuzu запущен</string>
+ <string name="notice_notification_channel_name">Уведомления и ошибки</string>
+ <string name="notice_notification_channel_description">Показывать уведомления, когда что-то пошло не так</string>
+ <string name="notification_permission_not_granted">Вы не предоставили разрешение уведомлений!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Добро пожаловать!</string>
+ <string name="welcome_description">Узнайте, как настроить &lt;b>yuzu&lt;/b> и перейти прямиком к эмуляции.</string>
+ <string name="get_started">Начать</string>
+ <string name="keys">Ключи</string>
+ <string name="keys_description">Выберите ваш файл &lt;b>prod.keys&lt;/b> с помощью кнопки ниже.</string>
+ <string name="select_keys">Выбрать ключи</string>
+ <string name="games">Игры</string>
+ <string name="games_description">Выберите вашу папку с &lt;b>играми&lt;/b> с помощью кнопки ниже.</string>
+ <string name="done">Готово</string>
+ <string name="done_description">Все готово.\nМожно играть!</string>
+ <string name="text_continue">Продолжить</string>
+ <string name="next">Далее</string>
+ <string name="back">Назад</string>
+ <string name="add_games">Добавить игры</string>
+ <string name="add_games_description">Выберите папку с играми</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Игры</string>
+ <string name="home_search">Поиск</string>
+ <string name="home_settings">Настройки</string>
+ <string name="empty_gamelist">Не найдены файлы или еще не выбрана папка с играми.</string>
+ <string name="search_and_filter_games">Поиск и фильтрация игр</string>
+ <string name="select_games_folder">Выберите папку с играми</string>
+ <string name="select_games_folder_description">Позволяет yuzu заполнить список игр</string>
+ <string name="add_games_warning">Пропустить выбор папки с играми?</string>
+ <string name="add_games_warning_description">Игры не будут отображаться в списке Игры, если папка не выбрана.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Найти игры</string>
+ <string name="games_dir_selected">Выбрана папка с играми</string>
+ <string name="install_prod_keys">Установить prod.keys</string>
+ <string name="install_prod_keys_description">Требуется для расшифровки розничных игр</string>
+ <string name="install_prod_keys_warning">Пропустить добавление ключей?</string>
+ <string name="install_prod_keys_warning_description">Для эмуляции розничных игр требуются действительные ключи. Если вы продолжите, будут работать только homebrew приложения.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Уведомления</string>
+ <string name="notifications_description">Предоставьте разрешение уведомлений с помощью кнопки ниже.</string>
+ <string name="give_permission">Предоставить разрешение</string>
+ <string name="notification_warning">Пропустить предоставление разрешения уведомлений?</string>
+ <string name="notification_warning_description">yuzu не сможет уведомлять вас о важной информации.</string>
+ <string name="permission_denied">Разрешение отказано</string>
+ <string name="permission_denied_description">Вы слишком часто отклоняли это разрешение, и теперь вам нужно будет вручную предоставить его в настройках системы.</string>
+ <string name="about">О нас</string>
+ <string name="about_description">Версия сборки, титры и другое</string>
+ <string name="warning_help">Помощь</string>
+ <string name="warning_skip">Пропустить</string>
+ <string name="warning_cancel">Отмена</string>
+ <string name="install_amiibo_keys">Установить ключи Amiibo</string>
+ <string name="install_amiibo_keys_description">Необходимо для использования Amiibo в играх</string>
+ <string name="invalid_keys_file">Выбран неверный файл ключей</string>
+ <string name="install_keys_success">Ключи успешно установлены</string>
+ <string name="reading_keys_failure">Ошибка при чтении ключей шифрования</string>
+ <string name="invalid_keys_error">Неверные ключи шифрования</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Выбранный файл неверен или поврежден. Пожалуйста, пере-дампите ваши ключи.</string>
+ <string name="install_gpu_driver">Установить драйвер ГП</string>
+ <string name="install_gpu_driver_description">Установите альтернативные драйверы для потенциально лучшей производительности и/или точности</string>
+ <string name="advanced_settings">Расширенные настройки</string>
+ <string name="settings_description">Настройка параметров эмулятора</string>
+ <string name="search_recently_played">Недавно сыграно</string>
+ <string name="search_recently_added">Недавно добавлено</string>
+ <string name="search_retail">Розничные</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Открыть папку yuzu</string>
+ <string name="open_user_folder_description">Управление внутренними файлами yuzu</string>
+ <string name="theme_and_color_description">Изменение внешнего вида приложения</string>
+ <string name="no_file_manager">Не найден файловый менеджер</string>
+ <string name="notification_no_directory_link">Не удалось открыть папку yuzu</string>
+ <string name="notification_no_directory_link_description">Пожалуйста, найдите папку пользователя с помощью боковой панели файлового менеджера вручную.</string>
+ <string name="manage_save_data">Управление данными сохранений</string>
+ <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string>
+ <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string>
+ <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string>
+ <string name="save_file_imported_success">Успешно импортировано</string>
+ <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string>
+ <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string>
+ <string name="import_saves">Импорт</string>
+ <string name="export_saves">Экспорт</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia не существует</string>
+ <string name="copied_to_clipboard">Скопировано в буфер обмена</string>
+ <string name="about_app_description">Эмулятор Switch с открытым исходным кодом</string>
+ <string name="contributors">Контрибьюторы</string>
+ <string name="contributors_description">Сделано с \u2764 от команды yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Сборка</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Ранний доступ</string>
+ <string name="get_early_access">Получить ранний доступ</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Новейшие возможности, ранний доступ к обновлениям и другое</string>
+ <string name="early_access_benefits">Преимущества раннего доступа</string>
+ <string name="cutting_edge_features">Новейшие возможности</string>
+ <string name="early_access_updates">Ранний доступ к обновлениям</string>
+ <string name="no_manual_installation">Без ручной установки</string>
+ <string name="prioritized_support">Приоритетная поддержка</string>
+ <string name="helping_game_preservation">Помощь в презервации игр</string>
+ <string name="our_eternal_gratitude">Наша бесконечная благодарность</string>
+ <string name="are_you_interested">Вы заинтересованы?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Включить ограничение скорости</string>
+ <string name="frame_limit_enable_description">Если эта функция включена, скорость эмуляции будет ограничена указанным процентом от нормальной скорости.</string>
+ <string name="frame_limit_slider">Ограничение процента cкорости</string>
+ <string name="frame_limit_slider_description">Указывает процент для ограничения скорости эмуляции. При значении по умолчанию 100% эмуляция будет ограничена нормальной скоростью. Значения выше или ниже будут увеличивать или уменьшать ограничение скорости.</string>
+ <string name="cpu_accuracy">Точность ЦП</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Режим док-станции</string>
+ <string name="use_docked_mode_description">Эмуляция режима док-станции, что увеличивает разрешение за счет снижения производительности.</string>
+ <string name="emulated_region">Эмулируемый регион</string>
+ <string name="emulated_language">Эмулируемый язык</string>
+ <string name="select_rtc_date">Выберите дату RTC</string>
+ <string name="select_rtc_time">Выберите время RTC</string>
+ <string name="use_custom_rtc">Включить пользовательский RTC</string>
+ <string name="use_custom_rtc_description">Этот параметр позволяет установить пользовательские часы реального времени отдельно от текущего системного времени</string>
+ <string name="set_custom_rtc">Установить пользовательский RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Уровень точности</string>
+ <string name="renderer_resolution">Разрешение</string>
+ <string name="renderer_vsync">Режим верт. синхронизации</string>
+ <string name="renderer_aspect_ratio">Соотношение сторон</string>
+ <string name="renderer_scaling_filter">Фильтр адаптации окна</string>
+ <string name="renderer_anti_aliasing">Метод сглаживания</string>
+ <string name="renderer_force_max_clock">Принудительно заставить максимальную тактовую частоту (только для Adreno)</string>
+ <string name="renderer_force_max_clock_description">Заставляет ГП работать на максимально возможных тактовых частотах (тепловые ограничения все равно будут применяться).</string>
+ <string name="renderer_asynchronous_shaders">Использовать асинхронные шейдеры</string>
+ <string name="renderer_asynchronous_shaders_description">Компилирует шейдеры асинхронно, что уменьшает зависания, но может взамен предоставить визуальные баги.</string>
+ <string name="renderer_debug">Включить отладку графики</string>
+ <string name="renderer_debug_description">Если включено, графический API переходит в более медленный режим отладки</string>
+ <string name="use_disk_shader_cache">Использовать кэш шейдеров на диске</string>
+ <string name="use_disk_shader_cache_description">Уменьшение зависаний за счет хранения и загрузки сгенерированных шейдеров на хранилище.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Громкость</string>
+ <string name="audio_volume_description">Задает громкость аудиовыхода.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">По умолчанию</string>
+ <string name="ini_saved">Сохраненные настройки</string>
+ <string name="gameid_saved">Настройки сохранены для %1$s</string>
+ <string name="error_saving">Ошибка сохранения %1$s.ini: %2$s</string>
+ <string name="loading">Загрузка...</string>
+ <string name="reset_setting_confirmation">Хотите ли вы вернуть этот параметр к значению по умолчанию?</string>
+ <string name="reset_to_default">Сброс к настройкам по умолчанию</string>
+ <string name="reset_all_settings">Сбросить все настройки?</string>
+ <string name="reset_all_settings_description">Все дополнительные настройки будут сброшены к настройке по умолчанию. Это невозможно отменить.</string>
+ <string name="settings_reset">Настройки сброшены</string>
+ <string name="close">Закрыть</string>
+ <string name="learn_more">Узнать больше</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Выбрать драйвер ГП</string>
+ <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>
+ <string name="select_gpu_driver_install">Установить</string>
+ <string name="select_gpu_driver_default">По умолчанию</string>
+ <string name="select_gpu_driver_install_success">Установлено %s</string>
+ <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string>
+ <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>
+ <string name="system_gpu_driver">Системный драйвер ГП</string>
+ <string name="installing_driver">Установка драйвера...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Настройки</string>
+ <string name="preferences_general">Общие</string>
+ <string name="preferences_system">Система</string>
+ <string name="preferences_graphics">Графика</string>
+ <string name="preferences_audio">Аудио</string>
+ <string name="preferences_theme">Тема и цвет</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Ваш ROM зашифрованный</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Пожалуйста, следуйте инструкциям, чтобы пере-дампить ваши <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">игровые картриджи</a> или <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">установленные игры</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Пожалуйста, убедитесь, что ваш файл <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> установлен, чтобы игры можно было расшифровать.]]></string>
+ <string name="loader_error_video_core">Произошла ошибка при инициализации видеоядра.</string>
+ <string name="loader_error_video_core_description">Обычно это вызвано несовместимым драйвером ГП. Установка пользовательского драйвера ГП может решить эту проблему.</string>
+ <string name="loader_error_invalid_format">Не удалось запустить ROM</string>
+ <string name="loader_error_file_not_found">Файл ROM не существует</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Выход из эмуляции</string>
+ <string name="emulation_done">Готово</string>
+ <string name="emulation_fps_counter">Счётчик FPS</string>
+ <string name="emulation_toggle_controls">Переключение управления</string>
+ <string name="emulation_rel_stick_center">Относительный центр стика</string>
+ <string name="emulation_dpad_slide">Слайд крестовиной</string>
+ <string name="emulation_haptics">Тактильная обратная связь</string>
+ <string name="emulation_show_overlay">Показать оверлей</string>
+ <string name="emulation_toggle_all">Переключить всё</string>
+ <string name="emulation_control_adjust">Настроить оверлей</string>
+ <string name="emulation_control_scale">Масштаб</string>
+ <string name="emulation_control_opacity">Непрозрачность</string>
+ <string name="emulation_touch_overlay_reset">Сбросить оверлей</string>
+ <string name="emulation_touch_overlay_edit">Изменить оверлей</string>
+ <string name="emulation_pause">Пауза эмуляции</string>
+ <string name="emulation_unpause">Возобновление эмуляции</string>
+ <string name="emulation_input_overlay">Настройки оверлея</string>
+ <string name="emulation_game_loading">Загрузка игры...</string>
+
+ <string name="load_settings">Загрузка настроек...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Виртуальная клавиатура</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Прервать</string>
+ <string name="continue_button">Продолжить</string>
+ <string name="system_archive_not_found">Системный архив не найден</string>
+ <string name="system_archive_not_found_message">%s отсутствует. Пожалуйста, сдампите ваши системные архивы.\nПродолжение эмуляции может привести к сбоям и ошибкам.</string>
+ <string name="system_archive_general">Системный архив</string>
+ <string name="save_load_error">Ошибка сохранения/загрузки</string>
+ <string name="fatal_error">Фатальная ошибка</string>
+ <string name="fatal_error_message">Произошла фатальная ошибка. Проверьте журнал для получения подробной информации.\nПродолжение эмуляции может привести к сбоям и ошибкам.</string>
+ <string name="performance_warning">Отключение этой настройки значительно снизит производительность эмуляции! Для достижения наилучших результатов рекомендуется оставить эту настройку включенной.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Япония</string>
+ <string name="region_usa">США</string>
+ <string name="region_europe">Европа</string>
+ <string name="region_australia">Австралия</string>
+ <string name="region_china">Китай</string>
+ <string name="region_korea">Корея</string>
+ <string name="region_taiwan">Тайвань</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Японский (日本語)</string>
+ <string name="language_english">Английский (English)</string>
+ <string name="language_french">Французский (Français)</string>
+ <string name="langauge_german">Немецкий (Deutsch)</string>
+ <string name="language_italian">Итальянский (Italiano)</string>
+ <string name="language_spanish">Испанский (Español)</string>
+ <string name="language_chinese">Китайский (简体中文)</string>
+ <string name="language_korean">Корейский (한국어)</string>
+ <string name="language_dutch">Голландский (Nederlands)</string>
+ <string name="language_portuguese">Португальский (Português)</string>
+ <string name="language_russian">Русский</string>
+ <string name="language_taiwanese">Тайваньский (台湾)</string>
+ <string name="language_british_english">Британский английский</string>
+ <string name="language_canadian_french">Канадский французский (Français canadien)</string>
+ <string name="language_latin_american_spanish">Латиноамериканский испанский (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Упрощенный китайский (简体中文)</string>
+ <string name="language_traditional_chinese">Традиционный китайский (正體中文)</string>
+ <string name="language_brazilian_portuguese">Бразильский португальский (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Никакой</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Нормальная</string>
+ <string name="renderer_accuracy_high">Высокая</string>
+ <string name="renderer_accuracy_extreme">Экстрим (медленный)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Медленно)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Медленно)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Медленно)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Моментальная (выключена) </string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (Включена)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Ближайший сосед</string>
+ <string name="scaling_filter_bilinear">Билинейный</string>
+ <string name="scaling_filter_bicubic">Бикубический</string>
+ <string name="scaling_filter_gaussian">Гаусс</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™️ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Выкл.</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Стандартное (16:9)</string>
+ <string name="ratio_force_four_three">Заставить 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Заставить 21:9</string>
+ <string name="ratio_force_sixteen_ten">Заставить 16:10</string>
+ <string name="ratio_stretch">Растянуть до окна</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Точно</string>
+ <string name="cpu_accuracy_unsafe">Небезопасно</string>
+ <string name="cpu_accuracy_paranoid">Параноик (медленно)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">Крестовина</string>
+ <string name="gamepad_left_stick">Левый мини-джойстик</string>
+ <string name="gamepad_right_stick">Правый мини-джойстик</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Скриншот</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Подготовка шейдеров</string>
+ <string name="building_shaders">Постройка шейдеров</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Изменить тему приложения</string>
+ <string name="theme_default">По умолчанию</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Изменить режим темы</string>
+ <string name="theme_mode_follow_system">Системная</string>
+ <string name="theme_mode_light">Светлая</string>
+ <string name="theme_mode_dark">Темная</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Использовать черный фон</string>
+ <string name="use_black_backgrounds_description">При использовании темной темы применяйте черный фон.</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
new file mode 100644
index 000000000..0d11eb2d2
--- /dev/null
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">Це програмне забезпечення дозволяє запускати ігри для ігрової консолі Nintendo Switch. Ми не надаємо самі ігри або ключі.&lt;br /&gt;&lt;br /&gt;Перед початком роботи знайдіть ваш файл <![CDATA[<b> prod.keys </b>]]> у сховищі пристрою.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">Дізнатися більше</a>]]></string>
+ <string name="emulation_notification_channel_name">Емуляція активна</string>
+ <string name="emulation_notification_channel_description">Показує постійне сповіщення, коли запущено емуляцію.</string>
+ <string name="emulation_notification_running">yuzu запущено</string>
+ <string name="notice_notification_channel_name">Сповіщення та помилки</string>
+ <string name="notice_notification_channel_description">Показувати сповіщення, коли щось пішло не так</string>
+ <string name="notification_permission_not_granted">Ви не надали дозвіл сповіщень!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Вітаємо!</string>
+ <string name="welcome_description">Дізнайтеся, як налаштувати &lt;b>yuzu&lt;/b> та перейти до емуляції.</string>
+ <string name="get_started">Розпочати</string>
+ <string name="keys">Ключі</string>
+ <string name="keys_description">Виберіть ваш файл &lt;b>prod.keys&lt;/b> за допомогою кнопки нижче.</string>
+ <string name="select_keys">Вибрати ключі</string>
+ <string name="games">Ігри</string>
+ <string name="games_description">Виберіть вашу папку з &lt;b>іграми&lt;/b> за допомогою кнопки нижче.</string>
+ <string name="done">Готово</string>
+ <string name="done_description">Все готово.\nМожна грати!</string>
+ <string name="text_continue">Продовжити</string>
+ <string name="next">Далі</string>
+ <string name="back">Назад</string>
+ <string name="add_games">Додати ігри</string>
+ <string name="add_games_description">Виберіть папку з іграми</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Ігри</string>
+ <string name="home_search">Пошук</string>
+ <string name="home_settings">Налаштування</string>
+ <string name="empty_gamelist">Не знайдено файлів або ще не вибрано папку з іграми.</string>
+ <string name="search_and_filter_games">Пошук та фільтрація ігор</string>
+ <string name="select_games_folder">Виберіть папку з іграми</string>
+ <string name="select_games_folder_description">Дозволяє yuzu заповнити список ігор</string>
+ <string name="add_games_warning">Пропустити вибір папки з іграми?</string>
+ <string name="add_games_warning_description">Ігри не відображатимуться у списку Ігри, якщо папку не вибрано.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Знайти ігри</string>
+ <string name="games_dir_selected">Вибрано папку з іграми</string>
+ <string name="install_prod_keys">Встановити prod.keys</string>
+ <string name="install_prod_keys_description">Потрібно для розшифровки роздрібних ігор</string>
+ <string name="install_prod_keys_warning">Пропустити додавання ключів?</string>
+ <string name="install_prod_keys_warning_description">Для емуляції роздрібних ігор потрібні дійсні ключі. Якщо ви продовжите, працюватимуть тільки homebrew додатки.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Сповіщення</string>
+ <string name="notifications_description">Надайте дозвіл сповіщень за допомогою кнопки нижче.</string>
+ <string name="give_permission">Надати дозвіл</string>
+ <string name="notification_warning">Пропустити надання дозволу сповіщень?</string>
+ <string name="notification_warning_description">yuzu не зможе повідомляти вас про важливу інформацію.</string>
+ <string name="permission_denied">У дозволі відмовлено</string>
+ <string name="permission_denied_description">Ви занадто часто відхиляли цей дозвіл, тож тепер вам потрібно буде вручну надати його в системних налаштуваннях.</string>
+ <string name="about">Про нас</string>
+ <string name="about_description">Версія збірки, титри та інше</string>
+ <string name="warning_help">Допомога</string>
+ <string name="warning_skip">Пропустити</string>
+ <string name="warning_cancel">Відміна</string>
+ <string name="install_amiibo_keys">Встановити ключі Amiibo</string>
+ <string name="install_amiibo_keys_description">Необхідно для використання Amiibo в іграх</string>
+ <string name="invalid_keys_file">Вибрано неправильний файл ключів</string>
+ <string name="install_keys_success">Ключі успішно встановлено</string>
+ <string name="reading_keys_failure">Помилка під час зчитування ключів шифрування</string>
+ <string name="invalid_keys_error">Невірні ключі шифрування</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">Обраний файл невірний або пошкоджений. Будь ласка, пере-дампіть ваші ключі.</string>
+ <string name="install_gpu_driver">Встановити драйвер ГП</string>
+ <string name="install_gpu_driver_description">Встановіть альтернативні драйвери для потенційно кращої продуктивності та/або точності</string>
+ <string name="advanced_settings">Розширені налаштування</string>
+ <string name="settings_description">Налаштування параметрів емулятора</string>
+ <string name="search_recently_played">Нещодавно зіграно</string>
+ <string name="search_recently_added">Нещодавно додано</string>
+ <string name="search_retail">Роздрібні</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Відкрити папку yuzu</string>
+ <string name="open_user_folder_description">Керування внутрішніми файлами yuzu</string>
+ <string name="theme_and_color_description">Змінити зовнішній вигляд застосунку</string>
+ <string name="no_file_manager">Не знайдено файлового менеджера</string>
+ <string name="notification_no_directory_link">Не вдалося відкрити папку yuzu</string>
+ <string name="notification_no_directory_link_description">Будь ласка, знайдіть папку користувача за допомогою бічної панелі файлового менеджера вручну.</string>
+ <string name="manage_save_data">Керування даними збережень</string>
+ <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string>
+ <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string>
+ <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string>
+ <string name="save_file_imported_success">Успішно імпортовано</string>
+ <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string>
+ <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string>
+ <string name="import_saves">Імпорт</string>
+ <string name="export_saves">Експорт</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia не існує</string>
+ <string name="copied_to_clipboard">Скопійовано в буфер обміну</string>
+ <string name="about_app_description">Емулятор Switch із відкритим першокодом</string>
+ <string name="contributors">Вкладники</string>
+ <string name="contributors_description">Зроблено з \u2764 від команди yuzu</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">Збірка</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Ранній доступ</string>
+ <string name="get_early_access">Отримати ранній доступ</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Новітні можливості, ранній доступ до оновлень та інше</string>
+ <string name="early_access_benefits">Переваги раннього доступу</string>
+ <string name="cutting_edge_features">Новітні можливості</string>
+ <string name="early_access_updates">Ранній доступ до оновлень</string>
+ <string name="no_manual_installation">Без ручного встановлення</string>
+ <string name="prioritized_support">Пріоритетна підтримка</string>
+ <string name="helping_game_preservation">Допомога в презервації ігор</string>
+ <string name="our_eternal_gratitude">Наша нескінченна вдячність</string>
+ <string name="are_you_interested">Ви зацікавлені?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Увімкнути обмеження швидкості</string>
+ <string name="frame_limit_enable_description">Якщо цю функцію ввімкнено, швидкість емуляції буде обмежена зазначеним відсотком від нормальної швидкості.</string>
+ <string name="frame_limit_slider">Обмеження відсотка швидкості</string>
+ <string name="frame_limit_slider_description">Вказує відсоток для обмеження швидкості емуляції. При значенні за замовчуванням 100% емуляція буде обмежена нормальною швидкістю. Значення вище або нижче збільшуватимуть або зменшуватимуть обмеження швидкості.</string>
+ <string name="cpu_accuracy">Точність ЦП</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Режим док-станції</string>
+ <string name="use_docked_mode_description">Емуляція режиму док-станції, що збільшує роздільну здатність за рахунок зниження продуктивності.</string>
+ <string name="emulated_region">Емульований регіон</string>
+ <string name="emulated_language">Емульована мова</string>
+ <string name="select_rtc_date">Оберіть дату RTC</string>
+ <string name="select_rtc_time">Оберіть час RTC</string>
+ <string name="use_custom_rtc">Увімкнути користувацький RTC</string>
+ <string name="use_custom_rtc_description">Цей параметр дає змогу встановити користувацький годинник реального часу окремо від поточного системного часу</string>
+ <string name="set_custom_rtc">Встановити користувацький RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Рівень точності</string>
+ <string name="renderer_resolution">Роздільна здатність</string>
+ <string name="renderer_vsync">Режим верт. синхронізації</string>
+ <string name="renderer_aspect_ratio">Співвідношення сторін</string>
+ <string name="renderer_scaling_filter">Фільтр адаптації вікна</string>
+ <string name="renderer_anti_aliasing">Метод згладжування</string>
+ <string name="renderer_force_max_clock">Примусово змусити максимальну тактову частоту (тільки для Adreno)</string>
+ <string name="renderer_force_max_clock_description">Змушує ГП працювати на максимально можливих тактових частотах (теплові обмеження все одно будуть застосовуватися).</string>
+ <string name="renderer_asynchronous_shaders">Використовувати асинхронні шейдери</string>
+ <string name="renderer_asynchronous_shaders_description">Компілює шейдери асинхронно, що зменшує зависання, але може натомість надати візуальні баги.</string>
+ <string name="renderer_debug">Увімкнути налагодження графіки</string>
+ <string name="renderer_debug_description">Якщо увімкнено, графічний API переходить у повільніший режим налагодження</string>
+ <string name="use_disk_shader_cache">Використовувати кеш шейдерів на диску</string>
+ <string name="use_disk_shader_cache_description">Зменшення зависань завдяки зберіганню та завантаженню згенерованих шейдерів на сховище.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Гучність</string>
+ <string name="audio_volume_description">Вказує гучність аудіовиходу.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">За замовчуванням</string>
+ <string name="ini_saved">Збережені налаштування</string>
+ <string name="gameid_saved">Налаштування збережені для %1$s</string>
+ <string name="error_saving">Помилка збереження %1$s.ini: %2$s</string>
+ <string name="loading">Завантаження...</string>
+ <string name="reset_setting_confirmation">Чи хочете ви повернути цей параметр до значення за замовчуванням?</string>
+ <string name="reset_to_default">Скидання до налаштувань за замовчуванням</string>
+ <string name="reset_all_settings">Скинути всі налаштування</string>
+ <string name="reset_all_settings_description">Усі додаткові налаштування буде скинуто до налаштування за замовчуванням. Це неможливо скасувати.</string>
+ <string name="settings_reset">Налаштування скинуто</string>
+ <string name="close">Закрити</string>
+ <string name="learn_more">Дізнатися більше</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Вибрати драйвер ГП</string>
+ <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>
+ <string name="select_gpu_driver_install">Встановити</string>
+ <string name="select_gpu_driver_default">За замовчуванням</string>
+ <string name="select_gpu_driver_install_success">Встановлено %s</string>
+ <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string>
+ <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>
+ <string name="system_gpu_driver">Системний драйвер ГП</string>
+ <string name="installing_driver">Встановлення драйвера...</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Налаштування</string>
+ <string name="preferences_general">Загальні</string>
+ <string name="preferences_system">Система</string>
+ <string name="preferences_graphics">Графіка</string>
+ <string name="preferences_audio">Аудіо</string>
+ <string name="preferences_theme">Тема і колір</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Ваш ROM зашифрований</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[Будь ласка, дотримуйтесь інструкцій, щоб пере-дампити ваші <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">ігрові картриджі</a> або <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">встановлені ігри</a>.]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[Будь ласка, переконайтеся, що ваш файл <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> встановлено, щоб ігри можна було розшифрувати.]]></string>
+ <string name="loader_error_video_core">Сталася помилка під час ініціалізації відеоядра.</string>
+ <string name="loader_error_video_core_description">Зазвичай це спричинено несумісним драйвером ГП. Встановлення користувацького драйвера ГП може вирішити цю проблему.</string>
+ <string name="loader_error_invalid_format">Не вдалося запустити ROM</string>
+ <string name="loader_error_file_not_found">Файл ROM не існує</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Вихід з емуляції</string>
+ <string name="emulation_done">Готово</string>
+ <string name="emulation_fps_counter">Лічильник FPS</string>
+ <string name="emulation_toggle_controls">Перемикання керування</string>
+ <string name="emulation_rel_stick_center">Відносний центр стіка</string>
+ <string name="emulation_dpad_slide">Слайд хрестовиною</string>
+ <string name="emulation_haptics">Тактильний зворотний зв\'язок</string>
+ <string name="emulation_show_overlay">Показати оверлей</string>
+ <string name="emulation_toggle_all">Перемкнути все</string>
+ <string name="emulation_control_adjust">Налаштувати оверлей</string>
+ <string name="emulation_control_scale">Масштаб</string>
+ <string name="emulation_control_opacity">Непрозорість</string>
+ <string name="emulation_touch_overlay_reset">Скинути оверлей</string>
+ <string name="emulation_touch_overlay_edit">Змінити оверлей</string>
+ <string name="emulation_pause">Пауза емуляції</string>
+ <string name="emulation_unpause">Відновлення емуляції</string>
+ <string name="emulation_input_overlay">Налаштування оверлея</string>
+ <string name="emulation_game_loading">Завантаження гри...</string>
+
+ <string name="load_settings">Завантаження налаштувань...</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Віртуальна клавіатура</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Перервати</string>
+ <string name="continue_button">Продовжити</string>
+ <string name="system_archive_not_found">Системний архів не знайдено</string>
+ <string name="system_archive_not_found_message">%s відсутній. Будь ласка, здампіть ваші системні архіви.\nПродовження емуляції може призвести до збоїв і помилок.</string>
+ <string name="system_archive_general">Системний архів</string>
+ <string name="save_load_error">Помилка збереження/завантаження</string>
+ <string name="fatal_error">Фатальна помилка</string>
+ <string name="fatal_error_message">Сталася фатальна помилка. Перевірте журнал для отримання докладної інформації.\nПродовження емуляції може призвести до збоїв і помилок.</string>
+ <string name="performance_warning">Вимкнення цього налаштування значно знизить продуктивність емуляції! Для досягнення найкращих результатів рекомендується залишити це налаштування увімкненим.</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">Японія</string>
+ <string name="region_usa">США</string>
+ <string name="region_europe">Європа</string>
+ <string name="region_australia">Австралія</string>
+ <string name="region_china">Китай</string>
+ <string name="region_korea">Корея</string>
+ <string name="region_taiwan">Тайвань</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Японська (日本語)</string>
+ <string name="language_english">Англійська (English)</string>
+ <string name="language_french">Французька (Français)</string>
+ <string name="langauge_german">Німецька (Deutsch)</string>
+ <string name="language_italian">Італійська (Italiano)</string>
+ <string name="language_spanish">Іспанська (Español)</string>
+ <string name="language_chinese">Китайскька (简体中文)</string>
+ <string name="language_korean">Корейська (한국어)</string>
+ <string name="language_dutch">Голландська (Nederlands)</string>
+ <string name="language_portuguese">Португальська (Português)</string>
+ <string name="language_russian">Російська (Русский)</string>
+ <string name="language_taiwanese">Тайванська (台湾)</string>
+ <string name="language_british_english">Британська англійська</string>
+ <string name="language_canadian_french">Канадська французька (Français canadien)</string>
+ <string name="language_latin_american_spanish">Латиноамериканська іспанська (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Спрощена китайська (简体中文)</string>
+ <string name="language_traditional_chinese">Традиційна китайська (正體中文)</string>
+ <string name="language_brazilian_portuguese">Бразильська португальська (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">Вимкнено</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Нормальна</string>
+ <string name="renderer_accuracy_high">Висока</string>
+ <string name="renderer_accuracy_extreme">Екстрим (повільно)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Повільно)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Повільно)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Повільно)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Моментальна (вимкнена)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (ввімкнута)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Найближчий сусід</string>
+ <string name="scaling_filter_bilinear">Білінійне</string>
+ <string name="scaling_filter_bicubic">Бікубічне</string>
+ <string name="scaling_filter_gaussian">Гауса</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">Вимкнено</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">За замовчуванням (16:9)</string>
+ <string name="ratio_force_four_three">Змусити 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Змусити 21:9</string>
+ <string name="ratio_force_sixteen_ten">Змусити 16:10</string>
+ <string name="ratio_stretch">Розтягнути до вікна</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Точно</string>
+ <string name="cpu_accuracy_unsafe">Небезпечно</string>
+ <string name="cpu_accuracy_paranoid">Параноїк (повільно)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">Кнопки напрямків</string>
+ <string name="gamepad_left_stick">Лівий міні-джойстик</string>
+ <string name="gamepad_right_stick">Правий міні-джойстик</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Знімок екрану</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Підготовка шейдерів</string>
+ <string name="building_shaders">Побудова шейдерів</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Змінити тему застосунку</string>
+ <string name="theme_default">За замовчуванням</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Змінити режим теми</string>
+ <string name="theme_mode_follow_system">Системна</string>
+ <string name="theme_mode_light">Світла</string>
+ <string name="theme_mode_dark">Темна</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Використовувати чорне тло</string>
+ <string name="use_black_backgrounds_description">У разі використання темної теми застосовуйте чорне тло.</string>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.Yuzu.Main.MaterialYou" parent="Theme.Yuzu.Main">
+ <item name="colorPrimary">@color/m3_sys_color_dynamic_light_primary</item>
+ <item name="colorOnPrimary">@color/m3_sys_color_dynamic_light_on_primary</item>
+ <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container</item>
+ <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_light_on_primary_container</item>
+ <item name="colorSecondary">@color/m3_sys_color_dynamic_light_secondary</item>
+ <item name="colorOnSecondary">@color/m3_sys_color_dynamic_light_on_secondary</item>
+ <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container</item>
+ <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_light_on_secondary_container</item>
+ <item name="colorTertiary">@color/m3_sys_color_dynamic_light_tertiary</item>
+ <item name="colorOnTertiary">@color/m3_sys_color_dynamic_light_on_tertiary</item>
+ <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container</item>
+ <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_light_on_tertiary_container</item>
+ <item name="android:colorBackground">@color/m3_sys_color_dynamic_light_background</item>
+ <item name="colorOnBackground">@color/m3_sys_color_dynamic_light_on_background</item>
+ <item name="colorSurface">@color/m3_sys_color_dynamic_light_surface</item>
+ <item name="colorOnSurface">@color/m3_sys_color_dynamic_light_on_surface</item>
+ <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_light_surface_variant</item>
+ <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
+ <item name="colorOutline">@color/m3_sys_color_dynamic_light_outline</item>
+ <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
+ <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_light_surface_variant</item>
+ <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_light_inverse_primary</item>
+
+ <item name="materialAlertDialogTheme">@style/ThemeOverlay.Material3.MaterialAlertDialog</item>
+ </style>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="small_layout">false</bool>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="spacing_navigation">0dp</dimen>
+ <dimen name="spacing_navigation_rail">80dp</dimen>
+</resources>
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 000000000..e00bbaa2e
--- /dev/null
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">此软件可以运行 Nintendo Switch 游戏,但不包含任何游戏和密钥文件。&lt;br /&gt;&lt;br /&gt;在开始前,请找到放置于设备存储中的 <![CDATA[<b> prod.keys </b>]]> 文件。&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">了解更多</a>]]></string>
+ <string name="emulation_notification_channel_name">正在进行模拟</string>
+ <string name="emulation_notification_channel_description">在模拟运行时显示持久通知。</string>
+ <string name="emulation_notification_running">yuzu 正在运行</string>
+ <string name="notice_notification_channel_name">通知及错误提醒</string>
+ <string name="notice_notification_channel_description">当发生错误时显示通知。</string>
+ <string name="notification_permission_not_granted">未授予通知权限!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">欢迎!</string>
+ <string name="welcome_description">了解如何设置 &lt;b>yuzu&lt;/b> 并进行模拟。</string>
+ <string name="get_started">开始</string>
+ <string name="keys">密钥文件</string>
+ <string name="keys_description">使用下方的按钮来选择你的 &lt;b>prod.keys&lt;/b> 文件。</string>
+ <string name="select_keys">选择密钥文件</string>
+ <string name="games">游戏</string>
+ <string name="games_description">使用下方的按钮选择你的 &lt;b>游戏&lt;/b> 文件夹。</string>
+ <string name="done">完成</string>
+ <string name="done_description">你完成了全部设置。\n玩的开心!</string>
+ <string name="text_continue">继续</string>
+ <string name="next">下一步</string>
+ <string name="back">上一步</string>
+ <string name="add_games">添加游戏</string>
+ <string name="add_games_description">选择你的游戏文件夹</string>
+
+ <!-- Home strings -->
+ <string name="home_games">游戏</string>
+ <string name="home_search">搜索</string>
+ <string name="home_settings">设置</string>
+ <string name="empty_gamelist">找不到游戏,或者尚未选择游戏文件夹。</string>
+ <string name="search_and_filter_games">搜索游戏</string>
+ <string name="select_games_folder">选择游戏文件夹</string>
+ <string name="select_games_folder_description">允许 yuzu 填充游戏列表</string>
+ <string name="add_games_warning">跳过选择游戏文件夹?</string>
+ <string name="add_games_warning_description">如果未选择游戏文件夹,游戏将不会显示在游戏列表中。</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">搜索游戏</string>
+ <string name="games_dir_selected">已选择游戏文件夹</string>
+ <string name="install_prod_keys">安装 prod.keys 文件</string>
+ <string name="install_prod_keys_description">需要密钥文件来解密游戏</string>
+ <string name="install_prod_keys_warning">跳过添加密钥文件?</string>
+ <string name="install_prod_keys_warning_description">对于商业游戏,需要有效的密钥文件才能运行。如果没有密钥文件,将只能运行自制软件。</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">通知</string>
+ <string name="notifications_description">使用下方的按钮授予通知权限。</string>
+ <string name="give_permission">授予权限</string>
+ <string name="notification_warning">跳过授予通知权限?</string>
+ <string name="notification_warning_description">yuzu 将无法通知您重要信息。</string>
+ <string name="permission_denied">授权遭拒</string>
+ <string name="permission_denied_description">您曾多次拒绝权限请求,现在您需要在系统设置中手动授予权限。</string>
+ <string name="about">关于</string>
+ <string name="about_description">开发版本、贡献者、以及更多</string>
+ <string name="warning_help">帮助</string>
+ <string name="warning_skip">跳过</string>
+ <string name="warning_cancel">取消</string>
+ <string name="install_amiibo_keys">安装 Amiibo 密钥文件</string>
+ <string name="install_amiibo_keys_description">在遊戏中使用 Amiibo 时必需</string>
+ <string name="invalid_keys_file">选择的密钥文件无效</string>
+ <string name="install_keys_success">密钥文件已成功安装</string>
+ <string name="reading_keys_failure">读取加密密钥时出错</string>
+ <string name="invalid_keys_error">无效的加密密钥</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">选择的密钥文件不正确或已损坏。请重新转储密钥文件。</string>
+ <string name="install_gpu_driver">安装 GPU 驱动</string>
+ <string name="install_gpu_driver_description">安装替代的驱动程序以获得更好的性能和精度</string>
+ <string name="advanced_settings">高级选项</string>
+ <string name="settings_description">更改模拟器设置</string>
+ <string name="search_recently_played">最近游玩</string>
+ <string name="search_recently_added">最近添加</string>
+ <string name="search_retail">商业游戏</string>
+ <string name="search_homebrew">自制游戏</string>
+ <string name="open_user_folder">打开 yuzu 文件夹</string>
+ <string name="open_user_folder_description">管理 yuzu 内部文件</string>
+ <string name="theme_and_color_description">更改外观</string>
+ <string name="no_file_manager">找不到可用的文件管理器</string>
+ <string name="notification_no_directory_link">无法打开 yuzu 文件夹</string>
+ <string name="notification_no_directory_link_description">请使用文件管理器的侧部面板手动定位用户文件夹。</string>
+ <string name="manage_save_data">管理存档数据</string>
+ <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string>
+ <string name="import_export_saves_description">导入或导出存档</string>
+ <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string>
+ <string name="save_file_imported_success">已成功导入存档</string>
+ <string name="save_file_invalid_zip_structure">无效的存档目录</string>
+ <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string>
+ <string name="import_saves">导入</string>
+ <string name="export_saves">导出</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia 不真实</string>
+ <string name="copied_to_clipboard">已复制到剪贴板</string>
+ <string name="about_app_description">一款开放源代码的 Switch 模拟器</string>
+ <string name="contributors">贡献者</string>
+ <string name="contributors_description">使用来自 yuzu 团队的 \u2764 制作</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">构建版本</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">抢先体验</string>
+ <string name="get_early_access">取得抢先体验</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">最新的功能、抢先更新、以及更多</string>
+ <string name="early_access_benefits">抢先体验的权益</string>
+ <string name="cutting_edge_features">最新功能</string>
+ <string name="early_access_updates">抢先更新</string>
+ <string name="no_manual_installation">无需手动安装</string>
+ <string name="prioritized_support">优先支持</string>
+ <string name="helping_game_preservation">帮助保留游戏</string>
+ <string name="our_eternal_gratitude">我们真诚的感激</string>
+ <string name="are_you_interested">您对此感兴趣吗?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">启用运行速度限制</string>
+ <string name="frame_limit_enable_description">启用后,模拟速度将限制在正常运行速度的指定百分比。</string>
+ <string name="frame_limit_slider">限制速度百分比</string>
+ <string name="frame_limit_slider_description">指定限制模拟速度的百分比。预设为 100%,此时模拟速度将被限制为标准速度。更高或更低的值将增加或降低速度限制上限。</string>
+ <string name="cpu_accuracy">CPU 精度</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">主机模式</string>
+ <string name="use_docked_mode_description">以主机模式进行模拟,牺牲性能并提高画面分辨率。</string>
+ <string name="emulated_region">模拟区域</string>
+ <string name="emulated_language">模拟语言</string>
+ <string name="select_rtc_date">选择日期</string>
+ <string name="select_rtc_time">选择时间</string>
+ <string name="use_custom_rtc">启用自定义系统时钟</string>
+ <string name="use_custom_rtc_description">此选项允许您设置与目前系统时间相独立的自定义系统时钟</string>
+ <string name="set_custom_rtc">设置自定义系统时钟</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">精度等级</string>
+ <string name="renderer_resolution">分辨率</string>
+ <string name="renderer_vsync">垂直同步模式</string>
+ <string name="renderer_aspect_ratio">屏幕纵横比</string>
+ <string name="renderer_scaling_filter">窗口滤镜</string>
+ <string name="renderer_anti_aliasing">抗锯齿方式</string>
+ <string name="renderer_force_max_clock">强制最大时钟 (仅限 Adreno)</string>
+ <string name="renderer_force_max_clock_description">强制 GPU 以最大时钟运行 (仍被温控限制)。</string>
+ <string name="renderer_asynchronous_shaders">使用异步着色器</string>
+ <string name="renderer_asynchronous_shaders_description">异步编译着色器,减少卡顿,但可能引入故障。</string>
+ <string name="renderer_debug">启用图形调试</string>
+ <string name="renderer_debug_description">启用时,图形 API 将进入较慢的调试模式。</string>
+ <string name="use_disk_shader_cache">使用磁盘着色器缓存</string>
+ <string name="use_disk_shader_cache_description">将生成的着色器缓存于磁盘中并进行读取以减少卡顿。</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">音量</string>
+ <string name="audio_volume_description">指定输出的音量。</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">系统默认</string>
+ <string name="ini_saved">已保存设置</string>
+ <string name="gameid_saved">已保存 %1$s 的设置</string>
+ <string name="error_saving">保存 %1$s.ini 时出错: %2$s</string>
+ <string name="loading">加载中…</string>
+ <string name="reset_setting_confirmation">您要将此设定重设为默认值吗?</string>
+ <string name="reset_to_default">恢复默认</string>
+ <string name="reset_all_settings">重置所有设置项?</string>
+ <string name="reset_all_settings_description">所有高级选项都将被重设,此动作无法还原。</string>
+ <string name="settings_reset">重设设置项</string>
+ <string name="close">关闭</string>
+ <string name="learn_more">了解更多</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">选择 GPU 驱动程序</string>
+ <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>
+ <string name="select_gpu_driver_install">安装</string>
+ <string name="select_gpu_driver_default">系统默认</string>
+ <string name="select_gpu_driver_install_success">已安装 %s</string>
+ <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string>
+ <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>
+ <string name="system_gpu_driver">系统 GPU 驱动程序</string>
+ <string name="installing_driver">正在安装驱动程序…</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">设置</string>
+ <string name="preferences_general">通用</string>
+ <string name="preferences_system">系统</string>
+ <string name="preferences_graphics">图形</string>
+ <string name="preferences_audio">声音</string>
+ <string name="preferences_theme">主题和色彩</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">您的 ROM 已加密</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[请参考指南重新转储你的<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">游戏卡带</a>或<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">已安装的游戏</a>。]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[请确保 <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> 文件已安装,使得游戏可以被解密。]]></string>
+ <string name="loader_error_video_core">初始化视频核心时发生错误</string>
+ <string name="loader_error_video_core_description">这通常由不兼容的 GPU 驱动程序造成,安装自定义 GPU 驱动程序可能解决此问题。</string>
+ <string name="loader_error_invalid_format">无法载入 ROM</string>
+ <string name="loader_error_file_not_found">ROM 文件不存在</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">退出模拟</string>
+ <string name="emulation_done">完成</string>
+ <string name="emulation_fps_counter">FPS 计数器</string>
+ <string name="emulation_toggle_controls">按键切换</string>
+ <string name="emulation_rel_stick_center">相对摇杆中心</string>
+ <string name="emulation_dpad_slide">十字方向键滑动</string>
+ <string name="emulation_haptics">触觉反馈</string>
+ <string name="emulation_show_overlay">显示虚拟按键</string>
+ <string name="emulation_toggle_all">全部切换</string>
+ <string name="emulation_control_adjust">调整虚拟按键</string>
+ <string name="emulation_control_scale">缩放</string>
+ <string name="emulation_control_opacity">不透明度</string>
+ <string name="emulation_touch_overlay_reset">重设虚拟按键</string>
+ <string name="emulation_touch_overlay_edit">编辑虚拟按键</string>
+ <string name="emulation_pause">暂停模拟</string>
+ <string name="emulation_unpause">继续模拟</string>
+ <string name="emulation_input_overlay">虚拟按键选项</string>
+ <string name="emulation_game_loading">载入游戏中…</string>
+
+ <string name="load_settings">正在载入设定…</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">软件键盘</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">中止</string>
+ <string name="continue_button">继续</string>
+ <string name="system_archive_not_found">未找到系统档案</string>
+ <string name="system_archive_not_found_message">%s 丢失,请转储您的系统档案。\n继续模拟可能造成崩溃和错误。</string>
+ <string name="system_archive_general">系统档案</string>
+ <string name="save_load_error">保存/载入发生错误</string>
+ <string name="fatal_error">致命错误</string>
+ <string name="fatal_error_message">发生致命错误,请查阅日志获取详细信息。\n继续模拟可能会造成崩溃和错误。</string>
+ <string name="performance_warning">关闭此项会显著降低模拟性能!建议您将此项保持为启用状态。</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">日本</string>
+ <string name="region_usa">美国</string>
+ <string name="region_europe">欧洲</string>
+ <string name="region_australia">澳大利亚</string>
+ <string name="region_china">中国</string>
+ <string name="region_korea">韩国</string>
+ <string name="region_taiwan">中国台湾</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">日语 (日本語)</string>
+ <string name="language_english">英语 (English)</string>
+ <string name="language_french">法语 (Français)</string>
+ <string name="langauge_german">德语 (Deutsch)</string>
+ <string name="language_italian">意大利语 (Italiano)</string>
+ <string name="language_spanish">西班牙语 (Español)</string>
+ <string name="language_chinese">中文 (简体中文)</string>
+ <string name="language_korean">韩语 (한국어)</string>
+ <string name="language_dutch">荷兰语 (Nederlands)</string>
+ <string name="language_portuguese">葡萄牙语 (Português)</string>
+ <string name="language_russian">俄语 (Русский)</string>
+ <string name="language_taiwanese">台湾中文 (台灣)</string>
+ <string name="language_british_english">英式英语</string>
+ <string name="language_canadian_french">加拿大法语 (Français canadien)</string>
+ <string name="language_latin_american_spanish">拉丁美洲西班牙语 (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">简体中文 (简体中文)</string>
+ <string name="language_traditional_chinese">繁体中文 (正體中文)</string>
+ <string name="language_brazilian_portuguese">巴西葡萄牙语 (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">无</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">正常</string>
+ <string name="renderer_accuracy_high">高</string>
+ <string name="renderer_accuracy_extreme">极高 (慢速)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (慢速)</string>
+ <string name="resolution_three">3X (2160p/3240p) (慢速)</string>
+ <string name="resolution_four">4X (2880p/4320p) (慢速)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">即时 (关闭)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (开启)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">近邻取样</string>
+ <string name="scaling_filter_bilinear">双线性过滤</string>
+ <string name="scaling_filter_bicubic">双三线过滤</string>
+ <string name="scaling_filter_gaussian">高斯模糊</string>
+ <string name="scaling_filter_scale_force">强制缩放</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™️ 超级分辨率锐画技术</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">无</string>
+ <string name="anti_aliasing_fxaa">快速近似抗锯齿</string>
+ <string name="anti_aliasing_smaa">子像素形态学抗锯齿</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">默认 (16:9)</string>
+ <string name="ratio_force_four_three">强制 4:3</string>
+ <string name="ratio_force_twenty_one_nine">强制 21:9</string>
+ <string name="ratio_force_sixteen_ten">强制 16:10</string>
+ <string name="ratio_stretch">拉伸窗口</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">高精度</string>
+ <string name="cpu_accuracy_unsafe">低精度</string>
+ <string name="cpu_accuracy_paranoid">偏执模式 (慢速)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">十字方向键</string>
+ <string name="gamepad_left_stick">左摇杆</string>
+ <string name="gamepad_right_stick">右摇杆</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">截图</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">正在准备着色器</string>
+ <string name="building_shaders">正在编译着色器</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">切换主题</string>
+ <string name="theme_default">系统默认</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">主题模式</string>
+ <string name="theme_mode_follow_system">跟随系统</string>
+ <string name="theme_mode_light">浅色</string>
+ <string name="theme_mode_dark">深色</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">使用黑色背景</string>
+ <string name="use_black_backgrounds_description">使用深色主题时,套用黑色背景。</string>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 000000000..a54d04248
--- /dev/null
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,336 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_disclaimer">此軟體可以執行 Nintendo Switch 主機遊戲,但不包含任何遊戲和金鑰。&lt;br /&gt;&lt;br /&gt;在您開始前,請找到放置於您的裝置儲存空間的 <![CDATA[<b> prod.keys </b>]]> 檔案。&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href=\"https://yuzu-emu.org/help/quickstart\">深入瞭解</a>]]></string>
+ <string name="emulation_notification_channel_name">模擬進行中</string>
+ <string name="emulation_notification_channel_description">在模擬執行時顯示持續通知。</string>
+ <string name="emulation_notification_running">yuzu 正在執行</string>
+ <string name="notice_notification_channel_name">通知和錯誤</string>
+ <string name="notice_notification_channel_description">發生錯誤時顯示通知。</string>
+ <string name="notification_permission_not_granted">未授予通知權限!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">歡迎!</string>
+ <string name="welcome_description">瞭解如何設定 &lt;b>yuzu&lt;/b> 並進入模擬。</string>
+ <string name="get_started">開始使用</string>
+ <string name="keys">金鑰</string>
+ <string name="keys_description">使用下方的按鈕選取您的 &lt;b>prod.keys&lt;/b> 檔案。</string>
+ <string name="select_keys">選取金鑰</string>
+ <string name="games">遊戲</string>
+ <string name="games_description">使用下方的按鈕選取您的&lt;b>遊戲&lt;/b>資料夾。</string>
+ <string name="done">完成</string>
+ <string name="done_description">您已準備就緒。\n盡情遊玩您的遊戲!</string>
+ <string name="text_continue">繼續</string>
+ <string name="next">下一步</string>
+ <string name="back">上一步</string>
+ <string name="add_games">新增遊戲</string>
+ <string name="add_games_description">選取您的遊戲資料夾</string>
+
+ <!-- Home strings -->
+ <string name="home_games">遊戲</string>
+ <string name="home_search">搜尋</string>
+ <string name="home_settings">設定</string>
+ <string name="empty_gamelist">找不到檔案,或者尚未選取遊戲目錄。</string>
+ <string name="search_and_filter_games">搜尋並篩選遊戲</string>
+ <string name="select_games_folder">選取遊戲資料夾</string>
+ <string name="select_games_folder_description">一律允許 yuzu 填入遊戲清單</string>
+ <string name="add_games_warning">跳過選取遊戲資料夾?</string>
+ <string name="add_games_warning_description">如果資料夾未選取,遊戲將不會顯示在遊戲清單。</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">搜尋遊戲</string>
+ <string name="games_dir_selected">遊戲目錄已選取</string>
+ <string name="install_prod_keys">安裝 prod.keys</string>
+ <string name="install_prod_keys_description">需要解密零售遊戲</string>
+ <string name="install_prod_keys_warning">跳過新增金鑰?</string>
+ <string name="install_prod_keys_warning_description">模擬零售遊戲需要有效的金鑰,若要繼續,將僅有自製遊戲應用程式可以運作。</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">通知</string>
+ <string name="notifications_description">使用下方的按鈕授予通知權限。</string>
+ <string name="give_permission">授予權限</string>
+ <string name="notification_warning">跳過授予通知權限?</string>
+ <string name="notification_warning_description">yuzu 將無法通知您重要資訊。</string>
+ <string name="permission_denied">權限遭拒</string>
+ <string name="permission_denied_description">您曾多次拒絕了權限要求,現在您需要在系統設定中手動授予權限。</string>
+ <string name="about">關於</string>
+ <string name="about_description">組建版本、製作群、以及更多</string>
+ <string name="warning_help">說明</string>
+ <string name="warning_skip">跳過</string>
+ <string name="warning_cancel">取消</string>
+ <string name="install_amiibo_keys">安裝 Amiibo 金鑰</string>
+ <string name="install_amiibo_keys_description">需要在遊戲中使用 Amiibo</string>
+ <string name="invalid_keys_file">無效的金鑰檔案已選取</string>
+ <string name="install_keys_success">金鑰已成功安裝</string>
+ <string name="reading_keys_failure">讀取加密金鑰時出現錯誤</string>
+ <string name="invalid_keys_error">無效的加密金鑰</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">選取的檔案不正確或已損毀,請重新傾印您的金鑰。</string>
+ <string name="install_gpu_driver">安裝 GPU 驅動程式</string>
+ <string name="install_gpu_driver_description">安裝替代驅動程式以取得潛在的更佳效能或準確度</string>
+ <string name="advanced_settings">進階設定</string>
+ <string name="settings_description">進行模擬器設定</string>
+ <string name="search_recently_played">最近遊玩</string>
+ <string name="search_recently_added">最近新增</string>
+ <string name="search_retail">零售</string>
+ <string name="search_homebrew">自製遊戲</string>
+ <string name="open_user_folder">開啟 yuzu 資料夾</string>
+ <string name="open_user_folder_description">管理 yuzu 的內部檔案</string>
+ <string name="theme_and_color_description">修改應用程式外觀</string>
+ <string name="no_file_manager">找不到檔案管理員</string>
+ <string name="notification_no_directory_link">無法開啟 yuzu 目錄</string>
+ <string name="notification_no_directory_link_description">請使用檔案管理員的側邊面板手動定位到使用者資料夾。</string>
+ <string name="manage_save_data">管理儲存資料</string>
+ <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string>
+ <string name="import_export_saves_description">匯入或匯出儲存檔案</string>
+ <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string>
+ <string name="save_file_imported_success">已成功匯入</string>
+ <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string>
+ <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string>
+ <string name="import_saves">匯入</string>
+ <string name="export_saves">匯出</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia 不真實</string>
+ <string name="copied_to_clipboard">已複製到剪貼簿</string>
+ <string name="about_app_description">一個開放原始碼的 Switch 模擬器</string>
+ <string name="contributors">參與者</string>
+ <string name="contributors_description">使用來自 yuzu 團隊的 \u2764 製作</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="build">組建</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">搶先體驗</string>
+ <string name="get_early_access">搶先體驗新功能</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">最新的功能、搶先版更新、以及更多</string>
+ <string name="early_access_benefits">搶先體驗權益</string>
+ <string name="cutting_edge_features">最新功能</string>
+ <string name="early_access_updates">搶先版更新</string>
+ <string name="no_manual_installation">無需手動安裝</string>
+ <string name="prioritized_support">優先支援</string>
+ <string name="helping_game_preservation">協助遊戲保留</string>
+ <string name="our_eternal_gratitude">我們永遠的感激</string>
+ <string name="are_you_interested">您仍感興趣嗎?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">啟用限制速度</string>
+ <string name="frame_limit_enable_description">若啟用,模擬速度將會限制在標準速度的指定百分比。</string>
+ <string name="frame_limit_slider">限制速度百分比</string>
+ <string name="frame_limit_slider_description">指定限制模擬速度的百分比。預設為 100%,模擬速度將被限制為標準速度。更高或更低的值將會增加或減少速度限制。</string>
+ <string name="cpu_accuracy">CPU 準確度</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">底座模式</string>
+ <string name="use_docked_mode_description">以底座模式模擬,以犧牲效能的代價提高解析度。</string>
+ <string name="emulated_region">模擬區域</string>
+ <string name="emulated_language">模擬語言</string>
+ <string name="select_rtc_date">選取 RTC 日期</string>
+ <string name="select_rtc_time">選取 RTC 時間</string>
+ <string name="use_custom_rtc">啟用自訂 RTC</string>
+ <string name="use_custom_rtc_description">此設定允許您設定與您的目前系統時間相互獨立的自訂即時時鐘</string>
+ <string name="set_custom_rtc">設定自訂 RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">準確度層級</string>
+ <string name="renderer_resolution">解析度</string>
+ <string name="renderer_vsync">VSync 模式</string>
+ <string name="renderer_aspect_ratio">長寬比</string>
+ <string name="renderer_scaling_filter">視窗適應過濾器</string>
+ <string name="renderer_anti_aliasing">消除鋸齒方法</string>
+ <string name="renderer_force_max_clock">強制最大時脈 (僅 Adreno)</string>
+ <string name="renderer_force_max_clock_description">強制 GPU 以最大可能時脈執行 (熱溫限制仍被套用)。</string>
+ <string name="renderer_asynchronous_shaders">使用非同步著色器</string>
+ <string name="renderer_asynchronous_shaders_description">非同步編譯著色器,將會減少間斷,但可能會引入故障。</string>
+ <string name="renderer_debug">啟用圖形偵錯</string>
+ <string name="renderer_debug_description">核取時,圖形 API 將會進入慢速偵錯模式。</string>
+ <string name="use_disk_shader_cache">使用磁碟著色器快取</string>
+ <string name="use_disk_shader_cache_description">透過將產生的著色器儲存並載入至磁碟,減少中斷。</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">音量</string>
+ <string name="audio_volume_description">指定音訊輸出音量。</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">預設</string>
+ <string name="ini_saved">已儲存設定</string>
+ <string name="gameid_saved">已儲存 %1$s 設定</string>
+ <string name="error_saving">儲存 %1$s 時發生錯誤 ini: %2$s</string>
+ <string name="loading">正在載入…</string>
+ <string name="reset_setting_confirmation">要將此設定重設回預設值嗎?</string>
+ <string name="reset_to_default">重設為預設值</string>
+ <string name="reset_all_settings">重設所有設定?</string>
+ <string name="reset_all_settings_description">所有進階設定將被重設為預設組態,此動作無法復原。</string>
+ <string name="settings_reset">設定已重設</string>
+ <string name="close">關閉</string>
+ <string name="learn_more">深入瞭解</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">選取 GPU 驅動程式</string>
+ <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>
+ <string name="select_gpu_driver_install">安裝</string>
+ <string name="select_gpu_driver_default">預設</string>
+ <string name="select_gpu_driver_install_success">已安裝 %s</string>
+ <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string>
+ <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>
+ <string name="system_gpu_driver">系統 GPU 驅動程式</string>
+ <string name="installing_driver">正在安裝驅動程式…</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">設定</string>
+ <string name="preferences_general">一般</string>
+ <string name="preferences_system">系統</string>
+ <string name="preferences_graphics">圖形</string>
+ <string name="preferences_audio">音訊</string>
+ <string name="preferences_theme">主題和色彩</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">您的 ROM 已加密</string>
+ <string name="loader_error_encrypted_roms_description"><![CDATA[請依循指南重新傾印您的<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games\">遊戲卡匣</a>或<a href=\"https://yuzu-emu.org/help/quickstart/#dumping-installed-titles-eshop\">安裝標題</a>。]]></string>
+ <string name="loader_error_encrypted_keys_description"><![CDATA[請確保您的 <a href=\"https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> 檔案已安裝,讓遊戲可以解密。]]></string>
+ <string name="loader_error_video_core">初始化視訊核心時發生錯誤</string>
+ <string name="loader_error_video_core_description">這經常由不相容的 GPU 驅動程式造成,安裝自訂 GPU 驅動程式可能會解決此問題。</string>
+ <string name="loader_error_invalid_format">無法載入 ROM</string>
+ <string name="loader_error_file_not_found">ROM 檔案不存在</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">結束模擬</string>
+ <string name="emulation_done">完成</string>
+ <string name="emulation_fps_counter">FPS 計數器</string>
+ <string name="emulation_toggle_controls">切換控制</string>
+ <string name="emulation_rel_stick_center">相對搖桿中心</string>
+ <string name="emulation_dpad_slide">方向鍵滑動</string>
+ <string name="emulation_haptics">觸覺回饋技術</string>
+ <string name="emulation_show_overlay">顯示覆疊</string>
+ <string name="emulation_toggle_all">全部切換</string>
+ <string name="emulation_control_adjust">調整覆疊</string>
+ <string name="emulation_control_scale">縮放</string>
+ <string name="emulation_control_opacity">不透明度</string>
+ <string name="emulation_touch_overlay_reset">重設覆疊</string>
+ <string name="emulation_touch_overlay_edit">編輯覆疊</string>
+ <string name="emulation_pause">暫停模擬</string>
+ <string name="emulation_unpause">取消暫停模擬</string>
+ <string name="emulation_input_overlay">覆疊選項</string>
+ <string name="emulation_game_loading">遊戲正在載入…</string>
+
+ <string name="load_settings">正在載入設定…</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">軟體鍵盤</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">中止</string>
+ <string name="continue_button">繼續</string>
+ <string name="system_archive_not_found">找不到系統檔案</string>
+ <string name="system_archive_not_found_message">%s 遺失,請傾印您的系統封存。\n繼續模擬可能會造成當機和錯誤。</string>
+ <string name="system_archive_general">系統封存</string>
+ <string name="save_load_error">儲存/載入發生錯誤</string>
+ <string name="fatal_error">嚴重錯誤</string>
+ <string name="fatal_error_message">發生嚴重錯誤,檢查記錄以取得詳細資訊。\n繼續模擬可能會造成當機和錯誤。</string>
+ <string name="performance_warning">關閉此設定會顯著降低模擬效能!如需最佳體驗,建議您將此設定保持為啟用狀態。</string>
+
+ <!-- Region Names -->
+ <string name="region_japan">日本</string>
+ <string name="region_usa">美國</string>
+ <string name="region_europe">歐洲</string>
+ <string name="region_australia">澳洲</string>
+ <string name="region_china">中國</string>
+ <string name="region_korea">南韓</string>
+ <string name="region_taiwan">台灣</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">日文 (日本語)</string>
+ <string name="language_english">英文</string>
+ <string name="language_french">法文 (Français)</string>
+ <string name="langauge_german">德文 (Deutsch)</string>
+ <string name="language_italian">義大利文 (Italiano)</string>
+ <string name="language_spanish">西班牙文 (Español)</string>
+ <string name="language_chinese">中文 (简体中文)</string>
+ <string name="language_korean">韓文 (한국어)</string>
+ <string name="language_dutch">荷蘭文 (Nederlands)</string>
+ <string name="language_portuguese">葡萄牙文 (Português)</string>
+ <string name="language_russian">俄文 (Русский)</string>
+ <string name="language_taiwanese">台文 (台灣)</string>
+ <string name="language_british_english">英式英文</string>
+ <string name="language_canadian_french">加拿大法文 (Français canadien)</string>
+ <string name="language_latin_american_spanish">拉丁美洲西班牙文 (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">簡體中文 (简体中文)</string>
+ <string name="language_traditional_chinese">正體中文 (正體中文)</string>
+ <string name="language_brazilian_portuguese">巴西葡萄牙文 (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">無</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">標準</string>
+ <string name="renderer_accuracy_high">高</string>
+ <string name="renderer_accuracy_extreme">極高 (慢)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (慢)</string>
+ <string name="resolution_three">3X (2160p/3240p) (慢)</string>
+ <string name="resolution_four">4X (2880p/4320p) (慢)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">即時 (關閉)</string>
+ <string name="renderer_vsync_mailbox">信箱</string>
+ <string name="renderer_vsync_fifo">FIFO (開啟)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO 寬鬆</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">最近鄰</string>
+ <string name="scaling_filter_bilinear">雙線性</string>
+ <string name="scaling_filter_bicubic">雙立方</string>
+ <string name="scaling_filter_gaussian">高斯</string>
+ <string name="scaling_filter_scale_force">強制縮放</string>
+ <string name="scaling_filter_fsr">AMD Radeon™ 超級解析度</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">無</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">預設 (16:9)</string>
+ <string name="ratio_force_four_three">強制 4:3</string>
+ <string name="ratio_force_twenty_one_nine">強制 21:9</string>
+ <string name="ratio_force_sixteen_ten">強制 16:10</string>
+ <string name="ratio_stretch">延伸視窗</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_unsafe">低精度</string>
+ <string name="cpu_accuracy_paranoid">不合理 (慢)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">方向鍵</string>
+ <string name="gamepad_left_stick">左搖桿</string>
+ <string name="gamepad_right_stick">右搖桿</string>
+ <string name="gamepad_home">HOME</string>
+ <string name="gamepad_screenshot">螢幕截圖</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">正在準備著色器</string>
+ <string name="building_shaders">正在建置著色器</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">變更應用程式主題</string>
+ <string name="theme_default">預設</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">變更主題模式</string>
+ <string name="theme_mode_follow_system">跟隨系統</string>
+ <string name="theme_mode_light">淺色</string>
+ <string name="theme_mode_dark">深色</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">使用黑色背景</string>
+ <string name="use_black_backgrounds_description">使用深色主題時,套用黑色背景。</string>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="regionNames">
+ <item>@string/auto</item>
+ <item>@string/region_australia</item>
+ <item>@string/region_china</item>
+ <item>@string/region_europe</item>
+ <item>@string/region_japan</item>
+ <item>@string/region_korea</item>
+ <item>@string/region_taiwan</item>
+ <item>@string/region_usa</item>
+ </string-array>
+
+ <integer-array name="regionValues">
+ <item>-1</item>
+ <item>3</item>
+ <item>4</item>
+ <item>2</item>
+ <item>0</item>
+ <item>5</item>
+ <item>6</item>
+ <item>1</item>
+ </integer-array>
+
+ <string-array name="languageNames">
+ <item>@string/language_brazilian_portuguese</item>
+ <item>@string/language_british_english</item>
+ <item>@string/language_canadian_french</item>
+ <item>@string/language_chinese</item>
+ <item>@string/language_dutch</item>
+ <item>@string/language_english</item>
+ <item>@string/language_french</item>
+ <item>@string/langauge_german</item>
+ <item>@string/language_italian</item>
+ <item>@string/language_japanese</item>
+ <item>@string/language_korean</item>
+ <item>@string/language_latin_american_spanish</item>
+ <item>@string/language_portuguese</item>
+ <item>@string/language_russian</item>
+ <item>@string/language_simplified_chinese</item>
+ <item>@string/language_spanish</item>
+ <item>@string/language_taiwanese</item>
+ <item>@string/language_traditional_chinese</item>
+ </string-array>
+
+ <integer-array name="languageValues">
+ <item>17</item>
+ <item>12</item>
+ <item>13</item>
+ <item>6</item>
+ <item>8</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>0</item>
+ <item>7</item>
+ <item>14</item>
+ <item>9</item>
+ <item>10</item>
+ <item>15</item>
+ <item>5</item>
+ <item>11</item>
+ <item>16</item>
+ </integer-array>
+
+ <string-array name="rendererApiNames">
+ <item>@string/renderer_vulkan</item>
+ <item>@string/renderer_none</item>
+ </string-array>
+
+ <integer-array name="rendererApiValues">
+ <item>1</item>
+ <item>2</item>
+ </integer-array>
+
+ <string-array name="rendererAccuracyNames">
+ <item>@string/renderer_accuracy_normal</item>
+ <item>@string/renderer_accuracy_high</item>
+ <item>@string/renderer_accuracy_extreme</item>
+ </string-array>
+
+ <integer-array name="rendererAccuracyValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </integer-array>
+
+ <string-array name="rendererResolutionNames">
+ <item>@string/resolution_half</item>
+ <item>@string/resolution_three_quarter</item>
+ <item>@string/resolution_one</item>
+ <item>@string/resolution_two</item>
+ <item>@string/resolution_three</item>
+ <item>@string/resolution_four</item>
+ </string-array>
+
+ <string-array name="rendererVSyncNames">
+ <item>@string/renderer_vsync_immediate</item>
+ <item>@string/renderer_vsync_mailbox</item>
+ <item>@string/renderer_vsync_fifo</item>
+ <item>@string/renderer_vsync_fifo_relaxed</item>
+ </string-array>
+
+ <integer-array name="rendererResolutionValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ </integer-array>
+
+ <integer-array name="rendererVSyncValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </integer-array>
+
+ <string-array name="rendererAspectRatioNames">
+ <item>@string/ratio_default</item>
+ <item>@string/ratio_force_four_three</item>
+ <item>@string/ratio_force_twenty_one_nine</item>
+ <item>@string/ratio_force_sixteen_ten</item>
+ <item>@string/ratio_stretch</item>
+ </string-array>
+
+ <integer-array name="rendererAspectRatioValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ </integer-array>
+
+ <string-array name="rendererScalingFilterNames">
+ <item>@string/scaling_filter_nearest_neighbor</item>
+ <item>@string/scaling_filter_bilinear</item>
+ <item>@string/scaling_filter_bicubic</item>
+ <item>@string/scaling_filter_gaussian</item>
+ <item>@string/scaling_filter_scale_force</item>
+ <item>@string/scaling_filter_fsr</item>
+ </string-array>
+
+ <integer-array name="rendererScalingFilterValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ </integer-array>
+
+ <string-array name="rendererAntiAliasingNames">
+ <item>@string/anti_aliasing_none</item>
+ <item>@string/anti_aliasing_fxaa</item>
+ <item>@string/anti_aliasing_smaa</item>
+ </string-array>
+
+ <integer-array name="rendererAntiAliasingValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </integer-array>
+
+ <string-array name="cpuAccuracyNames">
+ <item>@string/auto</item>
+ <item>@string/cpu_accuracy_accurate</item>
+ <item>@string/cpu_accuracy_unsafe</item>
+ <item>@string/cpu_accuracy_paranoid</item>
+ </string-array>
+
+ <integer-array name="cpuAccuracyValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </integer-array>
+
+ <string-array name="gamepadButtons">
+ <item>A</item>
+ <item>B</item>
+ <item>X</item>
+ <item>Y</item>
+ <item>L</item>
+ <item>R</item>
+ <item>ZL</item>
+ <item>ZR</item>
+ <item>+</item>
+ <item>-</item>
+ <item>@string/gamepad_d_pad</item>
+ <item>@string/gamepad_left_stick</item>
+ <item>@string/gamepad_right_stick</item>
+ <item>@string/gamepad_home</item>
+ <item>@string/gamepad_screenshot</item>
+ </string-array>
+
+ <string-array name="themeEntries">
+ <item>@string/theme_default</item>
+ </string-array>
+ <integer-array name="themeValues">
+ <item>0</item>
+ </integer-array>
+
+ <string-array name="themeEntriesA12">
+ <item>@string/theme_default</item>
+ <item>@string/theme_material_you</item>
+ </string-array>
+ <integer-array name="themeValuesA12">
+ <item>0</item>
+ <item>1</item>
+ </integer-array>
+
+ <string-array name="themeModeEntries">
+ <item>@string/theme_mode_follow_system</item>
+ <item>@string/theme_mode_light</item>
+ <item>@string/theme_mode_dark</item>
+ </string-array>
+ <integer-array name="themeModeValues">
+ <item>-1</item>
+ <item>1</item>
+ <item>2</item>
+ </integer-array>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="small_layout">true</bool>
+</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 @@
+<resources>
+ <dimen name="spacing_small">4dp</dimen>
+ <dimen name="spacing_med">8dp</dimen>
+ <dimen name="spacing_medlarge">12dp</dimen>
+ <dimen name="spacing_large">16dp</dimen>
+ <dimen name="spacing_xtralarge">32dp</dimen>
+ <dimen name="spacing_list">64dp</dimen>
+ <dimen name="spacing_chip">20dp</dimen>
+ <dimen name="spacing_navigation">80dp</dimen>
+ <dimen name="spacing_navigation_rail">0dp</dimen>
+ <dimen name="spacing_search">128dp</dimen>
+ <dimen name="spacing_refresh_end">72dp</dimen>
+ <dimen name="menu_width">256dp</dimen>
+ <dimen name="card_width">165dp</dimen>
+
+ <dimen name="dialog_margin">20dp</dimen>
+ <dimen name="elevated_app_bar">3dp</dimen>
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="game_title_lines">2</integer>
+
+ <!-- Default SWITCH landscape layout -->
+ <integer name="SWITCH_BUTTON_A_X">760</integer>
+ <integer name="SWITCH_BUTTON_A_Y">790</integer>
+ <integer name="SWITCH_BUTTON_B_X">710</integer>
+ <integer name="SWITCH_BUTTON_B_Y">900</integer>
+ <integer name="SWITCH_BUTTON_X_X">710</integer>
+ <integer name="SWITCH_BUTTON_X_Y">680</integer>
+ <integer name="SWITCH_BUTTON_Y_X">660</integer>
+ <integer name="SWITCH_BUTTON_Y_Y">790</integer>
+ <integer name="SWITCH_STICK_L_X">100</integer>
+ <integer name="SWITCH_STICK_L_Y">670</integer>
+ <integer name="SWITCH_STICK_R_X">900</integer>
+ <integer name="SWITCH_STICK_R_Y">670</integer>
+ <integer name="SWITCH_TRIGGER_L_X">70</integer>
+ <integer name="SWITCH_TRIGGER_L_Y">220</integer>
+ <integer name="SWITCH_TRIGGER_R_X">930</integer>
+ <integer name="SWITCH_TRIGGER_R_Y">220</integer>
+ <integer name="SWITCH_TRIGGER_ZL_X">70</integer>
+ <integer name="SWITCH_TRIGGER_ZL_Y">90</integer>
+ <integer name="SWITCH_TRIGGER_ZR_X">930</integer>
+ <integer name="SWITCH_TRIGGER_ZR_Y">90</integer>
+ <integer name="SWITCH_BUTTON_MINUS_X">460</integer>
+ <integer name="SWITCH_BUTTON_MINUS_Y">950</integer>
+ <integer name="SWITCH_BUTTON_PLUS_X">540</integer>
+ <integer name="SWITCH_BUTTON_PLUS_Y">950</integer>
+ <integer name="SWITCH_BUTTON_HOME_X">600</integer>
+ <integer name="SWITCH_BUTTON_HOME_Y">950</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_X">400</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_Y">950</integer>
+ <integer name="SWITCH_BUTTON_DPAD_X">260</integer>
+ <integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
+
+</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..c236811fa
--- /dev/null
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,874 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- General application strings -->
+ <string name="app_name" translatable="false">yuzu</string>
+ <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>
+ <string name="emulation_notification_channel_name">Emulation is Active</string>
+ <string name="emulation_notification_channel_id" translatable="false">emulationIsActive</string>
+ <string name="emulation_notification_channel_description">Shows a persistent notification when emulation is running.</string>
+ <string name="emulation_notification_running">yuzu is running</string>
+ <string name="notice_notification_channel_name">Notices and errors</string>
+ <string name="notice_notification_channel_id" translatable="false">noticesAndErrors</string>
+ <string name="notice_notification_channel_description">Shows notifications when something goes wrong.</string>
+ <string name="notification_permission_not_granted">Notification permission not granted!</string>
+
+ <!-- Setup strings -->
+ <string name="welcome">Welcome!</string>
+ <string name="welcome_description">Learn how to setup &lt;b>yuzu&lt;/b> and jump into emulation.</string>
+ <string name="get_started">Get started</string>
+ <string name="keys">Keys</string>
+ <string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
+ <string name="select_keys">Select Keys</string>
+ <string name="games">Games</string>
+ <string name="games_description">Select your &lt;b>Games&lt;/b> folder with the button below.</string>
+ <string name="done">Done</string>
+ <string name="done_description">You\'re all set.\nEnjoy your games!</string>
+ <string name="text_continue">Continue</string>
+ <string name="next">Next</string>
+ <string name="back">Back</string>
+ <string name="add_games">Add Games</string>
+ <string name="add_games_description">Select your games folder</string>
+
+ <!-- Home strings -->
+ <string name="home_games">Games</string>
+ <string name="home_search">Search</string>
+ <string name="home_settings">Settings</string>
+ <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
+ <string name="search_and_filter_games">Search and filter games</string>
+ <string name="select_games_folder">Select games folder</string>
+ <string name="select_games_folder_description">Allows yuzu to populate the games list</string>
+ <string name="add_games_warning">Skip selecting games folder?</string>
+ <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
+ <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
+ <string name="home_search_games">Search games</string>
+ <string name="games_dir_selected">Games directory selected</string>
+ <string name="install_prod_keys">Install prod.keys</string>
+ <string name="install_prod_keys_description">Required to decrypt retail games</string>
+ <string name="install_prod_keys_warning">Skip adding keys?</string>
+ <string name="install_prod_keys_warning_description">Valid keys are required to emulate retail games. Only homebrew apps will function if you continue.</string>
+ <string name="install_prod_keys_warning_help">https://yuzu-emu.org/help/quickstart/#guide-introduction</string>
+ <string name="notifications">Notifications</string>
+ <string name="notifications_description">Grant the notification permission with the button below.</string>
+ <string name="give_permission">Grant permission</string>
+ <string name="notification_warning">Skip granting the notification permission?</string>
+ <string name="notification_warning_description">yuzu won\'t be able to notify you of important information.</string>
+ <string name="permission_denied">Permission denied</string>
+ <string name="permission_denied_description">You denied this permission too many times and now you have to manually grant it in system settings.</string>
+ <string name="about">About</string>
+ <string name="about_description">Build version, credits, and more</string>
+ <string name="warning_help">Help</string>
+ <string name="warning_skip">Skip</string>
+ <string name="warning_cancel">Cancel</string>
+ <string name="install_amiibo_keys">Install Amiibo keys</string>
+ <string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
+ <string name="invalid_keys_file">Invalid keys file selected</string>
+ <string name="install_keys_success">Keys successfully installed</string>
+ <string name="reading_keys_failure">Error reading encryption keys</string>
+ <string name="install_prod_keys_failure_extension_description">Verify your keys file has a .keys extension and try again.</string>
+ <string name="install_amiibo_keys_failure_extension_description">Verify your keys file has a .bin extension and try again.</string>
+ <string name="invalid_keys_error">Invalid encryption keys</string>
+ <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
+ <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
+ <string name="install_gpu_driver">Install GPU driver</string>
+ <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
+ <string name="advanced_settings">Advanced settings</string>
+ <string name="settings_description">Configure emulator settings</string>
+ <string name="search_recently_played">Recently played</string>
+ <string name="search_recently_added">Recently added</string>
+ <string name="search_retail">Retail</string>
+ <string name="search_homebrew">Homebrew</string>
+ <string name="open_user_folder">Open yuzu folder</string>
+ <string name="open_user_folder_description">Manage yuzu\'s internal files</string>
+ <string name="theme_and_color_description">Modify the look of the app</string>
+ <string name="no_file_manager">No file manager found</string>
+ <string name="notification_no_directory_link">Could not open yuzu directory</string>
+ <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
+ <string name="manage_save_data">Manage save data</string>
+ <string name="manage_save_data_description">Save data found. Please select an option below.</string>
+ <string name="import_export_saves_description">Import or export save files</string>
+ <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>
+ <string name="save_file_imported_success">Imported successfully</string>
+ <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
+ <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
+ <string name="import_saves">Import</string>
+ <string name="export_saves">Export</string>
+ <string name="install_firmware">Install firmware</string>
+ <string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string>
+ <string name="firmware_installing">Installing firmware</string>
+ <string name="firmware_installed_success">Firmware installed successfully</string>
+ <string name="firmware_installed_failure">Firmware installation failed</string>
+ <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string>
+ <string name="share_log">Share debug logs</string>
+ <string name="share_log_description">Share yuzu\'s log file to debug issues</string>
+ <string name="share_log_missing">No log file found</string>
+ <string name="install_game_content">Install game content</string>
+ <string name="install_game_content_description">Install game updates or DLC</string>
+ <string name="install_game_content_failure">Error installing file to NAND</string>
+ <string name="install_game_content_failure_description">Game content installation failed. Please ensure content is valid and that the prod.keys file is installed.</string>
+ <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts. Please select an update or DLC instead.</string>
+ <string name="install_game_content_failure_file_extension">The selected file type is not supported. Only NSP and XCI content is supported for this action. Please verify the game content is valid.</string>
+ <string name="install_game_content_success">Game content installed successfully</string>
+ <string name="install_game_content_success_overwrite">Game content was overwritten successfully</string>
+ <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>
+
+ <!-- About screen strings -->
+ <string name="gaia_is_not_real">Gaia isn\'t real</string>
+ <string name="copied_to_clipboard">Copied to clipboard</string>
+ <string name="about_app_description">An open-source Switch emulator</string>
+ <string name="contributors">Contributors</string>
+ <string name="contributors_description">Made with \u2764 from the yuzu team</string>
+ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
+ <string name="licenses_description">Projects that make yuzu for Android possible</string>
+ <string name="build">Build</string>
+ <string name="support_link">https://discord.gg/u77vRWY</string>
+ <string name="website_link">https://yuzu-emu.org/</string>
+ <string name="github_link">https://github.com/yuzu-emu</string>
+
+ <!-- Early access upgrade strings -->
+ <string name="early_access">Early Access</string>
+ <string name="get_early_access">Get Early Access</string>
+ <string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
+ <string name="get_early_access_description">Cutting-edge features, early access to updates, and more</string>
+ <string name="early_access_benefits">Early Access Benefits</string>
+ <string name="cutting_edge_features">Cutting-edge features</string>
+ <string name="early_access_updates">Early access to updates</string>
+ <string name="no_manual_installation">No manual installation</string>
+ <string name="prioritized_support">Prioritized support</string>
+ <string name="helping_game_preservation">Helping game preservation</string>
+ <string name="our_eternal_gratitude">Our eternal gratitude</string>
+ <string name="are_you_interested">Are you interested?</string>
+
+ <!-- General settings strings -->
+ <string name="frame_limit_enable">Limit speed</string>
+ <string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
+ <string name="frame_limit_slider">Limit speed percent</string>
+ <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>
+ <string name="cpu_accuracy">CPU accuracy</string>
+
+ <!-- System settings strings -->
+ <string name="use_docked_mode">Docked Mode</string>
+ <string name="use_docked_mode_description">Increases resolution, decreasing performance. Handheld Mode is used when disabled, lowering resolution and increasing performance.</string>
+ <string name="emulated_region">Emulated region</string>
+ <string name="emulated_language">Emulated language</string>
+ <string name="select_rtc_date">Select RTC date</string>
+ <string name="select_rtc_time">Select RTC time</string>
+ <string name="use_custom_rtc">Custom RTC</string>
+ <string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string>
+ <string name="set_custom_rtc">Set custom RTC</string>
+
+ <!-- Graphics settings strings -->
+ <string name="renderer_api">API</string>
+ <string name="renderer_accuracy">Accuracy level</string>
+ <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
+ <string name="renderer_vsync">VSync mode</string>
+ <string name="renderer_aspect_ratio">Aspect ratio</string>
+ <string name="renderer_scaling_filter">Window adapting filter</string>
+ <string name="renderer_anti_aliasing">Anti-aliasing method</string>
+ <string name="renderer_force_max_clock">Force maximum clocks (Adreno only)</string>
+ <string name="renderer_force_max_clock_description">Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).</string>
+ <string name="renderer_asynchronous_shaders">Use asynchronous shaders</string>
+ <string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously, reducing stutter but may introduce glitches.</string>
+ <string name="renderer_reactive_flushing">Use reactive flushing</string>
+ <string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string>
+ <string name="renderer_debug">Graphics debugging</string>
+ <string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string>
+ <string name="use_disk_shader_cache">Disk shader cache</string>
+ <string name="use_disk_shader_cache_description">Reduces stuttering by locally storing and loading generated shaders.</string>
+
+ <!-- Audio settings strings -->
+ <string name="audio_volume">Volume</string>
+ <string name="audio_volume_description">Specifies the volume of audio output.</string>
+
+ <!-- Miscellaneous -->
+ <string name="slider_default">Default</string>
+ <string name="ini_saved">Saved settings</string>
+ <string name="gameid_saved">Saved settings for %1$s</string>
+ <string name="error_saving">Error saving %1$s.ini: %2$s</string>
+ <string name="loading">Loading…</string>
+ <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
+ <string name="reset_to_default">Reset to default</string>
+ <string name="reset_all_settings">Reset all settings?</string>
+ <string name="reset_all_settings_description">All advanced settings will be reset to their default configuration. This can not be undone.</string>
+ <string name="settings_reset">Settings reset</string>
+ <string name="close">Close</string>
+ <string name="learn_more">Learn more</string>
+ <string name="auto">Auto</string>
+ <string name="submit">Submit</string>
+
+ <!-- GPU driver installation -->
+ <string name="select_gpu_driver">Select GPU driver</string>
+ <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
+ <string name="select_gpu_driver_install">Install</string>
+ <string name="select_gpu_driver_default">Default</string>
+ <string name="select_gpu_driver_install_success">Installed %s</string>
+ <string name="select_gpu_driver_use_default">Using default GPU driver</string>
+ <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string>
+ <string name="system_gpu_driver">System GPU driver</string>
+ <string name="installing_driver">Installing driver…</string>
+
+ <!-- Preferences Screen -->
+ <string name="preferences_settings">Settings</string>
+ <string name="preferences_general">General</string>
+ <string name="preferences_system">System</string>
+ <string name="preferences_graphics">Graphics</string>
+ <string name="preferences_audio">Audio</string>
+ <string name="preferences_theme">Theme and color</string>
+ <string name="preferences_debug">Debug</string>
+
+ <!-- ROM loading errors -->
+ <string name="loader_error_encrypted">Your ROM is encrypted</string>
+ <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>
+ <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>
+ <string name="loader_error_video_core">An error occurred initializing the video core</string>
+ <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>
+ <string name="loader_error_invalid_format">Unable to load ROM</string>
+ <string name="loader_error_file_not_found">ROM file does not exist</string>
+
+ <!-- Emulation Menu -->
+ <string name="emulation_exit">Exit emulation</string>
+ <string name="emulation_done">Done</string>
+ <string name="emulation_fps_counter">FPS counter</string>
+ <string name="emulation_toggle_controls">Toggle controls</string>
+ <string name="emulation_rel_stick_center">Relative stick center</string>
+ <string name="emulation_dpad_slide">D-pad slide</string>
+ <string name="emulation_haptics">Touch haptics</string>
+ <string name="emulation_show_overlay">Show overlay</string>
+ <string name="emulation_toggle_all">Toggle all</string>
+ <string name="emulation_control_adjust">Adjust overlay</string>
+ <string name="emulation_control_scale">Scale</string>
+ <string name="emulation_control_opacity">Opacity</string>
+ <string name="emulation_touch_overlay_reset">Reset overlay</string>
+ <string name="emulation_touch_overlay_edit">Edit overlay</string>
+ <string name="emulation_pause">Pause emulation</string>
+ <string name="emulation_unpause">Unpause emulation</string>
+ <string name="emulation_input_overlay">Overlay options</string>
+ <string name="emulation_game_loading">Game loading…</string>
+
+ <string name="load_settings">Loading settings…</string>
+
+ <!-- Software keyboard -->
+ <string name="software_keyboard">Software keyboard</string>
+
+ <!-- Errors and warnings -->
+ <string name="abort_button">Abort</string>
+ <string name="continue_button">Continue</string>
+ <string name="system_archive_not_found">System Archive Not Found</string>
+ <string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string>
+ <string name="system_archive_general">A system archive</string>
+ <string name="save_load_error">Save/Load Error</string>
+ <string name="fatal_error">Fatal Error</string>
+ <string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string>
+ <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>
+
+ <!-- Region Names -->
+ <string name="region_japan">Japan</string>
+ <string name="region_usa">USA</string>
+ <string name="region_europe">Europe</string>
+ <string name="region_australia">Australia</string>
+ <string name="region_china">China</string>
+ <string name="region_korea">Korea</string>
+ <string name="region_taiwan">Taiwan</string>
+
+ <!-- Language Names -->
+ <string name="language_japanese">Japanese (日本語)</string>
+ <string name="language_english">English</string>
+ <string name="language_french">French (Français)</string>
+ <string name="langauge_german">German (Deutsch)</string>
+ <string name="language_italian">Italian (Italiano)</string>
+ <string name="language_spanish">Spanish (Español)</string>
+ <string name="language_chinese">Chinese (简体中文)</string>
+ <string name="language_korean">Korean (한국어)</string>
+ <string name="language_dutch">Dutch (Nederlands)</string>
+ <string name="language_portuguese">Portuguese (Português)</string>
+ <string name="language_russian">Russian (Русский)</string>
+ <string name="language_taiwanese">Taiwanese (台湾)</string>
+ <string name="language_british_english">British English</string>
+ <string name="language_canadian_french">Canadian French (Français canadien)</string>
+ <string name="language_latin_american_spanish">Latin American Spanish (Español latinoamericano)</string>
+ <string name="language_simplified_chinese">Simplified Chinese (简体中文)</string>
+ <string name="language_traditional_chinese">Traditional Chinese (正體中文)</string>
+ <string name="language_brazilian_portuguese">Brazilian Portuguese (Português do Brasil)</string>
+
+ <!-- Renderer APIs -->
+ <string name="renderer_vulkan">Vulkan</string>
+ <string name="renderer_none">None</string>
+
+ <!-- Renderer Accuracy -->
+ <string name="renderer_accuracy_normal">Normal</string>
+ <string name="renderer_accuracy_high">High</string>
+ <string name="renderer_accuracy_extreme">Extreme (Slow)</string>
+
+ <!-- Resolutions -->
+ <string name="resolution_half">0.5X (360p/540p)</string>
+ <string name="resolution_three_quarter">0.75X (540p/810p)</string>
+ <string name="resolution_one">1X (720p/1080p)</string>
+ <string name="resolution_two">2X (1440p/2160p) (Slow)</string>
+ <string name="resolution_three">3X (2160p/3240p) (Slow)</string>
+ <string name="resolution_four">4X (2880p/4320p) (Slow)</string>
+
+ <!-- Renderer VSync -->
+ <string name="renderer_vsync_immediate">Immediate (Off)</string>
+ <string name="renderer_vsync_mailbox">Mailbox</string>
+ <string name="renderer_vsync_fifo">FIFO (On)</string>
+ <string name="renderer_vsync_fifo_relaxed">FIFO Relaxed</string>
+
+ <!-- Scaling Filters -->
+ <string name="scaling_filter_nearest_neighbor">Nearest Neighbor</string>
+ <string name="scaling_filter_bilinear">Bilinear</string>
+ <string name="scaling_filter_bicubic">Bicubic</string>
+ <string name="scaling_filter_gaussian">Gaussian</string>
+ <string name="scaling_filter_scale_force">ScaleForce</string>
+ <string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
+
+ <!-- Anti-Aliasing -->
+ <string name="anti_aliasing_none">None</string>
+ <string name="anti_aliasing_fxaa">FXAA</string>
+ <string name="anti_aliasing_smaa">SMAA</string>
+
+ <!-- Aspect Ratios -->
+ <string name="ratio_default">Default (16:9)</string>
+ <string name="ratio_force_four_three">Force 4:3</string>
+ <string name="ratio_force_twenty_one_nine">Force 21:9</string>
+ <string name="ratio_force_sixteen_ten">Force 16:10</string>
+ <string name="ratio_stretch">Stretch to window</string>
+
+ <!-- CPU Accuracy -->
+ <string name="cpu_accuracy_accurate">Accurate</string>
+ <string name="cpu_accuracy_unsafe">Unsafe</string>
+ <string name="cpu_accuracy_paranoid">Paranoid (Slow)</string>
+
+ <!-- Gamepad Buttons -->
+ <string name="gamepad_d_pad">D-pad</string>
+ <string name="gamepad_left_stick">Left stick</string>
+ <string name="gamepad_right_stick">Right stick</string>
+ <string name="gamepad_home">Home</string>
+ <string name="gamepad_screenshot">Screenshot</string>
+
+ <!-- Disk shader cache -->
+ <string name="preparing_shaders">Preparing shaders</string>
+ <string name="building_shaders">Building shaders</string>
+
+ <!-- Theme options -->
+ <string name="change_app_theme">Change app theme</string>
+ <string name="theme_default">Default</string>
+ <string name="theme_material_you">Material You</string>
+
+ <!-- Theme Modes -->
+ <string name="change_theme_mode">Change theme mode</string>
+ <string name="theme_mode_follow_system">Follow System</string>
+ <string name="theme_mode_light">Light</string>
+ <string name="theme_mode_dark">Dark</string>
+
+ <!-- Black backgrounds theme -->
+ <string name="use_black_backgrounds">Black backgrounds</string>
+ <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
+
+ <!-- Licenses screen strings -->
+ <string name="licenses">Licenses</string>
+ <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
+ <string name="license_fidelityfx_fsr_description">High-quality upscaling from AMD</string>
+ <string name="license_fidelityfx_fsr_link" translatable="false">https://github.com/GPUOpen-Effects/FidelityFX-FSR</string>
+ <string name="license_fidelityfx_fsr_copyright" translatable="false">Copyright © 2021 Advanced Micro Devices, Inc.</string>
+ <string name="license_fidelityfx_fsr_text" translatable="false">
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the \"Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:\n\n
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.\n\n
+
+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ </string>
+ <string name="license_cubeb" translatable="false">cubeb</string>
+ <string name="license_cubeb_description" translatable="false">Cross platform audio library</string>
+ <string name="license_cubeb_link" translatable="false">https://github.com/mozilla/cubeb</string>
+ <string name="license_cubeb_copyright" translatable="false">Copyright © 2011 Mozilla Foundation</string>
+ <string name="license_cubeb_text" translatable="false">
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.\n\n
+
+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ </string>
+ <string name="license_dynarmic" translatable="false">Dynarmic</string>
+ <string name="license_dynarmic_description" translatable="false">An ARM dynamic recompiler</string>
+ <string name="license_dynarmic_link" translatable="false">https://github.com/merryhime/dynarmic</string>
+ <string name="license_dynarmic_copyright" translatable="false">Copyright © 2017 merryhime</string>
+ <string name="license_dynarmic_text" translatable="false">
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted.\n\n
+
+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ </string>
+ <string name="license_ffmpeg" translatable="false">FFmpeg</string>
+ <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>
+ <string name="license_ffmpeg_link" translatable="false">https://github.com/FFmpeg/FFmpeg</string>
+ <string name="license_ffmpeg_copyright" translatable="false">Copyright © 1991, 1999 Free Software Foundation, Inc.</string>
+ <string name="license_ffmpeg_text" translatable="false">
+GNU LESSER GENERAL PUBLIC LICENSE\n
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called \"this License\").
+Each licensee is addressed as \"you\".\n\n
+
+ A \"library\" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.\n\n
+
+ The \"Library\", below, refers to any such software library or work
+which has been distributed under these terms. A \"work based on the
+Library\" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term \"modification\".)\n\n
+
+ \"Source code\" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.\n\n
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.\n\n
+
+ 1. You may copy and distribute verbatim copies of the Library\'s
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.\n\n
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.\n\n
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:\n\n
+
+a) The modified work must itself be a software library.\n\n
+
+b) You must cause the files modified to carry prominent notices
+stating that you changed the files and the date of any change.\n\n
+
+c) You must cause the whole of the work to be licensed at no
+charge to all third parties under the terms of this License.\n\n
+
+d) If a facility in the modified Library refers to a function or a
+table of data to be supplied by an application program that uses
+the facility, other than as an argument passed when the facility
+is invoked, then you must make a good faith effort to ensure that,
+in the event an application does not supply such function or
+table, the facility still operates, and performs whatever part of
+its purpose remains meaningful.\n\n
+
+(For example, a function in a library to compute square roots has
+a purpose that is entirely well-defined independent of the
+application. Therefore, Subsection 2d requires that any
+application-supplied function or table used by this function must
+be optional: if the application does not supply it, the square
+root function must still compute square roots.)\n\n
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.\n\n
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.\n\n
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.\n\n
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.\n\n
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.\n\n
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.\n\n
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.\n\n
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.\n\n
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a \"work that uses the Library\". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.\n\n
+
+ However, linking a \"work that uses the Library\" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a \"work that uses the
+library\". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.\n\n
+
+ When a \"work that uses the Library\" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.\n\n
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)\n\n
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.\n\n
+
+ 6. As an exception to the Sections above, you may also combine or
+link a \"work that uses the Library\" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer\'s own use and reverse
+engineering for debugging such modifications.\n\n
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:\n\n
+
+a) Accompany the work with the complete corresponding
+machine-readable source code for the Library including whatever
+changes were used in the work (which must be distributed under
+Sections 1 and 2 above); and, if the work is an executable linked
+with the Library, with the complete machine-readable \"work that
+uses the Library\", as object code and/or source code, so that the
+user can modify the Library and then relink to produce a modified
+executable containing the modified Library. (It is understood
+that the user who changes the contents of definitions files in the
+Library will not necessarily be able to recompile the application
+to use the modified definitions.)\n\n
+
+b) Use a suitable shared library mechanism for linking with the
+Library. A suitable mechanism is one that (1) uses at run time a
+copy of the library already present on the user\'s computer system,
+rather than copying library functions into the executable, and (2)
+will operate properly with a modified version of the library, if
+the user installs one, as long as the modified version is
+interface-compatible with the version that the work was made with.\n\n
+
+c) Accompany the work with a written offer, valid for at
+least three years, to give the same user the materials
+specified in Subsection 6a, above, for a charge no more
+than the cost of performing this distribution.\n\n
+
+d) If distribution of the work is made by offering access to copy
+from a designated place, offer equivalent access to copy the above
+specified materials from the same place.\n\n
+
+e) Verify that the user has already received a copy of these
+materials or that you have already sent this user a copy.\n\n
+
+ For an executable, the required form of the \"work that uses the
+Library\" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.\n\n
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.\n\n
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:\n\n
+
+a) Accompany the combined library with a copy of the same work
+based on the Library, uncombined with any other library
+facilities. This must be distributed under the terms of the
+Sections above.\n\n
+
+b) Give prominent notice with the combined library of the fact
+that part of it is a work based on the Library, and explaining
+where to find the accompanying uncombined form of the same work.\n\n
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.\n\n
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.\n\n
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients\' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.\n\n
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.\n\n
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.\n\n
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.\n\n
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.\n\n
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.\n\n
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.\n\n
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version\", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.\n\n
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.\n\n
+
+NO WARRANTY\n\n
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+ </string>
+ <string name="license_opus" translatable="false">Opus</string>
+ <string name="license_opus_description" translatable="false">Modern audio compression for the internet</string>
+ <string name="license_opus_link" translatable="false">https://github.com/xiph/opus</string>
+ <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>
+ <string name="license_opus_text" translatable="false">
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:\n\n
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.\n\n
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.\n\n
+
+- Neither the name of Internet Society, IETF or IETF Trust, nor the
+names of specific contributors, may be used to endorse or promote
+products derived from this software without specific prior written
+permission.\n\n
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS\'\' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n
+
+Opus is subject to the royalty-free patent licenses which are
+specified at:\n\n
+
+Xiph.Org Foundation:
+https://datatracker.ietf.org/ipr/1524/ \n\n
+
+Microsoft Corporation:
+https://datatracker.ietf.org/ipr/1914/ \n\n
+
+Broadcom Corporation:
+https://datatracker.ietf.org/ipr/1526/
+ </string>
+ <string name="license_sirit" translatable="false">Sirit</string>
+ <string name="license_sirit_description" translatable="false">A runtime SPIR-V assembler</string>
+ <string name="license_sirit_link" translatable="false">https://github.com/ReinUsesLisp/sirit</string>
+ <string name="license_sirit_copyright" translatable="false">Copyright © 2019, sirit All rights reserved.</string>
+ <string name="license_sirit_text" translatable="false">
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:\n
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.\n
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.\n
+* Neither the name of the organization nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.\n\n
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ </string>
+ <string name="license_adreno_tools" translatable="false">Adreno Tools</string>
+ <string name="license_adreno_tools_description" translatable="false">A library for applying rootless Adreno GPU driver modifications/replacements</string>
+ <string name="license_adreno_tools_link" translatable="false">https://github.com/bylaws/libadrenotools</string>
+ <string name="license_adreno_tools_copyright" translatable="false">Copyright © 2021, Billy Laws</string>
+ <string name="license_adreno_tools_text" translatable="false">
+BSD 2-Clause License\n\n
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:\n\n
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.\n\n
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.\n\n
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ </string>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Custom button styles -->
+ <style name="InGameMenuOption" parent="Widget.Material3.Button.TextButton">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:textColor">@android:color/black</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:gravity">center_vertical|left</item>
+ <item name="android:paddingLeft">32dp</item>
+ <item name="android:paddingRight">32dp</item>
+ </style>
+
+ <style name="YuzuSlider" parent="Widget.Material3.Slider">
+ <item name="tickVisible">false</item>
+ <item name="labelBehavior">gone</item>
+ </style>
+
+ <style name="YuzuMaterialDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
+ <item name="colorPrimary">@color/yuzu_primaryContainer</item>
+ <item name="colorSurface">@color/yuzu_primaryContainer</item>
+ <item name="colorSecondary">@color/yuzu_primary</item>
+ <item name="android:textColorLink">@color/yuzu_primary</item>
+ <item name="buttonBarPositiveButtonStyle">@style/YuzuButton</item>
+ <item name="buttonBarNegativeButtonStyle">@style/YuzuButton</item>
+ <item name="buttonBarNeutralButtonStyle">@style/YuzuButton</item>
+ </style>
+
+ <style name="YuzuButton" parent="Widget.Material3.Button.TextButton.Dialog">
+ <item name="android:textColor">@color/yuzu_primary</item>
+ <item name="rippleColor">@color/yuzu_inversePrimary</item>
+ </style>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.Yuzu.Splash.Main" parent="Theme.SplashScreen">
+ <item name="windowSplashScreenBackground">@color/yuzu_surface</item>
+ <item name="windowSplashScreenAnimatedIcon">@drawable/ic_yuzu</item>
+ <item name="postSplashScreenTheme">@style/Theme.Yuzu.Main</item>
+ </style>
+
+ <style name="Theme.Yuzu.Main" parent="Theme.Material3.DayNight.NoActionBar">
+ <item name="colorPrimary">@color/yuzu_primary</item>
+ <item name="colorOnPrimary">@color/yuzu_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/yuzu_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/yuzu_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/yuzu_secondary</item>
+ <item name="colorOnSecondary">@color/yuzu_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/yuzu_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/yuzu_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/yuzu_tertiary</item>
+ <item name="colorOnTertiary">@color/yuzu_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/yuzu_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/yuzu_onTertiaryContainer</item>
+ <item name="colorError">@color/yuzu_error</item>
+ <item name="colorErrorContainer">@color/yuzu_errorContainer</item>
+ <item name="colorOnError">@color/yuzu_onError</item>
+ <item name="colorOnErrorContainer">@color/yuzu_onErrorContainer</item>
+ <item name="android:colorBackground">@color/yuzu_background</item>
+ <item name="colorOnBackground">@color/yuzu_onBackground</item>
+ <item name="colorSurface">@color/yuzu_surface</item>
+ <item name="colorOnSurface">@color/yuzu_onSurface</item>
+ <item name="colorSurfaceVariant">@color/yuzu_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/yuzu_onSurfaceVariant</item>
+ <item name="colorOutline">@color/yuzu_outline</item>
+ <item name="colorOnSurfaceInverse">@color/yuzu_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/yuzu_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/yuzu_inversePrimary</item>
+ <item name="android:shadowColor">@color/yuzu_shadow</item>
+
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+
+ <item name="sliderStyle">@style/YuzuSlider</item>
+ <item name="materialAlertDialogTheme">@style/YuzuMaterialDialog</item>
+
+ <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+
+ <item name="android:enforceStatusBarContrast">false</item>
+ <item name="android:enforceNavigationBarContrast">false</item>
+ </style>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <color name="yuzu_primary">#990E00</color>
+ <color name="yuzu_onPrimary">#FFFFFF</color>
+ <color name="yuzu_primaryContainer">#EEDEDD</color>
+ <color name="yuzu_onPrimaryContainer">#400200</color>
+ <color name="yuzu_secondary">#775650</color>
+ <color name="yuzu_onSecondary">#FFFFFF</color>
+ <color name="yuzu_secondaryContainer">#FFDAD4</color>
+ <color name="yuzu_onSecondaryContainer">#2C1511</color>
+ <color name="yuzu_tertiary">#6F5C2E</color>
+ <color name="yuzu_onTertiary">#FFFFFF</color>
+ <color name="yuzu_tertiaryContainer">#FAE0A6</color>
+ <color name="yuzu_onTertiaryContainer">#251A00</color>
+ <color name="yuzu_error">#BA1A1A</color>
+ <color name="yuzu_errorContainer">#FFDAD6</color>
+ <color name="yuzu_onError">#FFFFFF</color>
+ <color name="yuzu_onErrorContainer">#410002</color>
+ <color name="yuzu_background">#FFFBFF</color>
+ <color name="yuzu_onBackground">#201A19</color>
+ <color name="yuzu_surface">#FFFBFF</color>
+ <color name="yuzu_onSurface">#201A19</color>
+ <color name="yuzu_surfaceVariant">#F5DDD9</color>
+ <color name="yuzu_onSurfaceVariant">#534340</color>
+ <color name="yuzu_outline">#857370</color>
+ <color name="yuzu_inverseOnSurface">#FBEEEB</color>
+ <color name="yuzu_inverseSurface">#362F2D</color>
+ <color name="yuzu_inversePrimary">#FFB4A6</color>
+ <color name="yuzu_shadow">#000000</color>
+ <color name="yuzu_surfaceTint">#B52612</color>
+ <color name="yuzu_outlineVariant">#D8C2BE</color>
+
+ <color name="yuzu_ea_background_start">#99FFE1</color>
+ <color name="yuzu_ea_background_end">#76C5FF</color>
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content>
+
+ <exclude
+ domain="external"
+ path="./load/" />
+
+ <exclude
+ domain="external"
+ path="./log/" />
+
+ <include
+ domain="external"
+ path="." />
+
+ <include
+ domain="sharedpref"
+ path="." />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-extraction-rules>
+ <cloud-backup disableIfNoEncryptionCapabilities="false">
+
+ <exclude
+ domain="external"
+ path="./load/" />
+
+ <exclude
+ domain="external"
+ path="./log/" />
+
+ <include
+ domain="external"
+ path="." />
+
+ <include
+ domain="sharedpref"
+ path="." />
+
+ </cloud-backup>
+
+ <device-transfer>
+
+ <exclude
+ domain="external"
+ path="./load/" />
+
+ <exclude
+ domain="external"
+ path="./log/" />
+
+ <include
+ domain="external"
+ path="." />
+
+ <include
+ domain="sharedpref"
+ path="." />
+
+ </device-transfer>
+
+</data-extraction-rules>
diff --git a/src/android/app/src/main/res/xml/locales_config.xml b/src/android/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 000000000..51b88d9dc
--- /dev/null
+++ b/src/android/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+ <locale android:name="en" /> <!-- English (default) -->
+ <locale android:name="de" /> <!-- German -->
+ <locale android:name="es" /> <!-- Spanish -->
+ <locale android:name="fr" /> <!-- French -->
+ <locale android:name="it" /> <!-- Italian -->
+ <locale android:name="ja" /> <!-- Japanese -->
+ <locale android:name="nb" /> <!-- Norwegian Bokmal -->
+ <locale android:name="pl" /> <!-- Polish -->
+ <locale android:name="pt-rBR" /> <!-- Portuguese (Brazil) -->
+ <locale android:name="pt-RPT" /> <!-- Portuguese (Portugal) -->
+ <locale android:name="ru" /> <!-- Russian -->
+ <locale android:name="uk" /> <!-- Ukranian -->
+ <locale android:name="zh-rCN" /> <!-- Chinese (China) -->
+ <locale android:name="zh-rTW" /> <!-- Chinese (Taiwan) -->
+</locale-config>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <tech-list>
+ <tech>android.nfc.tech.NfcA</tech>
+ </tech-list>
+</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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.0.2" apply false
+ id("com.android.library") version "8.0.2" apply false
+ id("org.jetbrains.kotlin.android") version "1.8.21" apply false
+}
+
+tasks.register("clean").configure {
+ delete(rootProject.buildDir)
+}
diff --git a/src/android/gradle.properties b/src/android/gradle.properties
new file mode 100644
index 000000000..e2f278f33
--- /dev/null
+++ b/src/android/gradle.properties
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xms512m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+kotlin.parallel.tasks.in.project=true
+android.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 @@
+#Sun Feb 21 18:16:59 EST 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=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 @@
+#!/usr/bin/env sh
+
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$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 @@
+@rem SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+@rem SPDX-License-Identifier: GPL-3.0-or-later
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+: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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+include(":app")