From 6e49adb508c009c5f7998a38d47e1d0d174990d0 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 8 Jun 2021 16:34:27 +1000 Subject: [PATCH] Remove Android app This repository exists solely for the desktop version now. --- android/.gitignore | 14 - android/.idea/.name | 1 - android/.idea/codeStyles/Project.xml | 116 - android/.idea/codeStyles/codeStyleConfig.xml | 5 - android/.idea/compiler.xml | 6 - android/.idea/gradle.xml | 21 - android/.idea/jarRepositories.xml | 30 - android/.idea/misc.xml | 9 - android/.idea/runConfigurations.xml | 13 - android/.idea/vcs.xml | 6 - android/app/.gitignore | 1 - android/app/build.gradle | 90 - android/app/proguard-rules.pro | 21 - .../duckstation/ExampleInstrumentedTest.java | 27 - android/app/src/cpp/CMakeLists.txt | 25 - .../src/cpp/android_controller_interface.cpp | 249 -- .../src/cpp/android_controller_interface.h | 73 - .../app/src/cpp/android_host_interface.cpp | 2198 ----------------- android/app/src/cpp/android_host_interface.h | 171 -- .../app/src/cpp/android_http_downloader.cpp | 167 -- android/app/src/cpp/android_http_downloader.h | 45 - .../app/src/cpp/android_progress_callback.cpp | 113 - .../app/src/cpp/android_progress_callback.h | 38 - .../src/cpp/android_settings_interface.cpp | 425 ---- .../app/src/cpp/android_settings_interface.h | 54 - android/app/src/cpp/opensles_audio_stream.cpp | 210 -- android/app/src/cpp/opensles_audio_stream.h | 48 - android/app/src/main/AndroidManifest.xml | 80 - android/app/src/main/ic_launcher-web.png | Bin 38012 -> 0 bytes .../stenzek/duckstation/Achievement.java | 76 - .../duckstation/AchievementListFragment.java | 193 -- .../AchievementSettingsFragment.java | 266 -- .../duckstation/AndroidHostInterface.java | 211 -- .../duckstation/AndroidProgressCallback.java | 141 -- .../stenzek/duckstation/ConsoleRegion.java | 8 - .../duckstation/ControllerAutoMapper.java | 295 --- .../duckstation/ControllerBindingDialog.java | 170 -- .../ControllerBindingPreference.java | 268 -- .../ControllerSettingsActivity.java | 132 - .../ControllerSettingsCollectionFragment.java | 459 ---- .../stenzek/duckstation/DiscRegion.java | 8 - .../duckstation/EmptyGameListFragment.java | 36 - .../duckstation/EmulationActivity.java | 988 -------- .../duckstation/EmulationSurfaceView.java | 302 --- .../stenzek/duckstation/EmulationThread.java | 67 - .../stenzek/duckstation/FileHelper.java | 613 ----- .../duckstation/GameDirectoriesActivity.java | 348 --- .../stenzek/duckstation/GameGridFragment.java | 139 -- .../github/stenzek/duckstation/GameList.java | 84 - .../stenzek/duckstation/GameListEntry.java | 99 - .../stenzek/duckstation/GameListFragment.java | 204 -- .../duckstation/GamePropertiesActivity.java | 253 -- .../duckstation/GameSettingPreference.java | 83 - .../duckstation/GameTraitPreference.java | 34 - .../duckstation/GenerateCoverTask.java | 64 - .../duckstation/GridAutofitLayoutManager.java | 73 - .../stenzek/duckstation/HotkeyInfo.java | 29 - .../stenzek/duckstation/ImageLoadTask.java | 32 - .../stenzek/duckstation/MainActivity.java | 556 ----- .../duckstation/MemoryCardEditorActivity.java | 524 ---- .../duckstation/MemoryCardFileInfo.java | 64 - .../stenzek/duckstation/MemoryCardImage.java | 204 -- .../github/stenzek/duckstation/PatchCode.java | 40 - .../duckstation/PreferenceHelpers.java | 85 - .../duckstation/PropertyListAdapter.java | 84 - .../stenzek/duckstation/RatioPreference.java | 198 -- .../stenzek/duckstation/SaveStateInfo.java | 151 -- .../stenzek/duckstation/SettingsActivity.java | 38 - .../SettingsCollectionFragment.java | 90 - .../TouchscreenControllerAxisView.java | 191 -- .../TouchscreenControllerButtonView.java | 204 -- .../TouchscreenControllerDPadView.java | 178 -- .../TouchscreenControllerView.java | 858 ------- .../stenzek/duckstation/URLDownloader.java | 96 - .../stenzek/duckstation/UpdateNotes.java | 54 - .../drawable-v24/ic_launcher_foreground.xml | 34 - .../main/res/drawable/cover_placeholder.png | Bin 214262 -> 0 bytes android/app/src/main/res/drawable/duck.png | Bin 48659 -> 0 bytes android/app/src/main/res/drawable/flag_eu.xml | 71 - android/app/src/main/res/drawable/flag_jp.xml | 50 - android/app/src/main/res/drawable/flag_us.xml | 539 ---- .../res/drawable/ic_baseline_album_24.xml | 10 - .../drawable/ic_baseline_arrow_back_24.xml | 11 - .../res/drawable/ic_baseline_category_24.xml | 16 - .../res/drawable/ic_baseline_close_24.xml | 10 - .../ic_baseline_create_new_folder_24.xml | 10 - .../res/drawable/ic_baseline_delete_24.xml | 10 - .../drawable/ic_baseline_delete_sweep_24.xml | 10 - .../drawable/ic_baseline_exit_to_app_24.xml | 11 - .../drawable/ic_baseline_fast_forward_24.xml | 10 - .../drawable/ic_baseline_fast_rewind_24.xml | 10 - .../res/drawable/ic_baseline_folder_24.xml | 10 - .../drawable/ic_baseline_folder_open_24.xml | 10 - .../res/drawable/ic_baseline_gamepad_24.xml | 10 - .../res/drawable/ic_baseline_grid_view_24.xml | 10 - .../main/res/drawable/ic_baseline_help_24.xml | 10 - .../ic_baseline_import_contacts_24.xml | 10 - .../ic_baseline_insert_drive_file_24.xml | 11 - .../drawable/ic_baseline_library_music_24.xml | 10 - .../main/res/drawable/ic_baseline_lock_24.xml | 10 - .../res/drawable/ic_baseline_lock_open_24.xml | 10 - .../main/res/drawable/ic_baseline_menu_24.xml | 10 - .../ic_baseline_not_interested_60.xml | 5 - .../drawable/ic_baseline_play_arrow_24.xml | 10 - .../drawable/ic_baseline_playlist_play_24.xml | 10 - .../ic_baseline_radio_button_checked_24.xml | 10 - .../ic_baseline_radio_button_unchecked_24.xml | 10 - .../drawable/ic_baseline_restart_alt_24.xml | 13 - .../main/res/drawable/ic_baseline_save_24.xml | 10 - .../res/drawable/ic_baseline_sd_card_24.xml | 10 - .../res/drawable/ic_baseline_settings_24.xml | 10 - .../ic_baseline_tips_and_updates_24.xml | 10 - .../res/drawable/ic_baseline_touch_app_24.xml | 10 - .../res/drawable/ic_baseline_trophy_24.xml | 10 - .../res/drawable/ic_baseline_vibration_24.xml | 10 - .../res/drawable/ic_baseline_view_list_24.xml | 10 - .../res/drawable/ic_controller_a_button.xml | 17 - .../ic_controller_a_button_pressed.xml | 16 - .../drawable/ic_controller_analog_base.xml | 21 - .../drawable/ic_controller_analog_button.xml | 17 - .../ic_controller_analog_button_pressed.xml | 17 - .../ic_controller_analog_stick_pressed.xml | 11 - .../ic_controller_analog_stick_unpressed.xml | 11 - .../res/drawable/ic_controller_b_button.xml | 17 - .../ic_controller_b_button_pressed.xml | 16 - .../drawable/ic_controller_circle_button.xml | 19 - .../ic_controller_circle_button_pressed.xml | 19 - .../drawable/ic_controller_cross_button.xml | 27 - .../ic_controller_cross_button_pressed.xml | 27 - .../drawable/ic_controller_down_button.xml | 14 - .../ic_controller_down_button_pressed.xml | 14 - .../drawable/ic_controller_fast_forward.xml | 17 - .../ic_controller_fast_forward_pressed.xml | 17 - .../res/drawable/ic_controller_l1_button.xml | 28 - .../ic_controller_l1_button_pressed.xml | 28 - .../res/drawable/ic_controller_l2_button.xml | 28 - .../ic_controller_l2_button_pressed.xml | 28 - .../drawable/ic_controller_left_button.xml | 14 - .../ic_controller_left_button_pressed.xml | 14 - .../drawable/ic_controller_pause_button.xml | 9 - .../ic_controller_quick_load_button.xml | 9 - .../ic_controller_quick_save_button.xml | 9 - .../res/drawable/ic_controller_r1_button.xml | 28 - .../ic_controller_r1_button_pressed.xml | 28 - .../res/drawable/ic_controller_r2_button.xml | 28 - .../ic_controller_r2_button_pressed.xml | 28 - .../drawable/ic_controller_right_button.xml | 14 - .../ic_controller_right_button_pressed.xml | 14 - .../drawable/ic_controller_select_button.xml | 14 - .../ic_controller_select_button_pressed.xml | 14 - .../drawable/ic_controller_square_button.xml | 20 - .../ic_controller_square_button_pressed.xml | 20 - .../drawable/ic_controller_start_button.xml | 14 - .../ic_controller_start_button_pressed.xml | 14 - .../res/drawable/ic_controller_t1_button.xml | 20 - .../ic_controller_t1_button_pressed.xml | 19 - .../res/drawable/ic_controller_t2_button.xml | 20 - .../ic_controller_t2_button_pressed.xml | 19 - .../res/drawable/ic_controller_t3_button.xml | 20 - .../ic_controller_t3_button_pressed.xml | 19 - .../res/drawable/ic_controller_t4_button.xml | 20 - .../ic_controller_t4_button_pressed.xml | 19 - .../ic_controller_triangle_button.xml | 19 - .../ic_controller_triangle_button_pressed.xml | 19 - .../res/drawable/ic_controller_up_button.xml | 14 - .../ic_controller_up_button_pressed.xml | 14 - .../main/res/drawable/ic_emblem_system.xml | 45 - .../res/drawable/ic_launcher_background.xml | 170 -- .../src/main/res/drawable/ic_media_cdrom.xml | 97 - .../app/src/main/res/drawable/ic_star_0.png | Bin 5534 -> 0 bytes .../app/src/main/res/drawable/ic_star_1.png | Bin 6116 -> 0 bytes .../app/src/main/res/drawable/ic_star_2.png | Bin 6367 -> 0 bytes .../app/src/main/res/drawable/ic_star_3.png | Bin 6526 -> 0 bytes .../app/src/main/res/drawable/ic_star_4.png | Bin 6312 -> 0 bytes .../app/src/main/res/drawable/ic_star_5.png | Bin 5990 -> 0 bytes .../layout/activity_controller_mapping.xml | 9 - .../main/res/layout/activity_emulation.xml | 20 - .../res/layout/activity_game_directories.xml | 47 - .../app/src/main/res/layout/activity_main.xml | 53 - .../layout/activity_memory_card_editor.xml | 65 - .../res/layout/fragment_achievement_list.xml | 74 - .../layout/fragment_achievements_login.xml | 133 - .../layout/fragment_controller_settings.xml | 22 - .../res/layout/fragment_empty_game_list.xml | 44 - .../fragment_emulation_activity_overlay.xml | 113 - .../main/res/layout/fragment_game_grid.xml | 18 - .../main/res/layout/fragment_game_list.xml | 17 - .../res/layout/fragment_memory_card_file.xml | 14 - .../layout/fragment_settings_collection.xml | 22 - .../res/layout/layout_achievement_entry.xml | 79 - .../layout_controller_binding_preference.xml | 39 - .../layout/layout_game_directory_entry.xml | 53 - .../res/layout/layout_game_grid_entry.xml | 17 - .../res/layout/layout_game_list_entry.xml | 76 - .../res/layout/layout_game_property_entry.xml | 26 - .../res/layout/layout_memory_card_save.xml | 82 - .../res/layout/layout_ratio_preference.xml | 26 - ...ut_touchscreen_controller_analog_stick.xml | 255 -- ...t_touchscreen_controller_analog_sticks.xml | 277 --- .../layout_touchscreen_controller_digital.xml | 239 -- .../layout_touchscreen_controller_edit.xml | 18 - ...layout_touchscreen_controller_lightgun.xml | 138 -- .../main/res/layout/save_state_view_entry.xml | 66 - .../src/main/res/layout/settings_activity.xml | 9 - .../main/res/menu/menu_controller_mapping.xml | 20 - .../res/menu/menu_edit_game_directories.xml | 15 - .../main/res/menu/menu_game_list_entry.xml | 16 - android/app/src/main/res/menu/menu_main.xml | 67 - .../main/res/menu/menu_memory_card_editor.xml | 24 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2817 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 5163 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5004 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1741 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 2917 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2993 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4088 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 7937 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7370 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7155 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 14699 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 12470 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10680 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 18599 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 18804 -> 0 bytes android/app/src/main/res/values-es/arrays.xml | 220 -- .../app/src/main/res/values-es/strings.xml | 346 --- android/app/src/main/res/values-it/arrays.xml | 206 -- .../app/src/main/res/values-it/strings.xml | 16 - .../app/src/main/res/values-night/colors.xml | 12 - android/app/src/main/res/values-nl/arrays.xml | 206 -- .../app/src/main/res/values-nl/strings.xml | 152 -- .../app/src/main/res/values-pt-rBR/arrays.xml | 264 -- .../src/main/res/values-pt-rBR/strings.xml | 368 --- android/app/src/main/res/values-ru/arrays.xml | 277 --- .../app/src/main/res/values-ru/strings.xml | 368 --- android/app/src/main/res/values/arrays.xml | 525 ---- android/app/src/main/res/values/attrs.xml | 12 - ...trs_touchscreen_controller_button_view.xml | 6 - android/app/src/main/res/values/colors.xml | 12 - android/app/src/main/res/values/dimens.xml | 3 - .../res/values/ic_launcher_background.xml | 4 - android/app/src/main/res/values/strings.xml | 368 --- android/app/src/main/res/values/styles.xml | 45 - .../main/res/xml/achievement_preferences.xml | 59 - .../src/main/res/xml/advanced_preferences.xml | 256 -- .../src/main/res/xml/audio_preferences.xml | 51 - .../main/res/xml/controllers_preferences.xml | 93 - .../src/main/res/xml/display_preferences.xml | 129 - .../main/res/xml/enhancements_preferences.xml | 154 -- .../src/main/res/xml/general_preferences.xml | 83 - .../main/res/xml/network_security_config.xml | 6 - .../stenzek/duckstation/ExampleUnitTest.java | 17 - android/build.gradle | 25 - android/gradle.properties | 20 - android/gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - android/gradlew | 172 -- android/gradlew.bat | 84 - android/settings.gradle | 2 - 261 files changed, 22601 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/.idea/.name delete mode 100644 android/.idea/codeStyles/Project.xml delete mode 100644 android/.idea/codeStyles/codeStyleConfig.xml delete mode 100644 android/.idea/compiler.xml delete mode 100644 android/.idea/gradle.xml delete mode 100644 android/.idea/jarRepositories.xml delete mode 100644 android/.idea/misc.xml delete mode 100644 android/.idea/runConfigurations.xml delete mode 100644 android/.idea/vcs.xml delete mode 100644 android/app/.gitignore delete mode 100644 android/app/build.gradle delete mode 100644 android/app/proguard-rules.pro delete mode 100644 android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java delete mode 100644 android/app/src/cpp/CMakeLists.txt delete mode 100644 android/app/src/cpp/android_controller_interface.cpp delete mode 100644 android/app/src/cpp/android_controller_interface.h delete mode 100644 android/app/src/cpp/android_host_interface.cpp delete mode 100644 android/app/src/cpp/android_host_interface.h delete mode 100644 android/app/src/cpp/android_http_downloader.cpp delete mode 100644 android/app/src/cpp/android_http_downloader.h delete mode 100644 android/app/src/cpp/android_progress_callback.cpp delete mode 100644 android/app/src/cpp/android_progress_callback.h delete mode 100644 android/app/src/cpp/android_settings_interface.cpp delete mode 100644 android/app/src/cpp/android_settings_interface.h delete mode 100644 android/app/src/cpp/opensles_audio_stream.cpp delete mode 100644 android/app/src/cpp/opensles_audio_stream.h delete mode 100644 android/app/src/main/AndroidManifest.xml delete mode 100644 android/app/src/main/ic_launcher-web.png delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/Achievement.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/AchievementListFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/AchievementSettingsFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/EmptyGameListFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/EmulationThread.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameList.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GameTraitPreference.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GenerateCoverTask.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/GridAutofitLayoutManager.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/HotkeyInfo.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ImageLoadTask.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardEditorActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardFileInfo.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardImage.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/RatioPreference.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/SaveStateInfo.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerAxisView.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerDPadView.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/URLDownloader.java delete mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/UpdateNotes.java delete mode 100644 android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 android/app/src/main/res/drawable/cover_placeholder.png delete mode 100644 android/app/src/main/res/drawable/duck.png delete mode 100644 android/app/src/main/res/drawable/flag_eu.xml delete mode 100644 android/app/src/main/res/drawable/flag_jp.xml delete mode 100644 android/app/src/main/res/drawable/flag_us.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_album_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_category_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_close_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_create_new_folder_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_delete_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_folder_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_grid_view_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_help_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_import_contacts_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_library_music_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_lock_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_lock_open_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_menu_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_not_interested_60.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_radio_button_checked_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_radio_button_unchecked_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_save_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_sd_card_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_settings_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_trophy_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_vibration_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_baseline_view_list_24.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_a_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_a_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_analog_base.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_analog_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_analog_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_analog_stick_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_analog_stick_unpressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_b_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_b_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_circle_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_cross_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_down_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_fast_forward.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_fast_forward_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_l1_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_l2_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_left_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_pause_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_quick_load_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_quick_save_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_r1_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_r2_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_right_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_select_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_square_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_start_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t1_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t1_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t2_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t2_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t3_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t3_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t4_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_t4_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_triangle_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_up_button.xml delete mode 100644 android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml delete mode 100644 android/app/src/main/res/drawable/ic_emblem_system.xml delete mode 100644 android/app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 android/app/src/main/res/drawable/ic_media_cdrom.xml delete mode 100644 android/app/src/main/res/drawable/ic_star_0.png delete mode 100644 android/app/src/main/res/drawable/ic_star_1.png delete mode 100644 android/app/src/main/res/drawable/ic_star_2.png delete mode 100644 android/app/src/main/res/drawable/ic_star_3.png delete mode 100644 android/app/src/main/res/drawable/ic_star_4.png delete mode 100644 android/app/src/main/res/drawable/ic_star_5.png delete mode 100644 android/app/src/main/res/layout/activity_controller_mapping.xml delete mode 100644 android/app/src/main/res/layout/activity_emulation.xml delete mode 100644 android/app/src/main/res/layout/activity_game_directories.xml delete mode 100644 android/app/src/main/res/layout/activity_main.xml delete mode 100644 android/app/src/main/res/layout/activity_memory_card_editor.xml delete mode 100644 android/app/src/main/res/layout/fragment_achievement_list.xml delete mode 100644 android/app/src/main/res/layout/fragment_achievements_login.xml delete mode 100644 android/app/src/main/res/layout/fragment_controller_settings.xml delete mode 100644 android/app/src/main/res/layout/fragment_empty_game_list.xml delete mode 100644 android/app/src/main/res/layout/fragment_emulation_activity_overlay.xml delete mode 100644 android/app/src/main/res/layout/fragment_game_grid.xml delete mode 100644 android/app/src/main/res/layout/fragment_game_list.xml delete mode 100644 android/app/src/main/res/layout/fragment_memory_card_file.xml delete mode 100644 android/app/src/main/res/layout/fragment_settings_collection.xml delete mode 100644 android/app/src/main/res/layout/layout_achievement_entry.xml delete mode 100644 android/app/src/main/res/layout/layout_controller_binding_preference.xml delete mode 100644 android/app/src/main/res/layout/layout_game_directory_entry.xml delete mode 100644 android/app/src/main/res/layout/layout_game_grid_entry.xml delete mode 100644 android/app/src/main/res/layout/layout_game_list_entry.xml delete mode 100644 android/app/src/main/res/layout/layout_game_property_entry.xml delete mode 100644 android/app/src/main/res/layout/layout_memory_card_save.xml delete mode 100644 android/app/src/main/res/layout/layout_ratio_preference.xml delete mode 100644 android/app/src/main/res/layout/layout_touchscreen_controller_analog_stick.xml delete mode 100644 android/app/src/main/res/layout/layout_touchscreen_controller_analog_sticks.xml delete mode 100644 android/app/src/main/res/layout/layout_touchscreen_controller_digital.xml delete mode 100644 android/app/src/main/res/layout/layout_touchscreen_controller_edit.xml delete mode 100644 android/app/src/main/res/layout/layout_touchscreen_controller_lightgun.xml delete mode 100644 android/app/src/main/res/layout/save_state_view_entry.xml delete mode 100644 android/app/src/main/res/layout/settings_activity.xml delete mode 100644 android/app/src/main/res/menu/menu_controller_mapping.xml delete mode 100644 android/app/src/main/res/menu/menu_edit_game_directories.xml delete mode 100644 android/app/src/main/res/menu/menu_game_list_entry.xml delete mode 100644 android/app/src/main/res/menu/menu_main.xml delete mode 100644 android/app/src/main/res/menu/menu_memory_card_editor.xml delete mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 android/app/src/main/res/values-es/arrays.xml delete mode 100644 android/app/src/main/res/values-es/strings.xml delete mode 100644 android/app/src/main/res/values-it/arrays.xml delete mode 100644 android/app/src/main/res/values-it/strings.xml delete mode 100644 android/app/src/main/res/values-night/colors.xml delete mode 100644 android/app/src/main/res/values-nl/arrays.xml delete mode 100644 android/app/src/main/res/values-nl/strings.xml delete mode 100644 android/app/src/main/res/values-pt-rBR/arrays.xml delete mode 100644 android/app/src/main/res/values-pt-rBR/strings.xml delete mode 100644 android/app/src/main/res/values-ru/arrays.xml delete mode 100644 android/app/src/main/res/values-ru/strings.xml delete mode 100644 android/app/src/main/res/values/arrays.xml delete mode 100644 android/app/src/main/res/values/attrs.xml delete mode 100644 android/app/src/main/res/values/attrs_touchscreen_controller_button_view.xml delete mode 100644 android/app/src/main/res/values/colors.xml delete mode 100644 android/app/src/main/res/values/dimens.xml delete mode 100644 android/app/src/main/res/values/ic_launcher_background.xml delete mode 100644 android/app/src/main/res/values/strings.xml delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/app/src/main/res/xml/achievement_preferences.xml delete mode 100644 android/app/src/main/res/xml/advanced_preferences.xml delete mode 100644 android/app/src/main/res/xml/audio_preferences.xml delete mode 100644 android/app/src/main/res/xml/controllers_preferences.xml delete mode 100644 android/app/src/main/res/xml/display_preferences.xml delete mode 100644 android/app/src/main/res/xml/enhancements_preferences.xml delete mode 100644 android/app/src/main/res/xml/general_preferences.xml delete mode 100644 android/app/src/main/res/xml/network_security_config.xml delete mode 100644 android/app/src/test/java/com/github/stenzek/duckstation/ExampleUnitTest.java delete mode 100644 android/build.gradle delete mode 100644 android/gradle.properties delete mode 100644 android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 android/gradlew delete mode 100644 android/gradlew.bat delete mode 100644 android/settings.gradle diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 603b14077..000000000 --- a/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx diff --git a/android/.idea/.name b/android/.idea/.name deleted file mode 100644 index eea1d2e85..000000000 --- a/android/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -DuckStation \ No newline at end of file diff --git a/android/.idea/codeStyles/Project.xml b/android/.idea/codeStyles/Project.xml deleted file mode 100644 index 681f41ae2..000000000 --- a/android/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
-
-
\ No newline at end of file diff --git a/android/.idea/codeStyles/codeStyleConfig.xml b/android/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a179..000000000 --- a/android/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/.idea/compiler.xml b/android/.idea/compiler.xml deleted file mode 100644 index fb7f4a8a4..000000000 --- a/android/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml deleted file mode 100644 index 9bba60dad..000000000 --- a/android/.idea/gradle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/.idea/jarRepositories.xml b/android/.idea/jarRepositories.xml deleted file mode 100644 index e34606ccd..000000000 --- a/android/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml deleted file mode 100644 index 860da66a5..000000000 --- a/android/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml deleted file mode 100644 index e497da999..000000000 --- a/android/.idea/runConfigurations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml deleted file mode 100644 index 6c0b86358..000000000 --- a/android/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/.gitignore b/android/app/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/android/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 38ec9c8df..000000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,90 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 29 - buildToolsVersion "30.0.2" - defaultConfig { - applicationId "com.github.stenzek.duckstation" - minSdkVersion 23 - targetSdkVersion 29 - versionCode(getBuildVersionCode()) - versionName "${getVersion()}" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - ndk { - debugSymbolLevel "FULL" - } - } - } - - externalNativeBuild { - cmake { - path "../../CMakeLists.txt" - version "3.10.2" - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - defaultConfig { - externalNativeBuild { - cmake { - arguments "-DCMAKE_BUILD_TYPE=Release" - abiFilters "arm64-v8a", "armeabi-v7a", "x86_64" - } - } - } - sourceSets { - main.assets.srcDirs += "../../data" - } - - // Blocked on R21 until CMake is updated to 3.19 or later. - ndkVersion '21.4.7075529' -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.3.0' - implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation "androidx.viewpager2:viewpager2:1.0.0" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' -} - -// Adapted from Dolphin. - -def getVersion() { - def versionNumber = '0.0-unknown' - - try { - versionNumber = 'git describe --tags --exclude latest --exclude preview'.execute([], project.rootDir).text - .trim() - .replaceAll(/(-0)?-[^-]+$/, "") - } catch (Exception e) { - logger.error('Cannot find git, defaulting to dummy version number') - } - - return versionNumber -} - - -def getBuildVersionCode() { - try { - def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text - .trim() - return Integer.valueOf(versionNumber); - } catch (Exception e) { - logger.error('Cannot find git, defaulting to dummy version number') - } - - return 1; -} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro deleted file mode 100644 index f1b424510..000000000 --- a/android/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java deleted file mode 100644 index 71cc603ed..000000000 --- a/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.github.stenzek.duckstation", appContext.getPackageName()); - } -} diff --git a/android/app/src/cpp/CMakeLists.txt b/android/app/src/cpp/CMakeLists.txt deleted file mode 100644 index 2e3fb11ed..000000000 --- a/android/app/src/cpp/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -set(SRCS - android_controller_interface.cpp - android_controller_interface.h - android_host_interface.cpp - android_host_interface.h - android_http_downloader.cpp - android_http_downloader.h - android_progress_callback.cpp - android_progress_callback.h - android_settings_interface.cpp - android_settings_interface.h -) - -add_library(duckstation-native SHARED ${SRCS}) -target_link_libraries(duckstation-native PRIVATE android frontend-common core common glad imgui) - -find_package(OpenSLES) -if(OPENSLES_FOUND) - message("Enabling OpenSL ES audio stream") - target_sources(duckstation-native PRIVATE - opensles_audio_stream.cpp - opensles_audio_stream.h) - target_link_libraries(duckstation-native PRIVATE OpenSLES::OpenSLES) - target_compile_definitions(duckstation-native PRIVATE "-DUSE_OPENSLES=1") -endif() diff --git a/android/app/src/cpp/android_controller_interface.cpp b/android/app/src/cpp/android_controller_interface.cpp deleted file mode 100644 index 7fea10ecd..000000000 --- a/android/app/src/cpp/android_controller_interface.cpp +++ /dev/null @@ -1,249 +0,0 @@ -#include "android_controller_interface.h" -#include "android_host_interface.h" -#include "common/assert.h" -#include "common/file_system.h" -#include "common/log.h" -#include "core/controller.h" -#include "core/host_interface.h" -#include "core/system.h" -#include -Log_SetChannel(AndroidControllerInterface); - -AndroidControllerInterface::AndroidControllerInterface() = default; - -AndroidControllerInterface::~AndroidControllerInterface() = default; - -ControllerInterface::Backend AndroidControllerInterface::GetBackend() const -{ - return ControllerInterface::Backend::Android; -} - -bool AndroidControllerInterface::Initialize(CommonHostInterface* host_interface) -{ - if (!ControllerInterface::Initialize(host_interface)) - return false; - - return true; -} - -void AndroidControllerInterface::Shutdown() -{ - ControllerInterface::Shutdown(); -} - -void AndroidControllerInterface::PollEvents() {} - -void AndroidControllerInterface::ClearBindings() -{ - std::unique_lock lock(m_controllers_mutex); - for (ControllerData& cd : m_controllers) - { - cd.axis_mapping.clear(); - cd.button_mapping.clear(); - cd.axis_button_mapping.clear(); - cd.button_axis_mapping.clear(); - } -} - -std::optional AndroidControllerInterface::GetControllerIndex(const std::string_view& device) -{ - std::unique_lock lock(m_controllers_mutex); - for (u32 i = 0; i < static_cast(m_device_names.size()); i++) - { - if (device == m_device_names[i]) - return static_cast(i); - } - - return std::nullopt; -} - -bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, - AxisCallback callback) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback); - Log_DevPrintf("Bound controller %d axis %d side %u", controller_index, axis_number, static_cast(axis_side)); - return true; -} - -bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - m_controllers[controller_index].button_mapping[button_number] = std::move(callback); - Log_DevPrintf("Bound controller %d button %d", controller_index, button_number); - return true; -} - -bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, - ButtonCallback callback) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback); - Log_DevPrintf("Bound controller %d axis %d to button", controller_index, axis_number); - return true; -} - -bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number, - std::string_view hat_position, ButtonCallback callback) -{ - return false; -} - -bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, - AxisCallback callback) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback); - Log_DevPrintf("Bound controller %d button %d to axis", controller_index, button_number); - return true; -} - -void AndroidControllerInterface::SetDeviceNames(std::vector device_names) -{ - std::unique_lock lock(m_controllers_mutex); - m_device_names = std::move(device_names); - m_controllers.resize(m_device_names.size()); - - for (u32 i = 0; i < static_cast(m_device_names.size()); i++) - Log_DevPrintf("Controller %u: %s", i, m_device_names[i].c_str()); -} - -void AndroidControllerInterface::SetDeviceRumble(u32 index, bool has_vibrator) -{ - std::unique_lock lock(m_controllers_mutex); - if (index >= m_controllers.size()) - return; - - m_controllers[index].has_rumble = has_vibrator; -} - -void AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value) -{ - std::unique_lock lock(m_controllers_mutex); - if (index >= m_controllers.size()) - return; - - Log_DevPrintf("controller %u axis %u %f", index, axis, value); - if (DoEventHook(Hook::Type::Axis, index, axis, value)) - return; - - const ControllerData& cd = m_controllers[index]; - const auto am_iter = cd.axis_mapping.find(axis); - if (am_iter != cd.axis_mapping.end()) - { - const AxisCallback& cb = am_iter->second[AxisSide::Full]; - if (cb) - { - cb(value); - return; - } - } - - // set the other direction to false so large movements don't leave the opposite on - const bool outside_deadzone = (std::abs(value) >= cd.deadzone); - const bool positive = (value >= 0.0f); - const auto bm_iter = cd.axis_button_mapping.find(axis); - if (bm_iter != cd.axis_button_mapping.end()) - { - const ButtonCallback& other_button_cb = bm_iter->second[BoolToUInt8(!positive)]; - const ButtonCallback& button_cb = bm_iter->second[BoolToUInt8(positive)]; - if (button_cb) - { - button_cb(outside_deadzone); - if (other_button_cb) - other_button_cb(false); - return; - } - else if (other_button_cb) - { - other_button_cb(false); - return; - } - } -} - -void AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed) -{ - Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released"); - - std::unique_lock lock(m_controllers_mutex); - if (index >= m_controllers.size()) - return; - - if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f)) - return; - - const ControllerData& cd = m_controllers[index]; - const auto button_iter = cd.button_mapping.find(button); - if (button_iter != cd.button_mapping.end() && button_iter->second) - { - button_iter->second(pressed); - return; - } - - const auto axis_iter = cd.button_axis_mapping.find(button); - if (axis_iter != cd.button_axis_mapping.end() && axis_iter->second) - { - axis_iter->second(pressed ? 1.0f : -1.0f); - return; - } - - Log_DevPrintf("controller %u button %u has no binding", index, button); -} - -bool AndroidControllerInterface::HasButtonBinding(u32 index, u32 button) -{ - std::unique_lock lock(m_controllers_mutex); - if (index >= m_controllers.size()) - return false; - - const ControllerData& cd = m_controllers[index]; - return (cd.button_mapping.find(button) != cd.button_mapping.end() || - cd.button_axis_mapping.find(button) != cd.button_axis_mapping.end()); -} - -u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - return m_controllers[static_cast(controller_index)].has_rumble ? NUM_RUMBLE_MOTORS : 0; -} - -void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, - u32 num_motors) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return; - - const float small_motor = strengths[0]; - const float large_motor = strengths[1]; - static_cast(m_host_interface) - ->SetControllerVibration(static_cast(controller_index), small_motor, large_motor); -} - -bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */) -{ - std::unique_lock lock(m_controllers_mutex); - if (static_cast(controller_index) >= m_controllers.size()) - return false; - - m_controllers[static_cast(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f); - Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, - m_controllers[static_cast(controller_index)].deadzone); - return true; -} diff --git a/android/app/src/cpp/android_controller_interface.h b/android/app/src/cpp/android_controller_interface.h deleted file mode 100644 index a22b81f50..000000000 --- a/android/app/src/cpp/android_controller_interface.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once -#include "core/types.h" -#include "frontend-common/controller_interface.h" -#include -#include -#include -#include -#include - -class AndroidControllerInterface final : public ControllerInterface -{ -public: - AndroidControllerInterface(); - ~AndroidControllerInterface() override; - - ALWAYS_INLINE u32 GetControllerCount() const { return static_cast(m_controllers.size()); } - - Backend GetBackend() const override; - bool Initialize(CommonHostInterface* host_interface) override; - void Shutdown() override; - - // Removes all bindings. Call before setting new bindings. - void ClearBindings() override; - - // Binding to events. If a binding for this axis/button already exists, returns false. - std::optional GetControllerIndex(const std::string_view& device) override; - bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override; - bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; - bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, - ButtonCallback callback) override; - bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, - ButtonCallback callback) override; - bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; - - // Changing rumble strength. - u32 GetControllerRumbleMotorCount(int controller_index) override; - void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override; - - // Set deadzone that will be applied on axis-to-button mappings - bool SetControllerDeadzone(int controller_index, float size = 0.25f) override; - - void PollEvents() override; - - void SetDeviceNames(std::vector device_names); - void SetDeviceRumble(u32 index, bool has_vibrator); - void HandleAxisEvent(u32 index, u32 axis, float value); - void HandleButtonEvent(u32 index, u32 button, bool pressed); - bool HasButtonBinding(u32 index, u32 button); - -private: - enum : u32 - { - NUM_RUMBLE_MOTORS = 2 - }; - - struct ControllerData - { - float deadzone = 0.25f; - - std::map> axis_mapping; - std::map button_mapping; - std::map> axis_button_mapping; - std::map button_axis_mapping; - bool has_rumble = false; - }; - - std::vector m_device_names; - std::vector m_controllers; - std::mutex m_controllers_mutex; - - std::mutex m_event_intercept_mutex; - Hook::Callback m_event_intercept_callback; -}; diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp deleted file mode 100644 index ea4ec3dd0..000000000 --- a/android/app/src/cpp/android_host_interface.cpp +++ /dev/null @@ -1,2198 +0,0 @@ -#include "android_host_interface.h" -#include "android_controller_interface.h" -#include "android_progress_callback.h" -#include "common/assert.h" -#include "common/audio_stream.h" -#include "common/file_system.h" -#include "common/log.h" -#include "common/string.h" -#include "common/string_util.h" -#include "common/timer.h" -#include "common/timestamp.h" -#include "core/bios.h" -#include "core/cheats.h" -#include "core/controller.h" -#include "core/gpu.h" -#include "core/host_display.h" -#include "core/memory_card_image.h" -#include "core/system.h" -#include "frontend-common/cheevos.h" -#include "frontend-common/game_list.h" -#include "frontend-common/imgui_fullscreen.h" -#include "frontend-common/imgui_styles.h" -#include "frontend-common/opengl_host_display.h" -#include "frontend-common/vulkan_host_display.h" -#include "scmversion/scmversion.h" -#include -#include -#include -#include -#include -Log_SetChannel(AndroidHostInterface); - -#ifdef USE_OPENSLES -#include "opensles_audio_stream.h" -#endif - -static JavaVM* s_jvm; -static jclass s_String_class; -static jclass s_AndroidHostInterface_class; -static jmethodID s_AndroidHostInterface_constructor; -static jfieldID s_AndroidHostInterface_field_mNativePointer; -static jfieldID s_AndroidHostInterface_field_mEmulationActivity; -static jmethodID s_AndroidHostInterface_method_reportError; -static jmethodID s_AndroidHostInterface_method_reportMessage; -static jmethodID s_AndroidHostInterface_method_openAssetStream; -static jclass s_EmulationActivity_class; -static jmethodID s_EmulationActivity_method_reportError; -static jmethodID s_EmulationActivity_method_onEmulationStarted; -static jmethodID s_EmulationActivity_method_onEmulationStopped; -static jmethodID s_EmulationActivity_method_onRunningGameChanged; -static jmethodID s_EmulationActivity_method_setVibration; -static jmethodID s_EmulationActivity_method_getRefreshRate; -static jmethodID s_EmulationActivity_method_openPauseMenu; -static jmethodID s_EmulationActivity_method_getInputDeviceNames; -static jmethodID s_EmulationActivity_method_hasInputDeviceVibration; -static jmethodID s_EmulationActivity_method_setInputDeviceVibration; -static jclass s_PatchCode_class; -static jmethodID s_PatchCode_constructor; -static jclass s_GameListEntry_class; -static jmethodID s_GameListEntry_constructor; -static jclass s_SaveStateInfo_class; -static jmethodID s_SaveStateInfo_constructor; -static jclass s_Achievement_class; -static jmethodID s_Achievement_constructor; -static jclass s_MemoryCardFileInfo_class; -static jmethodID s_MemoryCardFileInfo_constructor; - -namespace AndroidHelpers { -JavaVM* GetJavaVM() -{ - return s_jvm; -} - -// helper for retrieving the current per-thread jni environment -JNIEnv* GetJNIEnv() -{ - JNIEnv* env; - if (s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) - return nullptr; - else - return env; -} - -AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj) -{ - return reinterpret_cast( - static_cast(env->GetLongField(obj, s_AndroidHostInterface_field_mNativePointer))); -} - -std::string JStringToString(JNIEnv* env, jstring str) -{ - if (str == nullptr) - return {}; - - jsize length = env->GetStringUTFLength(str); - if (length == 0) - return {}; - - const char* data = env->GetStringUTFChars(str, nullptr); - Assert(data != nullptr); - - std::string ret(data, length); - env->ReleaseStringUTFChars(str, data); - - return ret; -} - -jclass GetStringClass() -{ - return s_String_class; -} - -std::unique_ptr ReadInputStreamToMemory(JNIEnv* env, jobject obj, u32 chunk_size /* = 65536*/) -{ - std::unique_ptr bs = std::make_unique(nullptr, 0); - u32 position = 0; - - jclass cls = env->GetObjectClass(obj); - jmethodID read_method = env->GetMethodID(cls, "read", "([B)I"); - Assert(read_method); - - jbyteArray temp = env->NewByteArray(chunk_size); - for (;;) - { - int bytes_read = env->CallIntMethod(obj, read_method, temp); - if (bytes_read <= 0) - break; - - if ((position + static_cast(bytes_read)) > bs->GetMemorySize()) - { - const u32 new_size = std::max(bs->GetMemorySize() * 2, position + static_cast(bytes_read)); - bs->ResizeMemory(new_size); - } - - env->GetByteArrayRegion(temp, 0, bytes_read, reinterpret_cast(bs->GetMemoryPointer() + position)); - position += static_cast(bytes_read); - } - - bs->Resize(position); - env->DeleteLocalRef(temp); - env->DeleteLocalRef(cls); - return bs; -} - -std::vector ByteArrayToVector(JNIEnv* env, jbyteArray obj) -{ - std::vector ret; - const jsize size = obj ? env->GetArrayLength(obj) : 0; - if (size > 0) - { - jbyte* data = env->GetByteArrayElements(obj, nullptr); - ret.resize(static_cast(size)); - std::memcpy(ret.data(), data, ret.size()); - env->ReleaseByteArrayElements(obj, data, 0); - } - - return ret; -} - -jbyteArray NewByteArray(JNIEnv* env, const void* data, size_t size) -{ - if (!data || size == 0) - return nullptr; - - jbyteArray obj = env->NewByteArray(static_cast(size)); - jbyte* obj_data = env->GetByteArrayElements(obj, nullptr); - std::memcpy(obj_data, data, static_cast(static_cast(size))); - env->ReleaseByteArrayElements(obj, obj_data, 0); - return obj; -} - -jbyteArray VectorToByteArray(JNIEnv* env, const std::vector& data) -{ - if (data.empty()) - return nullptr; - - return NewByteArray(env, data.data(), data.size()); -} - -jobjectArray CreateObjectArray(JNIEnv* env, jclass object_class, const jobject* objects, size_t num_objects, - bool release_refs /* = false*/) -{ - if (!objects || num_objects == 0) - return nullptr; - - jobjectArray arr = env->NewObjectArray(static_cast(num_objects), object_class, nullptr); - for (jsize i = 0; i < static_cast(num_objects); i++) - { - env->SetObjectArrayElement(arr, i, objects[i]); - if (release_refs && objects[i]) - env->DeleteLocalRef(objects[i]); - } - - return arr; -} -} // namespace AndroidHelpers - -AndroidHostInterface::AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory) - : m_java_object(java_object) -{ - m_user_directory = std::move(user_directory); - m_settings_interface = std::make_unique(context_object); -} - -AndroidHostInterface::~AndroidHostInterface() -{ - ImGui::DestroyContext(); - AndroidHelpers::GetJNIEnv()->DeleteGlobalRef(m_java_object); -} - -bool AndroidHostInterface::Initialize() -{ - if (!CommonHostInterface::Initialize()) - return false; - - return true; -} - -void AndroidHostInterface::Shutdown() -{ - HostInterface::Shutdown(); -} - -const char* AndroidHostInterface::GetFrontendName() const -{ - return "DuckStation Android"; -} - -void AndroidHostInterface::RequestExit() -{ - ReportError("Ignoring RequestExit()"); -} - -void AndroidHostInterface::ReportError(const char* message) -{ - CommonHostInterface::ReportError(message); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jstring message_jstr = env->NewStringUTF(message); - if (m_emulation_activity_object) - env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_reportError, message_jstr); - else - env->CallVoidMethod(m_java_object, s_AndroidHostInterface_method_reportError, message_jstr); - env->DeleteLocalRef(message_jstr); -} - -void AndroidHostInterface::ReportMessage(const char* message) -{ - CommonHostInterface::ReportMessage(message); - - if (IsOnEmulationThread()) - { - // The toasts are not visible when the emulation activity is running anyway. - AddOSDMessage(message, 5.0f); - } - else - { - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder message_jstr(env, env->NewStringUTF(message)); - env->CallVoidMethod(m_java_object, s_AndroidHostInterface_method_reportMessage, message_jstr.Get()); - } -} - -std::unique_ptr AndroidHostInterface::OpenPackageFile(const char* path, u32 flags) -{ - Log_DevPrintf("OpenPackageFile(%s, %x)", path, flags); - if (flags & (BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_WRITE)) - return {}; - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jobject stream = - env->CallObjectMethod(m_java_object, s_AndroidHostInterface_method_openAssetStream, env->NewStringUTF(path)); - if (!stream) - { - Log_ErrorPrintf("Package file '%s' not found", path); - return {}; - } - - std::unique_ptr ret(AndroidHelpers::ReadInputStreamToMemory(env, stream, 65536)); - env->DeleteLocalRef(stream); - return ret; -} - -void AndroidHostInterface::RegisterHotkeys() -{ - RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("OpenPauseMenu"), - StaticString(TRANSLATABLE("Hotkeys", "Open Pause Menu")), [this](bool pressed) { - if (pressed) - { - AndroidHelpers::GetJNIEnv()->CallVoidMethod(m_emulation_activity_object, - s_EmulationActivity_method_openPauseMenu); - } - }); - - CommonHostInterface::RegisterHotkeys(); -} - -float AndroidHostInterface::GetRefreshRate() const -{ - if (!m_emulation_activity_object) - return 0.0f; - - const float value = AndroidHelpers::GetJNIEnv()->CallFloatMethod(m_emulation_activity_object, - s_EmulationActivity_method_getRefreshRate); - return (value > 0.0f) ? value : 0.0f; -} - -float AndroidHostInterface::GetSurfaceScale(int width, int height) const -{ - if (width <= 0 || height <= 0) - return 1.0f; - - // TODO: Really need a better way of determining this. - return (width > height) ? (static_cast(width) / 1280.0f) : (static_cast(height) / 1280.0f); -} - -void AndroidHostInterface::SetUserDirectory() -{ - // Already set in constructor. - Assert(!m_user_directory.empty()); -} - -void AndroidHostInterface::LoadSettings(SettingsInterface& si) -{ - const GPURenderer old_renderer = g_settings.gpu_renderer; - CommonHostInterface::LoadSettings(si); - - const std::string msaa_str = si.GetStringValue("GPU", "MSAA", "1"); - g_settings.gpu_multisamples = std::max(StringUtil::FromChars(msaa_str).value_or(1), 1); - g_settings.gpu_per_sample_shading = StringUtil::EndsWith(msaa_str, "-ssaa"); - - // turn percentage into fraction for overclock - const u32 overclock_percent = static_cast(std::max(si.GetIntValue("CPU", "Overclock", 100), 1)); - Settings::CPUOverclockPercentToFraction(overclock_percent, &g_settings.cpu_overclock_numerator, - &g_settings.cpu_overclock_denominator); - g_settings.cpu_overclock_enable = (overclock_percent != 100); - g_settings.UpdateOverclockActive(); - - m_vibration_enabled = si.GetBoolValue("Controller1", "Vibration", false); - - // Defer renderer changes, the app really doesn't like it. - if (System::IsValid() && g_settings.gpu_renderer != old_renderer) - { - AddFormattedOSDMessage(5.0f, - TranslateString("OSDMessage", "Change to %s GPU renderer will take effect on restart."), - Settings::GetRendererName(g_settings.gpu_renderer)); - g_settings.gpu_renderer = old_renderer; - } -} - -void AndroidHostInterface::UpdateInputMap(SettingsInterface& si) -{ - if (m_emulation_activity_object) - { - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - DebugAssert(env); - - std::vector device_names; - - jobjectArray const java_names = reinterpret_cast( - env->CallObjectMethod(m_emulation_activity_object, s_EmulationActivity_method_getInputDeviceNames)); - if (java_names) - { - const u32 count = static_cast(env->GetArrayLength(java_names)); - for (u32 i = 0; i < count; i++) - { - device_names.push_back( - AndroidHelpers::JStringToString(env, reinterpret_cast(env->GetObjectArrayElement(java_names, i)))); - } - - env->DeleteLocalRef(java_names); - } - - if (m_controller_interface) - { - AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); - if (ci) - { - ci->SetDeviceNames(std::move(device_names)); - for (u32 i = 0; i < ci->GetControllerCount(); i++) - { - const bool has_vibration = env->CallBooleanMethod( - m_emulation_activity_object, s_EmulationActivity_method_hasInputDeviceVibration, static_cast(i)); - ci->SetDeviceRumble(i, has_vibration); - } - } - } - } - - CommonHostInterface::UpdateInputMap(si); -} - -bool AndroidHostInterface::IsEmulationThreadPaused() const -{ - return System::IsValid() && System::IsPaused(); -} - -void AndroidHostInterface::PauseEmulationThread(bool paused) -{ - Assert(IsEmulationThreadRunning()); - RunOnEmulationThread([this, paused]() { PauseSystem(paused); }); -} - -void AndroidHostInterface::StopEmulationThreadLoop() -{ - if (!IsEmulationThreadRunning()) - return; - - std::unique_lock lock(m_mutex); - m_emulation_thread_stop_request.store(true); - m_sleep_cv.notify_one(); -} - -bool AndroidHostInterface::IsOnEmulationThread() const -{ - return std::this_thread::get_id() == m_emulation_thread_id; -} - -void AndroidHostInterface::RunOnEmulationThread(std::function function, bool blocking) -{ - if (!IsEmulationThreadRunning()) - { - function(); - return; - } - - m_mutex.lock(); - m_callback_queue.push_back(std::move(function)); - m_callbacks_outstanding.store(true); - m_sleep_cv.notify_one(); - - if (blocking) - { - // TODO: Don't spin - for (;;) - { - if (!m_callbacks_outstanding.load()) - break; - - m_mutex.unlock(); - m_mutex.lock(); - } - } - - m_mutex.unlock(); -} - -void AndroidHostInterface::RunLater(std::function func) -{ - std::unique_lock lock(m_mutex); - m_callback_queue.push_back(std::move(func)); - m_callbacks_outstanding.store(true); -} - -void AndroidHostInterface::EmulationThreadEntryPoint(JNIEnv* env, jobject emulation_activity, - SystemBootParameters boot_params, bool resume_state) -{ - if (!m_surface) - { - Log_ErrorPrint("Emulation thread started without surface set."); - env->CallVoidMethod(emulation_activity, s_EmulationActivity_method_onEmulationStopped); - return; - } - - emulation_activity = env->NewGlobalRef(emulation_activity); - Assert(emulation_activity != nullptr); - - { - std::unique_lock lock(m_mutex); - m_emulation_thread_running.store(true); - m_emulation_activity_object = emulation_activity; - m_emulation_thread_id = std::this_thread::get_id(); - env->SetObjectField(m_java_object, s_AndroidHostInterface_field_mEmulationActivity, emulation_activity); - } - - ApplySettings(true); - - // Boot system. - bool boot_result = false; - if (resume_state && boot_params.filename.empty()) - boot_result = ResumeSystemFromMostRecentState(); - else if (resume_state && CanResumeSystemFromFile(boot_params.filename.c_str())) - boot_result = ResumeSystemFromState(boot_params.filename.c_str(), true); - else - boot_result = BootSystem(boot_params); - - if (boot_result) - { - // System is ready to go. - EmulationThreadLoop(env); - PowerOffSystem(ShouldSaveResumeState()); - } - - // Drain any callbacks so we don't leave things in a screwed-up state for next boot. - { - std::unique_lock lock(m_mutex); - while (!m_callback_queue.empty()) - { - auto callback = std::move(m_callback_queue.front()); - m_callback_queue.pop_front(); - lock.unlock(); - callback(); - lock.lock(); - } - env->SetObjectField(m_java_object, s_AndroidHostInterface_field_mEmulationActivity, nullptr); - m_emulation_thread_running.store(false); - m_emulation_thread_id = {}; - m_emulation_activity_object = {}; - m_callbacks_outstanding.store(false); - } - - env->CallVoidMethod(emulation_activity, s_EmulationActivity_method_onEmulationStopped); - env->DeleteGlobalRef(emulation_activity); -} - -void AndroidHostInterface::EmulationThreadLoop(JNIEnv* env) -{ - env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStarted); - - for (;;) - { - // run any events - { - std::unique_lock lock(m_mutex); - for (;;) - { - if (!m_callback_queue.empty()) - { - do - { - auto callback = std::move(m_callback_queue.front()); - m_callback_queue.pop_front(); - lock.unlock(); - callback(); - lock.lock(); - } while (!m_callback_queue.empty()); - m_callbacks_outstanding.store(false); - } - - if (m_emulation_thread_stop_request.load()) - { - m_emulation_thread_stop_request.store(false); - return; - } - - if (System::IsPaused()) - { - // paused, wait for us to resume - m_sleep_cv.wait(lock); - } - else - { - // done with callbacks, run the frame - break; - } - } - } - - // we don't do a full PollAndUpdate() here - if (Cheevos::IsActive()) - Cheevos::Update(); - - // simulate the system if not paused - if (System::IsRunning()) - { - if (m_throttler_enabled) - System::RunFrames(); - else - System::RunFrame(); - - UpdateControllerMetaState(); - if (m_vibration_enabled) - UpdateVibration(); - } - - // rendering - { - ImGui::NewFrame(); - DrawImGuiWindows(); - - m_display->Render(); - ImGui::EndFrame(); - - if (System::IsRunning()) - { - System::UpdatePerformanceCounters(); - - if (m_throttler_enabled) - System::Throttle(); - } - } - } -} - -bool AndroidHostInterface::AcquireHostDisplay() -{ - WindowInfo wi; - wi.type = WindowInfo::Type::Android; - wi.window_handle = m_surface; - wi.surface_width = ANativeWindow_getWidth(m_surface); - wi.surface_height = ANativeWindow_getHeight(m_surface); - wi.surface_refresh_rate = GetRefreshRate(); - wi.surface_scale = GetSurfaceScale(wi.surface_width, wi.surface_height); - - switch (g_settings.gpu_renderer) - { - case GPURenderer::HardwareVulkan: - m_display = std::make_unique(); - break; - - case GPURenderer::HardwareOpenGL: - default: - m_display = std::make_unique(); - break; - } - - if (!m_display->CreateRenderDevice(wi, {}, g_settings.gpu_use_debug_device, g_settings.gpu_threaded_presentation) || - !m_display->InitializeRenderDevice(GetShaderCacheBasePath(), g_settings.gpu_use_debug_device, - g_settings.gpu_threaded_presentation)) - { - m_display->DestroyRenderDevice(); - m_display.reset(); - return false; - } - - // The alignment was set prior to booting. - m_display->SetDisplayAlignment(m_display_alignment); - - if (!CreateHostDisplayResources()) - { - ReportError("Failed to create host display resources"); - ReleaseHostDisplay(); - return false; - } - - return true; -} - -void AndroidHostInterface::ReleaseHostDisplay() -{ - ReleaseHostDisplayResources(); - if (m_display) - { - m_display->DestroyRenderDevice(); - m_display.reset(); - } -} - -std::unique_ptr AndroidHostInterface::CreateAudioStream(AudioBackend backend) -{ -#ifdef USE_OPENSLES - if (backend == AudioBackend::OpenSLES) - return OpenSLESAudioStream::Create(); -#endif - - return CommonHostInterface::CreateAudioStream(backend); -} - -void AndroidHostInterface::UpdateControllerInterface() -{ - if (m_controller_interface) - { - m_controller_interface->Shutdown(); - m_controller_interface.reset(); - } - - m_controller_interface = std::make_unique(); - if (!m_controller_interface || !m_controller_interface->Initialize(this)) - { - Log_WarningPrintf("Failed to initialize controller interface, bindings are not possible."); - if (m_controller_interface) - { - m_controller_interface->Shutdown(); - m_controller_interface.reset(); - } - } -} - -void AndroidHostInterface::OnSystemPaused(bool paused) -{ - CommonHostInterface::OnSystemPaused(paused); - - if (m_vibration_enabled) - SetVibration(false); -} - -void AndroidHostInterface::OnSystemDestroyed() -{ - CommonHostInterface::OnSystemDestroyed(); - ClearOSDMessages(); - - if (m_vibration_enabled) - SetVibration(false); -} - -void AndroidHostInterface::OnRunningGameChanged(const std::string& path, CDImage* image, const std::string& game_code, - const std::string& game_title) -{ - CommonHostInterface::OnRunningGameChanged(path, image, game_code, game_title); - - if (m_emulation_activity_object) - { - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - - jstring path_string = env->NewStringUTF(path.c_str()); - jstring code_string = env->NewStringUTF(game_code.c_str()); - jstring title_string = env->NewStringUTF(game_title.c_str()); - - const GameListEntry* game_list_entry = m_game_list->GetEntryForPath(path.c_str()); - std::string cover_path_str; - if (game_list_entry) - cover_path_str = m_game_list->GetCoverImagePathForEntry(game_list_entry); - else - cover_path_str = m_game_list->GetCoverImagePath(path, game_code, game_title); - - jstring cover_path = nullptr; - if (!cover_path_str.empty()) - cover_path = env->NewStringUTF(cover_path_str.c_str()); - - env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onRunningGameChanged, path_string, - code_string, title_string, cover_path); - - if (cover_path) - env->DeleteLocalRef(cover_path); - env->DeleteLocalRef(title_string); - env->DeleteLocalRef(code_string); - env->DeleteLocalRef(path_string); - } -} - -void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, int width, int height) -{ - Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height); - if (m_surface == surface) - { - if (m_display && (width != m_display->GetWindowWidth() || height != m_display->GetWindowHeight())) - { - m_display->ResizeRenderWindow(width, height); - OnHostDisplayResized(); - } - - return; - } - - m_surface = surface; - - if (m_display) - { - WindowInfo wi; - wi.type = surface ? WindowInfo::Type::Android : WindowInfo::Type::Surfaceless; - wi.window_handle = surface; - wi.surface_width = width; - wi.surface_height = height; - wi.surface_refresh_rate = GetRefreshRate(); - wi.surface_scale = GetSurfaceScale(width, height); - - const bool surface_valid = m_display->ChangeRenderWindow(wi) && surface; - if (surface_valid) - OnHostDisplayResized(); - - if (surface_valid && System::GetState() == System::State::Paused) - PauseSystem(false); - else if (!surface_valid && System::IsRunning()) - PauseSystem(true); - } -} - -void AndroidHostInterface::SetDisplayAlignment(HostDisplay::Alignment alignment) -{ - m_display_alignment = alignment; - if (m_display) - m_display->SetDisplayAlignment(alignment); -} - -void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code, bool pressed) -{ - if (!IsEmulationThreadRunning()) - return; - - RunOnEmulationThread( - [index, button_code, pressed]() { - Controller* controller = System::GetController(index); - if (!controller) - return; - - controller->SetButtonState(button_code, pressed); - }, - false); -} - -void AndroidHostInterface::SetControllerAxisState(u32 index, s32 button_code, float value) -{ - if (!IsEmulationThreadRunning()) - return; - - RunOnEmulationThread( - [index, button_code, value]() { - Controller* controller = System::GetController(index); - if (!controller) - return; - - controller->SetAxisState(button_code, value); - }, - false); -} - -void AndroidHostInterface::HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed) -{ - if (!IsEmulationThreadRunning()) - return; - - RunOnEmulationThread([this, controller_index, button_index, pressed]() { - AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); - if (ci) - ci->HandleButtonEvent(controller_index, button_index, pressed); - }); -} - -void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value) -{ - if (!IsEmulationThreadRunning()) - return; - - RunOnEmulationThread([this, controller_index, axis_index, value]() { - AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); - if (ci) - ci->HandleAxisEvent(controller_index, axis_index, value); - }); -} - -bool AndroidHostInterface::HasControllerButtonBinding(u32 controller_index, u32 button) -{ - AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); - if (!ci) - return false; - - return ci->HasButtonBinding(controller_index, button); -} - -void AndroidHostInterface::SetControllerVibration(u32 controller_index, float small_motor, float large_motor) -{ - if (!m_emulation_activity_object) - return; - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - DebugAssert(env); - - env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_setInputDeviceVibration, - static_cast(controller_index), static_cast(small_motor), - static_cast(large_motor)); -} - -void AndroidHostInterface::SetFastForwardEnabled(bool enabled) -{ - m_fast_forward_enabled = enabled; - UpdateSpeedLimiterState(); -} - -void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database, - ProgressCallback* progress_callback) -{ - m_game_list->SetSearchDirectoriesFromSettings(*m_settings_interface); - m_game_list->Refresh(invalidate_cache, invalidate_database, progress_callback); -} - -bool AndroidHostInterface::ImportPatchCodesFromString(const std::string& str) -{ - CheatList* cl = new CheatList(); - if (!cl->LoadFromString(str, CheatList::Format::Autodetect) || cl->GetCodeCount() == 0) - return false; - - RunOnEmulationThread([this, cl]() { - u32 imported_count; - if (!System::HasCheatList()) - { - imported_count = cl->GetCodeCount(); - System::SetCheatList(std::unique_ptr(cl)); - } - else - { - const u32 old_count = System::GetCheatList()->GetCodeCount(); - System::GetCheatList()->MergeList(*cl); - imported_count = System::GetCheatList()->GetCodeCount() - old_count; - delete cl; - } - - AddFormattedOSDMessage(20.0f, "Imported %u patch codes.", imported_count); - CommonHostInterface::SaveCheatList(); - }); - - return true; -} - -void AndroidHostInterface::SetVibration(bool enabled) -{ - const u64 current_time = Common::Timer::GetValue(); - if (Common::Timer::ConvertValueToSeconds(current_time - m_last_vibration_update_time) < 0.1f && - m_last_vibration_state == enabled) - { - return; - } - - m_last_vibration_state = enabled; - m_last_vibration_update_time = current_time; - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - if (m_emulation_activity_object) - { - env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_setVibration, - static_cast(enabled)); - } -} - -void AndroidHostInterface::UpdateVibration() -{ - static constexpr float THRESHOLD = 0.5f; - - bool vibration_state = false; - - for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) - { - Controller* controller = System::GetController(i); - if (!controller) - continue; - - const u32 motors = controller->GetVibrationMotorCount(); - for (u32 j = 0; j < motors; j++) - { - if (controller->GetVibrationMotorStrength(j) >= THRESHOLD) - { - vibration_state = true; - break; - } - } - } - - SetVibration(vibration_state); -} - -jobjectArray AndroidHostInterface::GetInputProfileNames(JNIEnv* env) const -{ - const InputProfileList profile_list(GetInputProfileList()); - if (profile_list.empty()) - return nullptr; - - jobjectArray name_array = env->NewObjectArray(static_cast(profile_list.size()), s_String_class, nullptr); - u32 name_array_index = 0; - Assert(name_array != nullptr); - for (const InputProfileEntry& e : profile_list) - { - jstring axis_name_jstr = env->NewStringUTF(e.name.c_str()); - env->SetObjectArrayElement(name_array, name_array_index++, axis_name_jstr); - env->DeleteLocalRef(axis_name_jstr); - } - - return name_array; -} - -extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) -{ - Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV); - s_jvm = vm; - - // Create global reference so it doesn't get cleaned up. - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jclass string_class, host_interface_class, patch_code_class, game_list_entry_class, save_state_info_class, - achievement_class, memory_card_file_info_class; - if ((string_class = env->FindClass("java/lang/String")) == nullptr || - (s_String_class = static_cast(env->NewGlobalRef(string_class))) == nullptr || - (host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr || - (s_AndroidHostInterface_class = static_cast(env->NewGlobalRef(host_interface_class))) == nullptr || - (patch_code_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr || - (s_PatchCode_class = static_cast(env->NewGlobalRef(patch_code_class))) == nullptr || - (game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr || - (s_GameListEntry_class = static_cast(env->NewGlobalRef(game_list_entry_class))) == nullptr || - (save_state_info_class = env->FindClass("com/github/stenzek/duckstation/SaveStateInfo")) == nullptr || - (s_SaveStateInfo_class = static_cast(env->NewGlobalRef(save_state_info_class))) == nullptr || - (achievement_class = env->FindClass("com/github/stenzek/duckstation/Achievement")) == nullptr || - (s_Achievement_class = static_cast(env->NewGlobalRef(achievement_class))) == nullptr || - (memory_card_file_info_class = env->FindClass("com/github/stenzek/duckstation/MemoryCardFileInfo")) == nullptr || - (s_MemoryCardFileInfo_class = static_cast(env->NewGlobalRef(memory_card_file_info_class))) == nullptr) - { - Log_ErrorPrint("AndroidHostInterface class lookup failed"); - return -1; - } - - env->DeleteLocalRef(string_class); - env->DeleteLocalRef(host_interface_class); - env->DeleteLocalRef(patch_code_class); - env->DeleteLocalRef(game_list_entry_class); - env->DeleteLocalRef(achievement_class); - env->DeleteLocalRef(memory_card_file_info_class); - - jclass emulation_activity_class; - if ((s_AndroidHostInterface_constructor = - env->GetMethodID(s_AndroidHostInterface_class, "", - "(Landroid/content/Context;Lcom/github/stenzek/duckstation/FileHelper;)V")) == nullptr || - (s_AndroidHostInterface_field_mNativePointer = - env->GetFieldID(s_AndroidHostInterface_class, "mNativePointer", "J")) == nullptr || - (s_AndroidHostInterface_field_mEmulationActivity = - env->GetFieldID(s_AndroidHostInterface_class, "mEmulationActivity", - "Lcom/github/stenzek/duckstation/EmulationActivity;")) == nullptr || - (s_AndroidHostInterface_method_reportError = - env->GetMethodID(s_AndroidHostInterface_class, "reportError", "(Ljava/lang/String;)V")) == nullptr || - (s_AndroidHostInterface_method_reportMessage = - env->GetMethodID(s_AndroidHostInterface_class, "reportMessage", "(Ljava/lang/String;)V")) == nullptr || - (s_AndroidHostInterface_method_openAssetStream = env->GetMethodID( - s_AndroidHostInterface_class, "openAssetStream", "(Ljava/lang/String;)Ljava/io/InputStream;")) == nullptr || - (emulation_activity_class = env->FindClass("com/github/stenzek/duckstation/EmulationActivity")) == nullptr || - (s_EmulationActivity_class = static_cast(env->NewGlobalRef(emulation_activity_class))) == nullptr || - (s_EmulationActivity_method_reportError = - env->GetMethodID(s_EmulationActivity_class, "reportError", "(Ljava/lang/String;)V")) == nullptr || - (s_EmulationActivity_method_onEmulationStarted = - env->GetMethodID(s_EmulationActivity_class, "onEmulationStarted", "()V")) == nullptr || - (s_EmulationActivity_method_onEmulationStopped = - env->GetMethodID(s_EmulationActivity_class, "onEmulationStopped", "()V")) == nullptr || - (s_EmulationActivity_method_onRunningGameChanged = - env->GetMethodID(s_EmulationActivity_class, "onRunningGameChanged", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) == nullptr || - (s_EmulationActivity_method_setVibration = env->GetMethodID(emulation_activity_class, "setVibration", "(Z)V")) == - nullptr || - (s_EmulationActivity_method_getRefreshRate = - env->GetMethodID(emulation_activity_class, "getRefreshRate", "()F")) == nullptr || - (s_EmulationActivity_method_openPauseMenu = env->GetMethodID(emulation_activity_class, "openPauseMenu", "()V")) == - nullptr || - (s_EmulationActivity_method_getInputDeviceNames = - env->GetMethodID(s_EmulationActivity_class, "getInputDeviceNames", "()[Ljava/lang/String;")) == nullptr || - (s_EmulationActivity_method_hasInputDeviceVibration = - env->GetMethodID(s_EmulationActivity_class, "hasInputDeviceVibration", "(I)Z")) == nullptr || - (s_EmulationActivity_method_setInputDeviceVibration = - env->GetMethodID(s_EmulationActivity_class, "setInputDeviceVibration", "(IFF)V")) == nullptr || - (s_PatchCode_constructor = - env->GetMethodID(s_PatchCode_class, "", "(ILjava/lang/String;Ljava/lang/String;Z)V")) == nullptr || - (s_GameListEntry_constructor = - env->GetMethodID(s_GameListEntry_class, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/" - "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) == nullptr || - (s_SaveStateInfo_constructor = env->GetMethodID( - s_SaveStateInfo_class, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZII[B)V")) == - nullptr || - (s_Achievement_constructor = env->GetMethodID( - s_Achievement_class, "", - "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZ)V")) == nullptr || - (s_MemoryCardFileInfo_constructor = env->GetMethodID(s_MemoryCardFileInfo_class, "", - "(Ljava/lang/String;Ljava/lang/String;III[[B)V")) == nullptr) - { - Log_ErrorPrint("AndroidHostInterface lookups failed"); - return -1; - } - - env->DeleteLocalRef(emulation_activity_class); - - return JNI_VERSION_1_6; -} - -#define DEFINE_JNI_METHOD(return_type, name) \ - extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(JNIEnv* env) - -#define DEFINE_JNI_ARGS_METHOD(return_type, name, ...) \ - extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(JNIEnv* env, __VA_ARGS__) - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getScmVersion, jobject unused) -{ - return env->NewStringUTF(g_scm_tag_str); -} - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getFullScmVersion, jobject unused) -{ - return env->NewStringUTF(SmallString::FromFormat("DuckStation for Android %s (%s)\nBuilt %s %s", g_scm_tag_str, - g_scm_branch_str, __DATE__, __TIME__)); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setThreadAffinity, jobject unused, jintArray cores) -{ - // https://github.com/googlearchive/android-audio-high-performance/blob/c232c21bf35d3bfea16537b781c526b8abdcc3cf/SimpleSynth/app/src/main/cpp/audio_player.cc - int length = env->GetArrayLength(cores); - int* p_cores = env->GetIntArrayElements(cores, nullptr); - - pid_t current_thread_id = gettid(); - cpu_set_t cpu_set; - CPU_ZERO(&cpu_set); - for (int i = 0; i < length; i++) - { - Log_InfoPrintf("Binding to CPU %d", p_cores[i]); - CPU_SET(p_cores[i], &cpu_set); - } - - int result = sched_setaffinity(current_thread_id, sizeof(cpu_set_t), &cpu_set); - if (result != 0) - Log_InfoPrintf("Thread affinity set."); - else - Log_ErrorPrintf("Error setting thread affinity: %d", result); - - env->ReleaseIntArrayElements(cores, p_cores, 0); -} - -DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, jobject context_object, - jobject file_helper_object, jstring user_directory) -{ - Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEBUG); - - // initialize the java side - jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor, context_object, - file_helper_object); - if (!java_obj) - { - Log_ErrorPrint("Failed to create Java AndroidHostInterface"); - return nullptr; - } - - jobject java_obj_ref = env->NewGlobalRef(java_obj); - Assert(java_obj_ref != nullptr); - - // initialize the C++ side - std::string user_directory_str = AndroidHelpers::JStringToString(env, user_directory); - AndroidHostInterface* cpp_obj = new AndroidHostInterface(java_obj_ref, context_object, std::move(user_directory_str)); - if (!cpp_obj->Initialize()) - { - // TODO: Do we need to release the original java object reference? - Log_ErrorPrint("Failed to create C++ AndroidHostInterface"); - env->DeleteGlobalRef(java_obj_ref); - return nullptr; - } - - env->SetLongField(java_obj, s_AndroidHostInterface_field_mNativePointer, - static_cast(reinterpret_cast(cpp_obj))); - - FileSystem::SetAndroidFileHelper(s_jvm, env, file_helper_object); - return java_obj; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isEmulationThreadRunning, jobject obj) -{ - return AndroidHelpers::GetNativeClass(env, obj)->IsEmulationThreadRunning(); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_runEmulationThread, jobject obj, jobject emulationActivity, - jstring filename, jboolean resume_state, jstring state_filename) -{ - std::string state_filename_str = AndroidHelpers::JStringToString(env, state_filename); - - SystemBootParameters boot_params; - boot_params.filename = AndroidHelpers::JStringToString(env, filename); - - AndroidHelpers::GetNativeClass(env, obj)->EmulationThreadEntryPoint(env, emulationActivity, std::move(boot_params), - resume_state); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_stopEmulationThreadLoop, jobject obj) -{ - AndroidHelpers::GetNativeClass(env, obj)->StopEmulationThreadLoop(); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, jobject surface, jint format, jint width, - jint height) -{ - ANativeWindow* native_surface = surface ? ANativeWindow_fromSurface(env, surface) : nullptr; - if (surface && !native_surface) - Log_ErrorPrint("ANativeWindow_fromSurface() returned null"); - - if (!surface && System::GetState() == System::State::Starting) - { - // User switched away from the app while it was compiling shaders. - Log_ErrorPrintf("Surface destroyed while starting, cancelling"); - System::CancelPendingStartup(); - } - - // We should wait for the emu to finish if the surface is being destroyed or changed. - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const bool block = (!native_surface || native_surface != hi->GetSurface()); - hi->RunOnEmulationThread( - [hi, native_surface, format, width, height]() { hi->SurfaceChanged(native_surface, format, width, height); }, - block); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setMousePosition, jobject obj, jint positionX, jint positionY) -{ - HostDisplay* display = AndroidHelpers::GetNativeClass(env, obj)->GetDisplay(); - if (!display) - return; - - // Technically a race, but shouldn't cause any issues. - display->SetMousePosition(positionX, positionY); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setControllerButtonState, jobject obj, jint index, jint button_code, - jboolean pressed) -{ - AndroidHelpers::GetNativeClass(env, obj)->SetControllerButtonState(index, button_code, pressed); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setControllerAutoFireState, jobject obj, jint controller_index, - jint autofire_index, jboolean active) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - if (!hi->IsEmulationThreadRunning()) - return; - - hi->RunOnEmulationThread([hi, controller_index, autofire_index, active]() { - hi->SetControllerAutoFireSlotState(controller_index, autofire_index, active); - }); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobject unused, jstring controller_type, - jstring button_name) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return -1; - - std::optional code = - Controller::GetButtonCodeByName(type.value(), AndroidHelpers::JStringToString(env, button_name)); - return code.value_or(-1); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setControllerAxisState, jobject obj, jint index, jint button_code, - jfloat value) -{ - AndroidHelpers::GetNativeClass(env, obj)->SetControllerAxisState(index, button_code, value); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject unused, jstring controller_type, - jstring axis_name) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return -1; - - std::optional code = - Controller::GetAxisCodeByName(type.value(), AndroidHelpers::JStringToString(env, axis_name)); - return code.value_or(-1); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisType, jobject unused, jstring controller_type, - jstring axis_name) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return -1; - - const std::string axis_name_str(AndroidHelpers::JStringToString(env, axis_name)); - for (const auto& [name, code, type] : Controller::GetAxisNames(type.value())) - { - if (name == axis_name_str) - return static_cast(type); - } - - return -1; -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused, - jstring controller_type) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return nullptr; - - const Controller::ButtonList buttons(Controller::GetButtonNames(type.value())); - if (buttons.empty()) - return nullptr; - - jobjectArray name_array = env->NewObjectArray(static_cast(buttons.size()), s_String_class, nullptr); - u32 name_array_index = 0; - Assert(name_array != nullptr); - for (const auto& [button_name, button_code] : buttons) - { - jstring button_name_jstr = env->NewStringUTF(button_name.c_str()); - env->SetObjectArrayElement(name_array, name_array_index++, button_name_jstr); - env->DeleteLocalRef(button_name_jstr); - } - - return name_array; -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused, - jstring controller_type) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return nullptr; - - const Controller::AxisList axes(Controller::GetAxisNames(type.value())); - if (axes.empty()) - return nullptr; - - jobjectArray name_array = env->NewObjectArray(static_cast(axes.size()), s_String_class, nullptr); - u32 name_array_index = 0; - Assert(name_array != nullptr); - for (const auto& [axis_name, axis_code, axis_type] : axes) - { - jstring axis_name_jstr = env->NewStringUTF(axis_name.c_str()); - env->SetObjectArrayElement(name_array, name_array_index++, axis_name_jstr); - env->DeleteLocalRef(axis_name_jstr); - } - - return name_array; -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerVibrationMotorCount, jobject unused, - jstring controller_type) -{ - std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); - if (!type) - return 0; - - return static_cast(Controller::GetVibrationMotorCount(type.value())); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index, - jint button_index, jboolean pressed) -{ - AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(static_cast(controller_index), - static_cast(button_index), pressed); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index, - jint axis_index, jfloat value) -{ - AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(static_cast(controller_index), - static_cast(axis_index), value); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasControllerButtonBinding, jobject obj, jint controller_index, - jint button_index) -{ - return AndroidHelpers::GetNativeClass(env, obj)->HasControllerButtonBinding(static_cast(controller_index), - static_cast(button_index)); -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getInputProfileNames, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - return hi->GetInputProfileNames(env); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_loadInputProfile, jobject obj, jstring name) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const std::string profile_name(AndroidHelpers::JStringToString(env, name)); - if (profile_name.empty()) - return false; - - const std::string profile_path(hi->GetInputProfilePath(profile_name.c_str())); - if (profile_path.empty()) - return false; - - return hi->ApplyInputProfile(profile_path.c_str()); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_saveInputProfile, jobject obj, jstring name) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const std::string profile_name(AndroidHelpers::JStringToString(env, name)); - if (profile_name.empty()) - return false; - - const std::string profile_path(hi->GetSavePathForInputProfile(profile_name.c_str())); - if (profile_path.empty()) - return false; - - return hi->SaveInputProfile(profile_path.c_str()); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, - jboolean invalidate_database, jobject progress_callback) -{ - AndroidProgressCallback cb(env, progress_callback); - AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database, &cb); -} - -static const char* DiscRegionToString(DiscRegion region) -{ - static std::array names = {{"NTSC_J", "NTSC_U", "PAL", "Other"}}; - return names[static_cast(region)]; -} - -static jobject CreateGameListEntry(JNIEnv* env, AndroidHostInterface* hi, const GameListEntry& entry) -{ - const Timestamp modified_ts( - Timestamp::FromUnixTimestamp(static_cast(entry.last_modified_time))); - const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry)); - - jstring path = env->NewStringUTF(entry.path.c_str()); - jstring code = env->NewStringUTF(entry.code.c_str()); - jstring title = env->NewStringUTF(entry.title.c_str()); - jstring region = env->NewStringUTF(DiscRegionToString(entry.region)); - jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type)); - jstring compatibility_rating = - env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating)); - jstring cover_path = (cover_path_str.empty()) ? nullptr : env->NewStringUTF(cover_path_str.c_str()); - jstring modified_time = env->NewStringUTF(modified_ts.ToString("%Y/%m/%d, %H:%M:%S")); - jlong size = entry.total_size; - - jobject entry_jobject = env->NewObject(s_GameListEntry_class, s_GameListEntry_constructor, path, code, title, size, - modified_time, region, type, compatibility_rating, cover_path); - - env->DeleteLocalRef(modified_time); - if (cover_path) - env->DeleteLocalRef(cover_path); - env->DeleteLocalRef(compatibility_rating); - env->DeleteLocalRef(type); - env->DeleteLocalRef(region); - env->DeleteLocalRef(title); - env->DeleteLocalRef(code); - env->DeleteLocalRef(path); - - return entry_jobject; -} - -DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), s_GameListEntry_class, nullptr); - Assert(entry_array != nullptr); - - u32 counter = 0; - for (const GameListEntry& entry : hi->GetGameList()->GetEntries()) - { - jobject entry_jobject = CreateGameListEntry(env, hi, entry); - env->SetObjectArrayElement(entry_array, counter++, entry_jobject); - env->DeleteLocalRef(entry_jobject); - } - - return entry_array; -} - -DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getGameListEntry, jobject obj, jstring path) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const std::string path_str(AndroidHelpers::JStringToString(env, path)); - const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str()); - if (!entry) - return nullptr; - - return CreateGameListEntry(env, hi, *entry); -} - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getGameSettingValue, jobject obj, jstring path, jstring key) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const std::string path_str(AndroidHelpers::JStringToString(env, path)); - const std::string key_str(AndroidHelpers::JStringToString(env, key)); - - const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str()); - if (!entry) - return nullptr; - - std::optional value = entry->settings.GetValueForKey(key_str); - if (!value.has_value()) - return nullptr; - else - return env->NewStringUTF(value->c_str()); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setGameSettingValue, jobject obj, jstring path, jstring key, - jstring value) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const std::string path_str(AndroidHelpers::JStringToString(env, path)); - const std::string key_str(AndroidHelpers::JStringToString(env, key)); - - const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str()); - if (!entry) - return; - - GameSettings::Entry new_entry(entry->settings); - - std::optional value_str; - if (value) - value_str = AndroidHelpers::JStringToString(env, value); - - new_entry.SetValueForKey(key_str, value_str); - hi->GetGameList()->UpdateGameSettings(path_str, entry->code, entry->title, new_entry, true); -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getHotkeyInfoList, jobject obj) -{ - jclass entry_class = env->FindClass("com/github/stenzek/duckstation/HotkeyInfo"); - Assert(entry_class != nullptr); - - jmethodID entry_constructor = - env->GetMethodID(entry_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - Assert(entry_constructor != nullptr); - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - const CommonHostInterface::HotkeyInfoList& hotkeys = hi->GetHotkeyInfoList(); - if (hotkeys.empty()) - return nullptr; - - jobjectArray entry_array = env->NewObjectArray(static_cast(hotkeys.size()), entry_class, nullptr); - Assert(entry_array != nullptr); - - u32 counter = 0; - for (const CommonHostInterface::HotkeyInfo& hk : hotkeys) - { - jstring category = env->NewStringUTF(hk.category.GetCharArray()); - jstring name = env->NewStringUTF(hk.name.GetCharArray()); - jstring display_name = env->NewStringUTF(hk.display_name.GetCharArray()); - - jobject entry_jobject = env->NewObject(entry_class, entry_constructor, category, name, display_name); - - env->SetObjectArrayElement(entry_array, counter++, entry_jobject); - env->DeleteLocalRef(entry_jobject); - env->DeleteLocalRef(display_name); - env->DeleteLocalRef(name); - env->DeleteLocalRef(category); - } - - return entry_array; -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - if (hi->IsEmulationThreadRunning()) - { - hi->RunOnEmulationThread([hi]() { hi->ApplySettings(false); }); - } - else - { - hi->ApplySettings(false); - } -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_updateInputMap, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - if (hi->IsEmulationThreadRunning()) - { - hi->RunOnEmulationThread([hi]() { hi->UpdateInputMap(); }); - } - else - { - hi->UpdateInputMap(); - } -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([hi]() { hi->ResetSystem(); }); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_loadState, jobject obj, jboolean global, jint slot) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([hi, global, slot]() { hi->LoadState(global, slot); }); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveState, jobject obj, jboolean global, jint slot) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([hi, global, slot]() { hi->SaveState(global, slot); }); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveResumeState, jobject obj, jboolean wait_for_completion) -{ - if (!System::IsValid() || System::GetState() == System::State::Starting) - { - // This gets called when the surface is destroyed, which can happen while starting. - return; - } - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([hi]() { hi->SaveResumeSaveState(); }, wait_for_completion); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setDisplayAlignment, jobject obj, jint alignment) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread( - [hi, alignment]() { hi->SetDisplayAlignment(static_cast(alignment)); }, false); -} - -DEFINE_JNI_ARGS_METHOD(bool, AndroidHostInterface_hasSurface, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - HostDisplay* display = hi->GetDisplay(); - if (display) - return display->HasRenderSurface(); - else - return false; -} - -DEFINE_JNI_ARGS_METHOD(bool, AndroidHostInterface_isEmulationThreadPaused, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - return hi->IsEmulationThreadPaused(); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_pauseEmulationThread, jobject obj, jboolean paused) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->PauseEmulationThread(paused); -} - -DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getPatchCodeList, jobject obj) -{ - if (!System::IsValid()) - return nullptr; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - if (!System::HasCheatList()) - { - // Hopefully this won't deadlock... - hi->RunOnEmulationThread( - [hi]() { - if (!hi->LoadCheatListFromGameTitle()) - hi->LoadCheatListFromDatabase(); - }, - true); - } - - if (!System::HasCheatList()) - return nullptr; - - CheatList* cl = System::GetCheatList(); - const u32 count = cl->GetCodeCount(); - - jobjectArray arr = env->NewObjectArray(count, s_PatchCode_class, nullptr); - for (u32 i = 0; i < count; i++) - { - const CheatCode& cc = cl->GetCode(i); - - jstring group_str = cc.group.empty() ? nullptr : env->NewStringUTF(cc.group.c_str()); - jstring desc_str = env->NewStringUTF(cc.description.c_str()); - jobject java_cc = - env->NewObject(s_PatchCode_class, s_PatchCode_constructor, static_cast(i), group_str, desc_str, cc.enabled); - env->SetObjectArrayElement(arr, i, java_cc); - env->DeleteLocalRef(java_cc); - env->DeleteLocalRef(desc_str); - env->DeleteLocalRef(group_str); - } - - return arr; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_importPatchCodesFromString, jobject obj, jstring str) -{ - if (!System::IsValid()) - return false; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - return hi->ImportPatchCodesFromString(AndroidHelpers::JStringToString(env, str)); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setPatchCodeEnabled, jobject obj, jint index, jboolean enabled) -{ - if (!System::IsValid() || !System::HasCheatList()) - return; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([index, enabled, hi]() { hi->SetCheatCodeState(static_cast(index), enabled, true); }); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_addOSDMessage, jobject obj, jstring message, jfloat duration) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->AddOSDMessage(AndroidHelpers::JStringToString(env, message), duration); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasAnyBIOSImages, jobject obj) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - return hi->HasAnyBIOSImages(); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isFastForwardEnabled, jobject obj) -{ - return AndroidHelpers::GetNativeClass(env, obj)->IsRunningAtNonStandardSpeed(); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setFastForwardEnabled, jobject obj, jboolean enabled) -{ - if (!System::IsValid()) - return; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([enabled, hi]() { hi->SetFastForwardEnabled(enabled); }); -} - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_importBIOSImage, jobject obj, jbyteArray data) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - - const jsize len = env->GetArrayLength(data); - if (len != BIOS::BIOS_SIZE) - return nullptr; - - BIOS::Image image; - image.resize(static_cast(len)); - env->GetByteArrayRegion(data, 0, len, reinterpret_cast(image.data())); - - const BIOS::Hash hash = BIOS::GetHash(image); - const BIOS::ImageInfo* ii = BIOS::GetImageInfoForHash(hash); - - const std::string dest_path(hi->GetUserDirectoryRelativePath("bios/%s.bin", hash.ToString().c_str())); - if (FileSystem::FileExists(dest_path.c_str()) || - !FileSystem::WriteBinaryFile(dest_path.c_str(), image.data(), image.size())) - { - return nullptr; - } - - if (ii) - return env->NewStringUTF(ii->description); - else - return env->NewStringUTF(hash.ToString().c_str()); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasMediaSubImages, jobject obj) -{ - if (!System::IsValid()) - return false; - - return System::HasMediaSubImages(); -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getMediaSubImageTitles, jobject obj) -{ - if (!System::IsValid()) - return nullptr; - - const u32 count = System::GetMediaSubImageCount(); - if (count == 0) - return nullptr; - - jobjectArray arr = env->NewObjectArray(static_cast(count), s_String_class, nullptr); - for (u32 i = 0; i < count; i++) - { - jstring str = env->NewStringUTF(System::GetMediaSubImageTitle(i).c_str()); - env->SetObjectArrayElement(arr, static_cast(i), str); - env->DeleteLocalRef(str); - } - - return arr; -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getMediaSubImageIndex, jobject obj) -{ - if (!System::IsValid()) - return -1; - - return System::GetMediaSubImageIndex(); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_switchMediaSubImage, jobject obj, jint index) -{ - if (!System::IsValid() || index < 0 || static_cast(index) >= System::GetMediaSubImageCount()) - return false; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([index, hi]() { - if (System::IsValid()) - { - if (!System::SwitchMediaSubImage(static_cast(index))) - hi->AddOSDMessage("Disc switch failed. Please make sure the file exists."); - } - }); - - return true; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_setMediaFilename, jstring obj, jstring filename) -{ - if (!System::IsValid() || !filename) - return false; - - std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([filename_str, hi]() { - if (System::IsValid()) - { - if (!System::InsertMedia(filename_str.c_str())) - hi->AddOSDMessage("Disc switch failed. Please make sure the file exists and is a supported disc image."); - } - }); - - return true; -} - -static jobject CreateSaveStateInfo(JNIEnv* env, const CommonHostInterface::ExtendedSaveStateInfo& ssi) -{ - LocalRefHolder path(env, env->NewStringUTF(ssi.path.c_str())); - LocalRefHolder title(env, env->NewStringUTF(ssi.title.c_str())); - LocalRefHolder code(env, env->NewStringUTF(ssi.game_code.c_str())); - LocalRefHolder media_path(env, env->NewStringUTF(ssi.media_path.c_str())); - LocalRefHolder timestamp(env, env->NewStringUTF(Timestamp::FromUnixTimestamp(ssi.timestamp).ToString("%c"))); - LocalRefHolder screenshot_data; - if (!ssi.screenshot_data.empty()) - { - const jsize data_size = static_cast(ssi.screenshot_data.size() * sizeof(u32)); - screenshot_data = LocalRefHolder(env, env->NewByteArray(data_size)); - env->SetByteArrayRegion(screenshot_data.Get(), 0, data_size, - reinterpret_cast(ssi.screenshot_data.data())); - } - - return env->NewObject(s_SaveStateInfo_class, s_SaveStateInfo_constructor, path.Get(), title.Get(), code.Get(), - media_path.Get(), timestamp.Get(), static_cast(ssi.slot), - static_cast(ssi.global), static_cast(ssi.screenshot_width), - static_cast(ssi.screenshot_height), screenshot_data.Get()); -} - -static jobject CreateEmptySaveStateInfo(JNIEnv* env, s32 slot, bool global) -{ - return env->NewObject(s_SaveStateInfo_class, s_SaveStateInfo_constructor, nullptr, nullptr, nullptr, nullptr, nullptr, - static_cast(slot), static_cast(global), static_cast(0), - static_cast(0), nullptr); -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getSaveStateInfo, jobject obj, jboolean includeEmpty) -{ - if (!System::IsValid()) - return nullptr; - - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - std::vector infos; - - // +1 for the quick save only in android. - infos.reserve(1 + CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS + CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS); - - const std::string& game_code = System::GetRunningCode(); - if (!game_code.empty()) - { - for (u32 i = 0; i <= CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS; i++) - { - std::optional esi = - hi->GetExtendedSaveStateInfo(game_code.c_str(), static_cast(i)); - if (esi.has_value()) - { - jobject obj = CreateSaveStateInfo(env, esi.value()); - if (obj) - infos.push_back(obj); - } - else if (includeEmpty) - { - jobject obj = CreateEmptySaveStateInfo(env, static_cast(i), false); - if (obj) - infos.push_back(obj); - } - } - } - - for (u32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++) - { - std::optional esi = - hi->GetExtendedSaveStateInfo(nullptr, static_cast(i)); - if (esi.has_value()) - { - jobject obj = CreateSaveStateInfo(env, esi.value()); - if (obj) - infos.push_back(obj); - } - else if (includeEmpty) - { - jobject obj = CreateEmptySaveStateInfo(env, static_cast(i), true); - if (obj) - infos.push_back(obj); - } - } - - if (infos.empty()) - return nullptr; - - jobjectArray ret = env->NewObjectArray(static_cast(infos.size()), s_SaveStateInfo_class, nullptr); - for (size_t i = 0; i < infos.size(); i++) - { - env->SetObjectArrayElement(ret, static_cast(i), infos[i]); - env->DeleteLocalRef(infos[i]); - } - - return ret; -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_toggleControllerAnalogMode, jobject obj) -{ - // hacky way to toggle analog mode - for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) - { - Controller* ctrl = System::GetController(i); - if (!ctrl) - continue; - - std::optional code = Controller::GetButtonCodeByName(ctrl->GetType(), "Analog"); - if (!code.has_value()) - continue; - - ctrl->SetButtonState(code.value(), true); - ctrl->SetButtonState(code.value(), false); - } -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setFullscreenUINotificationVerticalPosition, jobject obj, - jfloat position, jfloat direction) -{ - AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread( - [position, direction]() { ImGuiFullscreen::SetNotificationVerticalPosition(position, direction); }); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isCheevosActive, jobject obj) -{ - return Cheevos::IsActive(); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isCheevosChallengeModeActive, jobject obj) -{ - return Cheevos::IsChallengeModeActive(); -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getCheevoList, jobject obj) -{ - if (!Cheevos::IsActive()) - return nullptr; - - std::vector cheevos; - Cheevos::EnumerateAchievements([env, &cheevos](const Cheevos::Achievement& cheevo) { - jstring title = env->NewStringUTF(cheevo.title.c_str()); - jstring description = env->NewStringUTF(cheevo.description.c_str()); - jstring locked_badge_path = - cheevo.locked_badge_path.empty() ? nullptr : env->NewStringUTF(cheevo.locked_badge_path.c_str()); - jstring unlocked_badge_path = - cheevo.unlocked_badge_path.empty() ? nullptr : env->NewStringUTF(cheevo.unlocked_badge_path.c_str()); - - jobject object = env->NewObject(s_Achievement_class, s_Achievement_constructor, static_cast(cheevo.id), title, - description, locked_badge_path, unlocked_badge_path, - static_cast(cheevo.points), static_cast(cheevo.locked)); - cheevos.push_back(object); - - if (unlocked_badge_path) - env->DeleteLocalRef(unlocked_badge_path); - if (locked_badge_path) - env->DeleteLocalRef(locked_badge_path); - env->DeleteLocalRef(description); - env->DeleteLocalRef(title); - return true; - }); - - if (cheevos.empty()) - return nullptr; - - jobjectArray ret = env->NewObjectArray(static_cast(cheevos.size()), s_Achievement_class, nullptr); - for (size_t i = 0; i < cheevos.size(); i++) - { - env->SetObjectArrayElement(ret, static_cast(i), cheevos[i]); - env->DeleteLocalRef(cheevos[i]); - } - - return ret; -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoCount, jobject obj) -{ - return Cheevos::GetAchievementCount(); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getUnlockedCheevoCount, jobject obj) -{ - return Cheevos::GetUnlockedAchiementCount(); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoPointsForGame, jobject obj) -{ - return Cheevos::GetCurrentPointsForGame(); -} - -DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoMaximumPointsForGame, jobject obj) -{ - return Cheevos::GetMaximumPointsForGame(); -} - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getCheevoGameTitle, jobject obj) -{ - const std::string& title = Cheevos::GetGameTitle(); - return title.empty() ? nullptr : env->NewStringUTF(title.c_str()); -} - -DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getCheevoGameIconPath, jobject obj) -{ - const std::string& path = Cheevos::GetGameIcon(); - return path.empty() ? nullptr : env->NewStringUTF(path.c_str()); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_cheevosLogin, jobject obj, jstring username, jstring password) -{ - const std::string username_str(AndroidHelpers::JStringToString(env, username)); - const std::string password_str(AndroidHelpers::JStringToString(env, password)); - return Cheevos::Login(username_str.c_str(), password_str.c_str()); -} - -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_cheevosLogout, jobject obj) -{ - return Cheevos::Logout(); -} - -static_assert(sizeof(MemoryCardImage::DataArray) == MemoryCardImage::DATA_SIZE); - -static MemoryCardImage::DataArray* GetMemoryCardData(JNIEnv* env, jbyteArray obj) -{ - if (!obj || env->GetArrayLength(obj) != MemoryCardImage::DATA_SIZE) - return nullptr; - - return reinterpret_cast(env->GetByteArrayElements(obj, nullptr)); -} - -static void ReleaseMemoryCardData(JNIEnv* env, jbyteArray obj, MemoryCardImage::DataArray* data) -{ - env->ReleaseByteArrayElements(obj, reinterpret_cast(data), 0); -} - -DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_isValid, jclass clazz, jbyteArray obj) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return false; - - const bool res = MemoryCardImage::IsValid(*data); - ReleaseMemoryCardData(env, obj, data); - return res; -} - -DEFINE_JNI_ARGS_METHOD(void, MemoryCardImage_format, jclass clazz, jbyteArray obj) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return; - - MemoryCardImage::Format(data); - ReleaseMemoryCardData(env, obj, data); -} - -DEFINE_JNI_ARGS_METHOD(jint, MemoryCardImage_getFreeBlocks, jclass clazz, jbyteArray obj) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return 0; - - const u32 free_blocks = MemoryCardImage::GetFreeBlockCount(*data); - ReleaseMemoryCardData(env, obj, data); - return free_blocks; -} - -DEFINE_JNI_ARGS_METHOD(jobjectArray, MemoryCardImage_getFiles, jclass clazz, jbyteArray obj) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return nullptr; - - const std::vector files(MemoryCardImage::EnumerateFiles(*data)); - - std::vector file_objects; - file_objects.reserve(files.size()); - - jclass byteArrayClass = env->FindClass("[B"); - - for (const MemoryCardImage::FileInfo& file : files) - { - jobject filename = env->NewStringUTF(file.filename.c_str()); - jobject title = env->NewStringUTF(file.title.c_str()); - jobjectArray frames = nullptr; - if (!file.icon_frames.empty()) - { - frames = env->NewObjectArray(static_cast(file.icon_frames.size()), byteArrayClass, nullptr); - for (jsize i = 0; i < static_cast(file.icon_frames.size()); i++) - { - static constexpr jsize frame_size = MemoryCardImage::ICON_WIDTH * MemoryCardImage::ICON_HEIGHT * sizeof(u32); - jbyteArray frame_data = env->NewByteArray(frame_size); - jbyte* frame_data_ptr = env->GetByteArrayElements(frame_data, nullptr); - std::memcpy(frame_data_ptr, file.icon_frames[i].pixels, frame_size); - env->ReleaseByteArrayElements(frame_data, frame_data_ptr, 0); - env->SetObjectArrayElement(frames, i, frame_data); - env->DeleteLocalRef(frame_data); - } - } - - file_objects.push_back(env->NewObject(s_MemoryCardFileInfo_class, s_MemoryCardFileInfo_constructor, filename, title, - static_cast(file.size), static_cast(file.first_block), - static_cast(file.num_blocks), frames)); - - env->DeleteLocalRef(frames); - env->DeleteLocalRef(title); - env->DeleteLocalRef(filename); - } - - jobjectArray file_object_array = - AndroidHelpers::CreateObjectArray(env, s_MemoryCardFileInfo_class, file_objects.data(), file_objects.size(), true); - ReleaseMemoryCardData(env, obj, data); - return file_object_array; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_hasFile, jclass clazz, jbyteArray obj, jstring filename) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return false; - - const std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - bool result = false; - if (!filename_str.empty()) - { - const std::vector files(MemoryCardImage::EnumerateFiles(*data)); - result = std::any_of(files.begin(), files.end(), - [&filename_str](const MemoryCardImage::FileInfo& fi) { return fi.filename == filename_str; }); - } - - ReleaseMemoryCardData(env, obj, data); - return result; -} - -DEFINE_JNI_ARGS_METHOD(jbyteArray, MemoryCardImage_readFile, jclass clazz, jbyteArray obj, jstring filename) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return nullptr; - - const std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - jbyteArray ret = nullptr; - if (!filename_str.empty()) - { - const std::vector files(MemoryCardImage::EnumerateFiles(*data)); - auto iter = std::find_if(files.begin(), files.end(), [&filename_str](const MemoryCardImage::FileInfo& fi) { - return fi.filename == filename_str; - }); - if (iter != files.end()) - { - std::vector file_data; - if (MemoryCardImage::ReadFile(*data, *iter, &file_data)) - ret = AndroidHelpers::VectorToByteArray(env, file_data); - } - } - - ReleaseMemoryCardData(env, obj, data); - return ret; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_writeFile, jclass clazz, jbyteArray obj, jstring filename, - jbyteArray file_data) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return false; - - const std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - const std::vector file_data_vec(AndroidHelpers::ByteArrayToVector(env, file_data)); - bool ret = false; - if (!filename_str.empty()) - ret = MemoryCardImage::WriteFile(data, filename_str, file_data_vec); - - ReleaseMemoryCardData(env, obj, data); - return ret; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_deleteFile, jclass clazz, jbyteArray obj, jstring filename) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return false; - - const std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - bool ret = false; - - if (!filename_str.empty()) - { - const std::vector files(MemoryCardImage::EnumerateFiles(*data)); - auto iter = std::find_if(files.begin(), files.end(), [&filename_str](const MemoryCardImage::FileInfo& fi) { - return fi.filename == filename_str; - }); - - if (iter != files.end()) - ret = MemoryCardImage::DeleteFile(data, *iter); - } - - ReleaseMemoryCardData(env, obj, data); - return ret; -} - -DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_importCard, jclass clazz, jbyteArray obj, jstring filename, - jbyteArray import_data) -{ - MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj); - if (!data) - return false; - - const std::string filename_str(AndroidHelpers::JStringToString(env, filename)); - std::vector import_data_vec(AndroidHelpers::ByteArrayToVector(env, import_data)); - bool ret = false; - if (!filename_str.empty() && !import_data_vec.empty()) - ret = MemoryCardImage::ImportCard(data, filename_str.c_str(), std::move(import_data_vec)); - - ReleaseMemoryCardData(env, obj, data); - return ret; -} \ No newline at end of file diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h deleted file mode 100644 index 99f91b5f2..000000000 --- a/android/app/src/cpp/android_host_interface.h +++ /dev/null @@ -1,171 +0,0 @@ -#pragma once -#include "android_settings_interface.h" -#include "common/byte_stream.h" -#include "common/event.h" -#include "common/progress_callback.h" -#include "core/host_display.h" -#include "frontend-common/common_host_interface.h" -#include -#include -#include -#include -#include -#include -#include -#include - -struct ANativeWindow; - -class Controller; - -class AndroidHostInterface final : public CommonHostInterface -{ -public: - using CommonHostInterface::UpdateInputMap; - - AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory); - ~AndroidHostInterface() override; - - ALWAYS_INLINE ANativeWindow* GetSurface() const { return m_surface; } - - bool Initialize() override; - void Shutdown() override; - - const char* GetFrontendName() const override; - void RequestExit() override; - void RunLater(std::function func) override; - - void ReportError(const char* message) override; - void ReportMessage(const char* message) override; - - std::unique_ptr OpenPackageFile(const char* path, u32 flags) override; - - bool IsEmulationThreadRunning() const { return m_emulation_thread_running.load(); } - bool IsEmulationThreadPaused() const; - bool IsOnEmulationThread() const; - void RunOnEmulationThread(std::function function, bool blocking = false); - void PauseEmulationThread(bool paused); - void StopEmulationThreadLoop(); - - void EmulationThreadEntryPoint(JNIEnv* env, jobject emulation_activity, SystemBootParameters boot_params, - bool resume_state); - - void SurfaceChanged(ANativeWindow* surface, int format, int width, int height); - void SetDisplayAlignment(HostDisplay::Alignment alignment); - - void SetControllerButtonState(u32 index, s32 button_code, bool pressed); - void SetControllerAxisState(u32 index, s32 button_code, float value); - void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed); - void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value); - bool HasControllerButtonBinding(u32 controller_index, u32 button); - void SetControllerVibration(u32 controller_index, float small_motor, float large_motor); - void SetFastForwardEnabled(bool enabled); - - void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback); - - bool ImportPatchCodesFromString(const std::string& str); - - jobjectArray GetInputProfileNames(JNIEnv* env) const; - -protected: - void SetUserDirectory() override; - void RegisterHotkeys() override; - - bool AcquireHostDisplay() override; - void ReleaseHostDisplay() override; - std::unique_ptr CreateAudioStream(AudioBackend backend) override; - void UpdateControllerInterface() override; - - void OnSystemPaused(bool paused) override; - void OnSystemDestroyed() override; - void OnRunningGameChanged(const std::string& path, CDImage* image, const std::string& game_code, - const std::string& game_title) override; - -private: - void EmulationThreadLoop(JNIEnv* env); - - void LoadSettings(SettingsInterface& si) override; - void UpdateInputMap(SettingsInterface& si) override; - void SetVibration(bool enabled); - void UpdateVibration(); - float GetRefreshRate() const; - float GetSurfaceScale(int width, int height) const; - - jobject m_java_object = {}; - jobject m_emulation_activity_object = {}; - - ANativeWindow* m_surface = nullptr; - - std::mutex m_mutex; - std::condition_variable m_sleep_cv; - std::deque> m_callback_queue; - std::atomic_bool m_callbacks_outstanding{false}; - - std::atomic_bool m_emulation_thread_stop_request{false}; - std::atomic_bool m_emulation_thread_running{false}; - std::thread::id m_emulation_thread_id{}; - - HostDisplay::Alignment m_display_alignment = HostDisplay::Alignment::Center; - - u64 m_last_vibration_update_time = 0; - bool m_last_vibration_state = false; - bool m_vibration_enabled = false; -}; - -namespace AndroidHelpers { - -JavaVM* GetJavaVM(); -JNIEnv* GetJNIEnv(); -AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj); -std::string JStringToString(JNIEnv* env, jstring str); -std::unique_ptr ReadInputStreamToMemory(JNIEnv* env, jobject obj, u32 chunk_size = 65536); -jclass GetStringClass(); -std::vector ByteArrayToVector(JNIEnv* env, jbyteArray obj); -jbyteArray NewByteArray(JNIEnv* env, const void* data, size_t size); -jbyteArray VectorToByteArray(JNIEnv* env, const std::vector& data); -jobjectArray CreateObjectArray(JNIEnv* env, jclass object_class, const jobject* objects, size_t num_objects, - bool release_refs = false); -} // namespace AndroidHelpers - -template -class LocalRefHolder -{ -public: - LocalRefHolder() : m_env(nullptr), m_object(nullptr) {} - - LocalRefHolder(JNIEnv* env, T object) : m_env(env), m_object(object) {} - - LocalRefHolder(const LocalRefHolder&) = delete; - LocalRefHolder(LocalRefHolder&& move) : m_env(move.m_env), m_object(move.m_object) - { - move.m_env = nullptr; - move.m_object = {}; - } - - ~LocalRefHolder() - { - if (m_object) - m_env->DeleteLocalRef(m_object); - } - - operator T() const { return m_object; } - T operator*() const { return m_object; } - - LocalRefHolder& operator=(const LocalRefHolder&) = delete; - LocalRefHolder& operator=(LocalRefHolder&& move) - { - if (m_object) - m_env->DeleteLocalRef(m_object); - m_env = move.m_env; - m_object = move.m_object; - move.m_env = nullptr; - move.m_object = {}; - return *this; - } - - T Get() const { return m_object; } - -private: - JNIEnv* m_env; - T m_object; -}; diff --git a/android/app/src/cpp/android_http_downloader.cpp b/android/app/src/cpp/android_http_downloader.cpp deleted file mode 100644 index d1f462641..000000000 --- a/android/app/src/cpp/android_http_downloader.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "android_http_downloader.h" -#include "android_host_interface.h" -#include "common/assert.h" -#include "common/log.h" -#include "common/string_util.h" -#include "common/timer.h" -#include -#include -Log_SetChannel(AndroidHTTPDownloader); - -namespace FrontendCommon { - -AndroidHTTPDownloader::AndroidHTTPDownloader() : HTTPDownloader() {} - -AndroidHTTPDownloader::~AndroidHTTPDownloader() -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - if (m_URLDownloader_class) - env->DeleteGlobalRef(m_URLDownloader_class); -} - -std::unique_ptr HTTPDownloader::Create(const char* user_agent) -{ - std::unique_ptr instance(std::make_unique()); - if (!instance->Initialize(user_agent)) - return {}; - - return instance; -} - -bool AndroidHTTPDownloader::Initialize(const char* user_agent) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jclass klass = env->FindClass("com/github/stenzek/duckstation/URLDownloader"); - if (!klass) - return false; - - m_URLDownloader_class = static_cast(env->NewGlobalRef(klass)); - if (!m_URLDownloader_class) - return false; - - m_URLDownloader_constructor = env->GetMethodID(klass, "", "(Ljava/lang/String;)V"); - m_URLDownloader_get = env->GetMethodID(klass, "get", "(Ljava/lang/String;)Z"); - m_URLDownloader_post = env->GetMethodID(klass, "post", "(Ljava/lang/String;[B)Z"); - m_URLDownloader_getStatusCode = env->GetMethodID(klass, "getStatusCode", "()I"); - m_URLDownloader_getData = env->GetMethodID(klass, "getData", "()[B"); - if (!m_URLDownloader_constructor || !m_URLDownloader_get || !m_URLDownloader_post || !m_URLDownloader_getStatusCode || - !m_URLDownloader_getData) - { - return false; - } - - m_user_agent = user_agent; - m_thread_pool = std::make_unique(m_max_active_requests); - return true; -} - -void AndroidHTTPDownloader::ProcessRequest(Request* req) -{ - std::unique_lock cancel_lock(m_cancel_mutex); - if (req->closed.load()) - return; - - cancel_lock.unlock(); - req->status_code = -1; - req->start_time = Common::Timer::GetValue(); - - // TODO: Move to Java side... - JNIEnv* env; - if (AndroidHelpers::GetJavaVM()->AttachCurrentThread(&env, nullptr) == JNI_OK) - { - jstring url_string = env->NewStringUTF(req->url.c_str()); - jstring user_agent_string = env->NewStringUTF(m_user_agent.c_str()); - - jobject obj = env->NewObject(m_URLDownloader_class, m_URLDownloader_constructor, user_agent_string); - jboolean result; - if (req->post_data.empty()) - { - result = env->CallBooleanMethod(obj, m_URLDownloader_get, url_string); - } - else - { - jbyteArray post_data = env->NewByteArray(static_cast(req->post_data.size())); - env->SetByteArrayRegion(post_data, 0, static_cast(req->post_data.size()), - reinterpret_cast(req->post_data.data())); - result = env->CallBooleanMethod(obj, m_URLDownloader_post, url_string, post_data); - env->DeleteLocalRef(post_data); - } - - env->DeleteLocalRef(url_string); - env->DeleteLocalRef(user_agent_string); - - if (result) - { - req->status_code = env->CallIntMethod(obj, m_URLDownloader_getStatusCode); - - jbyteArray data = reinterpret_cast(env->CallObjectMethod(obj, m_URLDownloader_getData)); - if (data) - { - const u32 size = static_cast(env->GetArrayLength(data)); - req->data.resize(size); - if (size > 0) - { - jbyte* data_ptr = env->GetByteArrayElements(data, nullptr); - std::memcpy(req->data.data(), data_ptr, size); - env->ReleaseByteArrayElements(data, data_ptr, 0); - } - - env->DeleteLocalRef(data); - } - - Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes", req->url.c_str(), req->status_code, - req->data.size()); - } - else - { - Log_ErrorPrintf("Request for '%s' failed", req->url.c_str()); - } - - env->DeleteLocalRef(obj); - AndroidHelpers::GetJavaVM()->DetachCurrentThread(); - } - else - { - Log_ErrorPrintf("AttachCurrentThread() failed"); - } - - cancel_lock.lock(); - req->state = Request::State::Complete; - if (req->closed.load()) - delete req; - else - req->closed.store(true); -} - -HTTPDownloader::Request* AndroidHTTPDownloader::InternalCreateRequest() -{ - Request* req = new Request(); - return req; -} - -void AndroidHTTPDownloader::InternalPollRequests() -{ - // noop - uses thread pool -} - -bool AndroidHTTPDownloader::StartRequest(HTTPDownloader::Request* request) -{ - Request* req = static_cast(request); - Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str()); - req->state = Request::State::Started; - req->start_time = Common::Timer::GetValue(); - m_thread_pool->Schedule(std::bind(&AndroidHTTPDownloader::ProcessRequest, this, req)); - return true; -} - -void AndroidHTTPDownloader::CloseRequest(HTTPDownloader::Request* request) -{ - std::unique_lock cancel_lock(m_cancel_mutex); - Request* req = static_cast(request); - if (req->closed.load()) - delete req; - else - req->closed.store(true); -} - -} // namespace FrontendCommon diff --git a/android/app/src/cpp/android_http_downloader.h b/android/app/src/cpp/android_http_downloader.h deleted file mode 100644 index b5774846b..000000000 --- a/android/app/src/cpp/android_http_downloader.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include "common/thirdparty/thread_pool.h" -#include "frontend-common/http_downloader.h" -#include -#include -#include -#include - -namespace FrontendCommon { - -class AndroidHTTPDownloader final : public HTTPDownloader -{ -public: - AndroidHTTPDownloader(); - ~AndroidHTTPDownloader() override; - - bool Initialize(const char* user_agent); - -protected: - Request* InternalCreateRequest() override; - void InternalPollRequests() override; - bool StartRequest(HTTPDownloader::Request* request) override; - void CloseRequest(HTTPDownloader::Request* request) override; - -private: - struct Request : HTTPDownloader::Request - { - std::atomic_bool closed{false}; - }; - - void ProcessRequest(Request* req); - - std::string m_user_agent; - std::unique_ptr m_thread_pool; - std::mutex m_cancel_mutex; - - jclass m_URLDownloader_class = nullptr; - jmethodID m_URLDownloader_constructor = nullptr; - jmethodID m_URLDownloader_get = nullptr; - jmethodID m_URLDownloader_post = nullptr; - jmethodID m_URLDownloader_getStatusCode = nullptr; - jmethodID m_URLDownloader_getData = nullptr; -}; - -} // namespace FrontendCommon diff --git a/android/app/src/cpp/android_progress_callback.cpp b/android/app/src/cpp/android_progress_callback.cpp deleted file mode 100644 index a25d02624..000000000 --- a/android/app/src/cpp/android_progress_callback.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "android_progress_callback.h" -#include "android_host_interface.h" -#include "common/assert.h" -#include "common/log.h" -Log_SetChannel(AndroidProgressCallback); - -AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object) : m_java_object(java_object) -{ - jclass cls = env->GetObjectClass(java_object); - m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V"); - m_set_status_text_method = env->GetMethodID(cls, "setStatusText", "(Ljava/lang/String;)V"); - m_set_progress_range_method = env->GetMethodID(cls, "setProgressRange", "(I)V"); - m_set_progress_value_method = env->GetMethodID(cls, "setProgressValue", "(I)V"); - m_modal_error_method = env->GetMethodID(cls, "modalError", "(Ljava/lang/String;)V"); - m_modal_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V"); - m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z"); - Assert(m_set_status_text_method && m_set_progress_range_method && m_set_progress_value_method && - m_modal_error_method && m_modal_information_method && m_modal_confirmation_method); -} - -AndroidProgressCallback::~AndroidProgressCallback() = default; - -bool AndroidProgressCallback::IsCancelled() const -{ - return false; -} - -void AndroidProgressCallback::SetCancellable(bool cancellable) -{ - if (m_cancellable == cancellable) - return; - - BaseProgressCallback::SetCancellable(cancellable); -} - -void AndroidProgressCallback::SetTitle(const char* title) -{ - Assert(title); - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder text_jstr(env, env->NewStringUTF(title)); - env->CallVoidMethod(m_java_object, m_set_title_method, text_jstr.Get()); -} - -void AndroidProgressCallback::SetStatusText(const char* text) -{ - Assert(text); - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder text_jstr(env, env->NewStringUTF(text)); - env->CallVoidMethod(m_java_object, m_set_status_text_method, text_jstr.Get()); -} - -void AndroidProgressCallback::SetProgressRange(u32 range) -{ - BaseProgressCallback::SetProgressRange(range); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - env->CallVoidMethod(m_java_object, m_set_progress_range_method, static_cast(range)); -} - -void AndroidProgressCallback::SetProgressValue(u32 value) -{ - const u32 old_value = m_progress_value; - BaseProgressCallback::SetProgressValue(value); - if (old_value == m_progress_value) - return; - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - env->CallVoidMethod(m_java_object, m_set_progress_value_method, static_cast(value)); -} - -void AndroidProgressCallback::DisplayError(const char* message) -{ - Log_ErrorPrintf("%s", message); -} - -void AndroidProgressCallback::DisplayWarning(const char* message) -{ - Log_WarningPrintf("%s", message); -} - -void AndroidProgressCallback::DisplayInformation(const char* message) -{ - Log_InfoPrintf("%s", message); -} - -void AndroidProgressCallback::DisplayDebugMessage(const char* message) -{ - Log_DevPrintf("%s", message); -} - -void AndroidProgressCallback::ModalError(const char* message) -{ - Assert(message); - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder message_jstr(env, env->NewStringUTF(message)); - env->CallVoidMethod(m_java_object, m_modal_error_method, message_jstr.Get()); -} - -bool AndroidProgressCallback::ModalConfirmation(const char* message) -{ - Assert(message); - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder message_jstr(env, env->NewStringUTF(message)); - return env->CallBooleanMethod(m_java_object, m_modal_confirmation_method, message_jstr.Get()); -} - -void AndroidProgressCallback::ModalInformation(const char* message) -{ - Assert(message); - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder message_jstr(env, env->NewStringUTF(message)); - env->CallVoidMethod(m_java_object, m_modal_information_method, message_jstr.Get()); -} diff --git a/android/app/src/cpp/android_progress_callback.h b/android/app/src/cpp/android_progress_callback.h deleted file mode 100644 index 8b15bca18..000000000 --- a/android/app/src/cpp/android_progress_callback.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include "common/progress_callback.h" -#include - -class AndroidProgressCallback final : public BaseProgressCallback -{ -public: - AndroidProgressCallback(JNIEnv* env, jobject java_object); - ~AndroidProgressCallback(); - - bool IsCancelled() const override; - - void SetCancellable(bool cancellable) override; - void SetTitle(const char* title) override; - void SetStatusText(const char* text) override; - void SetProgressRange(u32 range) override; - void SetProgressValue(u32 value) override; - - void DisplayError(const char* message) override; - void DisplayWarning(const char* message) override; - void DisplayInformation(const char* message) override; - void DisplayDebugMessage(const char* message) override; - - void ModalError(const char* message) override; - bool ModalConfirmation(const char* message) override; - void ModalInformation(const char* message) override; - -private: - jobject m_java_object; - - jmethodID m_set_title_method; - jmethodID m_set_status_text_method; - jmethodID m_set_progress_range_method; - jmethodID m_set_progress_value_method; - jmethodID m_modal_error_method; - jmethodID m_modal_confirmation_method; - jmethodID m_modal_information_method; -}; diff --git a/android/app/src/cpp/android_settings_interface.cpp b/android/app/src/cpp/android_settings_interface.cpp deleted file mode 100644 index 52af477c8..000000000 --- a/android/app/src/cpp/android_settings_interface.cpp +++ /dev/null @@ -1,425 +0,0 @@ -#include "android_settings_interface.h" -#include "android_host_interface.h" -#include "common/assert.h" -#include "common/log.h" -#include "common/string.h" -#include "common/string_util.h" -#include -Log_SetChannel(AndroidSettingsInterface); - -ALWAYS_INLINE TinyString GetSettingKey(const char* section, const char* key) -{ - return TinyString::FromFormat("%s/%s", section, key); -} - -AndroidSettingsInterface::AndroidSettingsInterface(jobject java_context) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jclass c_preference_manager = env->FindClass("androidx/preference/PreferenceManager"); - jclass c_preference_editor = env->FindClass("android/content/SharedPreferences$Editor"); - jclass c_set = env->FindClass("java/util/Set"); - jclass c_helper = env->FindClass("com/github/stenzek/duckstation/PreferenceHelpers"); - jmethodID m_get_default_shared_preferences = - env->GetStaticMethodID(c_preference_manager, "getDefaultSharedPreferences", - "(Landroid/content/Context;)Landroid/content/SharedPreferences;"); - Assert(c_preference_manager && c_preference_editor && c_set && c_helper && m_get_default_shared_preferences); - m_set_class = reinterpret_cast(env->NewGlobalRef(c_set)); - m_shared_preferences_editor_class = reinterpret_cast(env->NewGlobalRef(c_preference_editor)); - m_helper_class = reinterpret_cast(env->NewGlobalRef(c_helper)); - Assert(m_set_class && m_shared_preferences_editor_class && m_helper_class); - - env->DeleteLocalRef(c_set); - env->DeleteLocalRef(c_preference_editor); - env->DeleteLocalRef(c_helper); - - jobject shared_preferences = - env->CallStaticObjectMethod(c_preference_manager, m_get_default_shared_preferences, java_context); - Assert(shared_preferences); - m_java_shared_preferences = env->NewGlobalRef(shared_preferences); - Assert(m_java_shared_preferences); - env->DeleteLocalRef(c_preference_manager); - env->DeleteLocalRef(shared_preferences); - - jclass c_shared_preferences = env->GetObjectClass(m_java_shared_preferences); - m_shared_preferences_class = reinterpret_cast(env->NewGlobalRef(c_shared_preferences)); - Assert(m_shared_preferences_class); - env->DeleteLocalRef(c_shared_preferences); - - m_get_boolean = env->GetMethodID(m_shared_preferences_class, "getBoolean", "(Ljava/lang/String;Z)Z"); - m_get_int = env->GetMethodID(m_shared_preferences_class, "getInt", "(Ljava/lang/String;I)I"); - m_get_float = env->GetMethodID(m_shared_preferences_class, "getFloat", "(Ljava/lang/String;F)F"); - m_get_string = env->GetMethodID(m_shared_preferences_class, "getString", - "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); - m_get_string_set = - env->GetMethodID(m_shared_preferences_class, "getStringSet", "(Ljava/lang/String;Ljava/util/Set;)Ljava/util/Set;"); - m_set_to_array = env->GetMethodID(m_set_class, "toArray", "()[Ljava/lang/Object;"); - Assert(m_get_boolean && m_get_int && m_get_float && m_get_string && m_get_string_set && m_set_to_array); - - m_edit = env->GetMethodID(m_shared_preferences_class, "edit", "()Landroid/content/SharedPreferences$Editor;"); - m_edit_set_string = - env->GetMethodID(m_shared_preferences_editor_class, "putString", - "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;"); - m_edit_commit = env->GetMethodID(m_shared_preferences_editor_class, "commit", "()Z"); - m_edit_remove = env->GetMethodID(m_shared_preferences_editor_class, "remove", - "(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;"); - Assert(m_edit && m_edit_set_string && m_edit_commit && m_edit_remove); - - m_helper_clear_section = - env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V"); - m_helper_add_to_string_list = env->GetStaticMethodID( - m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z"); - m_helper_remove_from_string_list = - env->GetStaticMethodID(m_helper_class, "removeFromStringList", - "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z"); - m_helper_set_string_list = env->GetStaticMethodID( - m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V"); - Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list && - m_helper_set_string_list); -} - -AndroidSettingsInterface::~AndroidSettingsInterface() -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - if (m_java_shared_preferences) - env->DeleteGlobalRef(m_java_shared_preferences); - if (m_shared_preferences_editor_class) - env->DeleteGlobalRef(m_shared_preferences_editor_class); - if (m_shared_preferences_class) - env->DeleteGlobalRef(m_shared_preferences_class); - if (m_set_class) - env->DeleteGlobalRef(m_set_class); - if (m_helper_class) - env->DeleteGlobalRef(m_helper_class); -} - -bool AndroidSettingsInterface::Save() -{ - return true; -} - -void AndroidSettingsInterface::Clear() -{ - Log_ErrorPrint("Not implemented"); -} - -int AndroidSettingsInterface::GetIntValue(const char* section, const char* key, int default_value /*= 0*/) -{ - // Some of these settings are string lists... - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%d", default_value))); - LocalRefHolder string_object( - env, reinterpret_cast(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), - default_value_string.Get()))); - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - - // it might actually be an int (e.g. seek bar preference) - const int int_value = - static_cast(env->CallIntMethod(m_java_shared_preferences, m_get_int, key_string.Get(), default_value)); - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - Log_DevPrintf("GetIntValue(%s, %s) -> %d (exception)", section, key, default_value); - return default_value; - } - - Log_DevPrintf("GetIntValue(%s, %s) -> %d (int)", section, key, int_value); - return int_value; - } - - if (!string_object) - return default_value; - - const char* data = env->GetStringUTFChars(string_object, nullptr); - Assert(data != nullptr); - Log_DevPrintf("GetIntValue(%s, %s) -> %s", section, key, data); - - std::optional value = StringUtil::FromChars(data); - env->ReleaseStringUTFChars(string_object, data); - return value.value_or(default_value); -} - -float AndroidSettingsInterface::GetFloatValue(const char* section, const char* key, float default_value /*= 0.0f*/) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%f", default_value))); - LocalRefHolder string_object( - env, reinterpret_cast(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), - default_value_string.Get()))); - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - Log_DevPrintf("GetFloatValue(%s, %s) -> %f (exception)", section, key, default_value); - return default_value; - } - - if (!string_object) - { - Log_DevPrintf("GetFloatValue(%s, %s) -> %f (null)", section, key, default_value); - return default_value; - } - - const char* data = env->GetStringUTFChars(string_object, nullptr); - Assert(data != nullptr); - Log_DevPrintf("GetFloatValue(%s, %s) -> %s", section, key, data); - - std::optional value = StringUtil::FromChars(data); - env->ReleaseStringUTFChars(string_object, data); - return value.value_or(default_value); -} - -bool AndroidSettingsInterface::GetBoolValue(const char* section, const char* key, bool default_value /*= false*/) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - jboolean bool_value = static_cast( - env->CallBooleanMethod(m_java_shared_preferences, m_get_boolean, key_string.Get(), default_value)); - if (env->ExceptionCheck()) - { - Log_DevPrintf("GetBoolValue(%s, %s) -> %u (exception)", section, key, static_cast(default_value)); - env->ExceptionClear(); - return default_value; - } - - Log_DevPrintf("GetBoolValue(%s, %s) -> %u", section, key, static_cast(bool_value)); - return bool_value; -} - -std::string AndroidSettingsInterface::GetStringValue(const char* section, const char* key, - const char* default_value /*= ""*/) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder default_value_string(env, env->NewStringUTF(default_value)); - LocalRefHolder string_object( - env, reinterpret_cast(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), - default_value_string.Get()))); - - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - Log_DevPrintf("GetStringValue(%s, %s) -> %s (exception)", section, key, default_value); - return default_value; - } - - if (!string_object) - { - Log_DevPrintf("GetStringValue(%s, %s) -> %s (null)", section, key, default_value); - return default_value; - } - - const std::string ret(AndroidHelpers::JStringToString(env, string_object)); - Log_DevPrintf("GetStringValue(%s, %s) -> %s", section, key, ret.c_str()); - return ret; -} - -jobject AndroidSettingsInterface::GetPreferencesEditor(JNIEnv* env) -{ - return env->CallObjectMethod(m_java_shared_preferences, m_edit); -} - -void AndroidSettingsInterface::CheckForException(JNIEnv* env, const char* task) -{ - if (!env->ExceptionCheck()) - return; - - Log_ErrorPrintf("JNI exception during %s", task); - env->ExceptionClear(); -} - -void AndroidSettingsInterface::SetIntValue(const char* section, const char* key, int value) -{ - Log_DevPrintf("SetIntValue(\"%s\", \"%s\", %d)", section, key, value); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder editor(env, GetPreferencesEditor(env)); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder str_value(env, env->NewStringUTF(TinyString::FromFormat("%d", value))); - - LocalRefHolder dummy(env, - env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get())); - env->CallBooleanMethod(editor, m_edit_commit); - - CheckForException(env, "SetIntValue"); -} - -void AndroidSettingsInterface::SetFloatValue(const char* section, const char* key, float value) -{ - Log_DevPrintf("SetFloatValue(\"%s\", \"%s\", %f)", section, key, value); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder editor(env, GetPreferencesEditor(env)); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder str_value(env, env->NewStringUTF(TinyString::FromFormat("%f", value))); - - LocalRefHolder dummy(env, - env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get())); - env->CallBooleanMethod(editor, m_edit_commit); - - CheckForException(env, "SetFloatValue"); -} - -void AndroidSettingsInterface::SetBoolValue(const char* section, const char* key, bool value) -{ - Log_DevPrintf("SetBoolValue(\"%s\", \"%s\", %u)", section, key, static_cast(value)); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder editor(env, GetPreferencesEditor(env)); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder str_value(env, env->NewStringUTF(value ? "true" : "false")); - - LocalRefHolder dummy(env, - env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get())); - env->CallBooleanMethod(editor, m_edit_commit); - - CheckForException(env, "SetBoolValue"); -} - -void AndroidSettingsInterface::SetStringValue(const char* section, const char* key, const char* value) -{ - Log_DevPrintf("SetStringValue(\"%s\", \"%s\", \"%s\")", section, key, value); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder editor(env, GetPreferencesEditor(env)); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder str_value(env, env->NewStringUTF(value)); - - LocalRefHolder dummy(env, - env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get())); - env->CallBooleanMethod(editor, m_edit_commit); - - CheckForException(env, "SetStringValue"); -} - -void AndroidSettingsInterface::DeleteValue(const char* section, const char* key) -{ - Log_DevPrintf("DeleteValue(\"%s\", \"%s\")", section, key); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder editor(env, GetPreferencesEditor(env)); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder dummy(env, env->CallObjectMethod(editor, m_edit_remove, key_string.Get())); - env->CallBooleanMethod(editor, m_edit_commit); - - CheckForException(env, "DeleteValue"); -} - -void AndroidSettingsInterface::ClearSection(const char* section) -{ - Log_DevPrintf("ClearSection(\"%s\")", section); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder str_section(env, env->NewStringUTF(section)); - env->CallStaticVoidMethod(m_helper_class, m_helper_clear_section, m_java_shared_preferences, str_section.Get()); - - CheckForException(env, "ClearSection"); -} - -std::vector AndroidSettingsInterface::GetStringList(const char* section, const char* key) -{ - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder values_set( - env, env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, key_string.Get(), nullptr)); - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - - // this might just be a string, not a string set - LocalRefHolder string_object(env, reinterpret_cast(env->CallObjectMethod( - m_java_shared_preferences, m_get_string, key_string.Get(), nullptr))); - - if (!env->ExceptionCheck()) - { - std::vector ret; - if (string_object) - ret.push_back(AndroidHelpers::JStringToString(env, string_object)); - - return ret; - } - - env->ExceptionClear(); - return {}; - } - - if (!values_set) - return {}; - - LocalRefHolder values_array( - env, reinterpret_cast(env->CallObjectMethod(values_set, m_set_to_array))); - if (env->ExceptionCheck()) - { - env->ExceptionClear(); - return {}; - } - - if (!values_array) - return {}; - - jsize size = env->GetArrayLength(values_array); - std::vector values; - values.reserve(size); - for (jsize i = 0; i < size; i++) - { - jstring str = reinterpret_cast(env->GetObjectArrayElement(values_array, i)); - values.push_back(AndroidHelpers::JStringToString(env, str)); - env->DeleteLocalRef(str); - } - - return values; -} - -void AndroidSettingsInterface::SetStringList(const char* section, const char* key, - const std::vector& items) -{ - Log_DevPrintf("SetStringList(\"%s\", \"%s\")", section, key); - if (items.empty()) - { - DeleteValue(section, key); - return; - } - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder items_array( - env, env->NewObjectArray(static_cast(items.size()), AndroidHelpers::GetStringClass(), nullptr)); - for (size_t i = 0; i < items.size(); i++) - { - LocalRefHolder item_jstr(env, env->NewStringUTF(items[i].c_str())); - env->SetObjectArrayElement(items_array, static_cast(i), item_jstr); - } - - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - env->CallStaticVoidMethod(m_helper_class, m_helper_set_string_list, m_java_shared_preferences, key_string.Get(), - items_array.Get()); - - CheckForException(env, "SetStringList"); -} - -bool AndroidSettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item) -{ - Log_DevPrintf("RemoveFromStringList(\"%s\", \"%s\", \"%s\")", section, key, item); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder item_string(env, env->NewStringUTF(item)); - const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_remove_from_string_list, - m_java_shared_preferences, key_string.Get(), item_string.Get()); - CheckForException(env, "RemoveFromStringList"); - return result; -} - -bool AndroidSettingsInterface::AddToStringList(const char* section, const char* key, const char* item) -{ - Log_DevPrintf("AddToStringList(\"%s\", \"%s\", \"%s\")", section, key, item); - - JNIEnv* env = AndroidHelpers::GetJNIEnv(); - LocalRefHolder key_string(env, env->NewStringUTF(GetSettingKey(section, key))); - LocalRefHolder item_string(env, env->NewStringUTF(item)); - const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_add_to_string_list, - m_java_shared_preferences, key_string.Get(), item_string.Get()); - CheckForException(env, "AddToStringList"); - return result; -} diff --git a/android/app/src/cpp/android_settings_interface.h b/android/app/src/cpp/android_settings_interface.h deleted file mode 100644 index 542a2f90e..000000000 --- a/android/app/src/cpp/android_settings_interface.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once -#include "core/settings.h" -#include - -class AndroidSettingsInterface : public SettingsInterface -{ -public: - AndroidSettingsInterface(jobject java_context); - ~AndroidSettingsInterface(); - - bool Save() override; - void Clear() override; - - int GetIntValue(const char* section, const char* key, int default_value = 0) override; - float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) override; - bool GetBoolValue(const char* section, const char* key, bool default_value = false) override; - std::string GetStringValue(const char* section, const char* key, const char* default_value = "") override; - - void SetIntValue(const char* section, const char* key, int value) override; - void SetFloatValue(const char* section, const char* key, float value) override; - void SetBoolValue(const char* section, const char* key, bool value) override; - void SetStringValue(const char* section, const char* key, const char* value) override; - void DeleteValue(const char* section, const char* key) override; - void ClearSection(const char* section) override; - - std::vector GetStringList(const char* section, const char* key) override; - void SetStringList(const char* section, const char* key, const std::vector& items) override; - bool RemoveFromStringList(const char* section, const char* key, const char* item) override; - bool AddToStringList(const char* section, const char* key, const char* item) override; - -private: - jobject GetPreferencesEditor(JNIEnv* env); - void CheckForException(JNIEnv* env, const char* task); - - jclass m_set_class{}; - jclass m_shared_preferences_class{}; - jclass m_shared_preferences_editor_class{}; - jclass m_helper_class{}; - jobject m_java_shared_preferences{}; - jmethodID m_get_boolean{}; - jmethodID m_get_int{}; - jmethodID m_get_float{}; - jmethodID m_get_string{}; - jmethodID m_get_string_set{}; - jmethodID m_edit{}; - jmethodID m_edit_set_string{}; - jmethodID m_edit_commit{}; - jmethodID m_edit_remove{}; - jmethodID m_set_to_array{}; - jmethodID m_helper_clear_section{}; - jmethodID m_helper_add_to_string_list{}; - jmethodID m_helper_remove_from_string_list{}; - jmethodID m_helper_set_string_list{}; -}; diff --git a/android/app/src/cpp/opensles_audio_stream.cpp b/android/app/src/cpp/opensles_audio_stream.cpp deleted file mode 100644 index 70f2c207c..000000000 --- a/android/app/src/cpp/opensles_audio_stream.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "opensles_audio_stream.h" -#include "common/assert.h" -#include "common/log.h" -#include -Log_SetChannel(OpenSLESAudioStream); - -// Based off Dolphin's OpenSLESStream class. - -OpenSLESAudioStream::OpenSLESAudioStream() = default; - -OpenSLESAudioStream::~OpenSLESAudioStream() -{ - if (IsOpen()) - OpenSLESAudioStream::CloseDevice(); -} - -std::unique_ptr OpenSLESAudioStream::Create() -{ - return std::make_unique(); -} - -bool OpenSLESAudioStream::OpenDevice() -{ - DebugAssert(!IsOpen()); - - SLresult res = slCreateEngine(&m_engine, 0, nullptr, 0, nullptr, nullptr); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("slCreateEngine failed: %d", res); - return false; - } - - res = (*m_engine)->Realize(m_engine, SL_BOOLEAN_FALSE); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("Realize(Engine) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_engine)->GetInterface(m_engine, SL_IID_ENGINE, &m_engine_engine); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("GetInterface(SL_IID_ENGINE) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix, 0, 0, 0); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("CreateOutputMix failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_output_mix)->Realize(m_output_mix, SL_BOOLEAN_FALSE); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("Realize(OutputMix) mix failed: %d", res); - CloseDevice(); - return false; - } - - SLDataLocator_AndroidSimpleBufferQueue dloc_bq{SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NUM_BUFFERS}; - SLDataFormat_PCM format = {SL_DATAFORMAT_PCM, - m_channels, - m_output_sample_rate * 1000u, - SL_PCMSAMPLEFORMAT_FIXED_16, - SL_PCMSAMPLEFORMAT_FIXED_16, - SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, - SL_BYTEORDER_LITTLEENDIAN}; - SLDataSource dsrc{&dloc_bq, &format}; - SLDataLocator_OutputMix dloc_outputmix{SL_DATALOCATOR_OUTPUTMIX, m_output_mix}; - SLDataSink dsink{&dloc_outputmix, nullptr}; - - const std::array ap_interfaces = {{SL_IID_BUFFERQUEUE, SL_IID_VOLUME}}; - const std::array ap_interfaces_req = {{true, true}}; - res = (*m_engine_engine) - ->CreateAudioPlayer(m_engine_engine, &m_player, &dsrc, &dsink, static_cast(ap_interfaces.size()), - ap_interfaces.data(), ap_interfaces_req.data()); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("CreateAudioPlayer failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_player)->Realize(m_player, SL_BOOLEAN_FALSE); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("Realize(AudioPlayer) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_player)->GetInterface(m_player, SL_IID_PLAY, &m_play_interface); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("GetInterface(SL_IID_PLAY) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_player)->GetInterface(m_player, SL_IID_BUFFERQUEUE, &m_buffer_queue_interface); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("GetInterface(SL_IID_BUFFERQUEUE) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_player)->GetInterface(m_player, SL_IID_VOLUME, &m_volume_interface); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("GetInterface(SL_IID_VOLUME) failed: %d", res); - CloseDevice(); - return false; - } - - res = (*m_buffer_queue_interface)->RegisterCallback(m_buffer_queue_interface, BufferCallback, this); - if (res != SL_RESULT_SUCCESS) - { - Log_ErrorPrintf("Failed to register callback: %d", res); - CloseDevice(); - return false; - } - - for (u32 i = 0; i < NUM_BUFFERS; i++) - m_buffers[i] = std::make_unique(m_buffer_size * m_channels); - - Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers", m_output_sample_rate, - m_channels, m_buffer_size, NUM_BUFFERS); - return true; -} - -void OpenSLESAudioStream::PauseDevice(bool paused) -{ - if (m_paused == paused) - return; - - SLresult res = - (*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING); - if (res != SL_RESULT_SUCCESS) - Log_ErrorPrintf("SetPlayState failed: %d", res); - - if (!paused && !m_buffer_enqueued) - { - m_buffer_enqueued = true; - EnqueueBuffer(); - } - - m_paused = paused; -} - -void OpenSLESAudioStream::CloseDevice() -{ - m_buffers = {}; - m_current_buffer = 0; - m_paused = true; - m_buffer_enqueued = false; - - if (m_player) - { - (*m_player)->Destroy(m_player); - m_volume_interface = {}; - m_buffer_queue_interface = {}; - m_play_interface = {}; - m_player = {}; - } - if (m_output_mix) - { - (*m_output_mix)->Destroy(m_output_mix); - m_output_mix = {}; - } - (*m_engine)->Destroy(m_engine); - m_engine_engine = {}; - m_engine = {}; -} - -void OpenSLESAudioStream::SetOutputVolume(u32 volume) -{ - const SLmillibel attenuation = (volume == 0) ? - SL_MILLIBEL_MIN : - static_cast(2000.0f * std::log10(static_cast(volume) / 100.0f)); - SLresult res = (*m_volume_interface)->SetVolumeLevel(m_volume_interface, attenuation); - if (res != SL_RESULT_SUCCESS) - Log_ErrorPrintf("SetVolumeLevel failed: %d", res); -} - -void OpenSLESAudioStream::EnqueueBuffer() -{ - SampleType* samples = m_buffers[m_current_buffer].get(); - ReadFrames(samples, m_buffer_size, false); - - SLresult res = (*m_buffer_queue_interface) - ->Enqueue(m_buffer_queue_interface, samples, m_buffer_size * m_channels * sizeof(SampleType)); - if (res != SL_RESULT_SUCCESS) - Log_ErrorPrintf("Enqueue buffer failed: %d", res); - - m_current_buffer = (m_current_buffer + 1) % NUM_BUFFERS; -} - -void OpenSLESAudioStream::BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context) -{ - OpenSLESAudioStream* const this_ptr = static_cast(context); - this_ptr->EnqueueBuffer(); -} - -void OpenSLESAudioStream::FramesAvailable() {} diff --git a/android/app/src/cpp/opensles_audio_stream.h b/android/app/src/cpp/opensles_audio_stream.h deleted file mode 100644 index 83c54aa6e..000000000 --- a/android/app/src/cpp/opensles_audio_stream.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once -#include "common/audio_stream.h" -#include -#include -#include -#include - -class OpenSLESAudioStream final : public AudioStream -{ -public: - OpenSLESAudioStream(); - ~OpenSLESAudioStream(); - - static std::unique_ptr Create(); - - void SetOutputVolume(u32 volume) override; - -protected: - enum : u32 - { - NUM_BUFFERS = 2 - }; - - ALWAYS_INLINE bool IsOpen() const { return (m_engine != nullptr); } - - bool OpenDevice() override; - void PauseDevice(bool paused) override; - void CloseDevice() override; - void FramesAvailable() override; - - void EnqueueBuffer(); - - static void BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context); - - SLObjectItf m_engine{}; - SLEngineItf m_engine_engine{}; - SLObjectItf m_output_mix{}; - - SLObjectItf m_player{}; - SLPlayItf m_play_interface{}; - SLAndroidSimpleBufferQueueItf m_buffer_queue_interface{}; - SLVolumeItf m_volume_interface{}; - - std::array, NUM_BUFFERS> m_buffers; - u32 m_current_buffer = 0; - bool m_paused = true; - bool m_buffer_enqueued = false; -}; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d1181b437..000000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/ic_launcher-web.png b/android/app/src/main/ic_launcher-web.png deleted file mode 100644 index 353ac1fe71e8fc6bc0b90ba1004a311f77a1ccc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38012 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 { - if (o2.isLocked() && !o1.isLocked()) - return -1; - else if (o1.isLocked() && !o2.isLocked()) - return 1; - - return o1.getName().compareTo(o2.getName()); - }); - } - - private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { - private final View mItemView; - - public ViewHolder(@NonNull View itemView) { - super(itemView); - mItemView = itemView; - mItemView.setOnClickListener(this); - mItemView.setOnLongClickListener(this); - } - - public void bindToEntry(Achievement cheevo) { - ImageView icon = ((ImageView) mItemView.findViewById(R.id.icon)); - icon.setImageDrawable(mItemView.getContext().getDrawable(R.drawable.ic_baseline_lock_24)); - - final String badgePath = cheevo.getBadgePath(); - if (badgePath != null) { - new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, badgePath); - } - - ((TextView) mItemView.findViewById(R.id.title)).setText(cheevo.getName()); - ((TextView) mItemView.findViewById(R.id.description)).setText(cheevo.getDescription()); - - ((ImageView) mItemView.findViewById(R.id.locked_icon)).setImageDrawable( - mItemView.getContext().getDrawable(cheevo.isLocked() ? R.drawable.ic_baseline_lock_24 : R.drawable.ic_baseline_lock_open_24)); - - final String pointsString = String.format(mItemView.getContext().getString(R.string.achievement_points_format_string), cheevo.getPoints()); - ((TextView) mItemView.findViewById(R.id.points)).setText(pointsString); - } - - @Override - public void onClick(View v) { - // - } - - @Override - public boolean onLongClick(View v) { - return false; - } - } - - private static class ViewAdapter extends RecyclerView.Adapter { - private final LayoutInflater mInflater; - private final Achievement[] mAchievements; - - public ViewAdapter(@NonNull Context context, Achievement[] achievements) { - mInflater = LayoutInflater.from(context); - mAchievements = achievements; - } - - @NonNull - @Override - public AchievementListFragment.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new AchievementListFragment.ViewHolder(mInflater.inflate(R.layout.layout_achievement_entry, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull AchievementListFragment.ViewHolder holder, int position) { - holder.bindToEntry(mAchievements[position]); - } - - @Override - public int getItemCount() { - return (mAchievements != null) ? mAchievements.length : 0; - } - - @Override - public int getItemViewType(int position) { - return R.layout.layout_game_list_entry; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AchievementSettingsFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/AchievementSettingsFragment.java deleted file mode 100644 index 0b4c1a149..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AchievementSettingsFragment.java +++ /dev/null @@ -1,266 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; - -import java.net.URLEncoder; -import java.text.DateFormat; -import java.util.Date; -import java.util.Locale; - -public class AchievementSettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener { - private static final String REGISTER_URL = "http://retroachievements.org/createaccount.php"; - private static final String PROFILE_URL_PREFIX = "https://retroachievements.org/user/"; - - private boolean isLoggedIn = false; - private String username; - private String loginTokenTime; - - public AchievementSettingsFragment() { - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.achievement_preferences, rootKey); - updateViews(); - } - - private void updateViews() { - final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - - username = prefs.getString("Cheevos/Username", ""); - isLoggedIn = (username != null && !username.isEmpty()); - if (isLoggedIn) { - try { - final String loginTokenTimeString = prefs.getString("Cheevos/LoginTimestamp", ""); - final long loginUnixTimestamp = Long.parseLong(loginTokenTimeString); - - // TODO: Extract to a helper function. - final Date date = new Date(loginUnixTimestamp * 1000); - final DateFormat format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT, Locale.getDefault()); - loginTokenTime = format.format(date); - } catch (Exception e) { - loginTokenTime = null; - } - } - - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - - Preference preference = preferenceScreen.findPreference("Cheevos/ChallengeMode"); - if (preference != null) { - // toggling this is disabled while it's running to avoid the whole power off thing - preference.setEnabled(!AndroidHostInterface.getInstance().isEmulationThreadRunning()); - } - - preference = preferenceScreen.findPreference("Cheevos/Login"); - if (preference != null) - { - preference.setVisible(!isLoggedIn); - preference.setOnPreferenceClickListener(this); - } - - preference = preferenceScreen.findPreference("Cheevos/Register"); - if (preference != null) - { - preference.setVisible(!isLoggedIn); - preference.setOnPreferenceClickListener(this); - } - - preference = preferenceScreen.findPreference("Cheevos/Logout"); - if (preference != null) - { - preference.setVisible(isLoggedIn); - preference.setOnPreferenceClickListener(this); - } - - preference = preferenceScreen.findPreference("Cheevos/Username"); - if (preference != null) - { - preference.setVisible(isLoggedIn); - preference.setSummary((username != null) ? username : ""); - } - - preference = preferenceScreen.findPreference("Cheevos/LoginTokenTime"); - if (preference != null) - { - preference.setVisible(isLoggedIn); - preference.setSummary((loginTokenTime != null) ? loginTokenTime : ""); - } - - preference = preferenceScreen.findPreference("Cheevos/ViewProfile"); - if (preference != null) - { - preference.setVisible(isLoggedIn); - preference.setOnPreferenceClickListener(this); - } - } - - @Override - public boolean onPreferenceClick(Preference preference) { - final String key = preference.getKey(); - if (key == null) - return false; - - switch (key) - { - case "Cheevos/Login": - { - handleLogin(); - return true; - } - - case "Cheevos/Logout": - { - handleLogout(); - return true; - } - - case "Cheevos/Register": - { - openUrl(REGISTER_URL); - return true; - } - - case "Cheevos/ViewProfile": - { - final String profileUrl = getProfileUrl(username); - if (profileUrl != null) - openUrl(profileUrl); - - return true; - } - - default: - return false; - } - } - - private void openUrl(String url) { - final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browserIntent); - } - - private void handleLogin() { - LoginDialogFragment loginDialog = new LoginDialogFragment(this); - loginDialog.show(getFragmentManager(), "fragment_achievement_login"); - } - - private void handleLogout() { - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setTitle(R.string.settings_achievements_confirm_logout_title); - builder.setMessage(R.string.settings_achievements_confirm_logout_message); - builder.setPositiveButton(R.string.settings_achievements_logout, (dialog, which) -> { - AndroidHostInterface.getInstance().cheevosLogout(); - updateViews(); - }); - builder.setNegativeButton(R.string.achievement_settings_login_cancel_button, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - private static String getProfileUrl(String username) { - try { - final String encodedUsername = URLEncoder.encode(username, "UTF-8"); - return PROFILE_URL_PREFIX + encodedUsername; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - public static class LoginDialogFragment extends DialogFragment { - private AchievementSettingsFragment mParent; - - public LoginDialogFragment(AchievementSettingsFragment parent) { - mParent = parent; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_achievements_login, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - ((Button)view.findViewById(R.id.login)).setOnClickListener((View.OnClickListener) v -> doLogin()); - ((Button)view.findViewById(R.id.cancel)).setOnClickListener((View.OnClickListener) v -> dismiss()); - } - - private static class LoginTask extends AsyncTask { - private LoginDialogFragment mParent; - private String mUsername; - private String mPassword; - private boolean mResult; - - public LoginTask(LoginDialogFragment parent, String username, String password) { - mParent = parent; - mUsername = username; - mPassword = password; - } - - @Override - protected Void doInBackground(Void... voids) { - final Activity activity = mParent.getActivity(); - if (activity == null) - return null; - - mResult = AndroidHostInterface.getInstance().cheevosLogin(mUsername, mPassword); - - activity.runOnUiThread(() -> { - if (!mResult) { - ((TextView) mParent.getView().findViewById(R.id.error)).setText(R.string.achievement_settings_login_failed); - mParent.enableUi(true); - return; - } - - mParent.mParent.updateViews(); - mParent.dismiss(); - }); - - return null; - } - } - - private void doLogin() { - final View rootView = getView(); - final String username = ((EditText)rootView.findViewById(R.id.username)).getText().toString(); - final String password = ((EditText)rootView.findViewById(R.id.password)).getText().toString(); - if (username == null || username.length() == 0 || password == null || password.length() == 0) - return; - - enableUi(false); - ((TextView)rootView.findViewById(R.id.error)).setText(""); - new LoginDialogFragment.LoginTask(this, username, password).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void enableUi(boolean enabled) { - final View rootView = getView(); - ((EditText)rootView.findViewById(R.id.username)).setEnabled(enabled); - ((EditText)rootView.findViewById(R.id.password)).setEnabled(enabled); - ((Button)rootView.findViewById(R.id.login)).setEnabled(enabled); - ((Button)rootView.findViewById(R.id.cancel)).setEnabled(enabled); - ((ProgressBar)rootView.findViewById(R.id.progressBar)).setVisibility(enabled ? View.GONE : View.VISIBLE); - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java deleted file mode 100644 index 48d70f7d3..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.AssetManager; -import android.os.Environment; -import android.os.Process; -import android.util.Log; -import android.view.Surface; -import android.widget.Toast; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Locale; - -public class AndroidHostInterface { - public final static int DISPLAY_ALIGNMENT_TOP_OR_LEFT = 0; - public final static int DISPLAY_ALIGNMENT_CENTER = 1; - public final static int DISPLAY_ALIGNMENT_RIGHT_OR_BOTTOM = 2; - - public final static int CONTROLLER_AXIS_TYPE_FULL = 0; - public final static int CONTROLLER_AXIS_TYPE_HALF = 1; - - private long mNativePointer; - private Context mContext; - private FileHelper mFileHelper; - private EmulationActivity mEmulationActivity; - - public AndroidHostInterface(Context context, FileHelper fileHelper) { - this.mContext = context; - this.mFileHelper = fileHelper; - } - - public void reportError(String message) { - Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); - } - - public void reportMessage(String message) { - Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); - } - - public InputStream openAssetStream(String path) { - try { - return mContext.getAssets().open(path, AssetManager.ACCESS_STREAMING); - } catch (IOException e) { - return null; - } - } - - public void setContext(Context context) { - mContext = context; - } - - public EmulationActivity getEmulationActivity() { return mEmulationActivity; } - - static public native String getScmVersion(); - - static public native String getFullScmVersion(); - - static public native boolean setThreadAffinity(int[] cpus); - - static public native AndroidHostInterface create(Context context, FileHelper fileHelper, String userDirectory); - - public native boolean isEmulationThreadRunning(); - - public native boolean runEmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String state_filename); - - public native boolean isEmulationThreadPaused(); - - public native void pauseEmulationThread(boolean paused); - - public native void stopEmulationThreadLoop(); - - public native boolean hasSurface(); - - public native void surfaceChanged(Surface surface, int format, int width, int height); - - public native void setControllerButtonState(int index, int buttonCode, boolean pressed); - - public native void setControllerAxisState(int index, int axisCode, float value); - - public native void setControllerAutoFireState(int controllerIndex, int autoFireIndex, boolean active); - - public native void setMousePosition(int positionX, int positionY); - - public static native int getControllerButtonCode(String controllerType, String buttonName); - - public static native int getControllerAxisCode(String controllerType, String axisName); - - public static native int getControllerAxisType(String controllerType, String axisName); - - public static native String[] getControllerButtonNames(String controllerType); - - public static native String[] getControllerAxisNames(String controllerType); - - public static native int getControllerVibrationMotorCount(String controllerType); - - public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed); - - public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value); - - public native boolean hasControllerButtonBinding(int controllerIndex, int buttonIndex); - - public native void toggleControllerAnalogMode(); - - public native String[] getInputProfileNames(); - - public native boolean loadInputProfile(String name); - - public native boolean saveInputProfile(String name); - - public native HotkeyInfo[] getHotkeyInfoList(); - - public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase, AndroidProgressCallback progressCallback); - - public native GameListEntry[] getGameListEntries(); - - public native GameListEntry getGameListEntry(String path); - - public native String getGameSettingValue(String path, String key); - - public native void setGameSettingValue(String path, String key, String value); - - public native void resetSystem(); - - public native void loadState(boolean global, int slot); - - public native void saveState(boolean global, int slot); - - public native void saveResumeState(boolean waitForCompletion); - - public native void applySettings(); - public native void updateInputMap(); - - public native void setDisplayAlignment(int alignment); - - public native PatchCode[] getPatchCodeList(); - - public native void setPatchCodeEnabled(int index, boolean enabled); - - public native boolean importPatchCodesFromString(String str); - - public native void addOSDMessage(String message, float duration); - - public native boolean hasAnyBIOSImages(); - - public native String importBIOSImage(byte[] data); - - public native boolean isFastForwardEnabled(); - - public native void setFastForwardEnabled(boolean enabled); - - public native boolean hasMediaSubImages(); - - public native String[] getMediaSubImageTitles(); - - public native int getMediaSubImageIndex(); - - public native boolean switchMediaSubImage(int index); - - public native boolean setMediaFilename(String filename); - - public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty); - - public native void setFullscreenUINotificationVerticalPosition(float position, float direction); - - public native boolean isCheevosActive(); - public native boolean isCheevosChallengeModeActive(); - public native Achievement[] getCheevoList(); - public native int getCheevoCount(); - public native int getUnlockedCheevoCount(); - public native int getCheevoPointsForGame(); - public native int getCheevoMaximumPointsForGame(); - public native String getCheevoGameTitle(); - public native String getCheevoGameIconPath(); - public native boolean cheevosLogin(String username, String password); - public native void cheevosLogout(); - - static { - System.loadLibrary("duckstation-native"); - } - - static private AndroidHostInterface mInstance; - static private String mUserDirectory; - - static public boolean createInstance(Context context) { - // Set user path. - mUserDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); - if (mUserDirectory.isEmpty()) - mUserDirectory = "/sdcard"; - - mUserDirectory += "/duckstation"; - Log.i("AndroidHostInterface", "User directory: " + mUserDirectory); - mInstance = create(context, new FileHelper(context), mUserDirectory); - return mInstance != null; - } - - static public boolean hasInstance() { - return mInstance != null; - } - - static public AndroidHostInterface getInstance() { - return mInstance; - } - - static public String getUserDirectory() { return mUserDirectory; } - - static public boolean hasInstanceAndEmulationThreadIsRunning() { - return hasInstance() && getInstance().isEmulationThreadRunning(); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java deleted file mode 100644 index a77b9b3b9..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.Activity; -import android.app.ProgressDialog; - -import androidx.appcompat.app.AlertDialog; - -public class AndroidProgressCallback { - private Activity mContext; - private ProgressDialog mDialog; - - public AndroidProgressCallback(Activity context) { - mContext = context; - mDialog = new ProgressDialog(context); - mDialog.setCancelable(false); - mDialog.setCanceledOnTouchOutside(false); - mDialog.setMessage(context.getString(R.string.android_progress_callback_please_wait)); - mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - mDialog.setIndeterminate(false); - mDialog.setMax(100); - mDialog.setProgress(0); - mDialog.show(); - } - - public void dismiss() { - mDialog.dismiss(); - } - - public void setTitle(String text) { - mContext.runOnUiThread(() -> { - mDialog.setTitle(text); - }); - } - - public void setStatusText(String text) { - mContext.runOnUiThread(() -> { - mDialog.setMessage(text); - }); - } - - public void setProgressRange(int range) { - mContext.runOnUiThread(() -> { - mDialog.setMax(range); - }); - } - - public void setProgressValue(int value) { - mContext.runOnUiThread(() -> { - mDialog.setProgress(value); - }); - } - - public void modalError(String message) { - Object lock = new Object(); - mContext.runOnUiThread(() -> { - new AlertDialog.Builder(mContext) - .setTitle("Error") - .setMessage(message) - .setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> { - dialog.dismiss(); - }) - .setOnDismissListener((dialogInterface) -> { - synchronized (lock) { - lock.notify(); - } - }) - .create() - .show(); - }); - - synchronized (lock) { - try { - lock.wait(); - } catch (InterruptedException e) { - } - } - } - - public void modalInformation(String message) { - Object lock = new Object(); - mContext.runOnUiThread(() -> { - new AlertDialog.Builder(mContext) - .setTitle(mContext.getString(R.string.android_progress_callback_information)) - .setMessage(message) - .setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> { - dialog.dismiss(); - }) - .setOnDismissListener((dialogInterface) -> { - synchronized (lock) { - lock.notify(); - } - }) - .create() - .show(); - }); - - synchronized (lock) { - try { - lock.wait(); - } catch (InterruptedException e) { - } - } - } - - private class ConfirmationResult { - public boolean result = false; - } - - public boolean modalConfirmation(String message) { - ConfirmationResult result = new ConfirmationResult(); - mContext.runOnUiThread(() -> { - new AlertDialog.Builder(mContext) - .setTitle(mContext.getString(R.string.android_progress_callback_confirmation)) - .setMessage(message) - .setPositiveButton(mContext.getString(R.string.android_progress_callback_yes), (dialog, button) -> { - result.result = true; - dialog.dismiss(); - }) - .setNegativeButton(mContext.getString(R.string.android_progress_callback_no), (dialog, button) -> { - result.result = false; - dialog.dismiss(); - }) - .setOnDismissListener((dialogInterface) -> { - synchronized (result) { - result.notify(); - } - }) - .create() - .show(); - }); - - synchronized (result) { - try { - result.wait(); - } catch (InterruptedException e) { - } - } - - return result.result; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java b/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java deleted file mode 100644 index 0f56417a1..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.stenzek.duckstation; - -public enum ConsoleRegion { - AutoDetect, - NTSC_J, - NTSC_U, - PAL -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java deleted file mode 100644 index 175fb3cb3..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Vibrator; -import android.text.InputType; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.preference.PreferenceManager; - -import java.util.ArrayList; -import java.util.List; - -public class ControllerAutoMapper { - public interface CompleteCallback { - public void onComplete(); - } - - private final Context context; - private final int port; - private final CompleteCallback completeCallback; - - private InputDevice device; - private SharedPreferences prefs; - private SharedPreferences.Editor editor; - private StringBuilder log; - private String keyBase; - private String controllerType; - - public ControllerAutoMapper(Context context, int port, CompleteCallback completeCallback) { - this.context = context; - this.port = port; - this.completeCallback = completeCallback; - } - - private void log(String format, Object... args) { - log.append(String.format(format, args)); - log.append('\n'); - } - - private void setButtonBindingToKeyCode(String buttonName, int keyCode) { - log("Binding button '%s' to key '%s' (%d)", buttonName, KeyEvent.keyCodeToString(keyCode), keyCode); - - final String key = String.format("%sButton%s", keyBase, buttonName); - final String value = String.format("%s/Button%d", device.getDescriptor(), keyCode); - editor.putString(key, value); - } - - private void setButtonBindingToAxis(String buttonName, int axis, int direction) { - final char directionIndicator = (direction < 0) ? '-' : '+'; - log("Binding button '%s' to axis '%s' (%d) direction %c", buttonName, MotionEvent.axisToString(axis), axis, directionIndicator); - - final String key = String.format("%sButton%s", keyBase, buttonName); - final String value = String.format("%s/%cAxis%d", device.getDescriptor(), directionIndicator, axis); - editor.putString(key, value); - } - - private void setAxisBindingToAxis(String axisName, int axis) { - log("Binding axis '%s' to axis '%s' (%d)", axisName, MotionEvent.axisToString(axis), axis); - - final String key = String.format("%sAxis%s", keyBase, axisName); - final String value = String.format("%s/Axis%d", device.getDescriptor(), axis); - editor.putString(key, value); - } - - private void doAutoBindingButton(String buttonName, int[] keyCodes, int[][] axisCodes) { - // Prefer the axis codes, as it dispatches to that first. - if (axisCodes != null) { - final List motionRangeList = device.getMotionRanges(); - for (int[] axisAndDirection : axisCodes) { - final int axis = axisAndDirection[0]; - final int direction = axisAndDirection[1]; - for (InputDevice.MotionRange range : motionRangeList) { - if (range.getAxis() == axis) { - setButtonBindingToAxis(buttonName, axis, direction); - return; - } - } - } - } - - if (keyCodes != null) { - final boolean[] keysPresent = device.hasKeys(keyCodes); - for (int i = 0; i < keysPresent.length; i++) { - if (keysPresent[i]) { - setButtonBindingToKeyCode(buttonName, keyCodes[i]); - return; - } - } - } - - log("No automatic bindings found for button '%s'", buttonName); - } - - private void doAutoBindingAxis(String axisName, int[] axisCodes) { - // Prefer the axis codes, as it dispatches to that first. - if (axisCodes != null) { - final List motionRangeList = device.getMotionRanges(); - for (final int axis : axisCodes) { - for (InputDevice.MotionRange range : motionRangeList) { - if (range.getAxis() == axis) { - setAxisBindingToAxis(axisName, axis); - return; - } - } - } - } - - log.append(String.format("No automatic bindings found for axis '%s'\n", axisName)); - } - - public void start() { - final ArrayList deviceList = new ArrayList<>(); - for (final int deviceId : InputDevice.getDeviceIds()) { - final InputDevice inputDevice = InputDevice.getDevice(deviceId); - if (inputDevice == null || !EmulationSurfaceView.isBindableDevice(inputDevice) || - !EmulationSurfaceView.isGamepadDevice(inputDevice)) { - continue; - } - - deviceList.add(inputDevice); - } - - if (deviceList.isEmpty()) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.main_activity_error); - builder.setMessage(R.string.controller_auto_mapping_no_devices); - builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - return; - } - - final String[] deviceNames = new String[deviceList.size()]; - for (int i = 0; i < deviceList.size(); i++) - deviceNames[i] = deviceList.get(i).getName(); - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.controller_auto_mapping_select_device); - builder.setItems(deviceNames, (dialog, which) -> { - process(deviceList.get(which)); - }); - builder.create().show(); - } - - private void process(InputDevice device) { - this.prefs = PreferenceManager.getDefaultSharedPreferences(context); - this.editor = prefs.edit(); - this.log = new StringBuilder(); - this.device = device; - - this.keyBase = String.format("Controller%d/", port); - this.controllerType = ControllerSettingsCollectionFragment.getControllerType(prefs, port); - - setButtonBindings(); - setAxisBindings(); - setVibrationBinding(); - - this.editor.commit(); - this.editor = null; - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.controller_auto_mapping_results); - - final EditText editText = new EditText(context); - editText.setText(log.toString()); - editText.setInputType(InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - editText.setSingleLine(false); - builder.setView(editText); - - builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - - if (completeCallback != null) - completeCallback.onComplete(); - } - - private void setButtonBindings() { - final String[] buttonNames = AndroidHostInterface.getInstance().getControllerButtonNames(controllerType); - if (buttonNames == null || buttonNames.length == 0) { - log("No axes to bind."); - return; - } - - for (final String buttonName : buttonNames) { - switch (buttonName) { - case "Up": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_UP}, new int[][]{{MotionEvent.AXIS_HAT_Y, -1}}); - break; - case "Down": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_DOWN}, new int[][]{{MotionEvent.AXIS_HAT_Y, 1}}); - break; - case "Left": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_LEFT}, new int[][]{{MotionEvent.AXIS_HAT_X, -1}}); - break; - case "Right": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_RIGHT}, new int[][]{{MotionEvent.AXIS_HAT_X, 1}}); - break; - case "Select": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_SELECT}, null); - break; - case "Start": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_START}, null); - break; - case "Triangle": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_Y}, null); - break; - case "Cross": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_A}, null); - break; - case "Circle": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_B}, null); - break; - case "Square": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_X}, null); - break; - case "L1": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L1}, null); - break; - case "L2": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L2}, new int[][]{{MotionEvent.AXIS_LTRIGGER, 1}, {MotionEvent.AXIS_Z, 1}, {MotionEvent.AXIS_BRAKE, 1}}); - break; - case "R1": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R1}, null); - break; - case "R2": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R2}, new int[][]{{MotionEvent.AXIS_RTRIGGER, 1}, {MotionEvent.AXIS_RZ, 1}, {MotionEvent.AXIS_GAS, 1}}); - break; - case "L3": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBL}, null); - break; - case "R3": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBR}, null); - break; - case "Analog": - doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_MODE}, null); - break; - default: - log("Button '%s' not supported by auto mapping.", buttonName); - break; - } - } - } - - private void setAxisBindings() { - final String[] axisNames = AndroidHostInterface.getInstance().getControllerAxisNames(controllerType); - if (axisNames == null || axisNames.length == 0) { - log("No axes to bind."); - return; - } - - for (final String axisName : axisNames) { - switch (axisName) { - case "LeftX": - doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_X}); - break; - case "LeftY": - doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_Y}); - break; - case "RightX": - doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_RX, MotionEvent.AXIS_Z}); - break; - case "RightY": - doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_RY, MotionEvent.AXIS_RZ}); - break; - default: - log("Axis '%s' not supported by auto mapping.", axisName); - break; - } - } - } - - private void setVibrationBinding() { - final int motorCount = AndroidHostInterface.getInstance().getControllerVibrationMotorCount(controllerType); - if (motorCount == 0) { - log("No vibration motors to bind."); - return; - } - - final Vibrator vibrator = device.getVibrator(); - if (vibrator == null || !vibrator.hasVibrator()) { - log("Selected device has no vibrator, cannot bind vibration."); - return; - } - - log("Binding vibration to device '%s'.", device.getDescriptor()); - - final String key = String.format("%sRumble", keyBase); - editor.putString(key, device.getDescriptor()); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java deleted file mode 100644 index 8b6344c25..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.ArraySet; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -import java.util.HashMap; -import java.util.List; - -public class ControllerBindingDialog extends AlertDialog { - final static float DETECT_THRESHOLD = 0.25f; - private final ControllerBindingPreference.Type mType; - private final String mSettingKey; - private String mCurrentBinding; - private int mUpdatedAxisCode = -1; - private final HashMap mStartingAxisValues = new HashMap<>(); - - public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, ControllerBindingPreference.Type type) { - super(context); - - mType = type; - mSettingKey = settingKey; - mCurrentBinding = currentBinding; - if (mCurrentBinding == null) - mCurrentBinding = getContext().getString(R.string.controller_binding_dialog_no_binding); - - setTitle(buttonName); - updateMessage(); - setButton(BUTTON_POSITIVE, context.getString(R.string.controller_binding_dialog_cancel), (dialogInterface, button) -> dismiss()); - setButton(BUTTON_NEGATIVE, context.getString(R.string.controller_binding_dialog_clear), (dialogInterface, button) -> { - mCurrentBinding = null; - updateBinding(); - dismiss(); - }); - - setOnKeyListener(new DialogInterface.OnKeyListener() { - @Override - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - return onKeyDown(keyCode, event); - } - }); - } - - private void updateMessage() { - setMessage(String.format(getContext().getString(R.string.controller_binding_dialog_message), mCurrentBinding)); - } - - private void updateBinding() { - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); - if (mCurrentBinding != null) { - ArraySet values = new ArraySet<>(); - values.add(mCurrentBinding); - editor.putStringSet(mSettingKey, values); - } else { - try { - editor.remove(mSettingKey); - } catch (Exception e) { - - } - } - - editor.commit(); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - final InputDevice device = event.getDevice(); - if (!EmulationSurfaceView.isBindableDevice(device) || !EmulationSurfaceView.isBindableKeyEvent(event)) { - return super.onKeyDown(keyCode, event); - } - - if (mType == ControllerBindingPreference.Type.BUTTON || mType == ControllerBindingPreference.Type.HALF_AXIS) { - mCurrentBinding = String.format("%s/Button%d", device.getDescriptor(), event.getKeyCode()); - } else if (mType == ControllerBindingPreference.Type.VIBRATION) { - if (device.getVibrator() == null || !device.getVibrator().hasVibrator()) { - Toast.makeText(getContext(), getContext().getString(R.string.controller_settings_vibration_unsupported), Toast.LENGTH_LONG).show(); - dismiss(); - return true; - } - - mCurrentBinding = device.getDescriptor(); - } else { - return super.onKeyDown(keyCode, event); - } - - updateMessage(); - updateBinding(); - dismiss(); - return true; - } - - private void setAxisCode(InputDevice device, int axisCode, boolean positive) { - if (mUpdatedAxisCode >= 0) - return; - - mUpdatedAxisCode = axisCode; - - final int controllerIndex = 0; - if (mType == ControllerBindingPreference.Type.AXIS || mType == ControllerBindingPreference.Type.HALF_AXIS) - mCurrentBinding = String.format("%s/Axis%d", device.getDescriptor(), axisCode); - else - mCurrentBinding = String.format("%s/%cAxis%d", device.getDescriptor(), (positive) ? '+' : '-', axisCode); - - updateBinding(); - updateMessage(); - dismiss(); - } - - private boolean doAxisDetection(MotionEvent event) { - if (!EmulationSurfaceView.isBindableDevice(event.getDevice()) || !EmulationSurfaceView.isJoystickMotionEvent(event)) - return false; - - final List motionEventList = event.getDevice().getMotionRanges(); - if (motionEventList == null || motionEventList.isEmpty()) - return false; - - final int deviceId = event.getDeviceId(); - if (!mStartingAxisValues.containsKey(deviceId)) { - final float[] axisValues = new float[motionEventList.size()]; - for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) { - final int axisCode = motionEventList.get(axisIndex).getAxis(); - - if (event.getHistorySize() > 0) - axisValues[axisIndex] = event.getHistoricalAxisValue(axisCode, 0); - else if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y) - axisValues[axisIndex] = 0.0f; - else - axisValues[axisIndex] = event.getAxisValue(axisCode); - } - - mStartingAxisValues.put(deviceId, axisValues); - } - - final float[] axisValues = mStartingAxisValues.get(deviceId); - for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) { - final int axisCode = motionEventList.get(axisIndex).getAxis(); - final float newValue = event.getAxisValue(axisCode); - final float delta = newValue - axisValues[axisIndex]; - if (Math.abs(delta) >= DETECT_THRESHOLD) { - setAxisCode(event.getDevice(), axisCode, delta >= 0.0f); - break; - } - } - - return true; - } - - @Override - public boolean onGenericMotionEvent(@NonNull MotionEvent event) { - if (mType != ControllerBindingPreference.Type.AXIS && - mType != ControllerBindingPreference.Type.HALF_AXIS && - mType != ControllerBindingPreference.Type.BUTTON) { - return false; - } - - if (doAxisDetection(event)) - return true; - - return super.onGenericMotionEvent(event); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java deleted file mode 100644 index 0aa4ad707..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java +++ /dev/null @@ -1,268 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.AttributeSet; -import android.view.InputDevice; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.content.ContextCompat; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceViewHolder; - -import java.util.Set; - -public class ControllerBindingPreference extends Preference { - public enum Type { - BUTTON, - AXIS, - HALF_AXIS, - VIBRATION - } - - private enum VisualType { - BUTTON, - AXIS, - VIBRATION, - HOTKEY - } - - private String mBindingName; - private String mDisplayName; - private String mValue; - private TextView mValueView; - private Type mType = Type.BUTTON; - private VisualType mVisualType = VisualType.BUTTON; - - private static int getIconForButton(String buttonName) { - if (buttonName.equals("Up")) { - return R.drawable.ic_controller_up_button_pressed; - } else if (buttonName.equals("Right")) { - return R.drawable.ic_controller_right_button_pressed; - } else if (buttonName.equals("Down")) { - return R.drawable.ic_controller_down_button_pressed; - } else if (buttonName.equals("Left")) { - return R.drawable.ic_controller_left_button_pressed; - } else if (buttonName.equals("Triangle")) { - return R.drawable.ic_controller_triangle_button_pressed; - } else if (buttonName.equals("Circle")) { - return R.drawable.ic_controller_circle_button_pressed; - } else if (buttonName.equals("Cross")) { - return R.drawable.ic_controller_cross_button_pressed; - } else if (buttonName.equals("Square")) { - return R.drawable.ic_controller_square_button_pressed; - } else if (buttonName.equals("Start")) { - return R.drawable.ic_controller_start_button_pressed; - } else if (buttonName.equals("Select")) { - return R.drawable.ic_controller_select_button_pressed; - } else if (buttonName.equals("L1")) { - return R.drawable.ic_controller_l1_button_pressed; - } else if (buttonName.equals("L2")) { - return R.drawable.ic_controller_l2_button_pressed; - } else if (buttonName.equals("R1")) { - return R.drawable.ic_controller_r1_button_pressed; - } else if (buttonName.equals("R2")) { - return R.drawable.ic_controller_r2_button_pressed; - } - - return R.drawable.ic_baseline_radio_button_unchecked_24; - } - - private static int getIconForAxis(String axisName) { - return R.drawable.ic_baseline_radio_button_checked_24; - } - - private static int getIconForHotkey(String hotkeyDisplayName) { - switch (hotkeyDisplayName) { - case "FastForward": - case "ToggleFastForward": - case "Turbo": - case "ToggleTurbo": - return R.drawable.ic_controller_fast_forward; - - default: - return R.drawable.ic_baseline_category_24; - } - } - - public ControllerBindingPreference(Context context, AttributeSet attrs) { - this(context, attrs, 0); - setWidgetLayoutResource(R.layout.layout_controller_binding_preference); - setIconSpaceReserved(false); - } - - public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setWidgetLayoutResource(R.layout.layout_controller_binding_preference); - setIconSpaceReserved(false); - } - - public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setWidgetLayoutResource(R.layout.layout_controller_binding_preference); - setIconSpaceReserved(false); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - - ImageView iconView = ((ImageView) holder.findViewById(R.id.controller_binding_icon)); - TextView nameView = ((TextView) holder.findViewById(R.id.controller_binding_name)); - mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value)); - - int drawableId = R.drawable.ic_baseline_radio_button_checked_24; - switch (mVisualType) { - case BUTTON: - drawableId = getIconForButton(mBindingName); - break; - case AXIS: - drawableId = getIconForAxis(mBindingName); - break; - case HOTKEY: - drawableId = getIconForHotkey(mBindingName); - break; - case VIBRATION: - drawableId = R.drawable.ic_baseline_vibration_24; - break; - } - - iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId)); - nameView.setText(mDisplayName); - updateValue(); - } - - @Override - protected void onClick() { - ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mType); - dialog.setOnDismissListener((dismissedDialog) -> updateValue()); - dialog.show(); - } - - public void initButton(int controllerIndex, String buttonName) { - mBindingName = buttonName; - mDisplayName = buttonName; - mType = Type.BUTTON; - mVisualType = VisualType.BUTTON; - setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName)); - updateValue(); - } - - public void initAxis(int controllerIndex, String axisName, int axisType) { - mBindingName = axisName; - mDisplayName = axisName; - mType = (axisType == AndroidHostInterface.CONTROLLER_AXIS_TYPE_HALF) ? Type.HALF_AXIS : Type.AXIS; - mVisualType = VisualType.AXIS; - setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName)); - updateValue(); - } - - public void initVibration(int controllerIndex) { - mBindingName = "Rumble"; - mDisplayName = getContext().getString(R.string.controller_binding_device_for_vibration); - mType = Type.VIBRATION; - mVisualType = VisualType.VIBRATION; - setKey(String.format("Controller%d/Rumble", controllerIndex)); - updateValue(); - } - - public void initHotkey(HotkeyInfo hotkeyInfo) { - mBindingName = hotkeyInfo.getName(); - mDisplayName = hotkeyInfo.getDisplayName(); - mType = Type.BUTTON; - mVisualType = VisualType.HOTKEY; - setKey(hotkeyInfo.getBindingConfigKey()); - updateValue(); - } - - public void initAutoFireButton(int controllerIndex, int autoFireSlot) { - mBindingName = String.format("AutoFire%d", autoFireSlot); - mDisplayName = getContext().getString(R.string.controller_binding_auto_fire_n, autoFireSlot); - mType = Type.BUTTON; - mVisualType = VisualType.BUTTON; - setKey(String.format("Controller%d/AutoFire%d", controllerIndex, autoFireSlot)); - updateValue(); - } - - private String prettyPrintBinding(String value) { - final int index = value.indexOf('/'); - String device, binding; - if (index >= 0) { - device = value.substring(0, index); - binding = value.substring(index); - } else { - device = value; - binding = ""; - } - - String humanName = device; - int deviceIndex = -1; - - final int[] deviceIds = InputDevice.getDeviceIds(); - for (int i = 0; i < deviceIds.length; i++) { - final InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]); - if (inputDevice == null || !inputDevice.getDescriptor().equals(device)) { - continue; - } - - humanName = inputDevice.getName(); - deviceIndex = i; - break; - } - - final int MAX_LENGTH = 40; - if (humanName.length() > MAX_LENGTH) { - final StringBuilder shortenedName = new StringBuilder(); - shortenedName.append(humanName, 0, MAX_LENGTH / 2); - shortenedName.append("..."); - shortenedName.append(humanName, humanName.length() - (MAX_LENGTH / 2), - humanName.length()); - - humanName = shortenedName.toString(); - } - - if (deviceIndex < 0) - return String.format("%s[??]%s", humanName, binding); - else - return String.format("%s[%d]%s", humanName, deviceIndex, binding); - } - - private void updateValue(String value) { - mValue = value; - if (mValueView != null) { - if (value != null) - mValueView.setText(value); - else - mValueView.setText(getContext().getString(R.string.controller_binding_dialog_no_binding)); - } - } - - public void updateValue() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - Set values = PreferenceHelpers.getStringSet(prefs, getKey()); - if (values != null) { - StringBuilder sb = new StringBuilder(); - for (String value : values) { - if (sb.length() > 0) - sb.append(", "); - sb.append(prettyPrintBinding(value)); - } - - updateValue(sb.toString()); - } else { - updateValue(null); - } - } - - public void clearBinding(SharedPreferences.Editor prefEditor) { - try { - prefEditor.remove(getKey()); - } catch (Exception e) { - - } - - updateValue(null); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java deleted file mode 100644 index 73448dbc8..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.EditText; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.PreferenceManager; - -import java.util.ArrayList; - -public class ControllerSettingsActivity extends AppCompatActivity { - private ControllerSettingsCollectionFragment fragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(null); - setContentView(R.layout.settings_activity); - - fragment = new ControllerSettingsCollectionFragment(); - fragment.setMultitapModeChangedListener(this::recreate); - - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.settings, fragment) - .commit(); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.controller_mapping_activity_title); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_controller_mapping, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - final int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_load_profile) { - doLoadProfile(); - return true; - } else if (id == R.id.action_save_profile) { - doSaveProfile(); - return true; - } else if (id == R.id.action_clear_bindings) { - fragment.clearAllBindings(); - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - private void displayError(String text) { - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_activity_error) - .setMessage(text) - .setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss())) - .create() - .show(); - } - - private void doLoadProfile() { - final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames(); - if (profileNames == null) { - displayError(getString(R.string.controller_mapping_activity_no_profiles_found)); - return; - } - - new AlertDialog.Builder(this) - .setTitle(R.string.controller_mapping_activity_select_input_profile) - .setItems(profileNames, (dialog, choice) -> { - doLoadProfile(profileNames[choice]); - dialog.dismiss(); - }) - .setNegativeButton(R.string.controller_mapping_activity_cancel, ((dialog, which) -> dialog.dismiss())) - .create() - .show(); - } - - private void doLoadProfile(String profileName) { - if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) { - displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName)); - return; - } - - fragment.updateAllBindings(); - } - - private void doSaveProfile() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final EditText input = new EditText(this); - builder.setTitle(R.string.controller_mapping_activity_input_profile_name); - builder.setView(input); - builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> { - final String name = input.getText().toString(); - if (name.isEmpty()) { - displayError(getString(R.string.controller_mapping_activity_name_must_be_provided)); - return; - } - - if (!AndroidHostInterface.getInstance().saveInputProfile(name)) { - displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile)); - return; - } - - Toast.makeText(ControllerSettingsActivity.this, String.format(ControllerSettingsActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name), - Toast.LENGTH_LONG).show(); - }); - builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java deleted file mode 100644 index 00b86191d..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java +++ /dev/null @@ -1,459 +0,0 @@ -package com.github.stenzek.duckstation; - - -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.preference.SeekBarPreference; -import androidx.preference.SwitchPreferenceCompat; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import java.util.ArrayList; -import java.util.HashMap; - -public class ControllerSettingsCollectionFragment extends Fragment { - public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode"; - private static final int NUM_MAIN_CONTROLLER_PORTS = 2; - private static final int NUM_SUB_CONTROLLER_PORTS = 4; - private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[]{'A', 'B', 'C', 'D'}; - private static final int NUM_AUTO_FIRE_BUTTONS = 4; - - public interface MultitapModeChangedListener { - void onChanged(); - } - - private final ArrayList preferences = new ArrayList<>(); - private SettingsCollectionAdapter adapter; - private ViewPager2 viewPager; - private String[] controllerPortNames; - - private MultitapModeChangedListener multitapModeChangedListener; - - public ControllerSettingsCollectionFragment() { - } - - public static String getControllerTypeKey(int port) { - return String.format("Controller%d/Type", port); - } - - public static String getControllerType(SharedPreferences prefs, int port) { - final String defaultControllerType = (port == 1) ? "DigitalController" : "None"; - return prefs.getString(getControllerTypeKey(port), defaultControllerType); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_controller_settings, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - final String multitapMode = PreferenceManager.getDefaultSharedPreferences(getContext()).getString( - MULTITAP_MODE_SETTINGS_KEY, "Disabled"); - - final ArrayList portNames = new ArrayList<>(); - for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) { - final boolean isMultitap = (multitapMode.equals("BothPorts") || - (i == 0 && multitapMode.equals("Port1Only")) || - (i == 1 && multitapMode.equals("Port2Only"))); - - if (isMultitap) { - for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) { - portNames.add(getContext().getString( - R.string.controller_settings_sub_port_format, - i + 1, SUB_CONTROLLER_PORT_NAMES[j])); - } - } else { - portNames.add(getContext().getString( - R.string.controller_settings_main_port_format, - i + 1)); - } - } - - controllerPortNames = new String[portNames.size()]; - portNames.toArray(controllerPortNames); - - adapter = new SettingsCollectionAdapter(this, controllerPortNames.length); - viewPager = view.findViewById(R.id.view_pager); - viewPager.setAdapter(adapter); - - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { - if (position == 0) - tab.setText(R.string.controller_settings_tab_settings); - else if (position <= controllerPortNames.length) - tab.setText(controllerPortNames[position - 1]); - else - tab.setText(R.string.controller_settings_tab_hotkeys); - }).attach(); - } - - public void setMultitapModeChangedListener(MultitapModeChangedListener multitapModeChangedListener) { - this.multitapModeChangedListener = multitapModeChangedListener; - } - - public void clearAllBindings() { - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); - for (ControllerBindingPreference pref : preferences) - pref.clearBinding(prefEdit); - prefEdit.commit(); - } - - public void updateAllBindings() { - for (ControllerBindingPreference pref : preferences) - pref.updateValue(); - } - - public static class SettingsFragment extends PreferenceFragmentCompat { - private final ControllerSettingsCollectionFragment parent; - - public SettingsFragment(ControllerSettingsCollectionFragment parent) { - this.parent = parent; - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.controllers_preferences, rootKey); - - final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY); - if (multitapModePreference != null) { - multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> { - if (parent.multitapModeChangedListener != null) - parent.multitapModeChangedListener.onChanged(); - - return true; - }); - } - } - } - - public static class ControllerPortFragment extends PreferenceFragmentCompat { - private final ControllerSettingsCollectionFragment parent; - private final int controllerIndex; - private PreferenceCategory mButtonsCategory; - private PreferenceCategory mAxisCategory; - private PreferenceCategory mSettingsCategory; - private PreferenceCategory mAutoFireCategory; - private PreferenceCategory mAutoFireBindingsCategory; - - public ControllerPortFragment(ControllerSettingsCollectionFragment parent, int controllerIndex) { - this.parent = parent; - this.controllerIndex = controllerIndex; - } - - private static void clearBindingsInCategory(SharedPreferences.Editor editor, PreferenceCategory category) { - for (int i = 0; i < category.getPreferenceCount(); i++) { - final Preference preference = category.getPreference(i); - if (preference instanceof ControllerBindingPreference) - ((ControllerBindingPreference) preference).clearBinding(editor); - } - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - setPreferenceScreen(ps); - createPreferences(); - } - - private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) { - final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext()); - pref.setKey(key); - pref.setTitle(title); - pref.setSummary(summary); - pref.setIconSpaceReserved(false); - pref.setDefaultValue(defaultValue); - return pref; - } - - private String getAutoToggleSummary(SharedPreferences sp, int slot) { - final String button = sp.getString(String.format("AutoFire%dButton", slot), null); - if (button == null || button.length() == 0) - return "Not Configured"; - - return String.format("%s every %d frames", button, sp.getInt("AutoFire%dFrequency", 2)); - } - - private void createPreferences() { - final PreferenceScreen ps = getPreferenceScreen(); - final SharedPreferences sp = getPreferenceManager().getSharedPreferences(); - final String controllerType = getControllerType(sp, controllerIndex); - final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType); - final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType); - final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType); - - final ListPreference typePreference = new ListPreference(getContext()); - typePreference.setEntries(R.array.settings_controller_type_entries); - typePreference.setEntryValues(R.array.settings_controller_type_values); - typePreference.setKey(getControllerTypeKey(controllerIndex)); - typePreference.setValue(controllerType); - typePreference.setTitle(R.string.settings_controller_type); - typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - typePreference.setIconSpaceReserved(false); - typePreference.setOnPreferenceChangeListener((pref, value) -> { - removePreferences(); - createPreferences(value.toString()); - return true; - }); - ps.addPreference(typePreference); - - final Preference autoBindPreference = new Preference(getContext()); - autoBindPreference.setTitle(R.string.controller_settings_automatic_mapping); - autoBindPreference.setSummary(R.string.controller_settings_summary_automatic_mapping); - autoBindPreference.setIconSpaceReserved(false); - autoBindPreference.setOnPreferenceClickListener(preference -> { - final ControllerAutoMapper mapper = new ControllerAutoMapper(getContext(), controllerIndex, () -> { - removePreferences(); - createPreferences(typePreference.getValue()); - }); - mapper.start(); - return true; - }); - ps.addPreference(autoBindPreference); - - final Preference clearBindingsPreference = new Preference(getContext()); - clearBindingsPreference.setTitle(R.string.controller_settings_clear_controller_bindings); - clearBindingsPreference.setSummary(R.string.controller_settings_summary_clear_controller_bindings); - clearBindingsPreference.setIconSpaceReserved(false); - clearBindingsPreference.setOnPreferenceClickListener(preference -> { - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setMessage(R.string.controller_settings_clear_controller_bindings_confirm); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - dialog.dismiss(); - clearBindings(); - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - return true; - }); - ps.addPreference(clearBindingsPreference); - - mButtonsCategory = new PreferenceCategory(getContext()); - mButtonsCategory.setTitle(R.string.controller_settings_category_button_bindings); - mButtonsCategory.setIconSpaceReserved(false); - ps.addPreference(mButtonsCategory); - - mAxisCategory = new PreferenceCategory(getContext()); - mAxisCategory.setTitle(R.string.controller_settings_category_axis_bindings); - mAxisCategory.setIconSpaceReserved(false); - ps.addPreference(mAxisCategory); - - mSettingsCategory = new PreferenceCategory(getContext()); - mSettingsCategory.setTitle(R.string.controller_settings_category_settings); - mSettingsCategory.setIconSpaceReserved(false); - ps.addPreference(mSettingsCategory); - - mAutoFireCategory = new PreferenceCategory(getContext()); - mAutoFireCategory.setTitle(R.string.controller_settings_category_auto_fire_buttons); - mAutoFireCategory.setIconSpaceReserved(false); - ps.addPreference(mAutoFireCategory); - - mAutoFireBindingsCategory = new PreferenceCategory(getContext()); - mAutoFireBindingsCategory.setTitle(R.string.controller_settings_category_auto_fire_bindings); - mAutoFireBindingsCategory.setIconSpaceReserved(false); - ps.addPreference(mAutoFireBindingsCategory); - - createPreferences(controllerType); - } - - @SuppressLint("DefaultLocale") - private void createPreferences(String controllerType) { - final PreferenceScreen ps = getPreferenceScreen(); - final SharedPreferences sp = getPreferenceManager().getSharedPreferences(); - final String[] buttonNames = AndroidHostInterface.getControllerButtonNames(controllerType); - final String[] axisNames = AndroidHostInterface.getControllerAxisNames(controllerType); - final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType); - - if (buttonNames != null) { - for (String buttonName : buttonNames) { - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initButton(controllerIndex, buttonName); - mButtonsCategory.addPreference(cbp); - parent.preferences.add(cbp); - } - } - - if (axisNames != null) { - for (String axisName : axisNames) { - final int axisType = AndroidHostInterface.getControllerAxisType(controllerType, axisName); - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initAxis(controllerIndex, axisName, axisType); - mAxisCategory.addPreference(cbp); - parent.preferences.add(cbp); - } - } - - if (vibrationMotors > 0) { - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initVibration(controllerIndex); - mSettingsCategory.addPreference(cbp); - parent.preferences.add(cbp); - } - - if (controllerType.equals("AnalogController")) { - mSettingsCategory.addPreference( - createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex), - R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_enable_analog_mode_on_reset, true)); - - mSettingsCategory.addPreference( - createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex), - R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_use_analog_sticks_for_dpad, true)); - } - - if (buttonNames != null) { - for (int autoFireSlot = 1; autoFireSlot <= NUM_AUTO_FIRE_BUTTONS; autoFireSlot++) { - final ListPreference autoFirePreference = new ListPreference(getContext()); - autoFirePreference.setEntries(buttonNames); - autoFirePreference.setEntryValues(buttonNames); - autoFirePreference.setKey(String.format("Controller%d/AutoFire%dButton", controllerIndex, autoFireSlot)); - autoFirePreference.setTitle(getContext().getString(R.string.controller_settings_auto_fire_n_button, autoFireSlot)); - autoFirePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - autoFirePreference.setIconSpaceReserved(false); - mAutoFireCategory.addPreference(autoFirePreference); - - final SeekBarPreference frequencyPreference = new SeekBarPreference(getContext()); - frequencyPreference.setMin(1); - frequencyPreference.setMax(60); - frequencyPreference.setKey(String.format("Controller%d/AutoFire%dFrequency", controllerIndex, autoFireSlot)); - frequencyPreference.setDefaultValue(2); - frequencyPreference.setTitle(getContext().getString(R.string.controller_settings_auto_fire_n_frequency, autoFireSlot)); - frequencyPreference.setIconSpaceReserved(false); - frequencyPreference.setShowSeekBarValue(true); - mAutoFireCategory.addPreference(frequencyPreference); - } - - for (int autoFireSlot = 1; autoFireSlot <= NUM_AUTO_FIRE_BUTTONS; autoFireSlot++) { - final ControllerBindingPreference bindingPreference = new ControllerBindingPreference(getContext(), null); - bindingPreference.initAutoFireButton(controllerIndex, autoFireSlot); - mAutoFireBindingsCategory.addPreference(bindingPreference); - } - } - } - - private void removePreferences() { - for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) { - parent.preferences.remove(mButtonsCategory.getPreference(i)); - } - mButtonsCategory.removeAll(); - - for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) { - parent.preferences.remove(mAxisCategory.getPreference(i)); - } - mAxisCategory.removeAll(); - - for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) { - parent.preferences.remove(mSettingsCategory.getPreference(i)); - } - mSettingsCategory.removeAll(); - - for (int i = 0; i < mAutoFireCategory.getPreferenceCount(); i++) { - parent.preferences.remove(mAutoFireCategory.getPreference(i)); - } - mAutoFireCategory.removeAll(); - - for (int i = 0; i < mAutoFireBindingsCategory.getPreferenceCount(); i++) { - parent.preferences.remove(mAutoFireBindingsCategory.getPreference(i)); - } - mAutoFireBindingsCategory.removeAll(); - } - - private void clearBindings() { - final SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); - clearBindingsInCategory(editor, mButtonsCategory); - clearBindingsInCategory(editor, mAxisCategory); - clearBindingsInCategory(editor, mSettingsCategory); - editor.commit(); - - Toast.makeText(parent.getContext(), parent.getString( - R.string.controller_settings_clear_controller_bindings_done, controllerIndex), - Toast.LENGTH_LONG).show(); - } - } - - public static class HotkeyFragment extends PreferenceFragmentCompat { - private final ControllerSettingsCollectionFragment parent; - private final HotkeyInfo[] mHotkeyInfo; - - public HotkeyFragment(ControllerSettingsCollectionFragment parent) { - this.parent = parent; - this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList(); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - if (mHotkeyInfo != null) { - final HashMap categoryMap = new HashMap<>(); - - for (HotkeyInfo hotkeyInfo : mHotkeyInfo) { - PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ? - categoryMap.get(hotkeyInfo.getCategory()) : null; - if (category == null) { - category = new PreferenceCategory(getContext()); - category.setTitle(hotkeyInfo.getCategory()); - category.setIconSpaceReserved(false); - categoryMap.put(hotkeyInfo.getCategory(), category); - ps.addPreference(category); - } - - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initHotkey(hotkeyInfo); - category.addPreference(cbp); - parent.preferences.add(cbp); - } - } - - setPreferenceScreen(ps); - } - } - - public static class SettingsCollectionAdapter extends FragmentStateAdapter { - private final ControllerSettingsCollectionFragment parent; - private final int controllerPorts; - - public SettingsCollectionAdapter(@NonNull ControllerSettingsCollectionFragment parent, int controllerPorts) { - super(parent); - this.parent = parent; - this.controllerPorts = controllerPorts; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - if (position == 0) - return new SettingsFragment(parent); - else if (position <= controllerPorts) - return new ControllerPortFragment(parent, position); - else - return new HotkeyFragment(parent); - } - - @Override - public int getItemCount() { - return controllerPorts + 2; - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java b/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java deleted file mode 100644 index 813d9e717..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.stenzek.duckstation; - -public enum DiscRegion { - NTSC_J, - NTSC_U, - PAL, - Other -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmptyGameListFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmptyGameListFragment.java deleted file mode 100644 index 793cf2d7d..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmptyGameListFragment.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.Bundle; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -public class EmptyGameListFragment extends Fragment { - private static final String SUPPORTED_FORMATS_STRING = - ".cue (Cue Sheets)\n" + - ".iso/.img (Single Track Image)\n" + - ".ecm (Error Code Modeling Image)\n" + - ".mds (Media Descriptor Sidecar)\n" + - ".chd (Compressed Hunks of Data)\n" + - ".pbp (PlayStation Portable, Only Decrypted)"; - - private MainActivity parent; - - public EmptyGameListFragment(MainActivity parent) { - super(R.layout.fragment_empty_game_list); - this.parent = parent; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - ((TextView) view.findViewById(R.id.supported_formats)).setText( - getString(R.string.main_activity_empty_game_list_supported_formats, SUPPORTED_FORMATS_STRING)); - ((Button) view.findViewById(R.id.add_game_directory)).setOnClickListener(v -> parent.startAddGameDirectory()); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java deleted file mode 100644 index 8192df706..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ /dev/null @@ -1,988 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.hardware.input.InputManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Vibrator; -import android.util.Log; -import android.view.Display; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceHolder; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -/** - * An example full-screen activity that shows and hides the system UI (i.e. - * status bar and navigation/system bar) with user interaction. - */ -public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback { - /** - * Settings interfaces. - */ - private SharedPreferences mPreferences; - private boolean mWasDestroyed = false; - private boolean mStopRequested = false; - private boolean mApplySettingsOnSurfaceRestored = false; - private String mGamePath = null; - private String mGameCode = null; - private String mGameTitle = null; - private String mGameCoverPath = null; - private EmulationSurfaceView mContentView; - private MenuDialogFragment mPauseMenu; - - private boolean getBooleanSetting(String key, boolean defaultValue) { - return mPreferences.getBoolean(key, defaultValue); - } - - private void setBooleanSetting(String key, boolean value) { - SharedPreferences.Editor editor = mPreferences.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - private String getStringSetting(String key, String defaultValue) { - return mPreferences.getString(key, defaultValue); - } - - private int getIntSetting(String key, int defaultValue) { - try { - return mPreferences.getInt(key, defaultValue); - } catch (ClassCastException e) { - try { - final String stringValue = mPreferences.getString(key, Integer.toString(defaultValue)); - return Integer.parseInt(stringValue); - } catch (Exception e2) { - return defaultValue; - } - } - } - - private void setStringSetting(String key, String value) { - SharedPreferences.Editor editor = mPreferences.edit(); - editor.putString(key, value); - editor.apply(); - } - - private void reportErrorOnUIThread(String message) { - // Toast.makeText(this, message, Toast.LENGTH_LONG); - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_activity_error) - .setMessage(message) - .setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> { - dialog.dismiss(); - enableFullscreenImmersive(); - }) - .create() - .show(); - } - - public void reportError(String message) { - Log.e("EmulationActivity", message); - - Object lock = new Object(); - runOnUiThread(() -> { - // Toast.makeText(this, message, Toast.LENGTH_LONG); - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_activity_error) - .setMessage(message) - .setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> { - dialog.dismiss(); - enableFullscreenImmersive(); - synchronized (lock) { - lock.notify(); - } - }) - .create() - .show(); - }); - - synchronized (lock) { - try { - lock.wait(); - } catch (InterruptedException e) { - } - } - } - - private EmulationThread mEmulationThread; - - private void stopEmulationThread() { - if (mEmulationThread == null) - return; - - mEmulationThread.stopAndJoin(); - mEmulationThread = null; - } - - public void onEmulationStarted() { - runOnUiThread(() -> { - updateRequestedOrientation(); - updateOrientation(); - }); - } - - public void onEmulationStopped() { - runOnUiThread(() -> { - if (!mWasDestroyed && !mStopRequested) - finish(); - }); - } - - public void onRunningGameChanged(String path, String code, String title, String coverPath) { - runOnUiThread(() -> { - mGamePath = path; - mGameTitle = title; - mGameCode = code; - mGameCoverPath = coverPath; - }); - } - - public float getRefreshRate() { - WindowManager windowManager = getWindowManager(); - if (windowManager == null) { - windowManager = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)); - if (windowManager == null) - return -1.0f; - } - - Display display = windowManager.getDefaultDisplay(); - if (display == null) - return -1.0f; - - return display.getRefreshRate(); - } - - public void openPauseMenu() { - runOnUiThread(() -> { - showPauseMenu(); - }); - } - - public String[] getInputDeviceNames() { - return (mContentView != null) ? mContentView.getInputDeviceNames() : null; - } - - public boolean hasInputDeviceVibration(int controllerIndex) { - return (mContentView != null) ? mContentView.hasInputDeviceVibration(controllerIndex) : null; - } - - public void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) { - if (mContentView != null) - mContentView.setInputDeviceVibration(controllerIndex, smallMotor, largeMotor); - } - - private void doApplySettings() { - AndroidHostInterface.getInstance().applySettings(); - updateRequestedOrientation(); - updateControllers(); - updateSustainedPerformanceMode(); - updateDisplayInCutout(); - } - - private void applySettings() { - if (!AndroidHostInterface.getInstance().isEmulationThreadRunning()) - return; - - if (AndroidHostInterface.getInstance().hasSurface()) { - doApplySettings(); - } else { - mApplySettingsOnSurfaceRestored = true; - } - } - - /// Ends the activity if it was restored without properly being created. - private boolean checkActivityIsValid() { - if (!AndroidHostInterface.hasInstance() || !AndroidHostInterface.getInstance().isEmulationThreadRunning()) { - finish(); - return false; - } - - return true; - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - // Once we get a surface, we can boot. - AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height); - - if (mEmulationThread != null) { - updateOrientation(); - - if (mApplySettingsOnSurfaceRestored) { - mApplySettingsOnSurfaceRestored = false; - doApplySettings(); - } - - return; - } - - final String bootPath = getIntent().getStringExtra("bootPath"); - final boolean saveStateOnExit = getBooleanSetting("Main/SaveStateOnExit", true); - final boolean resumeState = getIntent().getBooleanExtra("resumeState", saveStateOnExit); - final String bootSaveStatePath = getIntent().getStringExtra("saveStatePath"); - - mEmulationThread = EmulationThread.create(this, bootPath, resumeState, bootSaveStatePath); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - Log.i("EmulationActivity", "Surface destroyed"); - - if (mPauseMenu != null) - mPauseMenu.close(false); - - // Save the resume state in case we never get back again... - if (AndroidHostInterface.getInstance().isEmulationThreadRunning() && !mStopRequested) - AndroidHostInterface.getInstance().saveResumeState(true); - - AndroidHostInterface.getInstance().surfaceChanged(null, 0, 0, 0); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - mPreferences = PreferenceManager.getDefaultSharedPreferences(this); - super.onCreate(savedInstanceState); - - Log.i("EmulationActivity", "OnCreate"); - - // we might be coming from a third-party launcher if the host interface isn't setup - if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) { - finish(); - return; - } - - enableFullscreenImmersive(); - setContentView(R.layout.activity_emulation); - - mContentView = findViewById(R.id.fullscreen_content); - mContentView.getHolder().addCallback(this); - mContentView.setFocusableInTouchMode(true); - mContentView.setFocusable(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mContentView.setFocusedByDefault(true); - } - mContentView.requestFocus(); - - // Sort out rotation. - updateOrientation(); - updateSustainedPerformanceMode(); - updateDisplayInCutout(); - - // Hook up controller input. - updateControllers(); - registerInputDeviceListener(); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - enableFullscreenImmersive(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - enableFullscreenImmersive(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - Log.i("EmulationActivity", "OnStop"); - if (mEmulationThread != null) { - mWasDestroyed = true; - stopEmulationThread(); - } - - unregisterInputDeviceListener(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (!checkActivityIsValid()) { - // we must've got killed off in the background :( - return; - } - - if (requestCode == REQUEST_CODE_SETTINGS) { - if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { - applySettings(); - } - } else if (requestCode == REQUEST_IMPORT_PATCH_CODES) { - if (data == null || data.getData() == null) - return; - - importPatchesFromFile(data.getData()); - } else if (requestCode == REQUEST_CHANGE_DISC_FILE) { - if (data == null || data.getData() == null) - return; - - AndroidHostInterface.getInstance().setMediaFilename(data.getDataString()); - } - } - - @Override - public void onBackPressed() { - showPauseMenu(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (mContentView.onKeyDown(event.getKeyCode(), event)) - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (mContentView.onKeyUp(event.getKeyCode(), event)) - return true; - } - - return super.dispatchKeyEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent ev) { - if (mContentView.onGenericMotionEvent(ev)) - return true; - - return super.dispatchGenericMotionEvent(ev); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - if (checkActivityIsValid()) - updateOrientation(newConfig.orientation); - } - - private void updateRequestedOrientation() { - final String orientation = getStringSetting("Main/EmulationScreenOrientation", "unspecified"); - if (orientation.equals("portrait")) - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT); - else if (orientation.equals("landscape")) - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE); - else if (orientation.equals("sensor")) - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); - else - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - } - - private void updateOrientation() { - final int orientation = getResources().getConfiguration().orientation; - updateOrientation(orientation); - } - - private void updateOrientation(int newOrientation) { - if (newOrientation == Configuration.ORIENTATION_PORTRAIT) - AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_TOP_OR_LEFT); - else - AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_CENTER); - - if (mTouchscreenController != null) - mTouchscreenController.updateOrientation(); - } - - private void enableFullscreenImmersive() { - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - if (mContentView != null) - mContentView.requestFocus(); - } - - private void updateDisplayInCutout() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) - return; - - final boolean shouldExpand = getBooleanSetting("Display/ExpandToCutout", false); - final boolean isExpanded = getWindow().getAttributes().layoutInDisplayCutoutMode == - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - - if (shouldExpand == isExpanded) - return; - - WindowManager.LayoutParams attribs = getWindow().getAttributes(); - attribs.layoutInDisplayCutoutMode = shouldExpand ? - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; - getWindow().setAttributes(attribs); - } - - private static final int REQUEST_CODE_SETTINGS = 0; - private static final int REQUEST_IMPORT_PATCH_CODES = 1; - private static final int REQUEST_CHANGE_DISC_FILE = 2; - - private void onMenuClosed() { - enableFullscreenImmersive(); - - if (AndroidHostInterface.getInstance().isEmulationThreadPaused()) - AndroidHostInterface.getInstance().pauseEmulationThread(false); - } - - private boolean disableDialogMenuItem(AlertDialog dialog, int index) { - final ListView listView = dialog.getListView(); - if (listView == null) - return false; - - final View childItem = listView.getChildAt(index); - if (childItem == null) - return false; - - childItem.setEnabled(false); - childItem.setClickable(false); - childItem.setOnClickListener((v) -> {}); - return true; - } - - private void showPauseMenu() { - if (!AndroidHostInterface.getInstance().isEmulationThreadPaused()) { - AndroidHostInterface.getInstance().pauseEmulationThread(true); - } - - if (mPauseMenu != null) - mPauseMenu.close(false); - - mPauseMenu = new MenuDialogFragment(this); - mPauseMenu.show(getSupportFragmentManager(), "MenuDialogFragment"); - } - - private void showSaveStateMenu(boolean saving) { - final SaveStateInfo[] infos = AndroidHostInterface.getInstance().getSaveStateInfo(true); - if (infos == null) { - onMenuClosed(); - return; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final ListView listView = new ListView(this); - listView.setAdapter(new SaveStateInfo.ListAdapter(this, infos)); - builder.setView(listView); - builder.setOnDismissListener((dialog) -> { - onMenuClosed(); - }); - - final AlertDialog dialog = builder.create(); - - listView.setOnItemClickListener((parent, view, position, id) -> { - SaveStateInfo info = infos[position]; - if (saving) { - AndroidHostInterface.getInstance().saveState(info.isGlobal(), info.getSlot()); - } else { - AndroidHostInterface.getInstance().loadState(info.isGlobal(), info.getSlot()); - } - dialog.dismiss(); - }); - - dialog.show(); - } - - private void showTouchscreenControllerMenu() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_touchscreen_controller_settings); - builder.setItems(R.array.emulation_touchscreen_menu, (dialogInterface, i) -> { - switch (i) { - case 0: // Change Type - { - final String currentValue = getStringSetting("Controller1/TouchscreenControllerView", ""); - final String[] values = getResources().getStringArray(R.array.settings_touchscreen_controller_view_values); - int currentIndex = -1; - for (int k = 0; k < values.length; k++) { - if (currentValue.equals(values[k])) { - currentIndex = k; - break; - } - } - - final AlertDialog.Builder subBuilder = new AlertDialog.Builder(this); - subBuilder.setTitle(R.string.dialog_touchscreen_controller_type); - subBuilder.setSingleChoiceItems(R.array.settings_touchscreen_controller_view_entries, currentIndex, (dialog, j) -> { - setStringSetting("Controller1/TouchscreenControllerView", values[j]); - updateControllers(); - }); - subBuilder.setNegativeButton(R.string.dialog_done, (dialog, which) -> { - dialog.dismiss(); - }); - subBuilder.setOnDismissListener(dialog -> onMenuClosed()); - subBuilder.create().show(); - } - break; - - case 1: // Change Opacity - { - if (mTouchscreenController != null) { - AlertDialog.Builder subBuilder = mTouchscreenController.createOpacityDialog(this); - subBuilder.setOnDismissListener(dialog -> onMenuClosed()); - subBuilder.create().show(); - } else { - onMenuClosed(); - } - - } - break; - - case 2: // Add/Remove Buttons - { - if (mTouchscreenController != null) { - AlertDialog.Builder subBuilder = mTouchscreenController.createAddRemoveButtonDialog(this); - subBuilder.setOnDismissListener(dialog -> onMenuClosed()); - subBuilder.create().show(); - } else { - onMenuClosed(); - } - } - break; - - case 3: // Edit Positions - case 4: // Edit Scale - { - if (mTouchscreenController != null) { - // we deliberately don't call onMenuClosed() here to keep the system paused. - // but we need to re-enable immersive mode to get proper editing. - enableFullscreenImmersive(); - mTouchscreenController.startLayoutEditing( - (i == 4) ? TouchscreenControllerView.EditMode.SCALE : - TouchscreenControllerView.EditMode.POSITION); - } else { - // no controller - onMenuClosed(); - } - } - break; - } - }); - - builder.setOnCancelListener(dialogInterface -> onMenuClosed()); - builder.create().show(); - } - - private void showPatchesMenu() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - - final PatchCode[] codes = AndroidHostInterface.getInstance().getPatchCodeList(); - if (codes != null) { - CharSequence[] items = new CharSequence[codes.length]; - boolean[] itemsChecked = new boolean[codes.length]; - for (int i = 0; i < codes.length; i++) { - final PatchCode cc = codes[i]; - items[i] = cc.getDisplayText(); - itemsChecked[i] = cc.isEnabled(); - } - - builder.setMultiChoiceItems(items, itemsChecked, (dialogInterface, i, checked) -> { - AndroidHostInterface.getInstance().setPatchCodeEnabled(i, checked); - }); - } - - builder.setNegativeButton(R.string.emulation_activity_ok, (dialogInterface, i) -> { - dialogInterface.dismiss(); - }); - builder.setNeutralButton(R.string.emulation_activity_import_patch_codes, (dialogInterface, i) -> { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.emulation_activity_choose_patch_code_file)), REQUEST_IMPORT_PATCH_CODES); - }); - - builder.setOnDismissListener(dialogInterface -> onMenuClosed()); - builder.create().show(); - } - - private void importPatchesFromFile(Uri uri) { - String str = FileHelper.readStringFromUri(this, uri, 512 * 1024); - if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) { - reportErrorOnUIThread(getString(R.string.emulation_activity_failed_to_import_patch_codes)); - } - } - - private void startDiscChangeFromFile() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_CHANGE_DISC_FILE); - } - - private void showDiscChangeMenu() { - final AndroidHostInterface hi = AndroidHostInterface.getInstance(); - - if (!hi.hasMediaSubImages()) { - startDiscChangeFromFile(); - return; - } - - final String[] paths = AndroidHostInterface.getInstance().getMediaSubImageTitles(); - final int currentPath = AndroidHostInterface.getInstance().getMediaSubImageIndex(); - final int numPaths = (paths != null) ? paths.length : 0; - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - - CharSequence[] items = new CharSequence[numPaths + 1]; - for (int i = 0; i < numPaths; i++) - items[i] = FileHelper.getFileNameForPath(paths[i]); - items[numPaths] = getString(R.string.emulation_activity_change_disc_select_new_file); - - builder.setSingleChoiceItems(items, (currentPath < numPaths) ? currentPath : -1, (dialogInterface, i) -> { - dialogInterface.dismiss(); - onMenuClosed(); - - if (i < numPaths) { - AndroidHostInterface.getInstance().switchMediaSubImage(i); - } else { - startDiscChangeFromFile(); - } - }); - builder.setOnCancelListener(dialogInterface -> onMenuClosed()); - builder.create().show(); - } - - private void showAchievementsPopup() { - final Achievement[] achievements = AndroidHostInterface.getInstance().getCheevoList(); - if (achievements == null) { - onMenuClosed(); - return; - } - - final AchievementListFragment alf = new AchievementListFragment(achievements); - alf.show(getSupportFragmentManager(), "fragment_achievement_list"); - alf.setOnDismissListener(dialog -> onMenuClosed()); - } - - /** - * Touchscreen controller overlay - */ - TouchscreenControllerView mTouchscreenController; - - public void updateControllers() { - final int touchscreenControllerIndex = getIntSetting("TouchscreenController/PortIndex", 0); - final String touchscreenControllerPrefix = String.format("Controller%d/", touchscreenControllerIndex + 1); - final String controllerType = getStringSetting(touchscreenControllerPrefix + "Type", "DigitalController"); - final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital"); - final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false); - final boolean touchGliding = getBooleanSetting("Controller1/TouchGliding", false); - final boolean hapticFeedback = getBooleanSetting("Controller1/HapticFeedback", false); - final boolean vibration = getBooleanSetting("Controller1/Vibration", false); - final FrameLayout activityLayout = findViewById(R.id.frameLayout); - - Log.i("EmulationActivity", "Controller type: " + controllerType); - Log.i("EmulationActivity", "View type: " + viewType); - - mContentView.updateInputDevices(); - AndroidHostInterface.getInstance().updateInputMap(); - - final boolean hasAnyControllers = mContentView.hasAnyGamePads(); - if (controllerType.equals("None") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) { - if (mTouchscreenController != null) { - activityLayout.removeView(mTouchscreenController); - mTouchscreenController = null; - } - } else { - if (mTouchscreenController == null) { - mTouchscreenController = new TouchscreenControllerView(this); - activityLayout.addView(mTouchscreenController); - } - - mTouchscreenController.init(touchscreenControllerIndex, controllerType, viewType, hapticFeedback, touchGliding); - } - - if (vibration) - mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE); - else - mVibratorService = null; - - // Place notifications in the middle of the screen, rather then the bottom (because touchscreen). - float notificationVerticalPosition = 1.0f; - float notificationVerticalDirection = -1.0f; - if (mTouchscreenController != null) { - notificationVerticalPosition = 0.3f; - notificationVerticalDirection = -1.0f; - } - AndroidHostInterface.getInstance().setFullscreenUINotificationVerticalPosition( - notificationVerticalPosition, notificationVerticalDirection); - } - - private InputManager.InputDeviceListener mInputDeviceListener; - - private void registerInputDeviceListener() { - if (mInputDeviceListener != null) - return; - - mInputDeviceListener = new InputManager.InputDeviceListener() { - @Override - public void onInputDeviceAdded(int i) { - Log.i("EmulationActivity", String.format("InputDeviceAdded %d", i)); - updateControllers(); - } - - @Override - public void onInputDeviceRemoved(int i) { - Log.i("EmulationActivity", String.format("InputDeviceRemoved %d", i)); - updateControllers(); - } - - @Override - public void onInputDeviceChanged(int i) { - Log.i("EmulationActivity", String.format("InputDeviceChanged %d", i)); - updateControllers(); - } - }; - - InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE)); - if (inputManager != null) - inputManager.registerInputDeviceListener(mInputDeviceListener, null); - } - - private void unregisterInputDeviceListener() { - if (mInputDeviceListener == null) - return; - - InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE)); - if (inputManager != null) - inputManager.unregisterInputDeviceListener(mInputDeviceListener); - - mInputDeviceListener = null; - } - - private Vibrator mVibratorService; - - public void setVibration(boolean enabled) { - if (mVibratorService == null) - return; - - runOnUiThread(() -> { - if (mVibratorService == null) - return; - - if (enabled) - mVibratorService.vibrate(1000); - else - mVibratorService.cancel(); - }); - } - - private boolean mSustainedPerformanceModeEnabled = false; - - private void updateSustainedPerformanceMode() { - final boolean enabled = getBooleanSetting("Main/SustainedPerformanceMode", false); - if (mSustainedPerformanceModeEnabled == enabled) - return; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - getWindow().setSustainedPerformanceMode(enabled); - Log.i("EmulationActivity", String.format("%s sustained performance mode.", enabled ? "enabling" : "disabling")); - } else { - Log.e("EmulationActivity", "Sustained performance mode not supported."); - } - mSustainedPerformanceModeEnabled = enabled; - - } - - public static class MenuDialogFragment extends DialogFragment { - private EmulationActivity emulationActivity; - private boolean settingsChanged = false; - - public MenuDialogFragment(EmulationActivity emulationActivity) { - this.emulationActivity = emulationActivity; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setStyle(STYLE_NO_FRAME, R.style.EmulationActivityOverlay); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_emulation_activity_overlay, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - setContentFragment(new MenuSettingsFragment(this, emulationActivity), false); - - final ImageView coverView =((ImageView)view.findViewById(R.id.cover_image)); - if (emulationActivity.mGameCoverPath != null && !emulationActivity.mGameCoverPath.isEmpty()) { - new ImageLoadTask(coverView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, - emulationActivity.mGameCoverPath); - } else if (emulationActivity.mGameTitle != null) { - new GenerateCoverTask(getContext(), coverView, emulationActivity.mGameTitle) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - coverView.setOnClickListener(v -> close(true)); - - if (emulationActivity.mGameTitle != null) - ((TextView)view.findViewById(R.id.title)).setText(emulationActivity.mGameTitle); - - if (emulationActivity.mGameCode != null && emulationActivity.mGamePath != null) - { - final String subtitle = String.format("%s - %s", emulationActivity.mGameCode, - FileHelper.getFileNameForPath(emulationActivity.mGamePath)); - ((TextView)view.findViewById(R.id.subtitle)).setText(subtitle); - } - - ((ImageButton)view.findViewById(R.id.menu)).setOnClickListener(v -> onMenuClicked()); - ((ImageButton)view.findViewById(R.id.controller_settings)).setOnClickListener(v -> onControllerSettingsClicked()); - ((ImageButton)view.findViewById(R.id.settings)).setOnClickListener(v -> onSettingsClicked()); - ((ImageButton)view.findViewById(R.id.close)).setOnClickListener(v -> close(true)); - } - - @Override - public void onCancel(@NonNull DialogInterface dialog) { - onClosed(true); - } - - private void onClosed(boolean resumeGame) { - if (settingsChanged) - emulationActivity.applySettings(); - - if (resumeGame) - emulationActivity.onMenuClosed(); - - emulationActivity.mPauseMenu = null; - } - - public void close(boolean resumeGame) { - dismiss(); - onClosed(resumeGame); - } - - private void setContentFragment(Fragment fragment, boolean transition) { - FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); - if (transition) - transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out); - transaction.replace(R.id.content, fragment).commit(); - } - - private void onMenuClicked() { - setContentFragment(new MenuSettingsFragment(this, emulationActivity), true); - } - - private void onControllerSettingsClicked() { - ControllerSettingsCollectionFragment fragment = new ControllerSettingsCollectionFragment(); - setContentFragment(fragment, true); - fragment.setMultitapModeChangedListener(this::onControllerSettingsClicked); - settingsChanged = true; - } - - private void onSettingsClicked() { - setContentFragment(new SettingsCollectionFragment(), true); - settingsChanged = true; - } - } - - public static class MenuSettingsFragment extends PreferenceFragmentCompat { - private MenuDialogFragment menuDialogFragment; - private EmulationActivity emulationActivity; - - public MenuSettingsFragment(MenuDialogFragment menuDialogFragment, EmulationActivity emulationActivity) { - this.menuDialogFragment = menuDialogFragment; - this.emulationActivity = emulationActivity; - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); - - final boolean cheevosActive = AndroidHostInterface.getInstance().isCheevosActive(); - final boolean cheevosChallengeModeEnabled = AndroidHostInterface.getInstance().isCheevosChallengeModeActive(); - - createPreference(R.string.emulation_menu_load_state, R.drawable.ic_baseline_folder_open_24, !cheevosChallengeModeEnabled, preference -> { - menuDialogFragment.close(false); - emulationActivity.showSaveStateMenu(false); - return true; - }); - createPreference(R.string.emulation_menu_save_state, R.drawable.ic_baseline_save_24, true, preference -> { - menuDialogFragment.close(false); - emulationActivity.showSaveStateMenu(true); - return true; - }); - createPreference(R.string.emulation_menu_toggle_fast_forward, R.drawable.ic_baseline_fast_forward_24, !cheevosChallengeModeEnabled, preference -> { - AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled()); - menuDialogFragment.close(true); - return true; - }); - createPreference(R.string.emulation_menu_achievements, R.drawable.ic_baseline_trophy_24, cheevosActive, preference -> { - menuDialogFragment.close(false); - emulationActivity.showAchievementsPopup(); - return true; - }); - createPreference(R.string.emulation_menu_exit_game, R.drawable.ic_baseline_exit_to_app_24, true, preference -> { - menuDialogFragment.close(false); - emulationActivity.mStopRequested = true; - emulationActivity.finish(); - return true; - }); - createPreference(R.string.emulation_menu_patch_codes, R.drawable.ic_baseline_tips_and_updates_24, !cheevosChallengeModeEnabled, preference -> { - menuDialogFragment.close(false); - emulationActivity.showPatchesMenu(); - return true; - }); - createPreference(R.string.emulation_menu_change_disc, R.drawable.ic_baseline_album_24, true, preference -> { - menuDialogFragment.close(false); - emulationActivity.showDiscChangeMenu(); - return true; - }); - createPreference(R.string.emulation_menu_touchscreen_controller_settings, R.drawable.ic_baseline_touch_app_24, true, preference -> { - menuDialogFragment.close(false); - emulationActivity.showTouchscreenControllerMenu(); - return true; - }); - createPreference(R.string.emulation_menu_toggle_analog_mode, R.drawable.ic_baseline_gamepad_24, true, preference -> { - AndroidHostInterface.getInstance().toggleControllerAnalogMode(); - menuDialogFragment.close(true); - return true; - }); - createPreference(R.string.emulation_menu_reset_console, R.drawable.ic_baseline_restart_alt_24, true, preference -> { - AndroidHostInterface.getInstance().resetSystem(); - menuDialogFragment.close(true); - return true; - }); - } - - private void createPreference(int titleId, int icon, boolean enabled, Preference.OnPreferenceClickListener action) { - final Preference preference = new Preference(getContext()); - preference.setTitle(titleId); - preference.setIcon(icon); - preference.setOnPreferenceClickListener(action); - preference.setEnabled(enabled); - getPreferenceScreen().addPreference(preference); - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java deleted file mode 100644 index 2305caf16..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java +++ /dev/null @@ -1,302 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.os.Vibrator; -import android.util.AttributeSet; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.SurfaceView; - -import java.util.ArrayList; -import java.util.List; - -public class EmulationSurfaceView extends SurfaceView { - public EmulationSurfaceView(Context context) { - super(context); - } - - public EmulationSurfaceView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EmulationSurfaceView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public static boolean isBindableDevice(InputDevice inputDevice) { - if (inputDevice == null) - return false; - - // Accept all devices with an axis or buttons, filter in events. - final int sources = inputDevice.getSources(); - return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) || - ((sources & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON); - } - - public static boolean isGamepadDevice(InputDevice inputDevice) { - final int sources = (inputDevice != null) ? inputDevice.getSources() : 0; - return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); - } - - public static boolean isJoystickMotionEvent(MotionEvent event) { - final int source = event.getSource(); - return ((source & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK); - } - - public static boolean isBindableKeyEvent(KeyEvent event) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_HOME: - case KeyEvent.KEYCODE_POWER: - // We're okay if we get these from a gamepad. - return isGamepadDevice(event.getDevice()); - - default: - return true; - } - } - - private static boolean isSystemKeyCode(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_MENU: - case KeyEvent.KEYCODE_SOFT_RIGHT: - case KeyEvent.KEYCODE_HOME: - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_CALL: - case KeyEvent.KEYCODE_ENDCALL: - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_MUTE: - case KeyEvent.KEYCODE_MUTE: - case KeyEvent.KEYCODE_POWER: - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY: - case KeyEvent.KEYCODE_MEDIA_PAUSE: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MEDIA_STOP: - case KeyEvent.KEYCODE_MEDIA_NEXT: - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - case KeyEvent.KEYCODE_MEDIA_REWIND: - case KeyEvent.KEYCODE_MEDIA_RECORD: - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - case KeyEvent.KEYCODE_CAMERA: - case KeyEvent.KEYCODE_FOCUS: - case KeyEvent.KEYCODE_SEARCH: - case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: - case KeyEvent.KEYCODE_BRIGHTNESS_UP: - case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: - case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP: - case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN: - case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT: - case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: - return true; - - default: - return false; - } - } - - private class InputDeviceData { - private int deviceId; - private String descriptor; - private int[] axes; - private float[] axisValues; - private int controllerIndex; - private Vibrator vibrator; - - public InputDeviceData(InputDevice device, int controllerIndex) { - deviceId = device.getId(); - descriptor = device.getDescriptor(); - this.controllerIndex = controllerIndex; - - List motionRanges = device.getMotionRanges(); - if (motionRanges != null && !motionRanges.isEmpty()) { - axes = new int[motionRanges.size()]; - axisValues = new float[motionRanges.size()]; - for (int i = 0; i < motionRanges.size(); i++) - axes[i] = motionRanges.get(i).getAxis(); - } - - // device.getVibrator() always returns null, but might return a "null vibrator". - final Vibrator potentialVibrator = device.getVibrator(); - if (potentialVibrator != null && potentialVibrator.hasVibrator()) - vibrator = potentialVibrator; - } - } - - private InputDeviceData[] mInputDevices = null; - private String[] mControllerDescriptors = null; - private boolean mHasAnyGamepads = false; - - public boolean hasAnyGamePads() { - return mHasAnyGamepads; - } - - public synchronized void updateInputDevices() { - mInputDevices = null; - mControllerDescriptors = null; - mHasAnyGamepads = false; - - final ArrayList inputDeviceIds = new ArrayList<>(); - final ArrayList controllerDescriptors = new ArrayList<>(); - - for (int deviceId : InputDevice.getDeviceIds()) { - final InputDevice device = InputDevice.getDevice(deviceId); - if (device == null || !isBindableDevice(device)) { - Log.d("EmulationSurfaceView", - String.format("Skipping device %s sources %d", - (device != null) ? device.toString() : "", - (device != null) ? device.getSources() : 0)); - continue; - } - - if (isGamepadDevice(device)) - mHasAnyGamepads = true; - - // Some phones seem to have duplicate descriptors for multiple devices. - // Combine them all into one controller index if so. - final String descriptor = device.getDescriptor(); - int controllerIndex = controllerDescriptors.size(); - for (int i = 0; i < controllerDescriptors.size(); i++) { - if (controllerDescriptors.get(i).equals(descriptor)) { - controllerIndex = i; - break; - } - } - if (controllerIndex == controllerDescriptors.size()) { - controllerDescriptors.add(descriptor); - } - - Log.d("EmulationSurfaceView", String.format("Tracking device %d/%s (%s, sources %d, controller %d)", - controllerIndex, descriptor, device.getName(), device.getSources(), controllerIndex)); - inputDeviceIds.add(new InputDeviceData(device, controllerIndex)); - } - - if (inputDeviceIds.isEmpty()) - return; - - mInputDevices = new InputDeviceData[inputDeviceIds.size()]; - inputDeviceIds.toArray(mInputDevices); - - mControllerDescriptors = new String[controllerDescriptors.size()]; - controllerDescriptors.toArray(mControllerDescriptors); - } - - public synchronized String[] getInputDeviceNames() { - return mControllerDescriptors; - } - - public synchronized boolean hasInputDeviceVibration(int controllerIndex) { - if (mInputDevices == null || controllerIndex >= mInputDevices.length) - return false; - - return (mInputDevices[controllerIndex].vibrator != null); - } - - public synchronized void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) { - if (mInputDevices == null || controllerIndex >= mInputDevices.length) - return; - - // shouldn't get here - final InputDeviceData data = mInputDevices[controllerIndex]; - if (data.vibrator == null) - return; - - final float MINIMUM_INTENSITY = 0.1f; - if (smallMotor >= MINIMUM_INTENSITY || largeMotor >= MINIMUM_INTENSITY) - data.vibrator.vibrate(1000); - else - data.vibrator.cancel(); - } - - public InputDeviceData getDataForDeviceId(int deviceId) { - if (mInputDevices == null) - return null; - - for (InputDeviceData data : mInputDevices) { - if (data.deviceId == deviceId) - return data; - } - - return null; - } - - public int getControllerIndexForDeviceId(int deviceId) { - final InputDeviceData data = getDataForDeviceId(deviceId); - return (data != null) ? data.controllerIndex : -1; - } - - private boolean handleKeyEvent(int deviceId, int repeatCount, int keyCode, boolean pressed) { - final int controllerIndex = getControllerIndexForDeviceId(deviceId); - Log.d("EmulationSurfaceView", String.format("Controller %d Code %d RC %d Pressed %d", - controllerIndex, keyCode, repeatCount, pressed? 1 : 0)); - - final AndroidHostInterface hi = AndroidHostInterface.getInstance(); - if (repeatCount == 0 && controllerIndex >= 0) - hi.handleControllerButtonEvent(controllerIndex, keyCode, pressed); - - // We don't want to eat external button events unless it's actually bound. - if (isSystemKeyCode(keyCode)) - return (controllerIndex >= 0 && hi.hasControllerButtonBinding(controllerIndex, keyCode)); - else - return true; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return handleKeyEvent(event.getDeviceId(), event.getRepeatCount(), keyCode, true); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return handleKeyEvent(event.getDeviceId(), 0, keyCode, false); - } - - private float clamp(float value, float min, float max) { - return (value < min) ? min : ((value > max) ? max : value); - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - if (!isJoystickMotionEvent(event)) - return false; - - final InputDeviceData data = getDataForDeviceId(event.getDeviceId()); - if (data == null || data.axes == null) - return false; - - for (int i = 0; i < data.axes.length; i++) { - final int axis = data.axes[i]; - final float axisValue = event.getAxisValue(axis); - float emuValue; - - switch (axis) { - case MotionEvent.AXIS_BRAKE: - case MotionEvent.AXIS_GAS: - case MotionEvent.AXIS_LTRIGGER: - case MotionEvent.AXIS_RTRIGGER: - // Scale 0..1 -> -1..1. - emuValue = (clamp(axisValue, 0.0f, 1.0f) * 2.0f) - 1.0f; - break; - - default: - // Everything else should already by -1..1 as per Android documentation. - emuValue = clamp(axisValue, -1.0f, 1.0f); - break; - } - - if (data.axisValues[i] == emuValue) - continue; - - Log.d("EmulationSurfaceView", - String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue)); - - data.axisValues[i] = emuValue; - AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue); - } - - return true; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationThread.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationThread.java deleted file mode 100644 index 0bdcf25d9..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationThread.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.Build; -import android.os.Process; -import android.util.Log; -import android.view.Surface; - -import androidx.annotation.NonNull; - -public class EmulationThread extends Thread { - private EmulationActivity emulationActivity; - private String filename; - private boolean resumeState; - private String stateFilename; - - private EmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) { - super("EmulationThread"); - this.emulationActivity = emulationActivity; - this.filename = filename; - this.resumeState = resumeState; - this.stateFilename = stateFilename; - } - - public static EmulationThread create(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) { - Log.i("EmulationThread", String.format("Starting emulation thread (%s)...", filename)); - - EmulationThread thread = new EmulationThread(emulationActivity, filename, resumeState, stateFilename); - thread.start(); - return thread; - } - - private void setExclusiveCores() { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - int[] cores = Process.getExclusiveCores(); - if (cores == null || cores.length == 0) - throw new Exception("Invalid return value from getExclusiveCores()"); - - AndroidHostInterface.setThreadAffinity(cores); - } - } catch (Exception e) { - Log.e("EmulationThread", "getExclusiveCores() failed"); - } - } - - @Override - public void run() { - try { - Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE); - setExclusiveCores(); - } catch (Exception e) { - Log.i("EmulationThread", "Failed to set priority for emulation thread: " + e.getMessage()); - } - - AndroidHostInterface.getInstance().runEmulationThread(emulationActivity, filename, resumeState, stateFilename); - Log.i("EmulationThread", "Emulation thread exiting."); - } - - public void stopAndJoin() { - AndroidHostInterface.getInstance().stopEmulationThreadLoop(); - try { - join(); - } catch (InterruptedException e) { - Log.i("EmulationThread", "join() interrupted: " + e.getMessage()); - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java deleted file mode 100644 index 258350d0d..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java +++ /dev/null @@ -1,613 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.ImageDecoder; -import android.net.Uri; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.storage.StorageManager; -import android.provider.DocumentsContract; -import android.provider.MediaStore; - -import androidx.annotation.Nullable; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Array; -import java.lang.reflect.Method; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * File helper class - used to bridge native code to Java storage access framework APIs. - */ -public class FileHelper { - /** - * Native filesystem flags. - */ - public static final int FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY = 1; - public static final int FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY = 2; - public static final int FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED = 4; - - /** - * Native filesystem find result flags. - */ - public static final int FILESYSTEM_FIND_RECURSIVE = (1 << 0); - public static final int FILESYSTEM_FIND_RELATIVE_PATHS = (1 << 1); - public static final int FILESYSTEM_FIND_HIDDEN_FILES = (1 << 2); - public static final int FILESYSTEM_FIND_FOLDERS = (1 << 3); - public static final int FILESYSTEM_FIND_FILES = (1 << 4); - public static final int FILESYSTEM_FIND_KEEP_ARRAY = (1 << 5); - - /** - * Projection used when searching for files. - */ - private static final String[] findProjection = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED - }; - - /** - * Projection used when getting the display name for a file. - */ - private static final String[] getDisplayNameProjection = new String[]{ - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - }; - - /** - * Projection used when getting a relative file for a file. - */ - private static final String[] getRelativeFileProjection = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - }; - - private final Context context; - private final ContentResolver contentResolver; - - /** - * File helper class - used to bridge native code to Java storage access framework APIs. - * - * @param context Context in which to perform file actions as. - */ - public FileHelper(Context context) { - this.context = context; - this.contentResolver = context.getContentResolver(); - } - - /** - * Reads the specified file as a string, under the specified context. - * - * @param context context to access file under - * @param uri uri to write data to - * @param maxSize maximum file size to read - * @return String containing the file data, otherwise null - */ - public static String readStringFromUri(final Context context, final Uri uri, int maxSize) { - InputStream stream = null; - try { - stream = context.getContentResolver().openInputStream(uri); - } catch (FileNotFoundException e) { - return null; - } - - StringBuilder os = new StringBuilder(); - try { - char[] buffer = new char[1024]; - InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name())); - int len; - while ((len = reader.read(buffer)) > 0) { - os.append(buffer, 0, len); - if (os.length() > maxSize) - return null; - } - - stream.close(); - } catch (IOException e) { - return null; - } - - if (os.length() == 0) - return null; - - return os.toString(); - } - - /** - * Reads the specified file as a byte array, under the specified context. - * - * @param context context to access file under - * @param uri uri to write data to - * @param maxSize maximum file size to read - * @return byte array containing the file data, otherwise null - */ - public static byte[] readBytesFromUri(final Context context, final Uri uri, int maxSize) { - InputStream stream = null; - try { - stream = context.getContentResolver().openInputStream(uri); - } catch (FileNotFoundException e) { - return null; - } - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try { - byte[] buffer = new byte[512 * 1024]; - int len; - while ((len = stream.read(buffer)) > 0) { - os.write(buffer, 0, len); - if (maxSize > 0 && os.size() > maxSize) { - return null; - } - } - - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - - if (os.size() == 0) - return null; - - return os.toByteArray(); - } - - /** - * Writes the specified data to a file referenced by the URI, as the specified context. - * - * @param context context to access file under - * @param uri uri to write data to - * @param bytes data to write file to - * @return true if write was succesful, otherwise false - */ - public static boolean writeBytesToUri(final Context context, final Uri uri, final byte[] bytes) { - OutputStream stream = null; - try { - stream = context.getContentResolver().openOutputStream(uri); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } - - if (bytes != null && bytes.length > 0) { - try { - stream.write(bytes); - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - - return true; - } - - /** - * Deletes the file referenced by the URI, under the specified context. - * - * @param context context to delete file under - * @param uri uri to delete - * @return - */ - public static boolean deleteFileAtUri(final Context context, final Uri uri) { - try { - if (uri.getScheme() == "file") { - final File file = new File(uri.getPath()); - if (!file.isFile()) - return false; - - return file.delete(); - } - return (context.getContentResolver().delete(uri, null, null) > 0); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - /** - * Returns the name of the file pointed at by a SAF URI. - * - * @param context context to access file under - * @param uri uri to retrieve file name for - * @return the name of the file, or null - */ - public static String getDocumentNameFromUri(final Context context, final Uri uri) { - Cursor cursor = null; - try { - final String[] proj = {DocumentsContract.Document.COLUMN_DISPLAY_NAME}; - cursor = context.getContentResolver().query(uri, proj, null, null, null); - final int columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME); - cursor.moveToFirst(); - return cursor.getString(columnIndex); - } catch (Exception e) { - e.printStackTrace(); - return null; - } finally { - if (cursor != null) - cursor.close(); - } - } - - /** - * Loads a bitmap from the provided SAF URI. - * - * @param context context to access file under - * @param uri uri to retrieve file name for - * @return a decoded bitmap for the file, or null - */ - public static Bitmap loadBitmapFromUri(final Context context, final Uri uri) { - InputStream stream = null; - try { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - final ImageDecoder.Source source = ImageDecoder.createSource(context.getContentResolver(), uri); - return ImageDecoder.decodeBitmap(source); - } else { - return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri); - } - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - /** - * Returns the file name component of a path or URI. - * - * @param path Path/URI to examine. - * @return File name component of path/URI. - */ - public static String getFileNameForPath(String path) { - if (path.startsWith("content:/") || path.startsWith("file:/")) { - try { - final Uri uri = Uri.parse(path); - final String lastPathSegment = uri.getLastPathSegment(); - if (lastPathSegment != null) - path = lastPathSegment; - } catch (Exception e) { - } - } - - int lastSlash = path.lastIndexOf('/'); - if (lastSlash > 0 && lastSlash < path.length() - 1) - return path.substring(lastSlash + 1); - else - return path; - } - - /** - * Test if the given URI represents a {@link DocumentsContract.Document} tree. - */ - public static boolean isTreeUri(Uri uri) { - final List paths = uri.getPathSegments(); - return (paths.size() >= 2 && paths.get(0).equals("tree")); - } - - /** - * Retrieves a file descriptor for a content URI string. Called by native code. - * - * @param uriString string of the URI to open - * @param mode Java open mode - * @return file descriptor for URI, or -1 - */ - public int openURIAsFileDescriptor(String uriString, String mode) { - try { - final Uri uri = Uri.parse(uriString); - final ParcelFileDescriptor fd = contentResolver.openFileDescriptor(uri, mode); - if (fd == null) - return -1; - return fd.detachFd(); - } catch (Exception e) { - return -1; - } - } - - /** - * Recursively iterates documents in the specified tree, searching for files. - * - * @param treeUri Root tree in which to search for documents. - * @param documentId Document ID representing the directory to start searching. - * @param flags Native search flags. - * @param results Cumulative result array. - */ - private void doFindFiles(Uri treeUri, String documentId, int flags, ArrayList results) { - try { - final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId); - final Cursor cursor = contentResolver.query(queryUri, findProjection, null, null, null); - final int count = cursor.getCount(); - - while (cursor.moveToNext()) { - try { - final String mimeType = cursor.getString(2); - final String childDocumentId = cursor.getString(0); - final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId); - final long size = cursor.getLong(3); - final long lastModified = cursor.getLong(4); - - if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { - if ((flags & FILESYSTEM_FIND_FOLDERS) != 0) { - results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)); - } - - if ((flags & FILESYSTEM_FIND_RECURSIVE) != 0) - doFindFiles(treeUri, childDocumentId, flags, results); - } else { - if ((flags & FILESYSTEM_FIND_FILES) != 0) { - results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, 0)); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - cursor.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Recursively iterates documents in the specified URI, searching for files. - * - * @param uriString URI containing directory to search. - * @param flags Native filter flags. - * @return Array of find results. - */ - public FindResult[] findFiles(String uriString, int flags) { - try { - final Uri fullUri = Uri.parse(uriString); - final String documentId = DocumentsContract.getTreeDocumentId(fullUri); - final ArrayList results = new ArrayList<>(); - doFindFiles(fullUri, documentId, flags, results); - if (results.isEmpty()) - return null; - - final FindResult[] resultsArray = new FindResult[results.size()]; - results.toArray(resultsArray); - return resultsArray; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - /** - * Returns the display name for the given URI. - * - * @param uriString URI to resolve display name for. - * @return display name for the URI, or null. - */ - public String getDisplayNameForURIPath(String uriString) { - try { - final Uri fullUri = Uri.parse(uriString); - final Cursor cursor = contentResolver.query(fullUri, getDisplayNameProjection, - null, null, null); - if (cursor.getCount() == 0 || !cursor.moveToNext()) - return null; - - return cursor.getString(0); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - /** - * Returns the path for a sibling file relative to another URI. - * - * @param uriString URI to find the file relative to. - * @param newFileName Sibling file name. - * @return URI for the sibling file name, or null. - */ - public String getRelativePathForURIPath(String uriString, String newFileName) { - try { - final Uri fullUri = Uri.parse(uriString); - - // if this is a document (expected)... - Uri treeUri; - String treeDocId; - if (DocumentsContract.isDocumentUri(context, fullUri)) { - // we need to remove the last part of the URI (the specific document ID) to get the parent - final String lastPathSegment = fullUri.getLastPathSegment(); - int lastSeparatorIndex = lastPathSegment.lastIndexOf('/'); - if (lastSeparatorIndex < 0) - lastSeparatorIndex = lastPathSegment.lastIndexOf(':'); - if (lastSeparatorIndex < 0) - return null; - - // the parent becomes the document ID - treeDocId = lastPathSegment.substring(0, lastSeparatorIndex); - - // but, we need to access it through the subtree if this was a tree URI (permissions...) - if (isTreeUri(fullUri)) { - treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), DocumentsContract.getTreeDocumentId(fullUri)); - } else { - treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), treeDocId); - } - } else { - treeDocId = DocumentsContract.getDocumentId(fullUri); - treeUri = fullUri; - } - - final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, treeDocId); - final Cursor cursor = contentResolver.query(queryUri, getRelativeFileProjection, null, null, null); - final int count = cursor.getCount(); - - while (cursor.moveToNext()) { - try { - final String displayName = cursor.getString(1); - if (!displayName.equalsIgnoreCase(newFileName)) - continue; - - final String childDocumentId = cursor.getString(0); - final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId); - cursor.close(); - return uri.toString(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - cursor.close(); - return null; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - private static final String PRIMARY_VOLUME_NAME = "primary"; - - @Nullable - public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { - if (treeUri == null) return null; - String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); - if (volumePath == null) return File.separator; - if (volumePath.endsWith(File.separator)) - volumePath = volumePath.substring(0, volumePath.length() - 1); - - String documentPath = getDocumentPathFromTreeUri(treeUri); - if (documentPath.endsWith(File.separator)) - documentPath = documentPath.substring(0, documentPath.length() - 1); - - if (documentPath.length() > 0) { - if (documentPath.startsWith(File.separator)) - return volumePath + documentPath; - else - return volumePath + File.separator + documentPath; - } else return volumePath; - } - - @SuppressLint("ObsoleteSdkInt") - private static String getVolumePath(final String volumeId, Context context) { - if (volumeId == null) - return null; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null; - try { - StorageManager mStorageManager = - (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); - Class storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); - Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); - Method getUuid = storageVolumeClazz.getMethod("getUuid"); - Method getPath = storageVolumeClazz.getMethod("getPath"); - Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); - Object result = getVolumeList.invoke(mStorageManager); - - final int length = Array.getLength(result); - for (int i = 0; i < length; i++) { - Object storageVolumeElement = Array.get(result, i); - String uuid = (String) getUuid.invoke(storageVolumeElement); - Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); - - // primary volume? - if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) - return (String) getPath.invoke(storageVolumeElement); - - // other volumes? - if (uuid != null && uuid.equals(volumeId)) - return (String) getPath.invoke(storageVolumeElement); - } - // not found. - return null; - } catch (Exception ex) { - return null; - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static String getVolumeIdFromTreeUri(final Uri treeUri) { - final String docId = DocumentsContract.getTreeDocumentId(treeUri); - final String[] split = docId.split(":"); - if (split.length > 0) return split[0]; - else return null; - } - - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static String getDocumentPathFromTreeUri(final Uri treeUri) { - final String docId = DocumentsContract.getTreeDocumentId(treeUri); - final String[] split = docId.split(":"); - if ((split.length >= 2) && (split[1] != null)) return split[1]; - else return File.separator; - } - - @Nullable - public static String getFullPathFromUri(@Nullable final Uri treeUri, Context con) { - if (treeUri == null) return null; - String volumePath = getVolumePath(getVolumeIdFromUri(treeUri), con); - if (volumePath == null) return File.separator; - if (volumePath.endsWith(File.separator)) - volumePath = volumePath.substring(0, volumePath.length() - 1); - - String documentPath = getDocumentPathFromUri(treeUri); - if (documentPath.endsWith(File.separator)) - documentPath = documentPath.substring(0, documentPath.length() - 1); - - if (documentPath.length() > 0) { - if (documentPath.startsWith(File.separator)) - return volumePath + documentPath; - else - return volumePath + File.separator + documentPath; - } else return volumePath; - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static String getVolumeIdFromUri(final Uri treeUri) { - try { - final String docId = DocumentsContract.getDocumentId(treeUri); - final String[] split = docId.split(":"); - if (split.length > 0) return split[0]; - else return null; - } catch (Exception e) { - return null; - } - } - - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static String getDocumentPathFromUri(final Uri treeUri) { - try { - final String docId = DocumentsContract.getDocumentId(treeUri); - final String[] split = docId.split(":"); - if ((split.length >= 2) && (split[1] != null)) return split[1]; - else return File.separator; - } catch (Exception e) { - return null; - } - } - - /** - * Java class containing the data for a file in a find operation. - */ - public static class FindResult { - public String name; - public String relativeName; - public long size; - public long modifiedTime; - public int flags; - - public FindResult(String relativeName, String name, long size, long modifiedTime, int flags) { - this.relativeName = relativeName; - this.name = name; - this.size = size; - this.modifiedTime = modifiedTime; - this.flags = flags; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java deleted file mode 100644 index 4c1bd85b2..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java +++ /dev/null @@ -1,348 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.os.FileUtils; -import android.util.Log; -import android.util.Property; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.ListAdapter; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.ListFragment; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Set; - -public class GameDirectoriesActivity extends AppCompatActivity { - private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 1; - private static final String FORCE_SAF_CONFIG_KEY = "GameList/ForceStorageAccessFramework"; - - private class DirectoryListAdapter extends RecyclerView.Adapter { - private class Entry { - private String mPath; - private boolean mRecursive; - - public Entry(String path, boolean recursive) { - mPath = path; - mRecursive = recursive; - } - - public String getPath() { - return mPath; - } - - public boolean isRecursive() { - return mRecursive; - } - - public void toggleRecursive() { - mRecursive = !mRecursive; - } - } - - private class EntryComparator implements Comparator { - @Override - public int compare(Entry left, Entry right) { - return left.getPath().compareTo(right.getPath()); - } - } - - private Context mContext; - private Entry[] mEntries; - - public DirectoryListAdapter(Context context) { - mContext = context; - reload(); - } - - public void reload() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); - ArrayList entries = new ArrayList<>(); - - try { - Set paths = prefs.getStringSet("GameList/Paths", null); - if (paths != null) { - for (String path : paths) - entries.add(new Entry(path, false)); - } - } catch (Exception e) { - } - - try { - Set paths = prefs.getStringSet("GameList/RecursivePaths", null); - if (paths != null) { - for (String path : paths) - entries.add(new Entry(path, true)); - } - } catch (Exception e) { - } - - mEntries = new Entry[entries.size()]; - entries.toArray(mEntries); - Arrays.sort(mEntries, new EntryComparator()); - notifyDataSetChanged(); - } - - private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private int mPosition; - private Entry mEntry; - private TextView mPathView; - private TextView mRecursiveView; - private ImageButton mToggleRecursiveView; - private ImageButton mRemoveView; - - public ViewHolder(View rootView) { - super(rootView); - mPathView = rootView.findViewById(R.id.path); - mRecursiveView = rootView.findViewById(R.id.recursive); - mToggleRecursiveView = rootView.findViewById(R.id.toggle_recursive); - mToggleRecursiveView.setOnClickListener(this); - mRemoveView = rootView.findViewById(R.id.remove); - mRemoveView.setOnClickListener(this); - } - - public void bindData(int position, Entry entry) { - mPosition = position; - mEntry = entry; - updateText(); - } - - private void updateText() { - mPathView.setText(mEntry.getPath()); - mRecursiveView.setText(getString(mEntry.isRecursive() ? R.string.game_directories_scanning_subdirectories : R.string.game_directories_not_scanning_subdirectories)); - mToggleRecursiveView.setImageDrawable(getDrawable(mEntry.isRecursive() ? R.drawable.ic_baseline_folder_24 : R.drawable.ic_baseline_folder_open_24)); - } - - @Override - public void onClick(View v) { - if (mToggleRecursiveView == v) { - removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive()); - mEntry.toggleRecursive(); - addSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive()); - updateText(); - } else if (mRemoveView == v) { - removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive()); - reload(); - } - } - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((ViewHolder) holder).bindData(position, mEntries[position]); - } - - @Override - public int getItemViewType(int position) { - return R.layout.layout_game_directory_entry; - } - - @Override - public long getItemId(int position) { - return mEntries[position].getPath().hashCode(); - } - - @Override - public int getItemCount() { - return mEntries.length; - } - } - - DirectoryListAdapter mDirectoryListAdapter; - RecyclerView mRecyclerView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_game_directories); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - - mDirectoryListAdapter = new DirectoryListAdapter(this); - mRecyclerView = findViewById(R.id.recycler_view); - mRecyclerView.setAdapter(mDirectoryListAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(), - DividerItemDecoration.VERTICAL)); - - findViewById(R.id.fab).setOnClickListener((v) -> startAddGameDirectory()); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_edit_game_directories, menu); - - menu.findItem(R.id.force_saf) - .setEnabled(android.os.Build.VERSION.SDK_INT < 30) - .setChecked(PreferenceManager.getDefaultSharedPreferences(this).getBoolean( - FORCE_SAF_CONFIG_KEY, false)) - .setOnMenuItemClickListener(item -> { - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(this); - final boolean newValue =!sharedPreferences.getBoolean( - FORCE_SAF_CONFIG_KEY, false); - sharedPreferences.edit() - .putBoolean(FORCE_SAF_CONFIG_KEY, newValue) - .commit(); - item.setChecked(newValue); - return true; - }); - - return true; - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - if (item.getItemId() == R.id.add_directory) { - startAddGameDirectory(); - return true; - } else if (item.getItemId() == R.id.add_path) { - startAddPath(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - public static boolean useStorageAccessFramework(Context context) { - // Use legacy storage on devices older than Android 11... apparently some of them - // have broken storage access framework.... - if (android.os.Build.VERSION.SDK_INT >= 30) - return true; - - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean( - "GameList/ForceStorageAccessFramework", false); - } - - public static void addSearchDirectory(Context context, String path, boolean recursive) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths"; - PreferenceHelpers.addToStringList(prefs, key, path); - Log.i("GameDirectoriesActivity", "Added path '" + path + "' to game list search directories"); - } - - public static void removeSearchDirectory(Context context, String path, boolean recursive) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths"; - PreferenceHelpers.removeFromStringList(prefs, key, path); - Log.i("GameDirectoriesActivity", "Removed path '" + path + "' from game list search directories"); - } - - private void startAddGameDirectory() { - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)), - REQUEST_ADD_DIRECTORY_TO_GAME_LIST); - } - - private void startAddPath() { - final EditText text = new EditText(this); - text.setSingleLine(); - - new AlertDialog.Builder(this) - .setTitle(R.string.edit_game_directories_add_path) - .setMessage(R.string.edit_game_directories_add_path_summary) - .setView(text) - .setPositiveButton("Add", (dialog, which) -> { - final String path = text.getText().toString(); - if (!path.isEmpty()) { - addSearchDirectory(GameDirectoriesActivity.this, path, true); - mDirectoryListAdapter.reload(); - } - }) - .setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()) - .show(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: { - if (resultCode != RESULT_OK || data.getData() == null) - return; - - // Use legacy storage on devices older than Android 11... apparently some of them - // have broken storage access framework.... - if (!useStorageAccessFramework(this)) { - final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this); - if (path != null) { - addSearchDirectory(this, path, true); - mDirectoryListAdapter.reload(); - return; - } - } - - try { - getContentResolver().takePersistableUriPermission(data.getData(), - Intent.FLAG_GRANT_READ_URI_PERMISSION); - } catch (Exception e) { - Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG); - e.printStackTrace(); - } - - addSearchDirectory(this, data.getDataString(), true); - mDirectoryListAdapter.reload(); - } - break; - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java deleted file mode 100644 index d72b80501..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; - -public class GameGridFragment extends Fragment implements GameList.OnRefreshListener { - private static final int SPACING_DIPS = 25; - private static final int WIDTH_DIPS = 160; - - private final MainActivity mParent; - private RecyclerView mRecyclerView; - private ViewAdapter mAdapter; - private GridAutofitLayoutManager mLayoutManager; - - public GameGridFragment(MainActivity parent) { - super(R.layout.fragment_game_grid); - this.mParent = parent; - } - - private GameList getGameList() { - return mParent.getGameList(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mAdapter = new ViewAdapter(mParent, getGameList()); - getGameList().addRefreshListener(this); - - mRecyclerView = view.findViewById(R.id.game_list_view); - mRecyclerView.setAdapter(mAdapter); - - final int columnWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - WIDTH_DIPS + SPACING_DIPS, getResources().getDisplayMetrics())); - mLayoutManager = new GridAutofitLayoutManager(getContext(), columnWidth); - mRecyclerView.setLayoutManager(mLayoutManager); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - getGameList().removeRefreshListener(this); - } - - @Override - public void onGameListRefresh() { - mAdapter.notifyDataSetChanged(); - } - - private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { - private final MainActivity mParent; - private final ImageView mImageView; - private GameListEntry mEntry; - - public ViewHolder(@NonNull MainActivity parent, @NonNull View itemView) { - super(itemView); - mParent = parent; - mImageView = itemView.findViewById(R.id.imageView); - mImageView.setOnClickListener(this); - mImageView.setOnLongClickListener(this); - } - - public void bindToEntry(GameListEntry entry) { - mEntry = entry; - - // while it loads/generates - mImageView.setImageDrawable(ContextCompat.getDrawable(mParent, R.drawable.ic_media_cdrom)); - - final String coverPath = entry.getCoverPath(); - if (coverPath == null) { - new GenerateCoverTask(mParent, mImageView, mEntry.getTitle()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - return; - } - - new ImageLoadTask(mImageView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coverPath); - } - - @Override - public void onClick(View v) { - mParent.startEmulation(mEntry.getPath(), mParent.shouldResumeStateByDefault()); - } - - @Override - public boolean onLongClick(View v) { - mParent.openGamePopupMenu(v, mEntry); - return true; - } - } - - private static class ViewAdapter extends RecyclerView.Adapter { - private final MainActivity mParent; - private final LayoutInflater mInflater; - private final GameList mGameList; - - public ViewAdapter(@NonNull MainActivity parent, @NonNull GameList gameList) { - mParent = parent; - mInflater = LayoutInflater.from(parent); - mGameList = gameList; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ViewHolder(mParent, mInflater.inflate(R.layout.layout_game_grid_entry, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - GameListEntry entry = mGameList.getEntry(position); - holder.bindToEntry(entry); - } - - @Override - public int getItemCount() { - return mGameList.getEntryCount(); - } - - @Override - public int getItemViewType(int position) { - return R.layout.layout_game_grid_entry; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java deleted file mode 100644 index f835dce22..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.Activity; -import android.os.AsyncTask; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; - -public class GameList { - public interface OnRefreshListener { - void onGameListRefresh(); - } - - private Activity mContext; - private GameListEntry[] mEntries; - private ArrayList mRefreshListeners = new ArrayList<>(); - - public GameList(Activity context) { - mContext = context; - mEntries = new GameListEntry[0]; - } - - public void addRefreshListener(OnRefreshListener listener) { - mRefreshListeners.add(listener); - } - public void removeRefreshListener(OnRefreshListener listener) { - mRefreshListeners.remove(listener); - } - public void fireRefreshListeners() { - for (OnRefreshListener listener : mRefreshListeners) - listener.onGameListRefresh(); - } - - private class GameListEntryComparator implements Comparator { - @Override - public int compare(GameListEntry left, GameListEntry right) { - return left.getTitle().compareTo(right.getTitle()); - } - } - - public void refresh(boolean invalidateCache, boolean invalidateDatabase, Activity parentActivity) { - // Search and get entries from native code - AndroidProgressCallback progressCallback = new AndroidProgressCallback(mContext); - AsyncTask.execute(() -> { - AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase, progressCallback); - GameListEntry[] newEntries = AndroidHostInterface.getInstance().getGameListEntries(); - Arrays.sort(newEntries, new GameListEntryComparator()); - - mContext.runOnUiThread(() -> { - try { - progressCallback.dismiss(); - } catch (Exception e) { - Log.e("GameList", "Exception dismissing refresh progress"); - e.printStackTrace(); - } - mEntries = newEntries; - fireRefreshListeners(); - }); - }); - } - - public int getEntryCount() { - return mEntries.length; - } - - public GameListEntry getEntry(int index) { - return mEntries[index]; - } - - public GameListEntry getEntryForPath(String path) { - for (final GameListEntry entry : mEntries) { - if (entry.getPath().equals(path)) - return entry; - } - - return null; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java deleted file mode 100644 index 95e5674b5..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.AsyncTask; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.content.ContextCompat; - -public class GameListEntry { - public enum EntryType { - Disc, - PSExe, - Playlist, - PSF - } - - public enum CompatibilityRating { - Unknown, - DoesntBoot, - CrashesInIntro, - CrashesInGame, - GraphicalAudioIssues, - NoIssues, - } - - private String mPath; - private String mCode; - private String mTitle; - private long mSize; - private String mModifiedTime; - private DiscRegion mRegion; - private EntryType mType; - private CompatibilityRating mCompatibilityRating; - private String mCoverPath; - - public GameListEntry(String path, String code, String title, long size, String modifiedTime, String region, - String type, String compatibilityRating, String coverPath) { - mPath = path; - mCode = code; - mTitle = title; - mSize = size; - mModifiedTime = modifiedTime; - mCoverPath = coverPath; - - try { - mRegion = DiscRegion.valueOf(region); - } catch (IllegalArgumentException e) { - mRegion = DiscRegion.NTSC_U; - } - - try { - mType = EntryType.valueOf(type); - } catch (IllegalArgumentException e) { - mType = EntryType.Disc; - } - - try { - mCompatibilityRating = CompatibilityRating.valueOf(compatibilityRating); - } catch (IllegalArgumentException e) { - mCompatibilityRating = CompatibilityRating.Unknown; - } - } - - public String getPath() { - return mPath; - } - - public String getCode() { - return mCode; - } - - public String getTitle() { - return mTitle; - } - - public long getSize() { return mSize; } - - public String getModifiedTime() { - return mModifiedTime; - } - - public DiscRegion getRegion() { - return mRegion; - } - - public EntryType getType() { - return mType; - } - - public CompatibilityRating getCompatibilityRating() { - return mCompatibilityRating; - } - - public String getCoverPath() { return mCoverPath; } - - public void setCoverPath(String coverPath) { mCoverPath = coverPath; } - -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java deleted file mode 100644 index 01635d7b5..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class GameListFragment extends Fragment implements GameList.OnRefreshListener { - private MainActivity mParent; - private RecyclerView mRecyclerView; - private GameListFragment.ViewAdapter mAdapter; - - public GameListFragment(MainActivity parent) { - super(R.layout.fragment_game_list); - this.mParent = parent; - } - - private GameList getGameList() { - return mParent.getGameList(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mAdapter = new GameListFragment.ViewAdapter(mParent, getGameList()); - getGameList().addRefreshListener(this); - - mRecyclerView = view.findViewById(R.id.game_list_view); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(mParent)); - mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(), - DividerItemDecoration.VERTICAL)); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - getGameList().removeRefreshListener(this); - } - - @Override - public void onGameListRefresh() { - mAdapter.notifyDataSetChanged(); - } - - private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { - private MainActivity mParent; - private View mItemView; - private GameListEntry mEntry; - - public ViewHolder(@NonNull MainActivity parent, @NonNull View itemView) { - super(itemView); - mParent = parent; - mItemView = itemView; - mItemView.setOnClickListener(this); - mItemView.setOnLongClickListener(this); - } - - private String getSubTitle() { - String fileName = FileHelper.getFileNameForPath(mEntry.getPath()); - String sizeString = String.format("%.2f MB", (double) mEntry.getSize() / 1048576.0); - return String.format("%s (%s)", fileName, sizeString); - } - - public void bindToEntry(GameListEntry entry) { - mEntry = entry; - - ((TextView) mItemView.findViewById(R.id.game_list_view_entry_title)).setText(entry.getTitle()); - ((TextView) mItemView.findViewById(R.id.game_list_view_entry_subtitle)).setText(getSubTitle()); - - int regionDrawableId; - switch (entry.getRegion()) { - case NTSC_J: - regionDrawableId = R.drawable.flag_jp; - break; - case PAL: - regionDrawableId = R.drawable.flag_eu; - break; - case Other: - regionDrawableId = R.drawable.ic_baseline_help_24; - break; - case NTSC_U: - default: - regionDrawableId = R.drawable.flag_us; - break; - } - - ((ImageView) mItemView.findViewById(R.id.game_list_view_entry_region_icon)) - .setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), regionDrawableId)); - - int typeDrawableId; - switch (entry.getType()) { - case PSExe: - typeDrawableId = R.drawable.ic_emblem_system; - break; - - case Playlist: - typeDrawableId = R.drawable.ic_baseline_playlist_play_24; - break; - - case PSF: - typeDrawableId = R.drawable.ic_baseline_library_music_24; - break; - - case Disc: - default: - typeDrawableId = R.drawable.ic_media_cdrom; - break; - } - - ImageView icon = ((ImageView) mItemView.findViewById(R.id.game_list_view_entry_type_icon)); - icon.setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), typeDrawableId)); - - final String coverPath = entry.getCoverPath(); - if (coverPath != null) { - new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coverPath); - } - - int compatibilityDrawableId; - switch (entry.getCompatibilityRating()) { - case DoesntBoot: - compatibilityDrawableId = R.drawable.ic_star_1; - break; - case CrashesInIntro: - compatibilityDrawableId = R.drawable.ic_star_2; - break; - case CrashesInGame: - compatibilityDrawableId = R.drawable.ic_star_3; - break; - case GraphicalAudioIssues: - compatibilityDrawableId = R.drawable.ic_star_4; - break; - case NoIssues: - compatibilityDrawableId = R.drawable.ic_star_5; - break; - case Unknown: - default: - compatibilityDrawableId = R.drawable.ic_star_0; - break; - } - - ((ImageView) mItemView.findViewById(R.id.game_list_view_compatibility_icon)) - .setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), compatibilityDrawableId)); - } - - @Override - public void onClick(View v) { - mParent.startEmulation(mEntry.getPath(), mParent.shouldResumeStateByDefault()); - } - - @Override - public boolean onLongClick(View v) { - mParent.openGamePopupMenu(v, mEntry); - return true; - } - } - - private static class ViewAdapter extends RecyclerView.Adapter { - private MainActivity mParent; - private LayoutInflater mInflater; - private GameList mGameList; - - public ViewAdapter(@NonNull MainActivity parent, @NonNull GameList gameList) { - mParent = parent; - mInflater = LayoutInflater.from(parent); - mGameList = gameList; - } - - @NonNull - @Override - public GameListFragment.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new GameListFragment.ViewHolder(mParent, mInflater.inflate(R.layout.layout_game_list_entry, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull GameListFragment.ViewHolder holder, int position) { - GameListEntry entry = mGameList.getEntry(position); - holder.bindToEntry(entry); - } - - @Override - public int getItemCount() { - return mGameList.getEntryCount(); - } - - @Override - public int getItemViewType(int position) { - return R.layout.layout_game_list_entry; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java deleted file mode 100644 index 2ca556229..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListAdapter; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.ListFragment; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -public class GamePropertiesActivity extends AppCompatActivity { - PropertyListAdapter mPropertiesListAdapter; - GameListEntry mGameListEntry; - - public ListAdapter getPropertyListAdapter() { - if (mPropertiesListAdapter != null) - return mPropertiesListAdapter; - - mPropertiesListAdapter = new PropertyListAdapter(this); - mPropertiesListAdapter.addItem("title", "Title", mGameListEntry.getTitle()); - mPropertiesListAdapter.addItem("serial", "Serial", mGameListEntry.getCode()); - mPropertiesListAdapter.addItem("type", "Type", mGameListEntry.getType().toString()); - mPropertiesListAdapter.addItem("path", "Path", mGameListEntry.getPath()); - mPropertiesListAdapter.addItem("region", "Region", mGameListEntry.getRegion().toString()); - mPropertiesListAdapter.addItem("compatibility", "Compatibility Rating", mGameListEntry.getCompatibilityRating().toString()); - return mPropertiesListAdapter; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(null); - - String path = getIntent().getStringExtra("path"); - if (path == null || path.isEmpty()) { - finish(); - return; - } - - mGameListEntry = AndroidHostInterface.getInstance().getGameListEntry(path); - if (mGameListEntry == null) { - finish(); - return; - } - - setContentView(R.layout.settings_activity); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.settings, new SettingsCollectionFragment(this)) - .commit(); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - - setTitle(mGameListEntry.getTitle()); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - private void displayError(String text) { - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_activity_error) - .setMessage(text) - .setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss())) - .create() - .show(); - } - - private void createBooleanGameSetting(PreferenceScreen ps, String key, int titleId) { - GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId); - ps.addPreference(pref); - } - - private void createTraitGameSetting(PreferenceScreen ps, String key, int titleId) { - GameTraitPreference pref = new GameTraitPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId); - ps.addPreference(pref); - } - - private void createListGameSetting(PreferenceScreen ps, String key, int titleId, int entryId, int entryValuesId) { - GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId, entryId, entryValuesId); - ps.addPreference(pref); - } - - public static class GameSettingsFragment extends PreferenceFragmentCompat { - private GamePropertiesActivity activity; - - public GameSettingsFragment(GamePropertiesActivity activity) { - this.activity = activity; - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - activity.createListGameSetting(ps, "CPUOverclock", R.string.settings_cpu_overclocking, R.array.settings_advanced_cpu_overclock_entries, R.array.settings_advanced_cpu_overclock_values); - activity.createListGameSetting(ps, "CDROMReadSpeedup", R.string.settings_cdrom_read_speedup, R.array.settings_cdrom_read_speedup_entries, R.array.settings_cdrom_read_speedup_values); - activity.createListGameSetting(ps, "CDROMSeekSpeedup", R.string.settings_cdrom_seek_speedup, R.array.settings_cdrom_seek_speedup_entries, R.array.settings_cdrom_seek_speedup_values); - - activity.createListGameSetting(ps, "GPURenderer", R.string.settings_gpu_renderer, R.array.gpu_renderer_entries, R.array.gpu_renderer_values); - activity.createListGameSetting(ps, "DisplayAspectRatio", R.string.settings_aspect_ratio, R.array.settings_display_aspect_ratio_names, R.array.settings_display_aspect_ratio_values); - activity.createListGameSetting(ps, "DisplayCropMode", R.string.settings_crop_mode, R.array.settings_display_crop_mode_entries, R.array.settings_display_crop_mode_values); - activity.createListGameSetting(ps, "GPUDownsampleMode", R.string.settings_downsample_mode, R.array.settings_downsample_mode_entries, R.array.settings_downsample_mode_values); - activity.createBooleanGameSetting(ps, "DisplayLinearUpscaling", R.string.settings_linear_upscaling); - activity.createBooleanGameSetting(ps, "DisplayIntegerUpscaling", R.string.settings_integer_upscaling); - activity.createBooleanGameSetting(ps, "DisplayForce4_3For24Bit", R.string.settings_force_4_3_for_24bit); - - activity.createListGameSetting(ps, "GPUResolutionScale", R.string.settings_gpu_resolution_scale, R.array.settings_gpu_resolution_scale_entries, R.array.settings_gpu_resolution_scale_values); - activity.createListGameSetting(ps, "GPUMSAA", R.string.settings_msaa, R.array.settings_gpu_msaa_entries, R.array.settings_gpu_msaa_values); - activity.createBooleanGameSetting(ps, "GPUTrueColor", R.string.settings_true_color); - activity.createBooleanGameSetting(ps, "GPUScaledDithering", R.string.settings_scaled_dithering); - activity.createListGameSetting(ps, "GPUTextureFilter", R.string.settings_texture_filtering, R.array.settings_gpu_texture_filter_names, R.array.settings_gpu_texture_filter_values); - activity.createBooleanGameSetting(ps, "GPUForceNTSCTimings", R.string.settings_force_ntsc_timings); - activity.createBooleanGameSetting(ps, "GPUWidescreenHack", R.string.settings_widescreen_hack); - activity.createBooleanGameSetting(ps, "GPUPGXP", R.string.settings_pgxp_geometry_correction); - activity.createBooleanGameSetting(ps, "PGXPPreserveProjFP", R.string.settings_pgxp_preserve_projection_precision); - activity.createBooleanGameSetting(ps, "GPUPGXPDepthBuffer", R.string.settings_pgxp_depth_buffer); - activity.createTraitGameSetting(ps, "ForceSoftwareRenderer", R.string.settings_use_software_renderer); - activity.createTraitGameSetting(ps, "ForceSoftwareRendererForReadbacks", R.string.settings_use_software_renderer_for_readbacks); - activity.createTraitGameSetting(ps, "DisableWidescreen", R.string.settings_disable_widescreen); - activity.createTraitGameSetting(ps, "ForcePGXPVertexCache", R.string.settings_pgxp_vertex_cache); - activity.createTraitGameSetting(ps, "ForcePGXPCPUMode", R.string.settings_pgxp_cpu_mode); - - setPreferenceScreen(ps); - } - } - - public static class ControllerSettingsFragment extends PreferenceFragmentCompat { - private GamePropertiesActivity activity; - - public ControllerSettingsFragment(GamePropertiesActivity activity) { - this.activity = activity; - } - - private void createInputProfileSetting(PreferenceScreen ps) { - final GameSettingPreference pref = new GameSettingPreference(ps.getContext(), activity.mGameListEntry.getPath(), "InputProfileName", R.string.settings_input_profile); - - final String[] inputProfileNames = AndroidHostInterface.getInstance().getInputProfileNames(); - pref.setEntries(inputProfileNames); - pref.setEntryValues(inputProfileNames); - ps.addPreference(pref); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - - activity.createListGameSetting(ps, "Controller1Type", R.string.settings_controller_type, R.array.settings_controller_type_entries, R.array.settings_controller_type_values); - activity.createListGameSetting(ps, "MemoryCard1Type", R.string.settings_memory_card_1_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values); - activity.createListGameSetting(ps, "MemoryCard2Type", R.string.settings_memory_card_2_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values); - createInputProfileSetting(ps); - - setPreferenceScreen(ps); - } - } - - public static class SettingsCollectionFragment extends Fragment { - private GamePropertiesActivity activity; - private SettingsCollectionAdapter adapter; - private ViewPager2 viewPager; - - public SettingsCollectionFragment(GamePropertiesActivity activity) { - this.activity = activity; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_controller_settings, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - adapter = new SettingsCollectionAdapter(activity, this); - viewPager = view.findViewById(R.id.view_pager); - viewPager.setAdapter(adapter); - - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { - switch (position) { - case 0: - tab.setText(R.string.game_properties_tab_summary); - break; - case 1: - tab.setText(R.string.game_properties_tab_game_settings); - break; - case 2: - tab.setText(R.string.game_properties_tab_controller_settings); - break; - } - }).attach(); - } - } - - public static class SettingsCollectionAdapter extends FragmentStateAdapter { - private GamePropertiesActivity activity; - - public SettingsCollectionAdapter(@NonNull GamePropertiesActivity activity, @NonNull Fragment fragment) { - super(fragment); - this.activity = activity; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - switch (position) { - case 0: { // Summary - ListFragment lf = new ListFragment(); - lf.setListAdapter(activity.getPropertyListAdapter()); - return lf; - } - - case 1: { // Game Settings - return new GameSettingsFragment(activity); - } - - case 2: { // Controller Settings - return new ControllerSettingsFragment(activity); - } - - // TODO: Memory Card Editor - - default: - return null; - } - } - - @Override - public int getItemCount() { - return 3; - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java deleted file mode 100644 index d86e8af41..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.preference.ListPreference; - -public class GameSettingPreference extends ListPreference { - private String mGamePath; - - /** - * Creates a boolean game property preference. - */ - public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId) { - super(context); - mGamePath = gamePath; - setPersistent(false); - setTitle(titleId); - setKey(settingKey); - setIconSpaceReserved(false); - setSummaryProvider(SimpleSummaryProvider.getInstance()); - - setEntries(R.array.settings_boolean_entries); - setEntryValues(R.array.settings_boolean_values); - - updateValue(); - } - - /** - * Creates a list game property preference. - */ - public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId, int entryArray, int entryValuesArray) { - super(context); - mGamePath = gamePath; - setPersistent(false); - setTitle(titleId); - setKey(settingKey); - setIconSpaceReserved(false); - setSummaryProvider(SimpleSummaryProvider.getInstance()); - - setEntries(entryArray); - setEntryValues(entryValuesArray); - - updateValue(); - } - - private void updateValue() { - final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey()); - if (value == null) - super.setValue("null"); - else - super.setValue(value); - } - - @Override - public void setValue(String value) { - super.setValue(value); - if (value.equals("null")) - AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), null); - else - AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), value); - } - - @Override - public void setEntries(CharSequence[] entries) { - final int length = (entries != null) ? entries.length : 0; - CharSequence[] newEntries = new CharSequence[length + 1]; - newEntries[0] = getContext().getString(R.string.game_properties_preference_use_global_setting); - if (entries != null) - System.arraycopy(entries, 0, newEntries, 1, entries.length); - super.setEntries(newEntries); - } - - @Override - public void setEntryValues(CharSequence[] entryValues) { - final int length = (entryValues != null) ? entryValues.length : 0; - CharSequence[] newEntryValues = new CharSequence[length + 1]; - newEntryValues[0] = "null"; - if (entryValues != null) - System.arraycopy(entryValues, 0, newEntryValues, 1, length); - super.setEntryValues(newEntryValues); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameTraitPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameTraitPreference.java deleted file mode 100644 index a3eb13aab..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameTraitPreference.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; - -import androidx.preference.ListPreference; -import androidx.preference.SwitchPreference; - -public class GameTraitPreference extends SwitchPreference { - private String mGamePath; - - /** - * Creates a boolean game property preference. - */ - public GameTraitPreference(Context context, String gamePath, String settingKey, int titleId) { - super(context); - mGamePath = gamePath; - setPersistent(false); - setTitle(titleId); - setKey(settingKey); - setIconSpaceReserved(false); - updateValue(); - } - - private void updateValue() { - final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey()); - super.setChecked(value != null && value.equals("true")); - } - - @Override - public void setChecked(boolean checked) { - super.setChecked(checked); - AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), checked ? "true" : "false"); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GenerateCoverTask.java b/android/app/src/main/java/com/github/stenzek/duckstation/GenerateCoverTask.java deleted file mode 100644 index f503d4e15..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GenerateCoverTask.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.os.AsyncTask; -import android.text.Layout; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.widget.ImageView; - -import java.lang.ref.WeakReference; - -public class GenerateCoverTask extends AsyncTask { - private final Context mContext; - private final WeakReference mView; - private final String mTitle; - - public GenerateCoverTask(Context context, ImageView view, String title) { - mContext = context; - mView = new WeakReference<>(view); - mTitle = title; - } - - @Override - protected Bitmap doInBackground(Void... voids) { - try { - final Bitmap background = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.cover_placeholder); - if (background == null) - return null; - - final Bitmap bitmap = Bitmap.createBitmap(background.getWidth(), background.getHeight(), background.getConfig()); - final Canvas canvas = new Canvas(bitmap); - final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - canvas.drawBitmap(background, 0.0f, 0.0f, paint); - - paint.setColor(Color.rgb(255, 255, 255)); - paint.setTextSize(100); - paint.setShadowLayer(1.0f, 0.0f, 1.0f, Color.DKGRAY); - paint.setTextAlign(Paint.Align.CENTER); - - final StaticLayout staticLayout = new StaticLayout(mTitle, paint, - canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1, 0, false); - canvas.save(); - canvas.translate(canvas.getWidth() / 2, (canvas.getHeight() / 2) - (staticLayout.getHeight() / 2)); - staticLayout.draw(canvas); - canvas.restore(); - return bitmap; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - ImageView iv = mView.get(); - if (iv != null) - iv.setImageBitmap(bitmap); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GridAutofitLayoutManager.java b/android/app/src/main/java/com/github/stenzek/duckstation/GridAutofitLayoutManager.java deleted file mode 100644 index f7e3325c2..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GridAutofitLayoutManager.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.stenzek.duckstation; - -// https://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count - -import android.content.Context; -import android.util.TypedValue; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class GridAutofitLayoutManager extends GridLayoutManager -{ - private int columnWidth; - private boolean isColumnWidthChanged = true; - private int lastWidth; - private int lastHeight; - - public GridAutofitLayoutManager(@NonNull final Context context, final int columnWidth) { - /* Initially set spanCount to 1, will be changed automatically later. */ - super(context, 1); - setColumnWidth(checkedColumnWidth(context, columnWidth)); - } - - public GridAutofitLayoutManager( - @NonNull final Context context, - final int columnWidth, - final int orientation, - final boolean reverseLayout) { - - /* Initially set spanCount to 1, will be changed automatically later. */ - super(context, 1, orientation, reverseLayout); - setColumnWidth(checkedColumnWidth(context, columnWidth)); - } - - private int checkedColumnWidth(@NonNull final Context context, int columnWidth) { - if (columnWidth <= 0) { - /* Set default columnWidth value (48dp here). It is better to move this constant - to static constant on top, but we need context to convert it to dp, so can't really - do so. */ - columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, - context.getResources().getDisplayMetrics()); - } - return columnWidth; - } - - public void setColumnWidth(final int newColumnWidth) { - if (newColumnWidth > 0 && newColumnWidth != columnWidth) { - columnWidth = newColumnWidth; - isColumnWidthChanged = true; - } - } - - @Override - public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { - final int width = getWidth(); - final int height = getHeight(); - if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) { - final int totalSpace; - if (getOrientation() == VERTICAL) { - totalSpace = width - getPaddingRight() - getPaddingLeft(); - } else { - totalSpace = height - getPaddingTop() - getPaddingBottom(); - } - final int spanCount = Math.max(1, totalSpace / columnWidth); - setSpanCount(spanCount); - isColumnWidthChanged = false; - } - lastWidth = width; - lastHeight = height; - super.onLayoutChildren(recycler, state); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/HotkeyInfo.java b/android/app/src/main/java/com/github/stenzek/duckstation/HotkeyInfo.java deleted file mode 100644 index f16ce9f15..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/HotkeyInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.stenzek.duckstation; - -public class HotkeyInfo { - private String mCategory; - private String mName; - private String mDisplayName; - - public HotkeyInfo(String category, String name, String displayName) { - mCategory = category; - mName = name; - mDisplayName = displayName; - } - - public String getCategory() { - return mCategory; - } - - public String getName() { - return mName; - } - - public String getDisplayName() { - return mDisplayName; - } - - public String getBindingConfigKey() { - return String.format("Hotkeys/%s", mName); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ImageLoadTask.java b/android/app/src/main/java/com/github/stenzek/duckstation/ImageLoadTask.java deleted file mode 100644 index 447a4c87c..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ImageLoadTask.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import android.widget.ImageView; - -import java.lang.ref.WeakReference; - -public class ImageLoadTask extends AsyncTask { - private WeakReference mView; - - public ImageLoadTask(ImageView view) { - mView = new WeakReference<>(view); - } - - @Override - protected Bitmap doInBackground(String... strings) { - try { - return BitmapFactory.decodeFile(strings[0]); - } catch (Exception e) { - return null; - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - ImageView iv = mView.get(); - if (iv != null) - iv.setImageBitmap(bitmap); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java deleted file mode 100644 index c849fa1d7..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ /dev/null @@ -1,556 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.Manifest; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.widget.Toolbar; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Locale; - -public class MainActivity extends AppCompatActivity { - private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1; - private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2; - private static final int REQUEST_IMPORT_BIOS_IMAGE = 3; - private static final int REQUEST_START_FILE = 4; - private static final int REQUEST_SETTINGS = 5; - private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6; - private static final int REQUEST_CHOOSE_COVER_IMAGE = 7; - - private GameList mGameList; - private ListView mGameListView; - private GameListFragment mGameListFragment; - private GameGridFragment mGameGridFragment; - private Fragment mEmptyGameListFragment; - private boolean mHasExternalStoragePermissions = false; - private boolean mIsShowingGameGrid = false; - private String mPathForChosenCoverImage = null; - - public GameList getGameList() { - return mGameList; - } - - public boolean shouldResumeStateByDefault() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - if (!prefs.getBoolean("Main/SaveStateOnExit", true)) - return false; - - // don't resume with challenge mode on - if (Achievement.willChallengeModeBeEnabled(this)) - return false; - - return true; - } - - private void setLanguage() { - String language = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Language", "none"); - if (language == null || language.equals("none")) { - return; - } - - String[] parts = language.split("-"); - if (parts.length < 2) - return; - - Locale locale = new Locale(parts[0], parts[1]); - Locale.setDefault(locale); - - Resources res = getResources(); - Configuration config = res.getConfiguration(); - config.setLocale(locale); - res.updateConfiguration(config, res.getDisplayMetrics()); - } - - private void setTheme() { - String theme = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Theme", "follow_system"); - if (theme == null) - return; - - if (theme.equals("follow_system")) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } else if (theme.equals("light")) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - } else if (theme.equals("dark")) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - } - } - - private void loadSettings() { - setLanguage(); - setTheme(); - - mIsShowingGameGrid = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("Main/GameGridView", false); - } - - private void switchGameListView() { - mIsShowingGameGrid = !mIsShowingGameGrid; - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putBoolean("Main/GameGridView", mIsShowingGameGrid) - .commit(); - - updateGameListFragment(true); - invalidateOptionsMenu(); - } - - private void updateGameListFragment(boolean allowEmpty) { - final Fragment listFragment = (allowEmpty && mGameList.getEntryCount() == 0) ? - mEmptyGameListFragment : - (mIsShowingGameGrid ? mGameGridFragment : mGameListFragment); - - getSupportFragmentManager() - .beginTransaction() - .setReorderingAllowed(true). - replace(R.id.content_fragment, listFragment) - .commitAllowingStateLoss(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - loadSettings(); - setTitle(null); - - super.onCreate(null); - - setContentView(R.layout.activity_main); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startEmulation(null, shouldResumeStateByDefault()); - } - }); - - // Set up game list view. - mGameList = new GameList(this); - mGameList.addRefreshListener(() -> updateGameListFragment(true)); - mGameListFragment = new GameListFragment(this); - mGameGridFragment = new GameGridFragment(this); - mEmptyGameListFragment = new EmptyGameListFragment(this); - updateGameListFragment(false); - - mHasExternalStoragePermissions = checkForExternalStoragePermissions(); - if (mHasExternalStoragePermissions) - completeStartup(); - } - - private void completeStartup() { - if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) { - Log.i("MainActivity", "Failed to create host interface"); - throw new RuntimeException("Failed to create host interface"); - } - - AndroidHostInterface.getInstance().setContext(this); - mGameList.refresh(false, false, this); - UpdateNotes.displayUpdateNotes(this); - } - - public void startAddGameDirectory() { - if (!checkForExternalStoragePermissions()) - return; - - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)), - REQUEST_ADD_DIRECTORY_TO_GAME_LIST); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - - final MenuItem switchViewItem = menu.findItem(R.id.action_switch_view); - if (switchViewItem != null) { - switchViewItem.setTitle(mIsShowingGameGrid ? R.string.action_show_game_list : R.string.action_show_game_grid); - switchViewItem.setIcon(mIsShowingGameGrid ? R.drawable.ic_baseline_view_list_24 : R.drawable.ic_baseline_grid_view_24); - } - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_start_bios) { - startEmulation(null, false); - } else if (id == R.id.action_start_file) { - startStartFile(); - } else if (id == R.id.action_edit_game_directories) { - Intent intent = new Intent(this, GameDirectoriesActivity.class); - startActivityForResult(intent, REQUEST_EDIT_GAME_DIRECTORIES); - return true; - } else if (id == R.id.action_scan_for_new_games) { - mGameList.refresh(false, false, this); - } else if (id == R.id.action_rescan_all_games) { - mGameList.refresh(true, true, this); - } else if (id == R.id.action_import_bios) { - importBIOSImage(); - } else if (id == R.id.action_settings) { - Intent intent = new Intent(this, SettingsActivity.class); - startActivityForResult(intent, REQUEST_SETTINGS); - return true; - } else if (id == R.id.action_controller_settings) { - Intent intent = new Intent(this, ControllerSettingsActivity.class); - startActivity(intent); - return true; - } else if (id == R.id.action_memory_card_editor) { - Intent intent = new Intent(this, MemoryCardEditorActivity.class); - startActivity(intent); - return true; - } else if (id == R.id.action_switch_view) { - switchGameListView(); - return true; - } else if (id == R.id.action_show_version) { - showVersion(); - return true; - } else if (id == R.id.action_github_respository) { - openGithubRepository(); - return true; - } else if (id == R.id.action_discord_server) { - openDiscordServer(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: { - if (resultCode != RESULT_OK || data.getData() == null) - return; - - // Use legacy storage on devices older than Android 11... apparently some of them - // have broken storage access framework.... - if (!GameDirectoriesActivity.useStorageAccessFramework(this)) { - final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this); - if (path != null) { - GameDirectoriesActivity.addSearchDirectory(this, path, true); - mGameList.refresh(false, false, this); - return; - } - } - - try { - getContentResolver().takePersistableUriPermission(data.getData(), - Intent.FLAG_GRANT_READ_URI_PERMISSION); - } catch (Exception e) { - Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG); - e.printStackTrace(); - } - - GameDirectoriesActivity.addSearchDirectory(this, data.getDataString(), true); - mGameList.refresh(false, false, this); - } - break; - - case REQUEST_IMPORT_BIOS_IMAGE: { - if (resultCode != RESULT_OK) - return; - - onImportBIOSImageResult(data.getData()); - } - break; - - case REQUEST_START_FILE: { - if (resultCode != RESULT_OK || data.getData() == null) - return; - - startEmulation(data.getDataString(), shouldResumeStateByDefault()); - } - break; - - case REQUEST_SETTINGS: { - loadSettings(); - } - break; - - case REQUEST_EDIT_GAME_DIRECTORIES: { - mGameList.refresh(false, false, this); - } - break; - - case REQUEST_CHOOSE_COVER_IMAGE: { - final String gamePath = mPathForChosenCoverImage; - mPathForChosenCoverImage = null; - if (resultCode != RESULT_OK) - return; - - finishChooseCoverImage(gamePath, data.getData()); - } - break; - } - } - - private boolean checkForExternalStoragePermissions() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == - PackageManager.PERMISSION_GRANTED && - ContextCompat - .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == - PackageManager.PERMISSION_GRANTED) { - return true; - } - - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE}, - REQUEST_EXTERNAL_STORAGE_PERMISSIONS); - return false; - } - - public void onRequestPermissionsResult(int requestCode, String[] permissions, - int[] grantResults) { - // check that all were successful - for (int i = 0; i < grantResults.length; i++) { - if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - if (!mHasExternalStoragePermissions) { - mHasExternalStoragePermissions = true; - completeStartup(); - } - } else { - Toast.makeText(this, - R.string.main_activity_external_storage_permissions_error, - Toast.LENGTH_LONG); - finish(); - } - } - } - - public boolean openGameProperties(String path) { - Intent intent = new Intent(this, GamePropertiesActivity.class); - intent.putExtra("path", path); - startActivity(intent); - return true; - } - - public void openGamePopupMenu(View anchorToView, GameListEntry entry) { - androidx.appcompat.widget.PopupMenu menu = new androidx.appcompat.widget.PopupMenu(this, anchorToView, Gravity.RIGHT | Gravity.TOP); - menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu()); - menu.setOnMenuItemClickListener(item -> { - int id = item.getItemId(); - if (id == R.id.game_list_entry_menu_start_game) { - startEmulation(entry.getPath(), false); - return true; - } else if (id == R.id.game_list_entry_menu_resume_game) { - startEmulation(entry.getPath(), true); - return true; - } else if (id == R.id.game_list_entry_menu_properties) { - openGameProperties(entry.getPath()); - return true; - } else if (id == R.id.game_list_entry_menu_choose_cover_image) { - startChooseCoverImage(entry.getPath()); - return true; - } - return false; - }); - - // disable resume state when challenge mode is on - if (Achievement.willChallengeModeBeEnabled(this)) { - MenuItem item = menu.getMenu().findItem(R.id.game_list_entry_menu_resume_game); - if (item != null) - item.setEnabled(false); - } - - menu.show(); - } - - public boolean startEmulation(String bootPath, boolean resumeState) { - if (!doBIOSCheck()) - return false; - - Intent intent = new Intent(this, EmulationActivity.class); - intent.putExtra("bootPath", bootPath); - intent.putExtra("resumeState", resumeState); - startActivity(intent); - return true; - } - - public void startStartFile() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_START_FILE); - } - - private void startChooseCoverImage(String gamePath) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - mPathForChosenCoverImage = gamePath; - startActivityForResult(Intent.createChooser(intent, getString(R.string.menu_game_list_entry_choose_cover_image)), - REQUEST_CHOOSE_COVER_IMAGE); - } - - private void finishChooseCoverImage(String gamePath, Uri uri) { - final GameListEntry gameListEntry = mGameList.getEntryForPath(gamePath); - if (gameListEntry == null) - return; - - final Bitmap bitmap = FileHelper.loadBitmapFromUri(this, uri); - if (bitmap == null) { - Toast.makeText(this, "Failed to open/decode image.", Toast.LENGTH_LONG).show(); - return; - } - - final String coverFileName = String.format("%s/covers/%s.png", - AndroidHostInterface.getUserDirectory(), gameListEntry.getTitle()); - try { - final File file = new File(coverFileName); - final OutputStream outputStream = new FileOutputStream(file); - final boolean result = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); - outputStream.close();; - if (!result) { - file.delete(); - throw new Exception("Failed to compress bitmap."); - } - - gameListEntry.setCoverPath(coverFileName); - mGameList.fireRefreshListeners(); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "Failed to save image.", Toast.LENGTH_LONG).show(); - } - - bitmap.recycle(); - } - - private boolean doBIOSCheck() { - if (AndroidHostInterface.getInstance().hasAnyBIOSImages()) - return true; - - new AlertDialog.Builder(this) - .setTitle(R.string.main_activity_missing_bios_image) - .setMessage(R.string.main_activity_missing_bios_image_prompt) - .setPositiveButton(R.string.main_activity_yes, (dialog, button) -> importBIOSImage()) - .setNegativeButton(R.string.main_activity_no, (dialog, button) -> { - }) - .create() - .show(); - - return false; - } - - private void importBIOSImage() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_bios_image)), REQUEST_IMPORT_BIOS_IMAGE); - } - - private void onImportBIOSImageResult(Uri uri) { - // This should really be 512K but just in case we wanted to support the other BIOSes in the future... - final int MAX_BIOS_SIZE = 2 * 1024 * 1024; - - InputStream stream = null; - try { - stream = getContentResolver().openInputStream(uri); - } catch (FileNotFoundException e) { - Toast.makeText(this, R.string.main_activity_failed_to_open_bios_image, Toast.LENGTH_LONG); - return; - } - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try { - byte[] buffer = new byte[512 * 1024]; - int len; - while ((len = stream.read(buffer)) > 0) { - os.write(buffer, 0, len); - if (os.size() > MAX_BIOS_SIZE) { - throw new IOException(getString(R.string.main_activity_bios_image_too_large)); - } - } - } catch (IOException e) { - new AlertDialog.Builder(this) - .setMessage(getString(R.string.main_activity_failed_to_read_bios_image_prefix) + e.getMessage()) - .setPositiveButton(R.string.main_activity_ok, (dialog, button) -> { - }) - .create() - .show(); - return; - } - - String importResult = AndroidHostInterface.getInstance().importBIOSImage(os.toByteArray()); - String message = (importResult == null) ? getString(R.string.main_activity_invalid_error) : ("BIOS '" + importResult + "' imported."); - - new AlertDialog.Builder(this) - .setMessage(message) - .setPositiveButton(R.string.main_activity_ok, (dialog, button) -> { - }) - .create() - .show(); - } - - private void showVersion() { - final String message = AndroidHostInterface.getFullScmVersion(); - new AlertDialog.Builder(this) - .setTitle(R.string.main_activity_show_version_title) - .setMessage(message) - .setPositiveButton(R.string.main_activity_ok, (dialog, button) -> { - }) - .setNeutralButton(R.string.main_activity_copy, (dialog, button) -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) - clipboard.setPrimaryClip(ClipData.newPlainText(getString(R.string.main_activity_show_version_title), message)); - }) - .create() - .show(); - } - - private void openGithubRepository() { - final String url = "https://github.com/stenzek/duckstation"; - final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browserIntent); - } - - private void openDiscordServer() { - final String url = "https://discord.gg/Buktv3t"; - final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browserIntent); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardEditorActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardEditorActivity.java deleted file mode 100644 index dfe1323b3..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardEditorActivity.java +++ /dev/null @@ -1,524 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.AlertDialog; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.app.AppCompatActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.ArrayList; - -public class MemoryCardEditorActivity extends AppCompatActivity { - public static final int REQUEST_IMPORT_CARD = 1; - - private final ArrayList cards = new ArrayList<>(); - private CollectionAdapter adapter; - private ViewPager2 viewPager; - private TabLayout tabLayout; - private TabLayoutMediator tabLayoutMediator; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(null); - setContentView(R.layout.activity_memory_card_editor); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - - adapter = new CollectionAdapter(this); - viewPager = findViewById(R.id.view_pager); - viewPager.setAdapter(adapter); - - tabLayout = findViewById(R.id.tab_layout); - tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, adapter.getTabConfigurationStrategy()); - tabLayoutMediator.attach(); - - findViewById(R.id.open_card).setOnClickListener((v) -> openCard()); - findViewById(R.id.close_card).setOnClickListener((v) -> closeCard()); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_memory_card_editor, menu); - - final boolean hasCurrentCard = (getCurrentCard() != null); - menu.findItem(R.id.action_delete_card).setEnabled(hasCurrentCard); - menu.findItem(R.id.action_format_card).setEnabled(hasCurrentCard); - menu.findItem(R.id.action_import_card).setEnabled(hasCurrentCard); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - - case R.id.action_import_card: { - importCard(); - return true; - } - - case R.id.action_delete_card: { - deleteCard(); - return true; - } - - case R.id.action_format_card: { - formatCard(); - return true; - } - - default: { - return super.onOptionsItemSelected(item); - } - } - } - - private void openCard() { - final Uri[] uris = MemoryCardImage.getCardUris(this); - if (uris == null) { - displayMessage(getString(R.string.memory_card_editor_no_cards_found)); - return; - } - - final String[] uriTitles = new String[uris.length]; - for (int i = 0; i < uris.length; i++) - uriTitles[i] = MemoryCardImage.getTitleForUri(uris[i]); - - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.memory_card_editor_select_card); - builder.setItems(uriTitles, (dialog, which) -> { - final Uri uri = uris[which]; - for (int i = 0; i < cards.size(); i++) { - if (cards.get(i).getUri().equals(uri)) { - displayError(getString(R.string.memory_card_editor_card_already_open)); - tabLayout.getTabAt(i).select(); - return; - } - } - - final MemoryCardImage card = MemoryCardImage.open(MemoryCardEditorActivity.this, uri); - if (card == null) { - displayError(getString(R.string.memory_card_editor_failed_to_open_card)); - return; - } - - cards.add(card); - refreshView(card); - }); - builder.create().show(); - } - - private void closeCard() { - final int index = tabLayout.getSelectedTabPosition(); - if (index < 0) - return; - - cards.remove(index); - refreshView(index); - } - - private void displayMessage(String message) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } - - private void displayError(String message) { - final AlertDialog.Builder errorBuilder = new AlertDialog.Builder(this); - errorBuilder.setTitle(R.string.memory_card_editor_error); - errorBuilder.setMessage(message); - errorBuilder.setPositiveButton(R.string.main_activity_ok, (dialog1, which1) -> dialog1.dismiss()); - errorBuilder.create().show(); - } - - private void copySave(MemoryCardImage sourceCard, MemoryCardFileInfo sourceFile) { - if (cards.size() < 2) { - displayError(getString(R.string.memory_card_editor_must_have_at_least_two_cards_to_copy)); - return; - } - - if (cards.indexOf(sourceCard) < 0) { - // this shouldn't happen.. - return; - } - - final MemoryCardImage[] destinationCards = new MemoryCardImage[cards.size() - 1]; - final String[] cardTitles = new String[cards.size() - 1]; - for (int i = 0, d = 0; i < cards.size(); i++) { - if (cards.get(i) == sourceCard) - continue; - - destinationCards[d] = cards.get(i); - cardTitles[d] = cards.get(i).getTitle(); - d++; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.memory_card_editor_copy_save_to, sourceFile.getTitle())); - builder.setItems(cardTitles, (dialog, which) -> { - dialog.dismiss(); - - final MemoryCardImage destinationCard = destinationCards[which]; - - byte[] data = null; - if (destinationCard.getFreeBlocks() < sourceFile.getNumBlocks()) { - displayError(getString(R.string.memory_card_editor_copy_insufficient_blocks, sourceFile.getNumBlocks(), - destinationCard.getFreeBlocks())); - } else if (destinationCard.hasFile(sourceFile.getFilename())) { - displayError(getString(R.string.memory_card_editor_copy_already_exists, sourceFile.getFilename())); - } else if ((data = sourceCard.readFile(sourceFile.getFilename())) == null) { - displayError(getString(R.string.memory_card_editor_copy_read_failed, sourceFile.getFilename())); - } else if (!destinationCard.writeFile(sourceFile.getFilename(), data)) { - displayMessage(getString(R.string.memory_card_editor_copy_write_failed, sourceFile.getFilename())); - } else { - displayMessage(getString(R.string.memory_card_editor_copy_success, sourceFile.getFilename(), - destinationCard.getTitle())); - refreshView(destinationCard); - } - }); - builder.create().show(); - } - - private void deleteSave(MemoryCardImage card, MemoryCardFileInfo file) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.memory_card_editor_delete_confirm, file.getFilename())); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - if (card.deleteFile(file.getFilename())) { - displayMessage(getString(R.string.memory_card_editor_delete_success, file.getFilename())); - refreshView(card); - } else { - displayError(getString(R.string.memory_card_editor_delete_failed, file.getFilename())); - } - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - private void refreshView(int newSelection) { - final int oldPos = viewPager.getCurrentItem(); - tabLayoutMediator.detach(); - viewPager.setAdapter(null); - viewPager.setAdapter(adapter); - tabLayoutMediator.attach(); - - if (cards.isEmpty()) - return; - - if (newSelection < 0 || newSelection >= tabLayout.getTabCount()) { - if (oldPos < cards.size()) - tabLayout.getTabAt(oldPos).select(); - else - tabLayout.getTabAt(cards.size() - 1).select(); - } else { - tabLayout.getTabAt(newSelection).select(); - } - } - - private void refreshView(MemoryCardImage newSelectedCard) { - if (newSelectedCard == null) - refreshView(-1); - else - refreshView(cards.indexOf(newSelectedCard)); - - invalidateOptionsMenu(); - } - - private MemoryCardImage getCurrentCard() { - final int index = tabLayout.getSelectedTabPosition(); - if (index < 0 || index >= cards.size()) - return null; - - return cards.get(index); - } - - private void importCard() { - if (getCurrentCard() == null) { - displayMessage(getString(R.string.memory_card_editor_no_card_selected)); - return; - } - - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_IMPORT_CARD); - } - - private void importCard(Uri uri) { - final MemoryCardImage card = getCurrentCard(); - if (card == null) - return; - - final byte[] data = FileHelper.readBytesFromUri(this, uri, 16 * 1024 * 1024); - if (data == null) { - displayError(getString(R.string.memory_card_editor_import_card_read_failed, uri.toString())); - return; - } - - String importFileName = FileHelper.getDocumentNameFromUri(this, uri); - if (importFileName == null) { - importFileName = uri.getPath(); - if (importFileName == null || importFileName.isEmpty()) - importFileName = uri.toString(); - } - - final String captureImportFileName = importFileName; - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.memory_card_editor_import_card_confirm_message, - importFileName, card.getTitle())); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - dialog.dismiss(); - - if (!card.importCard(captureImportFileName, data)) { - displayError(getString(R.string.memory_card_editor_import_failed)); - return; - } - - refreshView(card); - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - private void formatCard() { - final MemoryCardImage card = getCurrentCard(); - if (card == null) { - displayMessage(getString(R.string.memory_card_editor_no_card_selected)); - return; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.memory_card_editor_format_card_confirm_message, card.getTitle())); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - dialog.dismiss(); - - if (!card.format()) { - displayError(getString(R.string.memory_card_editor_format_card_failed, card.getUri().toString())); - return; - } - - displayMessage(getString(R.string.memory_card_editor_format_card_success, card.getUri().toString())); - refreshView(card); - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - private void deleteCard() { - final MemoryCardImage card = getCurrentCard(); - if (card == null) { - displayMessage(getString(R.string.memory_card_editor_no_card_selected)); - return; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.memory_card_editor_delete_card_confirm_message, card.getTitle())); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - dialog.dismiss(); - - if (!card.delete()) { - displayError(getString(R.string.memory_card_editor_delete_card_failed, card.getUri().toString())); - return; - } - - displayMessage(getString(R.string.memory_card_editor_delete_card_success, card.getUri().toString())); - cards.remove(card); - refreshView(-1); - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case REQUEST_IMPORT_CARD: { - if (resultCode != RESULT_OK) - return; - - importCard(data.getData()); - } - break; - } - } - - private static class SaveViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private MemoryCardEditorActivity mParent; - private View mItemView; - private MemoryCardImage mCard; - private MemoryCardFileInfo mFile; - - public SaveViewHolder(MemoryCardEditorActivity parent, @NonNull View itemView) { - super(itemView); - mParent = parent; - mItemView = itemView; - mItemView.setOnClickListener(this); - } - - public void bindToEntry(MemoryCardImage card, MemoryCardFileInfo file) { - mCard = card; - mFile = file; - - ((TextView) mItemView.findViewById(R.id.title)).setText(mFile.getTitle()); - ((TextView) mItemView.findViewById(R.id.filename)).setText(mFile.getFilename()); - - final String blocksText = String.format("%d Blocks", mFile.getNumBlocks()); - final String sizeText = String.format("%.1f KB", (float)mFile.getSize() / 1024.0f); - ((TextView) mItemView.findViewById(R.id.block_size)).setText(blocksText); - ((TextView) mItemView.findViewById(R.id.file_size)).setText(sizeText); - - if (mFile.getNumIconFrames() > 0) { - final Bitmap bitmap = mFile.getIconFrameBitmap(0); - if (bitmap != null) { - ((ImageView) mItemView.findViewById(R.id.icon)).setImageBitmap(bitmap); - } - } - } - - @Override - public void onClick(View v) { - final AlertDialog.Builder builder = new AlertDialog.Builder(mItemView.getContext()); - builder.setTitle(mFile.getFilename()); - builder.setItems(R.array.memory_card_editor_save_menu, ((dialog, which) -> { - switch (which) { - // Copy Save - case 0: { - dialog.dismiss(); - mParent.copySave(mCard, mFile); - } - break; - - // Delete Save - case 1: { - dialog.dismiss(); - mParent.deleteSave(mCard, mFile); - } - break; - } - })); - builder.create().show(); - } - } - - private static class SaveViewAdapter extends RecyclerView.Adapter { - private MemoryCardEditorActivity parent; - private MemoryCardImage card; - private MemoryCardFileInfo[] files; - - public SaveViewAdapter(MemoryCardEditorActivity parent, MemoryCardImage card) { - this.parent = parent; - this.card = card; - this.files = card.getFiles(); - } - - @NonNull - @Override - public SaveViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final View rootView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_memory_card_save, parent, false); - return new SaveViewHolder(this.parent, rootView); - } - - @Override - public void onBindViewHolder(@NonNull SaveViewHolder holder, int position) { - holder.bindToEntry(card, files[position]); - } - - @Override - public int getItemCount() { - return (files != null) ? files.length : 0; - } - - @Override - public int getItemViewType(int position) { - return R.layout.layout_memory_card_save; - } - } - - public static class MemoryCardFileFragment extends Fragment { - private MemoryCardEditorActivity parent; - private MemoryCardImage card; - private SaveViewAdapter adapter; - private RecyclerView recyclerView; - - public MemoryCardFileFragment(MemoryCardEditorActivity parent, MemoryCardImage card) { - this.parent = parent; - this.card = card; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_memory_card_file, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - adapter = new SaveViewAdapter(parent, card); - recyclerView = view.findViewById(R.id.recyclerView); - recyclerView.setAdapter(adapter); - recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); - recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), - DividerItemDecoration.VERTICAL)); - } - } - - public static class CollectionAdapter extends FragmentStateAdapter { - private MemoryCardEditorActivity parent; - private final TabLayoutMediator.TabConfigurationStrategy tabConfigurationStrategy = (tab, position) -> { - tab.setText(parent.cards.get(position).getTitle()); - }; - - public CollectionAdapter(MemoryCardEditorActivity parent) { - super(parent); - this.parent = parent; - } - - public TabLayoutMediator.TabConfigurationStrategy getTabConfigurationStrategy() { - return tabConfigurationStrategy; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - return new MemoryCardFileFragment(parent, parent.cards.get(position)); - } - - @Override - public int getItemCount() { - return parent.cards.size(); - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardFileInfo.java b/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardFileInfo.java deleted file mode 100644 index e520c2db5..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardFileInfo.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.graphics.Bitmap; - -import java.nio.ByteBuffer; - -public class MemoryCardFileInfo { - public static final int ICON_WIDTH = 16; - public static final int ICON_HEIGHT = 16; - - private final String filename; - private final String title; - private final int size; - private final int firstBlock; - private final int numBlocks; - private final byte[][] iconFrames; - - public MemoryCardFileInfo(String filename, String title, int size, int firstBlock, int numBlocks, byte[][] iconFrames) { - this.filename = filename; - this.title = title; - this.size = size; - this.firstBlock = firstBlock; - this.numBlocks = numBlocks; - this.iconFrames = iconFrames; - } - - public String getFilename() { - return filename; - } - - public String getTitle() { - return title; - } - - public int getSize() { - return size; - } - - public int getFirstBlock() { - return firstBlock; - } - - public int getNumBlocks() { - return numBlocks; - } - - public int getNumIconFrames() { - return (iconFrames != null) ? iconFrames.length : 0; - } - - public byte[] getIconFrame(int index) { - return iconFrames[index]; - } - - public Bitmap getIconFrameBitmap(int index) { - final byte[] data = getIconFrame(index); - if (data == null) - return null; - - final Bitmap bitmap = Bitmap.createBitmap(ICON_WIDTH, ICON_HEIGHT, Bitmap.Config.ARGB_8888); - bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data)); - return bitmap; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardImage.java b/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardImage.java deleted file mode 100644 index 5417ecc3d..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MemoryCardImage.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.DocumentsContract; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; - -public class MemoryCardImage { - public static final int DATA_SIZE = 128 * 1024; - public static final String FILE_EXTENSION = ".mcd"; - - private final Context context; - private final Uri uri; - private final byte[] data; - - private MemoryCardImage(Context context, Uri uri, byte[] data) { - this.context = context; - this.uri = uri; - this.data = data; - } - - public static String getTitleForUri(Uri uri) { - String name = uri.getLastPathSegment(); - if (name != null) { - final int lastSlash = name.lastIndexOf('/'); - if (lastSlash >= 0) - name = name.substring(lastSlash + 1); - - if (name.endsWith(FILE_EXTENSION)) - name = name.substring(0, name.length() - FILE_EXTENSION.length()); - } else { - name = uri.toString(); - } - - return name; - } - - public static MemoryCardImage open(Context context, Uri uri) { - byte[] data = FileHelper.readBytesFromUri(context, uri, DATA_SIZE); - if (data == null) - return null; - - if (!isValid(data)) - return null; - - return new MemoryCardImage(context, uri, data); - } - - public static MemoryCardImage create(Context context, Uri uri) { - byte[] data = new byte[DATA_SIZE]; - format(data); - - MemoryCardImage card = new MemoryCardImage(context, uri, data); - if (!card.save()) - return null; - - return card; - } - - public static Uri[] getCardUris(Context context) { - final String directory = String.format("%s/memcards", - AndroidHostInterface.getUserDirectory()); - final ArrayList results = new ArrayList<>(); - - if (directory.charAt(0) == '/') { - // native path - final File directoryFile = new File(directory); - final File[] files = directoryFile.listFiles(); - for (File file : files) { - if (!file.isFile()) - continue; - - if (!file.getName().endsWith(FILE_EXTENSION)) - continue; - - results.add(Uri.fromFile(file)); - } - } else { - try { - final Uri baseUri = null; - final String[] scanProjection = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_MIME_TYPE}; - final ContentResolver resolver = context.getContentResolver(); - final String treeDocId = DocumentsContract.getTreeDocumentId(baseUri); - final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(baseUri, treeDocId); - final Cursor cursor = resolver.query(queryUri, scanProjection, null, null, null); - - while (cursor.moveToNext()) { - try { - final String mimeType = cursor.getString(2); - final String documentId = cursor.getString(0); - final Uri uri = DocumentsContract.buildDocumentUriUsingTree(baseUri, documentId); - if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { - continue; - } - - final String uriString = uri.toString(); - if (!uriString.endsWith(FILE_EXTENSION)) - continue; - - results.add(uri); - } catch (Exception e) { - e.printStackTrace(); - } - } - cursor.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (results.isEmpty()) - return null; - - Collections.sort(results, (a, b) -> a.compareTo(b)); - - final Uri[] resultArray = new Uri[results.size()]; - results.toArray(resultArray); - return resultArray; - } - - private static native boolean isValid(byte[] data); - - private static native void format(byte[] data); - - private static native int getFreeBlocks(byte[] data); - - private static native MemoryCardFileInfo[] getFiles(byte[] data); - - private static native boolean hasFile(byte[] data, String filename); - - private static native byte[] readFile(byte[] data, String filename); - - private static native boolean writeFile(byte[] data, String filename, byte[] fileData); - - private static native boolean deleteFile(byte[] data, String filename); - - private static native boolean importCard(byte[] data, String filename, byte[] importData); - - public boolean save() { - return FileHelper.writeBytesToUri(context, uri, data); - } - - public boolean delete() { - return FileHelper.deleteFileAtUri(context, uri); - } - - public boolean format() { - format(data); - return save(); - } - - public Uri getUri() { - return uri; - } - - public String getTitle() { - return getTitleForUri(uri); - } - - public int getFreeBlocks() { - return getFreeBlocks(data); - } - - public MemoryCardFileInfo[] getFiles() { - return getFiles(data); - } - - public boolean hasFile(String filename) { - return hasFile(data, filename); - } - - public byte[] readFile(String filename) { - return readFile(data, filename); - } - - public boolean writeFile(String filename, byte[] fileData) { - if (!writeFile(data, filename, fileData)) - return false; - - return save(); - } - - public boolean deleteFile(String filename) { - if (!deleteFile(data, filename)) - return false; - - return save(); - } - - public boolean importCard(String filename, byte[] importData) { - if (!importCard(data, filename, importData)) - return false; - - return save(); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java b/android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java deleted file mode 100644 index c8de67096..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.stenzek.duckstation; - -public class PatchCode { - private static final String UNGROUPED_NAME = "Ungrouped"; - - private int mIndex; - private String mGroup; - private String mDescription; - private boolean mEnabled; - - public PatchCode(int index, String group, String description, boolean enabled) { - mIndex = index; - mGroup = group; - mDescription = description; - mEnabled = enabled; - } - - public int getIndex() { - return mIndex; - } - - public String getGroup() { - return mGroup; - } - - public String getDescription() { - return mDescription; - } - - public boolean isEnabled() { - return mEnabled; - } - - public String getDisplayText() { - if (mGroup == null || mGroup.equals(UNGROUPED_NAME)) - return mDescription; - else - return String.format("(%s) %s", mGroup, mDescription); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java b/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java deleted file mode 100644 index 99e9ad815..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.SharedPreferences; -import android.util.ArraySet; - -import java.util.Set; - -public class PreferenceHelpers { - /** - * Clears all preferences in the specified section (starting with sectionName/). - * We really don't want to have to do this with JNI... - * - * @param prefs Preferences object. - * @param sectionName Section to clear keys for. - */ - public static void clearSection(SharedPreferences prefs, String sectionName) { - String testSectionName = sectionName + "/"; - SharedPreferences.Editor editor = prefs.edit(); - for (String keyName : prefs.getAll().keySet()) { - if (keyName.startsWith(testSectionName)) { - editor.remove(keyName); - } - } - - editor.commit(); - } - - public static Set getStringSet(SharedPreferences prefs, String keyName) { - Set values = null; - try { - values = prefs.getStringSet(keyName, null); - } catch (Exception e) { - try { - String singleValue = prefs.getString(keyName, null); - if (singleValue != null) { - values = new ArraySet<>(); - values.add(singleValue); - } - } catch (Exception e2) { - - } - } - - return values; - } - - public static boolean addToStringList(SharedPreferences prefs, String keyName, String valueToAdd) { - Set values = getStringSet(prefs, keyName); - if (values == null) { - values = new ArraySet<>(); - } else { - // We need to copy it otherwise the put doesn't save. - Set valuesCopy = new ArraySet<>(); - valuesCopy.addAll(values); - values = valuesCopy; - } - - final boolean result = values.add(valueToAdd); - prefs.edit().putStringSet(keyName, values).commit(); - return result; - } - - public static boolean removeFromStringList(SharedPreferences prefs, String keyName, String valueToRemove) { - Set values = getStringSet(prefs, keyName); - if (values == null) - return false; - - // We need to copy it otherwise the put doesn't save. - Set valuesCopy = new ArraySet<>(); - valuesCopy.addAll(values); - values = valuesCopy; - - final boolean result = values.remove(valueToRemove); - prefs.edit().putStringSet(keyName, values).commit(); - return result; - } - - public static void setStringList(SharedPreferences prefs, String keyName, String[] values) { - Set valueSet = new ArraySet(); - for (String value : values) - valueSet.add(value); - - prefs.edit().putStringSet(keyName, valueSet).commit(); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java b/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java deleted file mode 100644 index 3f9a01565..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import java.util.ArrayList; - -public class PropertyListAdapter extends BaseAdapter { - private class Item { - public String key; - public String title; - public String value; - - Item(String key, String title, String value) { - this.key = key; - this.title = title; - this.value = value; - } - } - - private Context mContext; - private ArrayList mItems = new ArrayList<>(); - - public PropertyListAdapter(Context context) { - mContext = context; - } - - public Item getItemByKey(String key) { - for (Item it : mItems) { - if (it.key.equals(key)) - return it; - } - - return null; - } - - public int addItem(String key, String title, String value) { - if (getItemByKey(key) != null) - return -1; - - Item it = new Item(key, title, value); - int position = mItems.size(); - mItems.add(it); - return position; - } - - public boolean removeItem(Item item) { - return mItems.remove(item); - } - - @Override - public int getCount() { - return mItems.size(); - } - - @Override - public Object getItem(int position) { - return mItems.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = LayoutInflater.from(mContext) - .inflate(R.layout.layout_game_property_entry, parent, false); - } - - TextView titleView = (TextView) convertView.findViewById(R.id.property_title); - TextView valueView = (TextView) convertView.findViewById(R.id.property_value); - Item prop = mItems.get(position); - titleView.setText(prop.title); - valueView.setText(prop.value); - return convertView; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/RatioPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/RatioPreference.java deleted file mode 100644 index fe6fc5b41..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/RatioPreference.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.Spinner; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceViewHolder; - -public class RatioPreference extends Preference { - private String mNumeratorKey; - private String mDenominatorKey; - - private int mNumeratorValue = 1; - private int mDenominatorValue = 1; - - private int mMinimumNumerator = 1; - private int mMaximumNumerator = 50; - private int mDefaultNumerator = 1; - private int mMinimumDenominator = 1; - private int mMaximumDenominator = 50; - private int mDefaultDenominator = 1; - - private void initAttributes(AttributeSet attrs) { - for (int i = 0; i < attrs.getAttributeCount(); i++) { - final String key = attrs.getAttributeName(i); - if (key.equals("numeratorKey")) { - mNumeratorKey = attrs.getAttributeValue(i); - } else if (key.equals("minimumNumerator")) { - mMinimumNumerator = attrs.getAttributeIntValue(i, 1); - } else if (key.equals("maximumNumerator")) { - mMaximumNumerator = attrs.getAttributeIntValue(i, 1); - } else if (key.equals("defaultNumerator")) { - mDefaultNumerator = attrs.getAttributeIntValue(i, 1); - } else if(key.equals("denominatorKey")) { - mDenominatorKey = attrs.getAttributeValue(i); - } else if (key.equals("minimumDenominator")) { - mMinimumDenominator = attrs.getAttributeIntValue(i, 1); - } else if (key.equals("maximumDenominator")) { - mMaximumDenominator = attrs.getAttributeIntValue(i, 1); - } else if (key.equals("defaultDenominator")) { - mDefaultDenominator = attrs.getAttributeIntValue(i, 1); - } - } - } - - public RatioPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setWidgetLayoutResource(R.layout.layout_ratio_preference); - initAttributes(attrs); - } - - public RatioPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setWidgetLayoutResource(R.layout.layout_ratio_preference); - initAttributes(attrs); - } - - public RatioPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setWidgetLayoutResource(R.layout.layout_ratio_preference); - initAttributes(attrs); - } - - public RatioPreference(Context context) { - super(context); - setWidgetLayoutResource(R.layout.layout_ratio_preference); - } - - private void persistValues() { - final PreferenceDataStore dataStore = getPreferenceDataStore(); - if (dataStore != null) { - if (mNumeratorKey != null) - dataStore.putInt(mNumeratorKey, mNumeratorValue); - if (mDenominatorKey != null) - dataStore.putInt(mDenominatorKey, mDenominatorValue); - } else { - SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); - if (mNumeratorKey != null) - editor.putInt(mNumeratorKey, mNumeratorValue); - if (mDenominatorKey != null) - editor.putInt(mDenominatorKey, mDenominatorValue); - editor.commit(); - } - } - - @Override - protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { - super.onAttachedToHierarchy(preferenceManager); - setInitialValue(); - } - - private void setInitialValue() { - final PreferenceDataStore dataStore = getPreferenceDataStore(); - if (dataStore != null) { - if (mNumeratorKey != null) - mNumeratorValue = dataStore.getInt(mNumeratorKey, mDefaultNumerator); - if (mDenominatorKey != null) - mDenominatorValue = dataStore.getInt(mDenominatorKey, mDefaultDenominator); - } else { - final SharedPreferences pm = getPreferenceManager().getSharedPreferences(); - if (mNumeratorKey != null) - mNumeratorValue = pm.getInt(mNumeratorKey, mDefaultNumerator); - if (mDenominatorKey != null) - mDenominatorValue = pm.getInt(mDenominatorKey, mDefaultDenominator); - } - } - - private static BaseAdapter generateDropdownItems(int min, int max) { - return new BaseAdapter() { - @Override - public int getCount() { - return (max - min) + 1; - } - - @Override - public Object getItem(int position) { - return Integer.toString(min + position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TextView view; - if (convertView != null) { - view = (TextView) convertView; - } else { - view = new TextView(parent.getContext()); - - Resources r = parent.getResources(); - float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, r.getDisplayMetrics()); - view.setPadding((int) px, (int) px, (int) px, (int) px); - } - - view.setText(Integer.toString(min + position)); - return view; - } - }; - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - holder.itemView.setClickable(false); - - Spinner numeratorSpinner = (Spinner) holder.findViewById(R.id.numerator); - numeratorSpinner.setAdapter(generateDropdownItems(mMinimumNumerator, mMaximumNumerator)); - numeratorSpinner.setSelection(mNumeratorValue - mMinimumNumerator); - numeratorSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - final int newValue = position + mMinimumNumerator; - if (newValue != mNumeratorValue) { - mNumeratorValue = newValue; - persistValues(); - } - } - - @Override - public void onNothingSelected(AdapterView parent) { - } - }); - Spinner denominatorSpinner = (Spinner) holder.findViewById(R.id.denominator); - denominatorSpinner.setAdapter(generateDropdownItems(mMinimumDenominator, mMaximumDenominator)); - denominatorSpinner.setSelection(mDenominatorValue - mMinimumDenominator); - denominatorSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - final int newValue = position + mMinimumDenominator; - if (newValue != mDenominatorValue) { - mDenominatorValue = newValue; - persistValues(); - } - } - - @Override - public void onNothingSelected(AdapterView parent) { - } - }); - } - - -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SaveStateInfo.java b/android/app/src/main/java/com/github/stenzek/duckstation/SaveStateInfo.java deleted file mode 100644 index 7acd33f32..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/SaveStateInfo.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.graphics.Bitmap; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import java.nio.ByteBuffer; - -public class SaveStateInfo { - private String mPath; - private String mGameTitle; - private String mGameCode; - private String mMediaPath; - private String mTimestamp; - private int mSlot; - private boolean mGlobal; - private Bitmap mScreenshot; - - public SaveStateInfo(String path, String gameTitle, String gameCode, String mediaPath, String timestamp, int slot, boolean global, - int screenshotWidth, int screenshotHeight, byte[] screenshotData) { - mPath = path; - mGameTitle = gameTitle; - mGameCode = gameCode; - mMediaPath = mediaPath; - mTimestamp = timestamp; - mSlot = slot; - mGlobal = global; - - if (screenshotData != null) { - try { - mScreenshot = Bitmap.createBitmap(screenshotWidth, screenshotHeight, Bitmap.Config.ARGB_8888); - mScreenshot.copyPixelsFromBuffer(ByteBuffer.wrap(screenshotData)); - } catch (Exception e) { - mScreenshot = null; - } - } - } - - public boolean exists() { - return mPath != null; - } - - public String getPath() { - return mPath; - } - - public String getGameTitle() { - return mGameTitle; - } - - public String getGameCode() { - return mGameCode; - } - - public String getMediaPath() { - return mMediaPath; - } - - public String getTimestamp() { - return mTimestamp; - } - - public int getSlot() { - return mSlot; - } - - public boolean isGlobal() { - return mGlobal; - } - - public Bitmap getScreenshot() { - return mScreenshot; - } - - private void fillView(Context context, View view) { - ImageView imageView = (ImageView) view.findViewById(R.id.image); - TextView summaryView = (TextView) view.findViewById(R.id.summary); - TextView gameView = (TextView) view.findViewById(R.id.game); - TextView pathView = (TextView) view.findViewById(R.id.path); - TextView timestampView = (TextView) view.findViewById(R.id.timestamp); - - if (mScreenshot != null) - imageView.setImageBitmap(mScreenshot); - else - imageView.setImageDrawable(context.getDrawable(R.drawable.ic_baseline_not_interested_60)); - - String summaryText; - if (mGlobal) - summaryView.setText(String.format(context.getString(R.string.save_state_info_global_save_n), mSlot)); - else if (mSlot == 0) - summaryView.setText(R.string.save_state_info_quick_save); - else - summaryView.setText(String.format(context.getString(R.string.save_state_info_game_save_n), mSlot)); - - if (exists()) { - gameView.setText(String.format("%s - %s", mGameCode, mGameTitle)); - - int lastSlashPosition = mMediaPath.lastIndexOf('/'); - if (lastSlashPosition >= 0) - pathView.setText(mMediaPath.substring(lastSlashPosition + 1)); - else - pathView.setText(mMediaPath); - - timestampView.setText(mTimestamp); - } else { - gameView.setText(R.string.save_state_info_slot_is_empty); - pathView.setText(""); - timestampView.setText(""); - } - } - - public static class ListAdapter extends BaseAdapter { - private final Context mContext; - private final SaveStateInfo[] mInfos; - - public ListAdapter(Context context, SaveStateInfo[] infos) { - mContext = context; - mInfos = infos; - } - - @Override - public int getCount() { - return mInfos.length; - } - - @Override - public Object getItem(int position) { - return mInfos[position]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = LayoutInflater.from(mContext).inflate(R.layout.save_state_view_entry, parent, false); - } - - mInfos[position].fillView(mContext, convertView); - return convertView; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java deleted file mode 100644 index 47a9313c9..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.Bundle; -import android.view.MenuItem; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceFragmentCompat; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(null); - setContentView(R.layout.settings_activity); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.settings, new SettingsCollectionFragment()) - .commit(); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - return super.onOptionsItemSelected(item); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java deleted file mode 100644 index b5aa1138b..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceFragmentCompat; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -public class SettingsCollectionFragment extends Fragment { - private SettingsCollectionAdapter adapter; - private ViewPager2 viewPager; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_settings_collection, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - adapter = new SettingsCollectionAdapter(this); - viewPager = view.findViewById(R.id.view_pager); - viewPager.setAdapter(adapter); - - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, - (tab, position) -> tab.setText(getResources().getStringArray(R.array.settings_tabs)[position]) - ).attach(); - } - - public static class SettingsFragment extends PreferenceFragmentCompat { - private final int resourceId; - - public SettingsFragment(int resourceId) { - this.resourceId = resourceId; - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(resourceId, rootKey); - } - } - - public static class SettingsCollectionAdapter extends FragmentStateAdapter { - public SettingsCollectionAdapter(@NonNull Fragment fragment) { - super(fragment); - } - - @NonNull - @Override - public Fragment createFragment(int position) { - switch (position) { - case 0: // General - return new SettingsFragment(R.xml.general_preferences); - - case 1: // Display - return new SettingsFragment(R.xml.display_preferences); - - case 2: // Audio - return new SettingsFragment(R.xml.audio_preferences); - - case 3: // Enhancements - return new SettingsFragment(R.xml.enhancements_preferences); - - case 4: // Achievements - return new AchievementSettingsFragment(); - - case 5: // Advanced - return new SettingsFragment(R.xml.advanced_preferences); - - default: - return new Fragment(); - } - } - - @Override - public int getItemCount() { - return 6; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerAxisView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerAxisView.java deleted file mode 100644 index eca099791..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerAxisView.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; - -public final class TouchscreenControllerAxisView extends View { - private Drawable mBaseDrawable; - private Drawable mStickUnpressedDrawable; - private Drawable mStickPressedDrawable; - private boolean mPressed = false; - private int mPointerId = 0; - private float mXValue = 0.0f; - private float mYValue = 0.0f; - private int mDrawXPos = 0; - private int mDrawYPos = 0; - - private String mConfigName; - private boolean mDefaultVisibility = true; - - private int mControllerIndex = -1; - private int mXAxisCode = -1; - private int mYAxisCode = -1; - private int mLeftButtonCode = -1; - private int mRightButtonCode = -1; - private int mUpButtonCode = -1; - private int mDownButtonCode = -1; - - public TouchscreenControllerAxisView(Context context) { - super(context); - init(); - } - - public TouchscreenControllerAxisView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public TouchscreenControllerAxisView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - private void init() { - mBaseDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_base); - mBaseDrawable.setCallback(this); - mStickUnpressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_unpressed); - mStickUnpressedDrawable.setCallback(this); - mStickPressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_pressed); - mStickPressedDrawable.setCallback(this); - } - - public String getConfigName() { - return mConfigName; - } - public void setConfigName(String configName) { - mConfigName = configName; - } - - public boolean getDefaultVisibility() { return mDefaultVisibility; } - public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; } - - public void setControllerAxis(int controllerIndex, int xCode, int yCode) { - mControllerIndex = controllerIndex; - mXAxisCode = xCode; - mYAxisCode = yCode; - mLeftButtonCode = -1; - mRightButtonCode = -1; - mUpButtonCode = -1; - mDownButtonCode = -1; - } - - public void setControllerButtons(int controllerIndex, int leftCode, int rightCode, int upCode, int downCode) { - mControllerIndex = controllerIndex; - mXAxisCode = -1; - mYAxisCode = -1; - mLeftButtonCode = leftCode; - mRightButtonCode = rightCode; - mUpButtonCode = upCode; - mDownButtonCode = downCode; - } - - public void setUnpressed() { - if (!mPressed && mXValue == 0.0f && mYValue == 0.0f) - return; - - mPressed = false; - mXValue = 0.0f; - mYValue = 0.0f; - mDrawXPos = 0; - mDrawYPos = 0; - invalidate(); - updateControllerState(); - } - - public void setPressed(int pointerId, float pointerX, float pointerY) { - final float dx = (pointerX / getScaleX()) - (float) (getWidth() / 2); - final float dy = (pointerY / getScaleY()) - (float) (getHeight() / 2); - // Log.i("SetPressed", String.format("px=%f,py=%f dx=%f,dy=%f", pointerX, pointerY, dx, dy)); - - final float pointerDistance = Math.max(Math.abs(dx), Math.abs(dy)); - final float angle = (float) Math.atan2((double) dy, (double) dx); - - final float maxDistance = (float) Math.min((getWidth() - getPaddingLeft() - getPaddingRight()) / 2, (getHeight() - getPaddingTop() - getPaddingBottom()) / 2); - final float length = Math.min(pointerDistance / maxDistance, 1.0f); - // Log.i("SetPressed", String.format("pointerDist=%f,angle=%f,w=%d,h=%d,maxDist=%f,length=%f", pointerDistance, angle, getWidth(), getHeight(), maxDistance, length)); - - final float xValue = (float) Math.cos((double) angle) * length; - final float yValue = (float) Math.sin((double) angle) * length; - mDrawXPos = (int) (xValue * maxDistance); - mDrawYPos = (int) (yValue * maxDistance); - - boolean doUpdate = (pointerId != mPointerId || !mPressed || (xValue != mXValue || yValue != mYValue)); - mPointerId = pointerId; - mPressed = true; - mXValue = xValue; - mYValue = yValue; - // Log.i("SetPressed", String.format("xval=%f,yval=%f,drawX=%d,drawY=%d", mXValue, mYValue, mDrawXPos, mDrawYPos)); - - if (doUpdate) { - invalidate(); - updateControllerState(); - } - } - - private void updateControllerState() { - final float BUTTON_THRESHOLD = 0.33f; - - AndroidHostInterface hostInterface = AndroidHostInterface.getInstance(); - if (mXAxisCode >= 0) - hostInterface.setControllerAxisState(mControllerIndex, mXAxisCode, mXValue); - if (mYAxisCode >= 0) - hostInterface.setControllerAxisState(mControllerIndex, mYAxisCode, mYValue); - - if (mLeftButtonCode >= 0) - hostInterface.setControllerButtonState(mControllerIndex, mLeftButtonCode, (mXValue <= -BUTTON_THRESHOLD)); - if (mRightButtonCode >= 0) - hostInterface.setControllerButtonState(mControllerIndex, mRightButtonCode, (mXValue >= BUTTON_THRESHOLD)); - if (mUpButtonCode >= 0) - hostInterface.setControllerButtonState(mControllerIndex, mUpButtonCode, (mYValue <= -BUTTON_THRESHOLD)); - if (mDownButtonCode >= 0) - hostInterface.setControllerButtonState(mControllerIndex, mDownButtonCode, (mYValue >= BUTTON_THRESHOLD)); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int paddingLeft = getPaddingLeft(); - final int paddingTop = getPaddingTop(); - final int paddingRight = getPaddingRight(); - final int paddingBottom = getPaddingBottom(); - final int contentWidth = getWidth() - paddingLeft - paddingRight; - final int contentHeight = getHeight() - paddingTop - paddingBottom; - - mBaseDrawable.setBounds(paddingLeft, paddingTop, - paddingLeft + contentWidth, paddingTop + contentHeight); - mBaseDrawable.draw(canvas); - - final int stickWidth = contentWidth / 3; - final int stickHeight = contentHeight / 3; - final int halfStickWidth = stickWidth / 2; - final int halfStickHeight = stickHeight / 2; - final int centerX = getWidth() / 2; - final int centerY = getHeight() / 2; - final int drawX = centerX + mDrawXPos; - final int drawY = centerY + mDrawYPos; - - Drawable stickDrawable = mPressed ? mStickPressedDrawable : mStickUnpressedDrawable; - stickDrawable.setBounds(drawX - halfStickWidth, drawY - halfStickHeight, drawX + halfStickWidth, drawY + halfStickHeight); - stickDrawable.draw(canvas); - } - - public boolean isPressed() { - return mPressed; - } - - public boolean hasPointerId() { - return mPointerId >= 0; - } - - public int getPointerId() { - return mPointerId; - } - - public void setPointerId(int mPointerId) { - this.mPointerId = mPointerId; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java deleted file mode 100644 index b8f70a8a5..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.HapticFeedbackConstants; -import android.view.View; - -/** - * TODO: document your custom view class. - */ -public final class TouchscreenControllerButtonView extends View { - public enum Hotkey - { - NONE, - FAST_FORWARD, - ANALOG_TOGGLE, - OPEN_PAUSE_MENU, - QUICK_LOAD, - QUICK_SAVE - } - - private Drawable mUnpressedDrawable; - private Drawable mPressedDrawable; - private boolean mPressed = false; - private boolean mHapticFeedback = false; - private int mControllerIndex = -1; - private int mButtonCode = -1; - private int mAutoFireSlot = -1; - private Hotkey mHotkey = Hotkey.NONE; - private String mConfigName; - private boolean mDefaultVisibility = true; - private boolean mIsGlidable = true; - - public TouchscreenControllerButtonView(Context context) { - super(context); - init(context, null, 0); - } - - public TouchscreenControllerButtonView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0); - } - - public TouchscreenControllerButtonView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context, attrs, defStyle); - } - - private void init(Context context, AttributeSet attrs, int defStyle) { - // Load attributes - final TypedArray a = getContext().obtainStyledAttributes( - attrs, R.styleable.TouchscreenControllerButtonView, defStyle, 0); - - if (a.hasValue(R.styleable.TouchscreenControllerButtonView_unpressedDrawable)) { - mUnpressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_unpressedDrawable); - mUnpressedDrawable.setCallback(this); - } - - if (a.hasValue(R.styleable.TouchscreenControllerButtonView_pressedDrawable)) { - mPressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_pressedDrawable); - mPressedDrawable.setCallback(this); - } - - a.recycle(); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - int leftBounds = 0; - int rightBounds = leftBounds + getWidth(); - int topBounds = 0; - int bottomBounds = topBounds + getHeight(); - - if (mPressed) { - final int expandSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - 10.0f, getResources().getDisplayMetrics()); - leftBounds -= expandSize; - rightBounds += expandSize; - topBounds -= expandSize; - bottomBounds += expandSize; - } - - // Draw the example drawable on top of the text. - Drawable drawable = mPressed ? mPressedDrawable : mUnpressedDrawable; - if (drawable != null) { - drawable.setBounds(leftBounds, topBounds, rightBounds, bottomBounds); - drawable.draw(canvas); - } - } - - public boolean isPressed() { - return mPressed; - } - - public void setPressed(boolean pressed) { - if (pressed == mPressed) - return; - - mPressed = pressed; - invalidate(); - updateControllerState(); - - if (mHapticFeedback) { - performHapticFeedback(pressed ? HapticFeedbackConstants.VIRTUAL_KEY : HapticFeedbackConstants.VIRTUAL_KEY_RELEASE); - } - } - - public void setButtonCode(int controllerIndex, int code) { - mControllerIndex = controllerIndex; - mButtonCode = code; - } - - public void setAutoFireSlot(int controllerIndex, int slot) { - mControllerIndex = controllerIndex; - mAutoFireSlot = slot; - } - - public void setHotkey(Hotkey hotkey) { - mHotkey = hotkey; - } - - public String getConfigName() { - return mConfigName; - } - public void setConfigName(String name) { - mConfigName = name; - } - - public boolean getIsGlidable() { return mIsGlidable; } - public void setIsGlidable(boolean isGlidable) { mIsGlidable = isGlidable; } - - public boolean getDefaultVisibility() { return mDefaultVisibility; } - public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; } - - public void setHapticFeedback(boolean enabled) { - mHapticFeedback = enabled; - } - - private void updateControllerState() { - final AndroidHostInterface hi = AndroidHostInterface.getInstance(); - if (mButtonCode >= 0) - hi.setControllerButtonState(mControllerIndex, mButtonCode, mPressed); - if (mAutoFireSlot >= 0) - hi.setControllerAutoFireState(mControllerIndex, mAutoFireSlot, mPressed); - - switch (mHotkey) - { - case FAST_FORWARD: { - hi.setFastForwardEnabled(mPressed); - } - break; - - case ANALOG_TOGGLE: { - if (!mPressed) - hi.toggleControllerAnalogMode(); - } - break; - - case OPEN_PAUSE_MENU: { - if (!mPressed) - hi.getEmulationActivity().openPauseMenu(); - } - break; - - case QUICK_LOAD: { - if (!mPressed) - hi.loadState(false, 0); - } - break; - - case QUICK_SAVE: { - if (!mPressed) - hi.saveState(false, 0); - } - break; - - case NONE: - default: - break; - } - } - - public Drawable getPressedDrawable() { - return mPressedDrawable; - } - - public void setPressedDrawable(Drawable pressedDrawable) { - mPressedDrawable = pressedDrawable; - } - - public Drawable getUnpressedDrawable() { - return mUnpressedDrawable; - } - - public void setUnpressedDrawable(Drawable unpressedDrawable) { - mUnpressedDrawable = unpressedDrawable; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerDPadView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerDPadView.java deleted file mode 100644 index 797abb267..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerDPadView.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.View; - -public final class TouchscreenControllerDPadView extends View { - private static final int NUM_DIRECTIONS = 4; - private static final int NUM_POSITIONS = 8; - private static final int DIRECTION_UP = 0; - private static final int DIRECTION_RIGHT = 1; - private static final int DIRECTION_DOWN = 2; - private static final int DIRECTION_LEFT = 3; - - private final Drawable[] mUnpressedDrawables = new Drawable[NUM_DIRECTIONS]; - private final Drawable[] mPressedDrawables = new Drawable[NUM_DIRECTIONS]; - private final int[] mDirectionCodes = new int[] { -1, -1, -1, -1 }; - private final boolean[] mDirectionStates = new boolean[NUM_DIRECTIONS]; - - private boolean mPressed = false; - private int mPointerId = 0; - private int mPointerX = 0; - private int mPointerY = 0; - - private String mConfigName; - private boolean mDefaultVisibility = true; - - private int mControllerIndex = -1; - - private static final int[][] DRAWABLES = { - {R.drawable.ic_controller_up_button,R.drawable.ic_controller_up_button_pressed}, - {R.drawable.ic_controller_right_button,R.drawable.ic_controller_right_button_pressed}, - {R.drawable.ic_controller_down_button,R.drawable.ic_controller_down_button_pressed}, - {R.drawable.ic_controller_left_button,R.drawable.ic_controller_left_button_pressed}, - }; - - - public TouchscreenControllerDPadView(Context context) { - super(context); - init(); - } - - public TouchscreenControllerDPadView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public TouchscreenControllerDPadView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - private void init() { - for (int i = 0; i < NUM_DIRECTIONS; i++) { - mUnpressedDrawables[i] = getContext().getDrawable(DRAWABLES[i][0]); - mPressedDrawables[i] = getContext().getDrawable(DRAWABLES[i][1]); - } - } - - public String getConfigName() { - return mConfigName; - } - public void setConfigName(String configName) { - mConfigName = configName; - } - - public boolean getDefaultVisibility() { return mDefaultVisibility; } - public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; } - - public void setControllerButtons(int controllerIndex, int leftCode, int rightCode, int upCode, int downCode) { - mControllerIndex = controllerIndex; - mDirectionCodes[DIRECTION_LEFT] = leftCode; - mDirectionCodes[DIRECTION_RIGHT] = rightCode; - mDirectionCodes[DIRECTION_UP] = upCode; - mDirectionCodes[DIRECTION_DOWN] = downCode; - } - - public void setUnpressed() { - if (!mPressed && mPointerX == 0 && mPointerY == 0) - return; - - mPressed = false; - mPointerX = 0; - mPointerY = 0; - updateControllerState(); - invalidate(); - } - - public void setPressed(int pointerId, int pointerX, int pointerY) { - final int posX = (int)(pointerX / getScaleX()); - final int posY = (int)(pointerY / getScaleY()); - - boolean doUpdate = (pointerId != mPointerId || !mPressed || (posX != mPointerX || posY != mPointerY)); - mPointerId = pointerId; - mPointerX = posX; - mPointerY = posY; - mPressed = true; - - if (doUpdate) { - updateControllerState(); - invalidate(); - } - } - - private void updateControllerState() { - final int subX = mPointerX / (getWidth() / 3); - final int subY = mPointerY / (getHeight() / 3); - - mDirectionStates[DIRECTION_UP] = (mPressed && subY == 0); - mDirectionStates[DIRECTION_RIGHT] = (mPressed && subX == 2); - mDirectionStates[DIRECTION_DOWN] = (mPressed && subY == 2); - mDirectionStates[DIRECTION_LEFT] = (mPressed && subX == 0); - - AndroidHostInterface hostInterface = AndroidHostInterface.getInstance(); - for (int i = 0; i < NUM_DIRECTIONS; i++) { - if (mDirectionCodes[i] >= 0) - hostInterface.setControllerButtonState(mControllerIndex, mDirectionCodes[i], mDirectionStates[i]); - } - } - - private void drawDirection(int direction, int subX, int subY, Canvas canvas, int buttonWidth, int buttonHeight) { - int leftBounds = subX * buttonWidth; - int rightBounds = leftBounds + buttonWidth; - int topBounds = subY * buttonHeight; - int bottomBounds = topBounds + buttonHeight; - - if (mDirectionStates[direction]) { - final int expandSize = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - 10.0f, getResources().getDisplayMetrics()); - leftBounds -= expandSize; - rightBounds += expandSize; - topBounds -= expandSize; - bottomBounds += expandSize; - } - - final Drawable drawable = mDirectionStates[direction] ? mPressedDrawables[direction] : mUnpressedDrawables[direction]; - drawable.setBounds(leftBounds, topBounds, rightBounds, bottomBounds); - drawable.draw(canvas); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int width = getWidth(); - final int height = getHeight(); - - // Divide it into thirds - draw between. - final int buttonWidth = width / 3; - final int buttonHeight = height / 3; - - drawDirection(DIRECTION_UP, 1, 0, canvas, buttonWidth, buttonHeight); - drawDirection(DIRECTION_RIGHT, 2, 1, canvas, buttonWidth, buttonHeight); - drawDirection(DIRECTION_DOWN, 1, 2, canvas, buttonWidth, buttonHeight); - drawDirection(DIRECTION_LEFT, 0, 1, canvas, buttonWidth, buttonHeight); - } - - public boolean isPressed() { - return mPressed; - } - - public boolean hasPointerId() { - return mPointerId >= 0; - } - - public int getPointerId() { - return mPointerId; - } - - public void setPointerId(int mPointerId) { - this.mPointerId = mPointerId; - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java deleted file mode 100644 index 137e0bc75..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java +++ /dev/null @@ -1,858 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.SeekBar; - -import androidx.appcompat.app.AlertDialog; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.preference.PreferenceManager; - -import java.util.ArrayList; -import java.util.Map; -import java.util.HashMap; - -/** - * TODO: document your custom view class. - */ -public class TouchscreenControllerView extends FrameLayout { - public static final int DEFAULT_OPACITY = 100; - - public static final float MIN_VIEW_SCALE = 0.25f; - public static final float MAX_VIEW_SCALE = 10.0f; - - public enum EditMode { - NONE, - POSITION, - SCALE - } - - private int mControllerIndex; - private String mControllerType; - private String mViewType; - private View mMainView; - private ArrayList mButtonViews = new ArrayList<>(); - private ArrayList mAxisViews = new ArrayList<>(); - private TouchscreenControllerDPadView mDPadView = null; - private int mPointerButtonCode = -1; - private int mPointerPointerId = -1; - private boolean mHapticFeedback; - private String mLayoutOrientation; - private EditMode mEditMode = EditMode.NONE; - private View mMovingView = null; - private String mMovingName = null; - private float mMovingLastX = 0.0f; - private float mMovingLastY = 0.0f; - private float mMovingLastScale = 0.0f; - private ConstraintLayout mEditLayout = null; - private int mOpacity = 100; - private Map mGlidePairs = new HashMap<>(); - - public TouchscreenControllerView(Context context) { - super(context); - setFocusable(false); - setFocusableInTouchMode(false); - } - - public TouchscreenControllerView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public TouchscreenControllerView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - private String getConfigKeyForXTranslation(String name) { - return String.format("TouchscreenController/%s/%s%sXTranslation", mViewType, name, mLayoutOrientation); - } - - private String getConfigKeyForYTranslation(String name) { - return String.format("TouchscreenController/%s/%s%sYTranslation", mViewType, name, mLayoutOrientation); - } - - private String getConfigKeyForScale(String name) { - return String.format("TouchscreenController/%s/%s%sScale", mViewType, name, mLayoutOrientation); - } - - private String getConfigKeyForVisibility(String name) { - return String.format("TouchscreenController/%s/%s%sVisible", mViewType, name, mLayoutOrientation); - } - - private void saveSettingsForButton(String name, View view) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putFloat(getConfigKeyForXTranslation(name), view.getTranslationX()); - editor.putFloat(getConfigKeyForYTranslation(name), view.getTranslationY()); - editor.putFloat(getConfigKeyForScale(name), view.getScaleX()); - editor.commit(); - } - - private void saveVisibilityForButton(String name, boolean visible) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(getConfigKeyForVisibility(name), visible); - editor.commit(); - } - - private void clearTranslationForAllButtons() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - final SharedPreferences.Editor editor = prefs.edit(); - - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - editor.remove(getConfigKeyForXTranslation(buttonView.getConfigName())); - editor.remove(getConfigKeyForYTranslation(buttonView.getConfigName())); - editor.remove(getConfigKeyForScale(buttonView.getConfigName())); - buttonView.setTranslationX(0.0f); - buttonView.setTranslationY(0.0f); - buttonView.setScaleX(1.0f); - buttonView.setScaleY(1.0f); - } - - for (TouchscreenControllerAxisView axisView : mAxisViews) { - editor.remove(getConfigKeyForXTranslation(axisView.getConfigName())); - editor.remove(getConfigKeyForYTranslation(axisView.getConfigName())); - editor.remove(getConfigKeyForScale(axisView.getConfigName())); - axisView.setTranslationX(0.0f); - axisView.setTranslationY(0.0f); - axisView.setScaleX(1.0f); - axisView.setScaleY(1.0f); - } - - if (mDPadView != null) { - editor.remove(getConfigKeyForXTranslation(mDPadView.getConfigName())); - editor.remove(getConfigKeyForYTranslation(mDPadView.getConfigName())); - editor.remove(getConfigKeyForScale(mDPadView.getConfigName())); - mDPadView.setTranslationX(0.0f); - mDPadView.setTranslationY(0.0f); - mDPadView.setScaleX(1.0f); - mDPadView.setScaleY(1.0f); - } - - editor.commit(); - requestLayout(); - } - - private void reloadButtonSettings() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - try { - buttonView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(buttonView.getConfigName()), 0.0f)); - buttonView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(buttonView.getConfigName()), 0.0f)); - buttonView.setScaleX(prefs.getFloat(getConfigKeyForScale(buttonView.getConfigName()), 1.0f)); - buttonView.setScaleY(prefs.getFloat(getConfigKeyForScale(buttonView.getConfigName()), 1.0f)); - //Log.i("TouchscreenController", String.format("Translation for %s %f %f", buttonView.getConfigName(), - // buttonView.getTranslationX(), buttonView.getTranslationY())); - - final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(buttonView.getConfigName()), buttonView.getDefaultVisibility()); - buttonView.setVisibility(visible ? VISIBLE : INVISIBLE); - } catch (ClassCastException ex) { - - } - } - - for (TouchscreenControllerAxisView axisView : mAxisViews) { - try { - axisView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(axisView.getConfigName()), 0.0f)); - axisView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(axisView.getConfigName()), 0.0f)); - axisView.setScaleX(prefs.getFloat(getConfigKeyForScale(axisView.getConfigName()), 1.0f)); - axisView.setScaleY(prefs.getFloat(getConfigKeyForScale(axisView.getConfigName()), 1.0f)); - - final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(axisView.getConfigName()), axisView.getDefaultVisibility()); - axisView.setVisibility(visible ? VISIBLE : INVISIBLE); - } catch (ClassCastException ex) { - - } - } - - if (mDPadView != null) { - try { - mDPadView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(mDPadView.getConfigName()), 0.0f)); - mDPadView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(mDPadView.getConfigName()), 0.0f)); - mDPadView.setScaleX(prefs.getFloat(getConfigKeyForScale(mDPadView.getConfigName()), 1.0f)); - mDPadView.setScaleY(prefs.getFloat(getConfigKeyForScale(mDPadView.getConfigName()), 1.0f)); - - final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(mDPadView.getConfigName()), mDPadView.getDefaultVisibility()); - mDPadView.setVisibility(visible ? VISIBLE : INVISIBLE); - } catch (ClassCastException ex) { - - } - } - } - - private void setOpacity(int opacity) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putInt("TouchscreenController/Opacity", opacity); - editor.commit(); - - updateOpacity(); - } - - private void updateOpacity() { - mOpacity = PreferenceManager.getDefaultSharedPreferences(getContext()).getInt("TouchscreenController/Opacity", DEFAULT_OPACITY); - - float alpha = (float)mOpacity / 100.0f; - alpha = (alpha < 0.0f) ? 0.0f : ((alpha > 1.0f) ? 1.0f : alpha); - - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - buttonView.setAlpha(alpha); - } - for (TouchscreenControllerAxisView axisView : mAxisViews) { - axisView.setAlpha(alpha); - } - if (mDPadView != null) - mDPadView.setAlpha(alpha); - } - - private String getOrientationString() { - switch (getContext().getResources().getConfiguration().orientation) { - case Configuration.ORIENTATION_PORTRAIT: - return "Portrait"; - case Configuration.ORIENTATION_LANDSCAPE: - default: - return "Landscape"; - } - } - - /** - * Checks if the orientation of the layout has changed, and if so, reloads button translations. - */ - public void updateOrientation() { - String newOrientation = getOrientationString(); - if (mLayoutOrientation != null && mLayoutOrientation.equals(newOrientation)) - return; - - Log.i("TouchscreenController", "New orientation: " + newOrientation); - mLayoutOrientation = newOrientation; - reloadButtonSettings(); - requestLayout(); - } - - public void init(int controllerIndex, String controllerType, String viewType, boolean hapticFeedback, boolean gliding) { - mControllerIndex = controllerIndex; - mControllerType = controllerType; - mViewType = viewType; - mHapticFeedback = hapticFeedback; - mLayoutOrientation = getOrientationString(); - - if (mEditMode != EditMode.NONE) - endLayoutEditing(); - - mButtonViews.clear(); - mAxisViews.clear(); - removeAllViews(); - - LayoutInflater inflater = LayoutInflater.from(getContext()); - String pointerButtonName = null; - switch (viewType) { - case "digital": - mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_digital, this, true); - break; - - case "analog_stick": - mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_stick, this, true); - break; - - case "analog_sticks": - mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_sticks, this, true); - break; - - case "lightgun": - mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_lightgun, this, true); - pointerButtonName = "Trigger"; - break; - - case "none": - default: - mMainView = null; - break; - } - - if (mMainView == null) - return; - - mMainView.setOnTouchListener((view1, event) -> { - if (mEditMode != EditMode.NONE) - return handleEditingTouchEvent(event); - else - return handleTouchEvent(event); - }); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - - linkDPadToButtons(mMainView, R.id.controller_dpad, "DPad", "", true); - linkButton(mMainView, R.id.controller_button_l1, "L1Button", "L1", true, gliding); - linkButton(mMainView, R.id.controller_button_l2, "L2Button", "L2", true, gliding); - linkButton(mMainView, R.id.controller_button_select, "SelectButton", "Select", true, gliding); - linkButton(mMainView, R.id.controller_button_start, "StartButton", "Start", true, gliding); - linkButton(mMainView, R.id.controller_button_triangle, "TriangleButton", "Triangle", true, gliding); - linkButton(mMainView, R.id.controller_button_circle, "CircleButton", "Circle", true, gliding); - linkButton(mMainView, R.id.controller_button_cross, "CrossButton", "Cross", true, gliding); - linkButton(mMainView, R.id.controller_button_square, "SquareButton", "Square", true, gliding); - linkButton(mMainView, R.id.controller_button_r1, "R1Button", "R1", true, gliding); - linkButton(mMainView, R.id.controller_button_r2, "R2Button", "R2", true, gliding); - - if (!linkAxis(mMainView, R.id.controller_axis_left, "LeftAxis", "Left", true)) - linkAxisToButtons(mMainView, R.id.controller_axis_left, "LeftAxis", ""); - - linkAxis(mMainView, R.id.controller_axis_right, "RightAxis", "Right", true); - - // GunCon - linkButton(mMainView, R.id.controller_button_a, "AButton", "A", true, true); - linkButton(mMainView, R.id.controller_button_b, "BButton", "B", true, true); - if (pointerButtonName != null) - linkPointer(pointerButtonName); - - // Turbo/autofire buttons - linkAutoFireButton(mMainView, R.id.controller_button_autofire_1, "AutoFire1", 0, false); - linkAutoFireButton(mMainView, R.id.controller_button_autofire_2, "AutoFire2", 1, false); - linkAutoFireButton(mMainView, R.id.controller_button_autofire_3, "AutoFire3", 2, false); - linkAutoFireButton(mMainView, R.id.controller_button_autofire_4, "AutoFire4", 3, false); - - // Hotkeys - linkHotkeyButton(mMainView, R.id.controller_button_fast_forward, "FastForward", - TouchscreenControllerButtonView.Hotkey.FAST_FORWARD, false); - linkHotkeyButton(mMainView, R.id.controller_button_analog, "AnalogToggle", - TouchscreenControllerButtonView.Hotkey.ANALOG_TOGGLE, false); - linkHotkeyButton(mMainView, R.id.controller_button_pause, "OpenPauseMenu", - TouchscreenControllerButtonView.Hotkey.OPEN_PAUSE_MENU, true); - linkHotkeyButton(mMainView, R.id.controller_button_quick_load, "QuickLoad", - TouchscreenControllerButtonView.Hotkey.QUICK_LOAD, false); - linkHotkeyButton(mMainView, R.id.controller_button_quick_save, "QuickSave", - TouchscreenControllerButtonView.Hotkey.QUICK_SAVE, false); - - reloadButtonSettings(); - updateOpacity(); - requestLayout(); - } - - private void linkButton(View view, int id, String configName, String buttonName, boolean defaultVisibility, boolean isGlidable) { - TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id); - if (buttonView == null) - return; - - buttonView.setConfigName(configName); - buttonView.setDefaultVisibility(defaultVisibility); - buttonView.setIsGlidable(isGlidable); - mButtonViews.add(buttonView); - - int code = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonName); - Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code)); - - if (code >= 0) { - buttonView.setButtonCode(mControllerIndex, code); - buttonView.setHapticFeedback(mHapticFeedback); - } else { - Log.e("TouchscreenController", String.format("Unknown button name '%s' " + - "for '%s'", buttonName, mControllerType)); - } - } - - private boolean linkAxis(View view, int id, String configName, String axisName, boolean defaultVisibility) { - TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id); - if (axisView == null) - return false; - - axisView.setConfigName(configName); - axisView.setDefaultVisibility(defaultVisibility); - mAxisViews.add(axisView); - - int xCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "X"); - int yCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "Y"); - Log.i("TouchscreenController", String.format("%s -> %d/%d", axisName, xCode, yCode)); - if (xCode < 0 && yCode < 0) - return false; - - axisView.setControllerAxis(mControllerIndex, xCode, yCode); - return true; - } - - private boolean linkAxisToButtons(View view, int id, String configName, String buttonPrefix) { - TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id); - if (axisView == null) - return false; - - int leftCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Left"); - int rightCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Right"); - int upCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Up"); - int downCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Down"); - Log.i("TouchscreenController", String.format("%s(ButtonAxis) -> %d,%d,%d,%d", buttonPrefix, leftCode, rightCode, upCode, downCode)); - if (leftCode < 0 && rightCode < 0 && upCode < 0 && downCode < 0) - return false; - - axisView.setControllerButtons(mControllerIndex, leftCode, rightCode, upCode, downCode); - return true; - } - - private boolean linkDPadToButtons(View view, int id, String configName, String buttonPrefix, boolean defaultVisibility) { - TouchscreenControllerDPadView dpadView = (TouchscreenControllerDPadView) view.findViewById(id); - if (dpadView == null) - return false; - - dpadView.setConfigName(configName); - dpadView.setDefaultVisibility(defaultVisibility); - mDPadView = dpadView; - - int leftCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Left"); - int rightCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Right"); - int upCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Up"); - int downCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Down"); - Log.i("TouchscreenController", String.format("%s(DPad) -> %d,%d,%d,%d", buttonPrefix, leftCode, rightCode, upCode, downCode)); - if (leftCode < 0 && rightCode < 0 && upCode < 0 && downCode < 0) - return false; - - dpadView.setControllerButtons(mControllerIndex, leftCode, rightCode, upCode, downCode); - return true; - } - - private void linkHotkeyButton(View view, int id, String configName, TouchscreenControllerButtonView.Hotkey hotkey, boolean defaultVisibility) { - TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id); - if (buttonView == null) - return; - - buttonView.setConfigName(configName); - buttonView.setDefaultVisibility(defaultVisibility); - buttonView.setHotkey(hotkey); - buttonView.setIsGlidable(false); - mButtonViews.add(buttonView); - } - - private void linkAutoFireButton(View view, int id, String configName, int slot, boolean defaultVisibility) { - TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id); - if (buttonView == null) - return; - - buttonView.setConfigName(configName); - buttonView.setDefaultVisibility(defaultVisibility); - buttonView.setAutoFireSlot(mControllerIndex, slot); - buttonView.setIsGlidable(true); - mButtonViews.add(buttonView); - } - - private boolean linkPointer(String buttonName) { - mPointerButtonCode = AndroidHostInterface.getInstance().getControllerButtonCode(mControllerType, buttonName); - Log.i("TouchscreenController", String.format("Pointer -> %s,%d", buttonName, mPointerButtonCode)); - return (mPointerButtonCode >= 0); - } - - private int dpToPixels(float dp) { - return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics())); - } - - public void startLayoutEditing(EditMode mode) { - if (mEditLayout == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - mEditLayout = (ConstraintLayout) inflater.inflate(R.layout.layout_touchscreen_controller_edit, this, false); - ((Button) mEditLayout.findViewById(R.id.options)).setOnClickListener((view) -> showEditorMenu()); - addView(mEditLayout); - } - - mEditMode = mode; - } - - public void endLayoutEditing() { - if (mEditLayout != null) { - ((ViewGroup) mMainView).removeView(mEditLayout); - mEditLayout = null; - } - - mEditMode = EditMode.NONE; - mMovingView = null; - mMovingName = null; - mMovingLastX = 0.0f; - mMovingLastY = 0.0f; - - // unpause if we're paused (from the setting) - if (AndroidHostInterface.getInstance().isEmulationThreadPaused()) - AndroidHostInterface.getInstance().pauseEmulationThread(false); - } - - private float snapToValue(float pos, float value) { - return Math.round(pos / value) * value; - } - - private float snapToGrid(float pos) { - final float value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20.0f, getResources().getDisplayMetrics()); - return snapToValue(pos, value); - } - - private boolean handleEditingTouchEvent(MotionEvent event) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_UP: { - if (mMovingView != null) { - // save position - saveSettingsForButton(mMovingName, mMovingView); - mMovingView = null; - mMovingName = null; - mMovingLastX = 0.0f; - mMovingLastY = 0.0f; - mMovingLastScale = 0.0f; - } - - return true; - } - - case MotionEvent.ACTION_DOWN: { - if (mMovingView != null) { - // already moving a button - return true; - } - - Rect rect = new Rect(); - final float x = event.getX(); - final float y = event.getY(); - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - buttonView.getHitRect(rect); - if (rect.contains((int) x, (int) y)) { - mMovingView = buttonView; - mMovingName = buttonView.getConfigName(); - mMovingLastX = snapToGrid(x); - mMovingLastY = snapToGrid(y); - mMovingLastScale = buttonView.getScaleX(); - return true; - } - } - - for (TouchscreenControllerAxisView axisView : mAxisViews) { - axisView.getHitRect(rect); - if (rect.contains((int) x, (int) y)) { - mMovingView = axisView; - mMovingName = axisView.getConfigName(); - mMovingLastX = snapToGrid(x); - mMovingLastY = snapToGrid(y); - mMovingLastScale = axisView.getScaleX(); - return true; - } - } - - if (mDPadView != null) { - mDPadView.getHitRect(rect); - if (rect.contains((int) x, (int) y)) { - mMovingView = mDPadView; - mMovingName = mDPadView.getConfigName(); - mMovingLastX = snapToGrid(x); - mMovingLastY = snapToGrid(y); - mMovingLastScale = mDPadView.getScaleX(); - return true; - } - } - - // nothing.. - return true; - } - - case MotionEvent.ACTION_MOVE: { - if (mMovingView == null) - return true; - - final float x = snapToGrid(event.getX()); - final float y = snapToGrid(event.getY()); - if (mEditMode == EditMode.POSITION) { - final float dx = x - mMovingLastX; - final float dy = y - mMovingLastY; - mMovingLastX = x; - mMovingLastY = y; - - final float posX = mMovingView.getX() + dx; - final float posY = mMovingView.getY() + dy; - //Log.d("Position", String.format("%f %f -> (%f %f) %f %f", - // mMovingView.getX(), mMovingView.getY(), dx, dy, posX, posY)); - mMovingView.setX(posX); - mMovingView.setY(posY); - } else { - final float lastDx = mMovingLastX - mMovingView.getX(); - final float lastDy = mMovingLastY - mMovingView.getY(); - final float dx = x - mMovingView.getX(); - final float dy = y - mMovingView.getY(); - final float lastDistance = Math.max(Math.abs(lastDx), Math.abs(lastDy)); - final float distance = Math.max(Math.abs(dx), Math.abs(dy)); - final float scaler = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50.0f, getResources().getDisplayMetrics()); - final float scaleDiff = snapToValue((distance - lastDistance) / scaler, 0.1f); - final float scale = Math.max(Math.min(mMovingLastScale + mMovingLastScale * scaleDiff, MAX_VIEW_SCALE), MIN_VIEW_SCALE); - mMovingView.setScaleX(scale); - mMovingView.setScaleY(scale); - } - - mMovingView.invalidate(); - mMainView.requestLayout(); - return true; - } - } - - return false; - } - - private boolean updateTouchButtonsFromEvent(MotionEvent event) { - if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning()) - return false; - - Rect rect = new Rect(); - final int actionMasked = event.getActionMasked(); - final int pointerCount = event.getPointerCount(); - final int liftedPointerIndex = (actionMasked == MotionEvent.ACTION_POINTER_UP) ? event.getActionIndex() : -1; - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - if (buttonView.getVisibility() != VISIBLE) - continue; - - buttonView.getHitRect(rect); - boolean pressed = false; - for (int i = 0; i < pointerCount; i++) { - if (i == liftedPointerIndex) - continue; - - final int x = (int) event.getX(i); - final int y = (int) event.getY(i); - if (rect.contains(x, y)) { - buttonView.setPressed(true); - final int pointerId = event.getPointerId(i); - if (!mGlidePairs.containsKey(pointerId) && !mGlidePairs.containsValue(buttonView)) { - if (buttonView.getIsGlidable()) - mGlidePairs.put(pointerId, buttonView); - else { mGlidePairs.put(pointerId, null); } - } - pressed = true; - break; - } - } - - if (!pressed && !mGlidePairs.containsValue(buttonView)) - buttonView.setPressed(pressed); - } - - for (TouchscreenControllerAxisView axisView : mAxisViews) { - if (axisView.getVisibility() != VISIBLE) - continue; - - axisView.getHitRect(rect); - boolean pressed = false; - for (int i = 0; i < pointerCount; i++) { - if (i == liftedPointerIndex) - continue; - - final int pointerId = event.getPointerId(i); - final int x = (int) event.getX(i); - final int y = (int) event.getY(i); - - if ((rect.contains(x, y) && !axisView.isPressed()) || - (axisView.isPressed() && axisView.getPointerId() == pointerId)) { - axisView.setPressed(pointerId, x - rect.left, y - rect.top); - pressed = true; - mGlidePairs.put(pointerId, null); - break; - } - } - if (!pressed) - axisView.setUnpressed(); - } - - if (mDPadView != null && mDPadView.getVisibility() == VISIBLE) { - mDPadView.getHitRect(rect); - - boolean pressed = false; - for (int i = 0; i < pointerCount; i++) { - if (i == liftedPointerIndex) - continue; - - final int x = (int) event.getX(i); - final int y = (int) event.getY(i); - if (rect.contains(x, y)) { - mDPadView.setPressed(event.getPointerId(i), x - rect.left, y - rect.top); - pressed = true; - } - } - - if (!pressed) - mDPadView.setUnpressed(); - } - - if (mPointerButtonCode >= 0) { - final int pointerIndex = event.getActionIndex(); - final int pointerId = event.getPointerId(pointerIndex); - if (mPointerPointerId < 0 && (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_POINTER_DOWN)) { - if (!mGlidePairs.containsKey(pointerId)) { - AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, - mPointerButtonCode, true); - mPointerPointerId = pointerId; - } - } else if (actionMasked == MotionEvent.ACTION_POINTER_UP) { - if (pointerId == mPointerPointerId) { - AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, - mPointerButtonCode, false); - mPointerPointerId = -1; - } - } - - AndroidHostInterface.getInstance().setMousePosition( - (int) event.getX(pointerIndex), - (int) event.getY(pointerIndex)); - } - - return true; - } - - private boolean handleTouchEvent(MotionEvent event) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_UP: { - if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning()) - return false; - - mGlidePairs.clear(); - - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - buttonView.setPressed(false); - } - - for (TouchscreenControllerAxisView axisView : mAxisViews) { - axisView.setUnpressed(); - } - - if (mDPadView != null) - mDPadView.setUnpressed(); - - if (mPointerPointerId >= 0) { - AndroidHostInterface.getInstance().setControllerButtonState( - mControllerIndex, mPointerButtonCode, false); - mPointerPointerId = -1; - } - - return true; - } - - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: { - final int pointerId = event.getPointerId(event.getActionIndex()); - if (mGlidePairs.containsKey(pointerId)) - mGlidePairs.remove(pointerId); - - return updateTouchButtonsFromEvent(event); - } - case MotionEvent.ACTION_MOVE: { - return updateTouchButtonsFromEvent(event); - } - } - - return false; - } - - public AlertDialog.Builder createAddRemoveButtonDialog(Context context) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - final CharSequence[] items = new CharSequence[mButtonViews.size() + mAxisViews.size()]; - final boolean[] itemsChecked = new boolean[mButtonViews.size() + mAxisViews.size()]; - int itemCount = 0; - for (TouchscreenControllerButtonView buttonView : mButtonViews) { - items[itemCount] = buttonView.getConfigName(); - itemsChecked[itemCount] = buttonView.getVisibility() == VISIBLE; - itemCount++; - } - for (TouchscreenControllerAxisView axisView : mAxisViews) { - items[itemCount] = axisView.getConfigName(); - itemsChecked[itemCount] = axisView.getVisibility() == VISIBLE; - itemCount++; - } - - builder.setTitle(R.string.dialog_touchscreen_controller_buttons); - builder.setMultiChoiceItems(items, itemsChecked, (dialog, which, isChecked) -> { - if (which < mButtonViews.size()) { - TouchscreenControllerButtonView buttonView = mButtonViews.get(which); - buttonView.setVisibility(isChecked ? VISIBLE : INVISIBLE); - saveVisibilityForButton(buttonView.getConfigName(), isChecked); - } else { - TouchscreenControllerAxisView axisView = mAxisViews.get(which - mButtonViews.size()); - axisView.setVisibility(isChecked ? VISIBLE : INVISIBLE); - saveVisibilityForButton(axisView.getConfigName(), isChecked); - } - }); - builder.setNegativeButton(R.string.dialog_done, (dialog, which) -> { - dialog.dismiss(); - }); - - return builder; - } - - public AlertDialog.Builder createOpacityDialog(Context context) { - final SeekBar seekBar = new SeekBar(context); - seekBar.setMax(100); - seekBar.setProgress(mOpacity); - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - setOpacity(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.dialog_touchscreen_controller_opacity); - builder.setView(seekBar); - builder.setNegativeButton(R.string.dialog_done, (dialog, which) -> { - dialog.dismiss(); - }); - return builder; - } - - private void showEditorMenu() { - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setItems(R.array.touchscreen_layout_menu, (dialogInterface, i) -> { - switch (i) { - case 0: // Change Opacity - { - AlertDialog.Builder subBuilder = createOpacityDialog(getContext()); - subBuilder.create().show(); - } - break; - - case 1: // Add/Remove Buttons - { - AlertDialog.Builder subBuilder = createAddRemoveButtonDialog(getContext()); - subBuilder.create().show(); - } - break; - - case 2: // Edit Positions - { - mEditMode = EditMode.POSITION; - } - break; - - case 3: // Edit Scale - { - mEditMode = EditMode.SCALE; - } - break; - - case 4: // Reset Layout - { - clearTranslationForAllButtons(); - } - break; - - case 5: // Exit Editor - { - endLayoutEditing(); - } - break; - } - }); - builder.create().show(); - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/URLDownloader.java b/android/app/src/main/java/com/github/stenzek/duckstation/URLDownloader.java deleted file mode 100644 index 6da425237..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/URLDownloader.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.stenzek.duckstation; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -/** - * Helper class for exposing HTTP downloads to native code without pulling in an external - * dependency for doing so. - */ -public class URLDownloader { - private int statusCode = -1; - private byte[] data = null; - private final String userAgent; - - public URLDownloader(String userAgent) { - this.userAgent = userAgent; - } - - private HttpURLConnection getConnection(String url) { - try { - final URL parsedUrl = new URL(url); - HttpURLConnection connection = (HttpURLConnection) parsedUrl.openConnection(); - if (connection == null) - throw new RuntimeException(String.format("openConnection(%s) returned null", url)); - - if (userAgent != null) - connection.addRequestProperty("User-Agent", userAgent); - return connection; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - public int getStatusCode() { - return statusCode; - } - - public byte[] getData() { - return data; - } - - private boolean download(HttpURLConnection connection) { - try { - statusCode = connection.getResponseCode(); - if (statusCode != HttpURLConnection.HTTP_OK) - return false; - - final InputStream inStream = new BufferedInputStream(connection.getInputStream()); - final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - final int CHUNK_SIZE = 128 * 1024; - final byte[] chunk = new byte[CHUNK_SIZE]; - int len; - while ((len = inStream.read(chunk)) > 0) { - outputStream.write(chunk, 0, len); - } - - data = outputStream.toByteArray(); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public boolean get(String url) { - final HttpURLConnection connection = getConnection(url); - if (connection == null) - return false; - - return download(connection); - } - - public boolean post(String url, byte[] postData) { - final HttpURLConnection connection = getConnection(url); - if (connection == null) - return false; - - try { - connection.setDoOutput(true); - connection.setChunkedStreamingMode(0); - - OutputStream postStream = new BufferedOutputStream(connection.getOutputStream()); - postStream.write(postData); - return download(connection); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } -} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/UpdateNotes.java b/android/app/src/main/java/com/github/stenzek/duckstation/UpdateNotes.java deleted file mode 100644 index 6ef63878f..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/UpdateNotes.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.app.AlertDialog; -import android.content.Intent; -import android.content.SharedPreferences; - -import androidx.preference.PreferenceManager; - -public class UpdateNotes { - private static final int VERSION_CONTROLLER_UPDATE = 1; - private static final int CURRENT_VERSION = VERSION_CONTROLLER_UPDATE; - - private static final String CONFIG_KEY = "Main/UpdateNotesVersion"; - - private static int getVersion(MainActivity parent) { - try { - final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(parent); - return sp.getInt(CONFIG_KEY, 0); - } catch (Exception e) { - e.printStackTrace(); - return CURRENT_VERSION; - } - } - - public static void setVersion(MainActivity parent, int version) { - final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(parent); - sp.edit().putInt(CONFIG_KEY, version).commit(); - } - - public static boolean displayUpdateNotes(MainActivity parent) { - final int version = getVersion(parent); - - if (version < VERSION_CONTROLLER_UPDATE ) { - displayControllerUpdateNotes(parent); - setVersion(parent, VERSION_CONTROLLER_UPDATE); - return true; - } - - return false; - } - - public static void displayControllerUpdateNotes(MainActivity parent) { - final AlertDialog.Builder builder = new AlertDialog.Builder(parent); - builder.setTitle(R.string.update_notes_title); - builder.setMessage(R.string.update_notes_message_version_controller_update); - builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> { - dialog.dismiss(); - Intent intent = new Intent(parent, ControllerSettingsActivity.class); - parent.startActivity(intent); - }); - builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } -} diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 1f6bb2906..000000000 --- a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/cover_placeholder.png b/android/app/src/main/res/drawable/cover_placeholder.png deleted file mode 100644 index 18dc446febdbae6950771d71ad21d47bd848a440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214262 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

_YmsK2nO6?ww; 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%0Aer}jdHV-K>Yf?w@qPml7J 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%0AeMqE=AA2N7n~{C z*G_#Bnu_jRYBU{Xi8Ob760}jz6F=M~eFwPoKH^feYkmR!SrE-6(h!{RFC9?8kzfy= zIUbQ(D5H=57qCPM)Oe%-qt(WU)6l;8bl4R6ZtT$=HBhh!-84BxWCc&F<0cqD85srN zJvbrYIRMG`x^bH!@M6TK=;-$&FJ9j90pWM-A7$jEK+k*V4tQrjViYdggi)>wn@+nB zpCBRx^SZd=oy@`o4=la++nY%#T0l@6u&J&!pw^(xOZr{Q63Y^!jpa*GSq=N3K@S-Z z7@Chef~^$)Rp@I-8{0kB*x65WYSz0A>hpGfJ=h=dNHJZh7_8_aQ~j~Y^%k%M_`}-x z>=-&Hz@&tv_N2Z0GEy$zsmi(l&luv#nTjzu{ZbIB~r{^f@-S*kXEs!9Ux~ zvrNXHqe>Lct*EzdtF376bWJuc!C&rO!xtxE;nhgbCyDrnLVOGP4N6i`Pti|Sv${7N z34UfQ_dXH*N~tJMTi-fthqT~aX7u5TllP}p9d5wte(9Q~Q8}HS&n}jf1&_N$ZquaY zja3ujk7O3t^ayvvKQV}M07-g9Up@<=-?jqBTBL_fr~C@9!~kk;y_&6>Pvw=w0ZU%^ zzH#bx*5zjta{6VET_DL*bo4bpm=ZO+KrsjI({IbhN0cv80i#sd1uDp6$vwsfxhkhsN@6hD4J9ML7RI+ac{b;|j45f-2q-pr%+s z(8)oV>&IqfP(z+O&44%^SM(o%QTw5~l{{W{KWUA@Y^bql+`RL-+mvb+-jof! zj2#6!iv`VjqH7Vf>j)O9;p1L-)V(!r9U>gsH+k;iP2YR~!f+r3E`3Kl8hndW8&*$g zpFK{F#P6yz+~(i*E<|dIg%w|va0I!~J)RA7!yH<#gpk2sWr%7l#%4I7RaYceaqut& zc02ge_kUdeKAc2A&PNiLRr4y@r9AO9+kY~;M(K1~y}1{#xZeUv>kYrF2w{P|A_Cu` z%q9dBUpcWU7;?*1uZJ9}J21)W0^7BWs7OK}@7Z7TC=M+Ws!tY@lorT7QWh$hB$Ffl zBs-WEOMUOUa|oWzM%>x@q8DS_!N!h79me)^Mezse3njPQ#VG8x^ynksmU6cAfTME4 zkgq1M+HM@VctKpvAygP+Rp>V1{d_Z-_k$RLKIm|>@xAgXq0JV{Rgv36f9yiaU0gYc z2bZTX(T%Z>?))@$P$Xh2z{DMKVdDD29E~E8-C96JnTf%O?VjVk3ho{R;k>P-0ZVZU z;th3`M2|BJwaA{M zsPKqlDS<7P{C#P|(EuXln@c0Tt*oHyRrcYGp+B-4LG9id%#RyQUfOeoYzOB8eVpwc}H~OV3HcRE}Un)E`rixoqg`U#pMko|h%(H{Hu)W~z)AcOc_z=f>8c0dJ zm|79vFB8{v4^6}AYPT>oL1h*R11=?HSmak+34HB%5I3&j`qrq@#f22U*}}u*MnO9q zS@o_hWL|Li{jmt^DEB*#c9x@-Z}R8hYlV9++;q|r*Mp4L#nJ~Ev28+ao+k!RIwudz5#?i3Hp8mhbT}4s$=)}Xt zzT!-iugsF$^{o8L0vE{z3dGp<9GZH-@^#b@_mSQ1(xltmydtMMrDN3_doflPeIRF4J_8jEj)|aEA|p zeGpYVm@Uv%kEtux17=jLjW!7f9Eg|ng(@pe8GuZze#;CL))~3^W4ich%3HiRkZ#uL z%PIxHu7@lFd(bg?qEC_=kGGGohvId|Zw4CM9L?ycR|#QCuel8NpagtGm3Rh#;P=e} zEpJAf#Pa_%)6QN1PJ>+@FvgpLbGPumKk1q3zApCZK!OzXhKruopOJ1e&D`wN(CpIb zeR0b^&wl7%BfEsn(3{ZHC(m~VY2tUds2`I+ENRnIW+C4KE~M-iuWDd7>5Uz6crb7- z9;RwPD!6UCmiLhSTm@UZQtiI9Lq4fD9?jlr|5%o(J$WPk519)>${bC4wmj*3Y}~bU z4JWWAYikl{{CIt$oEV&-y$DlKpRXI8ngFWc%H%($8~7IJh|xQCx3q^X)2$^OHpYEn z^mXwJ1syccAWvn6zef@67!haN>9iKpkMg+K7yZ?Spx@n#9{S{xD_)C1f!<4Dk8{Rn z)Id-hOg&_}3!2r&ZAZ(XN#jB@1#Kd41y{=0uPm+nilo~ljFw5fAi;Y!2~GO9+9FAk zwC(rq^DvUk>K&(Y0N!WLH5bV`!Si0(YRzfMB6fzpmsy=rF3ln6xt^S?AcVDyfE*5biv=+pHXJzSc+7uK?j)@yCmTxa1ctucIy{?vA1v zt9V7+nfxy=q98#|N*Em0G{CGHGatLE3_-dRGeQm2! zlsiv%n{I{Pl1Y&9krBp`l)Az9fVTtU3r&HSIZDM8QgLtS(^Uhd z1tL%`2IM5R-Iwtpv`U0-{t7r2J8&g`kDTY`I8esJDkgbA{ZnnY!Xxzt%*cCqy4M|R zG?03_n%S?H_1*WT9v8>yy~Pae(9M40m+S4%>pP}z=LVuOGI&i`V{4Z0Xzym?xZc_d z5}Q<@gV{Gi!i`D_r?G!#2Gmi`i>F%gg8#1E?Pc|`L*1TN=B9TpDyJ5y*Oo}5w$`FB+of>ry8O6hZPa6I;Qnu+eqf*doo+pRRAP3AlX6BTqV%^O$3)X$8g zK-r$jqQXm)?R?-aNs$chcIZp+`KFWpRAMS`JL=f26>l7ej zxqcLP!@RWo7G!(Ujc)_*7IN~jq3+|+N;hN!@ruUnf^OyOV^ISXktNr4)+iOf1 z6?$xqz{MHZjU_c2r>mNE1hqk}3Q9lSuX&^G%jgoD+XV4t+}Y509l6FTjXvzOM2T*G9%OG!(XJ9JBTEH^1raG@4J@Tr&Q zE)&nfeU8 z`~2&IzfFJ5rEctKB-^>ZP#*gj?_BnkC3uU5Nx3YL-0(57wLv(Z%1!rHhwW|WMR{|x zZ#qq2k_Mr_O%3U@J4F9E%fl4F3Kq)&BKNj1ZCWj%W+4?PvRRRIsz@h=cK*P9J3*ePA(crC`a zaE5fYT)ibmjb|?E?moaFI@e&(yv|Q3r9)%jYf#N$5aQaS+0`G|@b3r&Gn4~SD=%a& zb94=Fe&+-u2vD4W5vVVJ_Tzojv>rH7G8(__xTf$0eW(+?GWikKi?;L~bTbQ`qqO0D zsTZ}q=uOrZLw-Y?*NZI|Pl(B9B|$o#+NB)VMGdt8|w*Rgk=Wi@o zr|UxEB!v|x{lu-M&RRdUY{PuGj+7&Zk-y^x-#O(!-K^2FZoC3ZfRZ(NdQiLstfZe` z@bQ3F?jmKIBw#)Fb@Nc&B^MN13_2jy6k1X%IN5vEMzM`Q?w|op0Gi$;Zv}ho>k~AT z{gVsX{q08jfo==GZ|kTfB+_uv?Jm~ix4Fi9t>BK^R>U3Iy#koXYH~|3oAofC3V9Mq zh16Mu&?>HbG*81HRgj&<;SS71%vs(>9i@rFeRr{Ui@-Uj(>oe@ zhAoZLN_i4|8IjO`rZ`ZK?(PeRAy7~YwKIDwh23LuzlH~;H?VjE%?H+6M?UG zXzH5PxEDDdKzkikcFk=K=z~dSdH&i}fKESJR;Sp#?s1pF)9%5}-Isyq($q|7(1&k>~t(PR4h)#wQ43cz%vdpjuuI zB1RQ1R;&xQGU>{u zDTjqHVQgS37TN>4HRphS%sTG=h%>wtJHzlGb^^x;*kv|&=_7VqpOPh!HJB%Zf`8`Q z{WXCH)i-9h;wm|*IizJv+U@$XrU3j z*y5n^)Qa_Y#Ap&K2upfsr7GK3%g6C-lqp(M(sw`J^d_j2bQgtmGd(PN`TdoVKhVkc zW-tcVn9Q_x`^LIG{+CL!3j_TJ!}u(p0}T=d+fOt|{pHW2;w18L)O|H3BP@CwM4$F) zYrn!yS6S%^`5#h^u5TkaXuD{gXr?cHCY4WFbY8{Gs2+ zocVxOwpbfPsO5_-Hg$JB;$QO!!fjQfuipKh1WSTUXb3^r_$xLP&BM}-OO14fFKH;o z9ilkKfg+8z(KvK4unMOPtI@Eq1p2@d;<1D|R2F_wR2)e=i^+vazSMPIWdP9=HuFNthe8WNz*;st z_$ev`ZE&7=-@309{Xmqr_C=J7dRcz!Y1PDiJehHV9Pi}17U}s$UovOWX}`v*!P2!+ zy++EbR~2{yU#*GBUPKhbD*hwaH7SyPZW*;c-ZR8#Lt%WD956og{MQX|j(=Rsf$Nto zLmr+0?16oJzBwfxJeY|ilD@5jY=M&@k7!=Pa|<*(yHz}l5C`x(=U-SFLnh;>$U|{8}$(T z5Ujyy^b8|&zq#&R{-LJH)$;rv3)?CH|6P;-ssD_}duuo+%wj#Z^AnQ80aON{m@k~= zl*?x_B6Ji{S&A&NY%Kgq8CKcy8wctvbMnh<${hNh9sJmfi^VQQ1wpbVzDg`{)QRy3 zQVi51;UjGM3JRt>U%=E-`@lAMagk83d;)@HitgSDkw_6^(gaLTn2Yh=-uE}r+dO#bB4I3R ze}CI-{#X>?$!tEKo&I7*#q{#Pwrtx<({8>+TY0AZeAdvpCF5*mQbZpqqf^mJf8T8p z81tpLl{D&9e{exc=Z}GZx&OhBKLKSw0$?>0-kTOFylqL9h36oMY$2aM|bo+bpo{7{&x$uVNe0UJN(zz?#|4tHsX1(11)Nx953Q5&*w{Nr zXA`PB`6aUNt2Q4RsE>ZYG@9`MyUMt%uU<~B#oidJ1PDLp-S4gGfjNhp32v8fc5x@j zE<`S=RN8et@#h#9$`OJUoYFOKR;U_}=cT*EHr6RDA=6tj{Bjyv%m^0FQJu`v-m*G0 zgJl;WD6w@^H(LCS|3MXj8O|7*X$8MZUu9(GlzGlHU@^SQnIMz-DgGjA6j^=~qH zi$z|X;P8vH>mCPFY*&FAQfuS0^odMCJ?SQ!n~+4X_ONk&n}Mc~?7tsygP1+I$;EN( zIIt|atDhJwz~{Gq&}P-&BeX)pv`DcBNO> z19YuTPG{m(e|OWq5Uh4L>z8282mUOYTy=1Pf*|cs!BQLTiEzNv#tBlDHPR2@N=wOcjd=+wZ$u_0*OtOhm9OlV~CT zV4XjJI<{p9TX4rkv7vR`c2$Wu7cZ~hPxhL5a+=+5N{|DjKnzYe=kWc2E#||W$n9k7 zFH&J`@QU7U;9bLyx(2?K>qB~fVL_oR0B&{Ln|D?5pf0G*f8pwq_<)yV$lW=<>MtsT zBreZMTPHyCCyUK}bJhF5)+y)ixE1h!J5xO}3zeVY=2eeKkyQ|5nCHP~S+>9!f|HuD z?{3fYBb8JQYpb?BepFB2&c98IHK|>{F()u?y)20@ar3_5Uze?u&DWg*W;d_t zFICL$)(`Fyz2@eB1t4s2!h!lKyPSGLKp! z5eH@j7j&=cLQAQSN8qA<_?mA&M-vH4S{B437@IPSpjM;6LOvqkr%tFSH}K(_$_gpC zdR5kqnI`NjZ6jVZJG&L91oFsI=Mp2voV{EVbcug{WLfi@X9H6LPc58I!8p?;tmt65 zbS#e)rC_7*`0^VKEPWj8``_*(P}I{H#TW$}#!kvFs)U}ZXy{iiGj3`uf>pD`2Gx?z z&5SWe0qH3R;oUDf6#mzC%tz#@*K@U;0RmsvRD9fn%Cb9JFu+7b|8@=y5u^0(i%4Nb}^R?)^fBR&io4z^7IPr(?WwGXXv6Z^ru`Q=D zHmFtW$N*+1@D>^?cKtN62h@3FWEP^djOaE|XZp zIdH*Va~%`Xl+7J7Xpm~8&~d<;K2aKPx8R{}{?z@A;+9;cA1GYK&Q%--T^i<5M+9&V zr_e7(j1mF2WMi|BMdz&*)~OTNVhO(CnBZtio47CFmuwdOG3V_OKVt_|oLO_;P+b!5 zgGROe_lTKNSL)YXEXu`Ea?!(W@Q!;&UhfxRPeq5x36o!PSH53D^X%_Im2bbbHNNTxRn?$YU} z$>sGO-~lD&KEn-nlU-WK8&W@y;b6xDQGvZQ{ShCU*yz9W;MA1pK>E#ost41-u$7>s z#5WdGoPQ^Yg{V)WU5}jNB|&Op;Yi>aWP-^lHfygWk^$m=Cp7-DC{>(OUZ#%)ETOEP zX{L|wEy;d_y@cID_tU>p>|SMFTfUd%P=Jss_R4&h^*(JG*m{8zP60eQQc*fJVZ=4n zW(YcuRGQ_W4~=1+;4e5H)rrRPJ3_|^2y#ZyrQ3qtrD=@$^H#d!3y1}MLoT&vFX{+) zi-}zG?~ogN1nO_F)SF+EF;{?J46pg6YJ1C;Y)>F%O_?ti^vxvgX_aRS#9@F=kf$Ea zO&U(9p9tN%Om%JVDZ#&7fg63q3hP|`a3dt!++-*aXB)=T?89LbpK+rT(O zsMx^I2&MmH1Z3H0>Myo3*ziF)<$>`uQNOz+VL*l5>3HPo$B+jNQC1!@xo}DX6n(2L zR%U@s+_4Y-5lU>?G=iR$6nh2{BiiB~yJM4F{Un~4!9DK&m)H*nIJd8WYQ9Qe7(kqY z@feVgytpAtI7gTR*#-j~v^s+9#zt&di>eFbHzW1tv27e@kKe#wHA?Vj*hp^ao|UUK8in@~f)NfN!CVl8xlI+JsqreJ8_gs4exKbl z7L-YU^8WdKc$u}Y{aNj6tIU{`^I<!{#xlR(PXEt!7nHA7t9lM z<0*R~n3s$E3xSFj-E8U+XMW4xFWzk;UW>aguzaTMY9ThZ<{lQPMGEx2G37IHG-$~> zV+sAs+j=ig5%f@^`TP0ll3GX#@^+mUj=^GS7b!VM_s#o%#$Gop9i@LhM2!dzZFi39 z1--&6{K8C*S4f|(CdQ+S6^yfxSivk&`?zn3vBJp>U|b!9JsIG*m{k)G+1GIyC#%m7 zTn3;nl883^K@5TiB0u0UpHtSV>7N6ox-sU3i@O4P&~Dx}{KFR404OROl(hfNs(P38 z`FUoOb`vYMQH0uRd5R1VYQIqpTjv1OEyt({bcytIY~0vfjt%#< zl;C`4ZFEU7_Vwu!d#pVab3vaenp5Awfz=hq1-Iz6K{dsxPykk1Oy!}mOYNHNb7>xW z*Ff~#gL7$!2bc!aPsv@MN~^H!(2Wh2lOXOMdTFONuLo6jLT~KJ5nh?>=Bk6j877a2 zSIz&%1>5{1{nfuPiW+=l%^H@2{6CJ)!=I}EkK^aud%4EtUb2O2MK%=;+>1h#RYH^- z8Hud2x!0a`rHoQml$NX_;a($zjFOR=kv+4od++ajfB%5TeK_ZQKJW2*Jztxt2Wu#o ztt|gOG<}u4d|{E5>r8~i-w=SJ7(4zaT!uoPmaV^17`#Vqf zix9$FZ|w}v+_f(fcC3&TAT)!ei`+^MHbFSn&Gb7bYkhtOe(nuZ;V0V^! zUdJpwQKL=P2MnJZl?f3nx}f7LQS0u*rMru9W;u`iI;C`rYqiTk|@Pa(|x) zwzi^#0A;ccx3<(LDM}fe&1+_xj2DX%2Ntc7-^EQX(qW$n+eRn?(Z}u%_rqsnmRESM zY6aVY9qHHgc_*O3AFp1m-I2K3iAc1{!6@>H6kLk_Usz5i>a1ppwlWD0S0e>WU|C)G znzcU= z@^AXkpGhNJovqZ~pxeZc5!

5c3<%4t;=iY}SNJ-aPr0EnZX5_X?O=RuQk5^E+K$ zT^BPt%!=5B1o^aKwN+`>`EMU}?%|ogYYL5S1@%i1JYON>!vEe$x?*A)@Pey5ZZKQ6 zYpi{ccz$M6XgG3R>s-bf4#K*uoo{}VbJ2*8!Sq`s4|tJz4l?G5d*xWS3Bh4>wBsFI z?R2`xhTQYS)hHT zdeCf_L7z4WSZcJsP0~s(b3e(A@@)G8YD>Ou^79A8(B#f7b0W|DF-OvC^W_{3)F5RW z=sS=F%Kp0DD%Fmr6}CU!2QN73K9qcgG9LGQ(f1s9jFpRcl06cMqFzOOR8|SDhKlg> z(Cd2+9iYCR2VwO)1a9si@R?4TJ)u)F!ak4?JbB+GVSOHnr=yVk+vecO#jZeoV1;)o zlpFgJgz^aMDptbAQ^prT{Nj6F$Zrg>*DOR}J`W=9@{>3>nZ|JO{kej8oFRK5m}st= zcn9pcSSZR*hmYRQG2#qHTlWpSB}Mmeme+>SY_YBnZFG1VmeD5bGkJ~VUKX(yXrXMq2; zfjY*(g?8nu3&;aW@QfQaskO~9Tl+fmp#W!ra1P z^m{i=L2ionFlq6p&|0yYEC|=LsoUM^6r|*=A7r8!C+{~%{E5{-@a2b#Zpk&aIl1o5 zcSjrrv$j3i@aRW2N5>?nh3O5n#=@Cg^RzN;*YGKboW6FnK^F#880=#bQ2*+=lD?5{ z$iLk1LSx4PHc(zq!2o|0Vjf7Z#kc-Ab`^Ld*5_RkzyI9HVGT$o8do5D*hs~YAn3QA zBm#fSU~@Gm-JbocNslX=d>6U)+V3^pW8hkSIzEQ`tF!1PYw1I9!gC&U*u`Inp35aO zulVgu7@eA9swapR4l-}}hF{1=h%?+9U(Bd|NF)dz@hY`T@UfUnzz!vy;uJ%_B_9o;#g@dk@9u} z`AK4_^n@ep1Hot>Q(+?kue~vXw%lXu+HS15*n;qb%^L^(;_Of#4sGB}7tz_G`boB9{Ny(gzl4_WA+k@hnqB;|6e4NU6-hvB!?wCj%#QYPo zfmBiaNt=%rUCfCq9rMET-rwk_NX^fpYH=}GQV4!oU{3Qrp8YNTL9YfXUBUSKJY8m02&y<$IGbCF#i;d-+0}*n%^EPuly@u@gh+xv?(mr!Nd9 ztHoDEE!+feS7A6GZ3{hl*CC|RQ=m*W3h9Cp1R~2S4kW`l#096nrQ!-`cjHjToi{fv zK1ROwE*kqQ3ELa|D0R6I0wCh^F5J!LR<@|wBdoMzG-`c1cq9#(QxKPYoMSx##UP;e z$t)1&KgP3+@*_*@&1jSY0IemmRwVCQ$zQjbeTQzKjtjg1Fr)T>h+F+ZdKo1lyQiKF zFB_hF1ijvpi(w+UoOr1EKc;=uh@= zgCCbvt0e&1aE7B2#C>qTAuJC|70HXhONK|=3rORl&m0&7oeUjY_JiV>M3b$`jDRIo zDg!S<0D)4R20$R{O>_5(3IHg*!qxC@)BVPQUfw<#-k!Q33NjXYciOl=&RvIMAKqgH znWWnT%efS@B?&xhAGc!CBVHeM&83t>=(+I1Se%CLu0BJ6O*p7%N$!uMHN!&Vp5Z1uK zl%Ad5y!M}0L>H_IYky`NBsVoHr<|K%Ww~nJwa!`+CiyqizjrqC^TsRq5*qT-;|tN0 zot~+X2JyCQujj=p8L)dSPTYhlwWD2eC=iM|-BivC-3JGyFhy$Yqzj&p^iS&1ndS(5 z0E%(=3xL`I9za=P5^D0m_nw|%w`mz4MPN{Q>ye0b@c0~p#Js}v2$h~w0avioUB{DG zPnbuw668CgJum6>%`K+Sk8>XhX%lmm6H9!skIXzc^P^R>B-mF%1KSz4zG!#5WAFh+ zy6_2C_fv1DOnysT^s9RuDpPd0IC~qUu!{nB zu1F@z>O8zduB!X%ed?t5VngsS_xItK$M3(7hY$1Gqr3S(9ab0&nfv`6`C-&g9&1n> zb7$d|2*HR;n1w%&+YQsL`NfX7k^{BjH2J6j9ebaaV0-td%1_ z?G=z}^%AxJ0&IGy>qJMa3{Ho=|CUQ9Pd%Xcd)Ck*NARi;L?K#lDp26Q49_! zL)iJodVwY&rtS%{G4d(i;u1e#83yeW9=x$LONsA=fkHn>QUDA_KtT)n(*k#)kB|?n zzQo7udJ@7L9t zpK}E+1GN0|xVjgBQhsz)1Jl5$LG}0rWNHkh?8wq2ENpZD9?z3(XIBb#W$04yr4y3-~dXb*N57-6`d*fKkD(s)b37!ytC%P{3A+$EIi*afG*kOFl zs?MrL#UN=*9f|cS#>?r8y1t*sH0_@gg}wJG&e9!pDEEv(Mz~RtvY5^#SK`4n*dO$Z0h5Z2oo#ttOhBAv9HcpoHD&B?4lsu}NpIya*;rUebq1wg7Df<#Pb z=8E>RGL8bd&8EMimsHnKLB)RvAZ0R^ECzqkO+PG3g+lGiSZpYv(2x zxu78gim-4ZS`r_eG}@)a%6}{vx*lXH4K$?!7M|zt{76K}G`czzxty=?>K>AInF|`k z@u~zX#e%W%mV>x{k#NfHuT{bNZF-|2@OvC}VJGtNoN8wI+QE0Mb%5!+WxMJvTC9D2 zE@kV>xeo_wM0>URvbCAj*t}f@)<$5hx$7Z>(+8S;H3+-kNsNwQ8W5bp`bG_AU zy$>lC2OsX)9{fgg5}9-<;SYx`aoE4?R{V(bQ5Z&FTv;ihV@Ha9ZaAW6**b{_mY2=k z*coC^NR?PXo~0Ks@@Ng>fH(rrhhDV@icvLu)&(5+`1|U($M&aph8(+Y_lwv5EsB;3 z;RHx4@hm{rjD2ManTsOo;PF~Qv<3O|QUAqtpVObPuB|@>NHZ?BD5*8A9FFZBQQ%T0 znQf12(2@Fy(n^B_Vaj6P1dJ{wh>cI4E7gSTQ`0e%t+2k;%dnn}2vJ%#eKEnp0{50= z%XW8Vbe&-%%78;IIRu6#$Ol4v_UCT>@3Bfi6ck^C$v<;Y#fy5Rd-OlNG(_^Q@^(7> z*Lr(=?LKRWP{h>;*FB{`bNf$jNk2|Ld0R^K+<(}i6VE~oFx}S)@)$r~v4uzNEOsbW zWa}{}=$&!A?-JjKp52FuU@@yz%UA3~w(+4?UW}%SZ4Xvdb%LFvm?_#nEpdnHqjNdb z@xJb9uTFfDgACl|L6r*6x81uBS@6BL&!ki+XY6OK59M(k*sp9JE%COQ6y*iR#x#~c z$(IGdDK8W^Rp?Cc;LX<${7H^d88+yN9h?$_zLS9nBKE$0QUKZo2^9_YW9i~Dz&|UX z5Yq(1_nBW6qQolz78Dnfi4%yqZ0#fPZgIW7jCs|PavFS5ERituon}a;+b;;dca{bM zU_+QHVTm8{JvX-3!4A&H@#`$2u(vp_V%!nPCG&*px$RP*^@Qyu&u-n;ncH3S4ViOX zY-LvJ$FV`3m*XG>yi;u%_D4LuA=c$kXI?~6`r?^9FsdLT(x73GV{*x4U3tJc`R6D* zi7vd5s1^C|T*{?KW#nO795_l$c!d*8k$updKH~gxWxkI?l+uPm2>Yu!w|J^;Fx6CCpr#GD9>@GhWS%I`b zKv1oR;#pM9XDjIdC}ZtP7-6BIaOYw=F=8;e|ay?yDa01-74LCG?CfoSmu zdt(mp{p6R(wT@=e)w{Rx{i_HZFS(tT$(Qtu(s zJ>-XLfrHUFslZD(AyWW=sda8Rd=+5(S7yJU)%;gF-_$AK66NT1>d|Q9@i>bh;;)iK z#Q-yh&;FC;VvW=M&jHM?0go?mGY34imq|HSw-lU9l{dkh?YP$4AREJ{&7 z;Bw*?R(i0W8`kvc29ca44LIyO-tZZTvuSD+W?QLOaj$E#_)}8YW4Io9n5{7Ye0WAl z6%;asS69jhY$cBB#C}y}+HwaCF~WtuZRsosUf7(ZfBw8-^!j=ErHX79eGX>R6ImB; zox_T;nqt*gV5RDBV_2|N+)5wi1y_&pAO1i-YwT;>r&?kJgpejfH3)Jd0K}Ie*aAxE zCcHRFXuM^!53m()$z%mw?9T){&gaCS?!gf3ROrbEUVQM8`NfIez0obFnjAbqE$^0N zA1piXpu%-IUYxzc4#=yJx<=_?3e!Q4-) zBM{H(0XnANh)t**<&n>1LOKf(jFSa2>}tXa5B##89R@~CCIr?guP0Vb=3$?}#8@JE~8l}6| z;rPRZ8G=%jiu)g4(oUzUW z)WF#A0Wb&xamVZlaYu>FnSP2?f+Rq4iM&=uRIsBG`OiC*GJf|Z)(|Ba9!;&vj+Zn* zzhJ{ZSma;mexLJ7iRpP&oYuoZva6R3AKs7#UPkl?0$*uOFshI{2T4c*#YtV#)(aKt zJ(UXFq{$e*t=l3%)8XFltIh_`#<`S$!hc`6IoP#U4$a#G7`Xfy!MK1(V)AK82NJ;7 zyyV-XQ_0nVV;nSgHV#37^ms=pQ?Mq zBmf1DX>~45`nHXVH>43J)T8zVcWyxyW;9JX8`560fc-T9_TYKJ$fLBQFSC(3&5XZd&p-s8xN; zPI8$1=7WP_w*P(9k(uWd0E+%=NIBO0t7f5u2Mc#sE+~Wd_7jBo92lcqW8RFhuwv>7 zo6J1mWdzjlw6u0Fj_V;K@^^vu$E zK5*w$77Sw$Ri`WEq$Vdo8(5P$ON+1Sv*4@C$Z*-7O?>VoX3Ge6n-af6A)r? zVk>qbLTLdSJ=+lP0sjv6W>`;-*18G6p!N;KLoIM>{jQk7t`W2x&upkWw8rVXgJj9m z7S$=mEDz3o&ps`~M_bLRS3m%5M*4z%UvnV*&M(cveQOzNHs%jWJJ_E9S{ z&-!v>{NN&O)*IDnTAUJ3x`|~y#nGH`)2Ovkd2DrQZPIH`93Ozanzd=~_7>?*#T|i% z_1GScR%rO-v##c7qoU&eL7X#W9E*+{l6aBnau-Ur33k*_BZd;6;4(dv=jcptTc_g=3>`IRcj04O@OHsNB%nD9~xI1y<~`oi`1+VYM}5R z)m4y0&7j@q`QaYGBD3LJ717IUo z*-1a3yGb_7Kia$ukVyiJ)!3Q$1~A6XRu(Q<2JG5>y1lRg%~KO5oeip> z=vEjj0xjb@ak6*~SY9Gq}$y$iySwe2bH|%y-1RwptVIzN^ zC!g(stBZ5OanBR^^omm(MUziMHm5$O#TU|nj z`#e-^uh;YGrP)oKZBu}m5sO)`xuc6_bRaA2z>ZlAWS_4wMLF_V2us+&{lz#DC3t}!3(X{5c1=$4YM=m_uACZ~1KjN+ z3ta{iQgai>`v7)obNZJvpsfnj(Bb(>`g8%H;y!)?y(Kr5NtcK40PRuV6`ZD#`1K=n zP0j&D&MD3yzR_8!T|rZ)t1bC!iJIF+tev!`lmAqF!{vY_ zRuRMhTrN`kklIidOGUDgEFMqvSDEcCXa5&7oIASl_nQ_aq8x$!L1Mw)iCM8ZJfG8~ z8(jUO*s&Yi54*PA>~_eiSasmV)i*}`^p72x=g@@qA3;A=yMpY80$@%s=zbLt7Q*X5 zlFb=}Sz0#Z!r}8CUa6LkvEQ!sN!;p!D^8=`ni-DW9L+_k@q(-(ZyriT9UHH zkkUI87`?m}_H|30LMz0$ z4SdC#G9G;}2?eL!ZK(C;A}zA4f2sJSIMrH!4Zb;zw_x8>20B+B@B!eq%^MAuwO0@a z83$=$sChu8NGh}f*}(4yvwA()MRVZ&LWH|XGb{+8M)2wxh=l|;hVe|Xpu-;9ht`LV zg|9v%GSAG8%1O)Dw=hy8+@lfLVBC`bO7PCJ&~csF75yC;44W{S-pI&^G(&A%oH(|U zActmk4#Z$PNA zJ1`H|!x3syv|UAh2$B`=N6+2#*RW)zeGO7EFT-5UN4|}~X2Q3g-p4w>6e3;(?IV)b zV^5XU{i_;`-8{C9?7j2rQ^k#c9JWhypTWbI6+k??PjrG`67xGRN3Vw-7#x)BGT~F3 zCGvrE@66o?a0GybFI%&P3n`?4fx~1kS1ozh;l-T}kMIM^2ZOy2^y+D1gu~-h1$D#c|H_UYf^JJ$`?g?c(-04N{LV4wmkkoIB3je0kRTd zX^~4HBh7gi1143(w>7O|c@doPvLy`=$8{_m!4p#J4ni>+I#8%H&rCo?gb^1C|Bb6qZkjqS&eL zPB=z8>@vAHna)oFntidx<06>1xPg@~0neR+#||dNCe#p*^LR*u!Y}o0f{iYlJq%I= zT)6Z2Nwvq15Quq=Uj3b8(JBq|t^w^G{nl)Cu{e2qm$UoJp^bhFCneBbe@oot4`QgF zti$_vC#Th}1KQjr%K%(knFFLt@W0;tG)3iy5o9_`Be8;lC718&DYE`^`q|UuuO)Fu z*m3SAfv)#A>DRUDBUg}Ohxq{O_qOogqOj9%04NiV)Vf7fDz1CXYR#qo#j5Po5D^cI zz|{^j%#*~ay)4)xaE2L3dX2EeEp;cvrkG9@Hew)B?H{7yrptwK_pf>7%vb*{_hMP7 z2F$XNe+=6#;#hb-cq_~&q8{M`o!z;}fL(NpK`Fr~Vb34WpEJ)~~lu~ZyJzyX& zSLG=GdXOX(@c!wW*M>S}@*Z#S_cAFq^S{JLc-_T4Gz)J{|LxuqBjnd|f6_e-I7j#jUC9CGYS(Mh=hs@aiQ zOrGxlxog1U^J&8B8N!YQ%gtrhc4;4U^-zPcWsc zV25E7jgjwhk4Lee7}VQT4Sm4r4mK1}mEZ|kLN{+A7;HfC-CX~#_7xI&gE+};v^?LS z5FJSEyzZL9xBKG1UkgzHt0L?VZCkRCt}b(o|J!_Oz$s!j{L6wkUp_q4%m41L^bT|Q9|IfNY59(4MFhaR>pw&mvb`WJUUR% z6Z!0Agy^TQjKav%i;6Tz_wD!NJ~44Kj&eBD*=@8h39}leQ}!RX-uZ?=kH5y2QLA$P zY^(IZzb>=!*+(`l>r4#~^o0MB2>zwk1Y#WXl67P`x8=8DD;Pgdo40*B)VyYqfu*DxsncLs3Mw9J| z>Bk0EviGbS-hHI8*f=*>50oB?c6u`n!6>vI3DlhBEa(M;J+p`YgjAV4!)LxjM2XZJ zOSDu;>brx^DCuIN1u*d>(VV-MedG|yq5GJ|Haf%2-IyA;@|~tko2q;mkiwAb6_hQm zCCfDui`1811UaVU%F*{r$Hz>V4|}X{0tQRyEqkb2y`}@8!MdYh!Wy$*s|aKx2}>BW zKjT=7;4H=o96%>IyIDO$wXyro?pq54K5%cNl~psW{@nu_vH+>34;)b$FLFC=T10CZwvNRSGpFKMRTh#oz|kE?+{Ro9RyeQ@>O@fz{#{fawGgt^G z6{`TWgi3*P#-&fV@2bdu#Ex~HpjACP!>dLs1Fqr(*1qe! zvK#v}NNfmLzc*raBrww-apt)?zbP48xwr5$lv?u#B^o_S(Fj-!<(Umg|{AE+fjar#B3j0 zSL2`wLMW~(V2v$ZYzZtyzDq5m3{p@GTfk|k`&jy>y&B@O8YVjiIJxj)Nn-n-<E2vMOp7hE6o_HH49bs) z-5qDgyNV`!x#lNS^F~9XdFk)|LlWI3VWeyxK_CM#i`N3#o5jteRtG10sviDcE4ndv z`{=D760A$O82x=V0Q0KjGk#iSTX59oM{dw1UB6uhFJf146A8tHWWcBEmW29^qyj+!yR;% zczGS4b=Gxg#FBX3E=a;JerKJ{bJJC}ZK>y^3%7)TxjDFo)en7+v#K6yFxH4~ja={aP46@(TFtvtzMDrIKnBA6dfQ z#=Pc_>VMv-DA5=S7J$%1=8e8F%=%t=J*B;YN6~cu8S+xcv1#)Bcr0g+%?luy;jG0t>dq9@r$T=U(J$v&MG04;{I-bN_(E>3{Jl;wEhsFF;~3;jwnbgjU#4^vVjUTOm>gb_;+L=UjWd9T%%xjfc?Bp&DSwPsjJ&jVncxo8t=^pO z0I@qEiYy*W-px_De%q$+_P%Y^^D=B#i5Fq^HeVj-^Ko_#U3MkKo)YEmdhn4|z3w<9 zA-L7DZ!k#*->LdH{s0mpglc4L*%8fDs5psz?s3M6c?Gx$RF4&+*X+>>Cc8LoM2wKU zSU;RJp4r$n6B}h4TSvkh7|n12#*|-x;X&j<#H{I25n$^D_&2;syqF@!=5)>Cm@t$P z-Wkz6L3Scm;6R4$n)Tcm9Y`@`l>xkSB0~RDR>d$M_S-qKQ;oSu>n_lY5aRV>;c!@< zI1niO?RjAYo`ZT*ha@)@d?8JyhQFyEy-SMKrAzxS&1?6De!?4^)Yz_B4M-k|zt8f$ zzh{c@M>9W1%ICP!?~+uh3?nEnx`kxczSz z=$|hc;xdX$J@kTwGyWZTB?>wuzWDU6>ekOO%vC}r4zZJ5AP1uBfu>^CTz-V3Rnjb! z-=ZVkLhNG?`j;{cnUo_9fjq_));1|Ps|El`)LFC+wx&-rF2#{72Ce{;>dh|?`$qYX z?UOkAo)h(R(^+Y%sxSK@nanlj~ zLgCqszJijnS11djJPUpvR}DFS91r|jI2}ofHpJjx$&Tz5*OMM$KV1FytMi0o6&@ZJ zi6V46(7wMLIV-Zg%8t)w;e{U*>pzU2Q-5sfT*IB%*5Z_Zkw;UUldtUbk7uhw+qR{z zpXpZ+IB&}ic^ntGsM7ZR@9&3qQ8Hj3m_ay z;(jfwHYqZqAT%mGm=&_2C`2m{#;xf5{>hDC_q})S3FE$Go}4o`HAZ?&6*99}#h|ob z&-n{3tx_HF@a8FQW_FJ*EBzrzm5qJ^ROd8E;6HIdk*<4tHQr~Ccr=jE{sg5q5q@BD zZr!Ku3^*5wEx;Xfq_tO3SrK`8IUqq6^EG)h^ezs+IV5TECY2d8e!$MBBXkN$6;qv` zI1=U#x6_Z%`(#X9@H1nyH(h5%K)bLUga98cnQj22t#g&MljsKgIS;C+ox7S&Z1RSY zPFVTU(=9{{>FsqT2Z`+k{mt|A^ONa|>r;HtTYXklK~=lt_^mqUe}$|4&Y0axByk|; zjt^;6dQL)S>rvA4b91dEr6ctvq@U*|S^mtIu!Ls4B0brGw-7I)4Ayk)g=4}1mR zCpRln>`{#suDR9X#8k%9I-B}kuXsH(EuW|Le^ws4M{f#vfiB2a|Mte5)AbUEKI>pU zcaTQDLH?v?+{=k8>y@?S-sm7^xt#7HmGF&<4v#j~gdMRQft$6e+mz#E$dVi`?I*Z) z`Np87^!{LO#egEuu z>c7w9vk*%G1q$I48vaSbl{-XrUddm-i#|-@)G^bVxC=&P${E+?@oaSpOc?8Oy z@wwHJf4b5Cq)||PwuCCz#JoXf$dq=T*e_vGUzE(L*7L6GYPx2WgR46bkGu;-lA$`7Jnw9nL8f zatRH-M$k&SVsnBcM)GfK9j*x()zd4ZsJTu3x!w3;CF+^%IsSKCywP@_33}LrKs3pW zbi*1iq=vW^UG5Zow!oTGu=LOkyqc2e9N6$hQb7wFKIrI9H#xVX@eYLXsk$NuSHRR* zx|}hP(K6YTNWKSY0Loh+i~p+V#VeI1RGbwmDtxZ~M2rHj`VQx)G>`#G(!koiEhxhh zZGlsxCC}tX7}zC)>-C-$6nu(A4j<{KH<4aBd>yykKOLR{(6{ejLS_PbD{VkS{cA^T zvLh^y3SFzeC*eAwU^+ce!(LYY@}=0L9MXAKft@Qhf=H{eU@DL>7Z zou>f8c_`sTG3EsJ`RL*QhF=n(vCeLrG$R8{ZeT&TJ+jv;#&1Ki>)X^)piKzc{Jn=} zYbIyFYWn^Ehmch?A@;#i%89(xh$y}M0RGfAv2DRMh-MGCvrwtdm9^uh4V7*&Cx6Et zyZ#bKJE1|quf)wqDA~#=v3gw*p&rIxfEVg~)*yd-B_vhc%DI(1qdpOS6Q749^)7;y zh306gG5R@VIg$U-J%;ZDEC1lwWQ(V8UJ4mi;DDkStH=bLq6Dmqta*7q4;apNB7VcO z;g^=ure=Z}YPMDtsPSe2O0?#kQ3eqmr03-i!6M}j z1G?9OX;AZ2q0r;9pD43Pfs9Daw`oE;F*rL^d9qgFS+D#0sEL_7+ z%$H#smrvFp|Iv)8yG@Si#F{oddXFdE%cqV3CCJvIpYeZwB;rea`V;2UF4Pab+>&0g z%y?HMEz_p1B+>co&Mc@m;{{w-C`3Y)#cPBl@RIS720grEht|`4xR(Efw5~c^Qt&s1 zoAQbh7I|2G>#znndUHPj8Te%QX#^l#Dg!*GVz!3mU*SIG@Bx}xJbX0H8?%4YU^4JO zm%E1k2ona!N%e@qai`Qi37@6aHOhknNTuRS{ye|?KbGA|6_mo6kpTC_BCEzL)g3738tcE|m` z5RiP6YS~-#cwlzp}MZJ0?s3GA7Z{Eu*v%Wk;%kRq#;yQU9acF?jr@WHV zqwAWo2qUTo<**)ROz&H{IzX!npm)cKn2>Y1c0BqO$V(RoV1zkd?Xl=Rv1UWOV!-U8 zUhuT)nTDg;Ibjdy{;?@Ag5s4vBM>xzVJB3Qaw$ce5O#_9J9giEcNG^0%;gS*e|vseIa}kd{Wp9=#4Ck5`eGC)kzHvDH#9L49`UtbUyLDDoLEb!E^| z(@qr=qT-r3EP^iA;dA&{Q_ChGt$*8z`brEIHJSc9T9W)B%Cq|%?>nSVopTuJr=4E2 zGWkb8yHDfN%Z(ewBo)ifTluv8*DChT_P)Xwv;^{M`{DOFk{y*_EpY_>cB((lYkHkV zS+3TuUfKDxlH6y?NyXYvt{v3Be6T!CBHLZJ&I}qkF*I_YD5QWF*86G*OLE&E zAWT;2#cYd_<>r$|xK0VzNhW#gokSl|8UA&DSzEaCPK?`pSNX>5y7_(6oRqsx&$phZ zHuWxTk+}xWln8ciu|}UVkazG-LMwbYYPNgrhOVLVODdB-vO~dcGdh3P_W%g$bXOZW zKCqo+e(d)lxb7Bd{HOPnLuPqhMf|SLc=nWb(hhthVPA~a5jW|Q-0%BzhUJ<^d^kR+ znQ@-Z3;d}*Z7A?NOMdn2+^3|SsS6)#x3qZ)dUrzH;A>~gUL`w!!Ur4{6_!31DI_Rz zQjY4Z7dJ>5z~R{e#n^U5m`U!cz^Q6sLh+58iu)e@3*Iphc1ggsZkH>e>BfRe{&Jp$ zcM6-VdnBtZdRh;a2QNuG$Bu!!9c67}%|>y=VI#bkuV-wy!<`6cHQCJ^jwoEddRa2y zY=W|{r_#s5RjYx;7xABq-pSud42UVch$dR7Rn zv-3Ee^=5bZU6m>Nhs^D3(mHKT36s|%kF><^*l*i#M?n4@P#P;vYyA~g)sZru*kJW_m*cSi!9?QovaOplhng-QE440a z#4ku1m*kO2w{GFF;45wu6J1aZ+ZbcAX$JsG8j;Ir4sd{IyRGCT55Zy2sdt4fhD z(D|Om44GesY{j`Q&!5eu{h|dq6-oCmKE8qG#B2Atk`mqy=5xSr$LueuMg%5-tM;sj zVn^{J-wf(8-ku_`?T0udP-^_FGx+_mpi6rPB6@H`O!i{l@~i22 zOJ&1SO1W>3N&KGu6rw~s_3a{2D6I76zoL=eJpAw452A*5hWZ($*ZakXgAqs&CK6~3o=Y>u`9FGHjywZZiApCm$aN)!qXU;iS z7@m;YdUAGUr<-@{m71+6{=q?QXb3yg0JxbOF${qfdnHMWvVcjb2(SvtcM@`_l%fb= zRj@^FyY*kT6wu1d#j*9mtB-H@#_bpYDiOtpNLfI!2kPmpB?5>*hD6kRWXFrJWPDXB zJMSn$btpTL7k>qnLLk0wtZ@9w_)+5_T%4Z$TPd~ZJe#`mL6}57LKQ)C{Jh^$?jdg! z4{=!QN+5>i^^Tx%YBy_u8v$*%=LFT_Z#xM83+3>=ClL*A}u@5mymH zX2`r`E4wnX_ukvR_ji8(`p@Is^Ev0d->=v6iG}K`@kFmEhH>^&y*R(W%$gWnSrWzA zRfwUJ|7&8NRc+d79n>%@a8nEMOpZFJ;uo{yS6(o|?NiwsjIzPQY+P!%NGcqIPbBY2 zv8N&$1JhcR17O3%x$+XA6;d<1t0PP5s_Hbp!YBRn7;4pW^*O+TGBtu2P@ZnV%6KMg z&RfUm#bkXs_-;S`X$5*Bjo;2c06JvwefVBfKlNy{WU~Xg^iq;arkhd2?(WH=XUoG= z@v^L)2TW&+g=E;8A64k0U-DbPcKyogDE`4X#~eH7O;=yJJp6&5$h~(_|?HVXI&jYBnaVJu}>2*#E`>ua&)NHucwym14L+nCXW$z*{r>b&WuF zvUv10`{si!x1L+*smwhYl6J$E@YaED(h2?V=qI~>4RA!#3=P$cAwbPkfwj{$Cpwi| zTVz|Y{@>B|sa7si1Qs9y5>HtIq_#u}M8ag1{4Tx1YLg2_Tn(qILwvT5F zhfm5Gg7MvZf-!={t>hSj%PahSL|5~WBK-)tJDJ4bU#KrDW|ilxm(6RzGVmS=lpXnW z06OH#NdBKpD=euIhE5>L_9GKGiv;TcbW2RkQ&(Drj778;UG!7j!N}=5;67zJZ>eA@;4!TgTgwgKwhf!Ud zW|Bg)*oW{QOsrF{+-SHO}MHy3DW z4n-Dy#R}5DaEQzX&+Y@!&cvwFNpjLaQ2&Jz{GwWPJ){b5qy$??er;zl~uoW9aN2UnTVKiRuxlD z8@W@WzzCVKx}Nsftm>A;jNM+8Q3a5Fl@-GXcm_qQ!2DQ=Z+Nhbz;qH847++&Tkq6) zf3CmO_T$txGn3ry#~jgN9Qu70jkG~O#f>1y*WG=kyIzRhMLSwR67~~0M4cCTHRfRA zlbjUlp7 z``63d94JZdcsb9wXJG#oLRhlkPxP|5jR%`cL zz11+`qsr_YR~8-Zd6Uz#+8ok~v9=?ccI!qw!!jzhe!l7f4&8`gbQ*7qvxR=Il(^ zdJ$rVJHY;?fY_M?lmM1f5=8^C%RK%CuH>>bexLt#shZ$$dUY6=(Dzon-Zbo%fj*@F zkI;W!8qvFew5toZ9Vi&u_}yfARx5g!!@R+8T|KF#p0_PT(w$1dkdzyGe)kF_D~&GU z=PE^je5tBOm`zD8P}9`ay4E%XbonJhKzTP^X&jMUiYYJw1j1de1a;szSuKRMx+HWk z^llXDc($__V1dEey+Rp^YLY#`$#B~Cu7vD;L2SomqUrNqt>a+J~ z&YeO+l=B(!d%0^t=$ATYgL*Cx0>zNWC7B~Ll1X1hHZm3{8{YXdD`cF6Krg90ewgI6 zd3E!gNK)*b z5P^Dxf-LU<5Rc+tnZ(j0a_4d5TU-B0ng5e!mA!(5?k+(pFim`eT;TJ-}}Q>*UX0l z;jgw=m^kwux0wW1Vpk?mp^3TByZV*e)R|=fx=e2badT{#T$Hz71hGL!o)3M1ehf~O zk%`1~5Wp5!PD)%Q395M8p??gv_8A-x!nuR0m7!R%HYIk+#Ii+YC>~QAL&XeZ2g(W} z6u7g}Cx6S_GX2M{{O(db!yJS$g+KGu?4h4FsNxu+ zM7fDY_X79v$mWUJF^g@j;!5RcTP&D`=>1~WX5Zj8d~ZIeXf}uQQU;7( zg3MVyJQRuCH~HJwO>;NOAGxl)jX7!-iWPV%sshRi0{TAsnJC9En3F ze!R?mvQY;n4}7)|NFa|1_E?7$S{WReKIkxJfmHEo zO0@s}f|Zx0Km-^@_7}dgCwzX|iCCAIyl4*L45yBxB<2zBQk9bMz||_1b6D9aM_As4 zs&=F9X34~bd4m9XxH9wYpEF4<5=q~Hy?}8lt*6rUY|k#yZ($_3i6P9j^L;~LTMZKI ztze2Jb3w~d^3EdTSxEnIWjF52Q-BuUHJTpG?35H2OfZcZ@uZ=+7Zh_*2= z=@I4(<(qofNIlONb8p4XyEBgqH^>a1e{rS?O9iCttLWi3Vhs7m>o{X35$j-Cy1NYRRl2M>IcG6yg6 z4W)4dq;1C_e&XTL11~%+L~Qm|&^of?8m1cja-$hq41$eB?Yz0z2Lm1GcpgxohB4sl zz;07=1FE8c3SJJ+(XWWUu7=WA29Ho4Fn)Mp@}15v3@lwA|9y_|aTV1wE8 zl`8Tr>HB_@gtf@E3qP0vGLuEcpCLhos-9Z zOIV+sgNOWkffzdn{cB=Z-+V2+X4~0^py^zia2vIYNQH_poB&lACPnX<+m4a-S;Bbx z79Y;IBC!AL3?y37+d2F~ct|9bs}L>QYJX7GmOOmtddGTfnaLR0d*+%KZWWOIAN3^7 z3f9eX6{!vEpp+il+-PYHR12KCI#WC^J`?ruZp}vAo1(uL{a|_pogY^`D~;?*|MBm4 zz0Ps^!sEU}cB*MZvsNG7gO(8|Pj(Csw|z6|&v?l9#aGOk3s&ds9gN67vKzOYPP)mR~f9YFPD0S#p%VaAIVdtH|AC?1~Vk_ewM5j>WKAz{R)X5 zurYlK=;|X-Q2hX41?)Qa6GBPn1c~R9uhK5ok)-RMc22V~9MdcS-M!l0>)ZhCm6>`J zx6j*1ycu}Y3VL&#oss}{g213L#5euNa#mS>?ys_Z&nel0>DrO~mw+90U{*Ov#*;iA zSV{i@ky$c$P&j7S&k}8T*J#GzM+Mnl5@)rL6K-n}BOUQ)bTm2GiK<97*1K_N+$V36 z%G#wxt9md?rE$s+hB`@-4fHj;c00HGj}C%8pa18S2j;q5>Q1H`$H6LuQ^<$y!v zOHT36e~&KEr-I2?&-U8DXZ+@U*W!-fi*JZa!~ggjcSxfh%KV@Iy3Xv&tRR+CKCIp1 zr0V+=|L)d9sS#FB-H6d&i_qHal~)#b3hNkm^)*t17?&I!^sTub|B-N8^`$tlpUu9(qP|lGLO^^-!yR6E*yIHW&EQ^~ z$Q-NfYv=(F@wx)QEe~sy1FET9y7#zB^l!$YO?y#o;wPeQ7&bA4h3pT3i|XH8)OW0a z7i6L05u`ve)|om_Q&HTGCcqyM{lCZyx3uR&cc7N)T1&Jb!6)CV?db=Gy1&8 zZ)Jo+(twF_PwcDaA8PId(YXKx&Fd51QYCcCRe>DD;j60zFJS^z!7Jp758tlA?o756&|Zv4zO%4D{+bdJ-PtWcOLBlsg9vd77FTH(K%OPHVdA1} z#o&tQ@#C~!V*u-W^HIrNp4OX>;+wB|>zaP!tKiK(AU8DL$q_ptdvH{GY(s6=KGJZg z7rzzYoiROYKtb85=WE>^F1XMd!o+S2v_K0nVBuCR$fnu;6ou zwEJ?e<<&_L2&|fI+&?gh$^>MUPl^TWlo5h@U7g)`DXYe&FZD3~!UC zBNOIm%qlm_V|=yiZv^LVuFy>R)MbzChVS0(}ZW@-HmA2x`K&H@Jtsh*2C}%H20h=qlIf$Ywiu^czeV6%YHO$49nDOGvU|rm&sfzzL`bS8 z+#q4?09A54E_6q?Yw2rDEySHOCCkE2njYBKd}E2VQ5nyK&Q;VE%szMe?@tO~3PQf%3Sn*AhP9U!pkk8AfvM?>n_x zG?iV6D;05FL#MX3h?j3zj2j%`<}ChPcXBi0{yW_DE6(KeVY+Kvz$ORP(!8J5;*!-o znF1baP7>g-yY=0M~!}%Y-=)JK9Dci_ggLGH97+8pe-kE!!k&dfy+XK8{`%R5aHJ zP-h8G+f%Ci?AIEm@Lj7p{?O}I^9`S((t_YAR`F@Iecy^Vcpb#t3}T>p7c_1At^N!E zS*1BX1K(Gl^VEi<081o*XrTHLJuuur&Conwlv^6NgYC|my?4AKqsMSOd-YPaOJ2Ek z{a5bnQCG1{vv3bG*!zct-uL-qL)XA&v-!iU!2Y^{qxQ47?Zk)+whg+U1dvY}--tTB z@1rTBBeOFPCdbYeV+)EB0zG;^<-*6B{g0}34qTiSVTyvtm}xI)q!k2*f_?6C#ZDi& zO}^59n8U<3lW}W+DkaIm9v9~koZo?RU^{mpMQKoJwK4&8io$Vjjjsm`8m%orA@Ni4 zBLh=%l1{`F?wK;}LrU>!&j+RkmwEKnZ>pisV4;P{LD}pTPrIF%_GbXPpiqXT&ivSp zG|FZRf>m#=(o~m~Rob!|oPC4r3p$FTW*)G!+(CXw~I%G??pET zM2IdllXMq*6eESgYi$E*J;1D)wvph&lOAW+16rq|#Q@Wi}zL;g_i01Zc7(H$nz~FQqNg%0i8qhOlZ-2M`&C>4Gi`qHJdL*BI z_2>=5M)4PA{cj6bWKqP7q=u@{w{m*6IR_q2)}lZq_}E>J!n{QKWEpO!Jb zy7fH0o4$J~R9u9!c1O z3a(G8-~u^hOeD*pUkH5$*6Jqt)d>P45y zq>65Bor5h_5D<5-O?J=W#b1NolaIvkUa_dccanjQdSw+wc5JF8n6W^@AZ(b62mMy9 zG8FfL;NVd+<;Fa|ppeAbe2M>jL|Q-WwQx5Gp11`7RZ`SPC=f>@JN=<4i0k=*yBPeJ zS@!AaJZjssmpELoK{6(H+JCSuKggu#|FQIBIE+N$bsUreS5-xAv{z3Yo8++ksi~j( z*vSmdj`>27x=rUlHW|5@Rse|ULHgX^?4eIKnoU~wewD7aWS<9?65srqQy+cpmjJCB zXQdVL4%naa%|bu==(>9c>g1s}XTNI84x~q2*!qkO4K(FEF_qq~mb+-RELnT~kooR{lcB0VPS^wSp~qzE$Zds7{AfD(fv@zkQXC@Y6-Q-S z-^vQi7C*+gK4{LBaA#)Q5$Np3=xqu7_F}W>!_WEtlJsWlHTebm^86gC?X2>2sGrjI zt;g2W-!zoppdcNFCfiDxE0$XUwPDO%L>D1Auj8R{t91J@wtwc^(38f(vX8%Z-qkKp zh0LHh0kG8b`C#t`GTgEuN@$Mc3^D@-ZvbkwtzDKj%ls|M)I{YcDotGdETsPj3R7Gn zHdUw`faj2Vkkwg^c6V=Gs3#|i7Silbgh_SFQl(tJ`ijtS|u^^v;x&)dmNq5> zA(q@9hV|lJ6kM*5b*20nJq|;NZ8wutm_^fs8W&05Xa7VL^|8qNFD;|bG&%C-{amb( zw#uDvWR4Y95EOmU5vwl}urmHhrX|>t^XSk}J74ee;SlDN%U?#dBJox=+231Ce+3Q0 zhda)A>DJ&LA)_Ity`;~6CKif^Dbh}?8^AA@#TR^SODhF{p*6{5zy2LMUZ3cCS~lAl zJK)0TD^#1!a)CuG*fg)fAk-@;kmQKwxZLEtxX10La<#C6hR$*CAoLZXcf7~k#eck`4AiNOVgA2wB+Ih5@WuJ3_YrT3|u-qa7g*AP!Jyr+BLjL!>M)=m;cC=WKVr;!A+ye z4ylpW>@7%W@oW=fD;1DL+St^g*trFK*l545Zt~-DpoG55A}k-=$Jy6L%raaOrt2A`?lCyoA}RQD3{05NZE7uW|%hK3Qrr ztvaydf_aR6Iyey$AIcI3$^iA>a|xs4q0h>2hQZrUhlu1LIS>aOnt8bR-*Jico*juQ zMc*6C~UhS zSi@-Bm`bo3{BEcI=Z0-uYvP314xn|E@ z;S^jrQ92P)Z!XNIsvY8tE#VD9kmW&MO$KDcu1Sz|9H5y=f5QOXd6LYFySQF+YX4CS z+uUp&)d&VRp(Z__wlxQgFINQCllvrt`kGH$5EuEYk$vu^RQR*2#sszh{~8QM8}fb& z)yb1Bic#c|{K0VJU%J{YEQa)R5xoqq;T5pkwTllnlVq6M)4DRN9u5o^mi46EFoYRU zf26c#{R8_Vfm^)y$OZm&-$e0K&zg?PL`jb5{^NBup(%`X(#yN|mGDGD? zh@aUBmHSOCbl&rVx2Yhq$^fsgJCp`e`}6RDUYm79?dDTrLEKs;IA6g7$c z)wwI((sJPUq_Di4mm^tUOSwL{f-I4f0#*DYQslc6XbAfiqQh6-(A?v``0>v7<1|?@ z|Gml`zI*t7PnimMGFjb@xsFe5A*8*do9V3k9fX0CrlIE%WhUp@RuyYK$eCeX5Fh~- z=;!?c4;@i4Tv&cc+jYmrnr3Ga}LV5>Z?k1)HY+;#Qj z*OP$lpik4Q*{2O%PyUvEciat_b$aqvO0-nu1Z`e@`c~B{P$f=p61u^1@Bs+v#5(9b zHujn&2h?++L;bvf7u5S*(B_3)oeO3^zRX*y=^Iw7CV-)zwo2vEb|#O{+sCTE z3OK#)kfrMStpv=g`#@U;ta!g&F+{GUIGm3ss!HCM0vFasz{{i%akPh_!IKK_wu(a1 z)W4rPX>Bujp8oC}2wp4%m7c?E*4xm~S9z;Me2?3iTWI-${HDaIGJTGeJY+wY-R5wm z2PeU|>yBt4zEXc!dAY{)Ia~@ZBK=RYX#hyHeDnm z<{B|6&uH%@LWaUAACgB>xz|bw9_Y^Z)@fw-5@g!{+t_v9CEXsiL}HeL>RXrq+Xvm9 zMte6aTA$A3b9sUEttg^TU<3ME#t(7WKh4`%x+y&b=QS$cM>VF3ItBjO3PvIRs3%%; z4oSbYP(YC%?)fl~R$f-j=k%R2f4BtS*Y{3bkYiAb8zBrU^xbr*GoS#1&=j~g$Ei~6 z-cwBhwK3mJQOsx^E?$<2UKLj860N1w85PPd^h#00xtdeiFYVhQGXVcH=ywQSkE?y> z`C~|}i{=%vm86wtg_Ynv6VpnyT7DH}VC%$&S531D*Y3he^WWe+Lyta4zf6(&60ws< z)kL5gI_-b(fG1Zq_GCRjs$27YPhp%9V^Z`!uZr{>8)+x=M&NDv2S?1b6wG0EE%I%< zjrA{UrL-qq?l5OHn#)99kqust4C$935g&wSgwZnkX(Q$>#I z1CkT3Fi~P83$G#qS~CN7)4$Jj;&zkT?&~Yx6+@_-I^>sef~CcOFch4`F2fbMVl)yP zjO&s+*1|9+DnYSZS`hN+{bnkC>!m+##Y0Da+0H1+h~F6q&GDbMEE99nnwd?du*uJ44;rX#h)(dXvk71sM6^hbN!Pe~;5UMigdYnUb#wG&1mk}r8T<$}~1L`;lL zMQ#Q#$xGOJ0yT`(JX3U$W`BNIT;J|gt-LMdJDUTvW&fpBeERaNYQD}06%+CnNaXY; zvP7NR{vJfUj+#4E+r_KO%6iudszz)E6fwX!=^P(8qu{QHFqU0YE zM=FqUkDTUrkGv%VO}iL!kY_&;Y7KH1Nbp20+oFQ*z=av6ko(TC-0S}#;utOcGJ{yR zJv((@g46x$8a#^;vUe7Pr`sEZ({cVOJm(S+V@>Lvp$%JUj3x(B7LyN0n+*jwDIJ!4BDfx&> z3WXdEXglT0)BhgCG1FbaDc}Qsqp@NP`x8sre%EZtatZzeF}Z`L1kc3c2kGvO_ao;O z!%EgCD1`ob?tnF#aHSHXR_364mycRg0>2XYnAt18-ZqzzbvsvX9{8PQe&!17=>5|$ zs`&S?s^NE+Xe%{$Hsi|o8U)o|Pk3NU1=^rB?k+U(QTo$)bv9dCw#?-he)d*@GfZpq z9fhs(%f+hua~*}xdOz{-mt}bK)Tku5|69CnB&HysYQXSm^^uv5UYE_QJN4%K!KKJf z_si!ezmu#3lW@5$3RejhoCF-|XU5N=FfC;jp<)!n8B;;PT`rGPyY@C(q5#DVkFL${ zO)`=&HnN$5r?rbgAw1ak;+3t%%GR?JIx@dnwo^>&TIh41SS}R3Y;nUvKwymo6O6lgxla5Z zH2U*w_CMqtVy66Kw$$ckW3P(~Dd-Bjh0Eais#z4ECzV^1*d#GUr@{vWZCI@A&9nZ_ zx%Z8u7%C=O~ef_1YOOf)uw_d`Z4Zi?dP$scRXzh^h{-ns?f{b8u` zMBplP}*MJeS&hp?kU9$ z9(HP-a0vrt^o$7^)Db{9r=%#bF_}jvyT9(H8?4Pl*kl$|D874;HgcCLJ|Iv&(a@nK zQDV)JmD8{Q0<{77FImPyCbmA*?Y+k-cT(cW;I+(fHVI07ZT1D`NH+RD&cKYlcYklr zh|);Ps~mm2VNNAGB%yG=3}{GLOMW5;#y}dl9}r#nlcdAhU6*RG>9t}xCDn3JrP<%X zRL-$G*tbCR{$Kb{!l>Ix8?b;M!|?mFxhraIu&m$O8YM^3T&)HqaQofpqLX@4Wm{8+ z@V9_phhCNkx@CYrmw{(hDp1M7_`SXIkxiaGH+Uemf)yiVj+yL=`oIrMA`IWIW}NIU zB}<^r=f^NcGLHZVLPuY9k<}MZGw8Ie9+jLS(|^6LG>SVK2oX* z{LBg+WM|{%0KG&uVuT@F)ft|XsmoH{nd}xvI8kI)@^}Rn}Yzc~js9)QH~R0xKZAN)|WN3qwwVC=C*nsr*GI=|+H@c`3L zhi=|-Gixo!5*^?8E2hg5^sr{I<9-e5aK^|rvhX5iEF|}u*Wb?{yMEn9DRB0qpa~;^ zU$H?~69}!I^yx&5!eTZ!CsznTs9H0BRGHE@6~?4wK-$a}8++%`onQ9hiDO=S&$Bv=DqTC( zCKhh`#^pdXq?`g5msl$8|wIPFMS|=>hv4Bu~bcJfIO!6mbig3;s>neXt5>21j`nGmz z2}<@OC{XJV1^o2>Ao(9~_#fXzNsd|F%((>r&*S^n5*vH{Kkk%L;>phswPJ} zQ<8KDM_@WM6(M_U64$lI5V2M;Sz5GtyS*YspIW@NU!;FUUhsIUAh23?C2#*hJh{y$ z^aJCf&K2y)hv`g$D5!>7Ed)Ah?<{#Esu% zHN>{)VNfXPw&qCs28$~E{renUHrZj*4GY9jrSKTsl-|#WcP(_u;dujVcvCt zu|HZWE@^VuvE~=oBoK|y1=nE;j>iwE^wl-qvdO=!J?7sgx8|YK?E{dfJF=9-0?xyl!+c*KDjYo4;1^OcsFs*(vORip5LW!F3Dp1JE56Gdc z9a5Sj$yo|I0#a$zKWM{iL0JwpB^wX*bWMe$8Td`bXPXc20ai|nCbl#6sLD$&t69bc z;5Inup2;b|J0cN%pBw~$a1f9Gte|I6hTKXZb&|)Ns`HVBBj{rWcq>% zHwbTQsoS9of*F3-x)r5_lAy}6GtQ-Vr@VgnRnpx6qZ2CEK#A&~#4?^;pF^-^KQKkB z?WeZ(2qpRh#8jRWmYe_4nf3g}5uZ&}-t1|7_a3jv%t-vOL;u58(Ooz$`H@~k)Z)>z zxt+kL;(G{-Wnsa^ZW*C(d40%D(iY|4b`Rsl0g0!8Zo>GiFCi*K7ek7rrMdF~P*+WS zw3V9y9yWzew6~dg%o0&qj&v_H9V2(D>{1w;gzOV9*2ygET)6(znnBLR{H!%fSsG>k zcw?fS$2Lsv6qpMN;l$XB$EjizNfcuQJI_A;qA}-~d<`9gkwc28)U428U(L1*63ck$ zmRV`9mKHLs2!MEZNNQqhRjYHhzOutY9VssAUjTWJ^1Tb`P>awZ;^!lbJi~qJJVJPt z6f%G8KatDCaZw}i{_%g7n|Oa=`kZH#)D1s73nL~kw{DQhcKx;3&m~CAUo8{s-8+>fp0V8; z>?h9W3a(Nn5KL(FFY2f^u!+tE8*I<85{#E-ht~7@+;I0MX000Ydcs@T+u|9za%rn% zZEm^E1Vcr$GV*G!HBsdp!sK9K$>_<3USagF<7+Ub$kJi}L&>c+_*de6^I3}fq>i$b z1LL6vcwtZW;$Lu{uViX^Ut2zn zdSNKnN!XHYc5al3d}BQ82rv#;g**G+pwJZdN`00J#i1hpmUT$;WNxF}ZfpQ7h-#=- zD27IcoPdjI~MV(aQdQ=AMEC!pNar?8Q#E=Ml8%JZoW_&1b5hDkx|S6lRDG(sDyv>p?_fP=?S;!I;DAqb6JQl8cXl*J4e?+V zp~0mRmyh4?mn@xHLVQh|h(8?K2%)0XI#HbmbSV7~^dh)_xMIqy`xh#wiqQ+=AYZy& zSmi=|sThj&29*dQ%;CULeU-3n<0nN89ixZFk(lVg_~6?lbMxg1!8QR~RNo&Z(gpmK z<1p*aqEsg@1CA1*;OklP1!OG$_KgrlGH#Ku1(GDz=u$aJdC>nN^fr1Tjw=NH4)RjW z{!dx(V=R48R1RRYVWD>~9l#wp#O^$hstAB4jv=~sApcq%{){}T-VqurM}rUNV|04q z5m2wvU@`0_JuZE3T=@v6^Q!;sznND#l=-d!}d{4{A{xQ>W7PdIM0I*}?T$kyVjWf&^2wRRUy+c2RY- z;ewK=Zy7vqMm%k=BcOF{5vsNw>nlD!biuG?zRtTFWCaf{@SIc|{y;x5_yO1)$C3L6 z%XXDodqroZoJ^FjYZL@t=td4zb$@Xs_}H~v;NSUclwaAOoPI5tiI=|_!T%yaM@iAg z?awyNr4bwBVge-XRYb99q#-c&O02&(a^5^t^ow8!*}LcNJ+l?WRvy6d&TWA_7uzaQ zc5a;K6@z2KR6e9z@oP=fot3p;ikR|M;UyoyPO?|?50ys7YevY6BXSb8j)O~55Fgui zT2IFPlJDaq^|+}@v>t#Bw!hVlPE9Y-QF_Pl0TZbAbX?{}NK#0UA~V0#9>04Zn7hOT zqYi?rlxv?7X6Z1d7tlCP#M_*??()ACAmwMYowLK|tNskdtH(}=Zs>cn8uh+?L%eC$; z8#D5Ki76cL9e|YmdM%!u~mJEPfQ5DZO6O6Z?mD|QaG ztz3<`$#A>jAa~$PzQXb1vCh_dMbjCNMFCFNU}*9M_Hqt8t%`mOo%c6*Wv}1U9KW7zf~LJfChi-uIT{K+%qJcU+BF}Inkr?@ zYtDQ1Hth%DUSo~o#;ePtZkZD%9=B5|=j<=&*|nUdh$e5b1S@QxOIOVrS1`w|tjg-C zUd z>A?-NSU7q71wVWiNxWgb?#bnm5N55;21A5ib+tp{^NV2n(t{uOzpeO86V4nGkNdB4DBO`Pl>_#C0@%d4x; zhi>EO&OHb`qJYxP(s*4NQqari1-pYr@tpIRC@+%E%+;oY{_o%oKH~&l__WsC)4o{^ z)n{E;&CjhAOgbasGE=2P%sbCF@|K@L=%aoy<-a(R+-hAaBQ7jTle8r2&?k=pYRXDj zB^82d_4m$$jH^?y06l8)m6rY7G{zKu?@CDj?BCzujV&y5efTXj4h9+)USNtT^sOkt$stX%WKrJHRh;aJ&>_0UR8NJZe& zJp;qaS1+eKZ~Xc~&7Kff#XjkKvB{8ob&*d$S>E)88*n3dxYqL#(dcH$1<36w(Ygmn@IYo@9%I7^`-)inM!s ziab9+`Np0-TwmJ$*+iAS?QJ!-Vb%{TnPs4($Kv{7^k2_zo_Ri!xt=u$iu}zICB**$ zP7b4WyIK)~j6UV5gFB-qDtVhEem49f>yWuCfo7w}m@3`19Np5sP>m;cK=n1QXxJo` zq$@ljYY;^gpuTSppeVpeznlQr1j4yNWAPJ|yxuLwtM?BIEYMTkp6R8tZtL&_(g^sN zQb?nm5JVb>ryrxR#E7i`ss8fz=|Gal=F}?0S4-QB&A!d^KHxhe3_Iy)(A(&4it0~l z{~~oHRBbHq@!)ljqxSm`7?S6ZfZ^Y<6Fy zmA*M&!$>KYS{Yed;T1Fo*TQoMS*tzB02-&Sp z2V@M-SC-3Dq_iZmA?C}7?mZ5w2eA%{c zY_X?lBTUgX;8<%j7LZ&S1wa!4J@E^GyOj1I7m|92eQ~&^xA=9GtLhMx*7`$qz{QachAj@#t4PQ*DcO z+Avjkk<52u#6c9Q5XWYsrUp1QH(cDxDsOi-L{@LjIf{W8qT24HJ4$>oD7SdSbg$O!>5-#gHTz~l@nc7!66i=ll(pd&snrThyN zc}b8z(S(gNzL%T7T0L$2@Cxk{s>-@7Ce2L&*7*Z~pAh#?1z#KfRw$od-wQvG6Ii5L z54rH0QSi_RCLzH&V7q9|O^}-^Izq!K5Jxj7sy(G19eZ2_YxKpt^+ynQs$#|jnz0!z zALyMPD5=+AfOu4fXMOALP2&2-?%&elt4^*#|M_lDM%GB0)&QwDXU&s#v1=Cisqf|Z zUtYw%SrXfjYEYdac}X@`nodY&xW(ijD8lSY<7;ZU@$DhbcvVZ%YAStJPG~k*Wcz6A zpTyekK{dm+-XyrFn|XIv)E%*3Q^#nA;)zUh%scuAzMOy;{`s|{6CGu34pmO;FSvC* zTiK$r|8aEQ;Z*+rAHUDwm>t<89HWnwY{@#u$f^)ZQpku(QOV3X$DU=UBBKZ)GbEgo zT`DrN!m%bI$Mn{{A|bbNTDs*L~mb_xtsHJ|34mNcF)YO>uelwfVvijFf_yp(aA5awP%QutUQE>{{}_8?(P;ISq0iSC2lI!Uk> z$1?12yCM2$Jn^gGN%FBlFJa);@|KzC?G6nE7+}Wh-qBR+6+i)a3qgVXr_-fX!jW*m z6IpN!Zi)ORt2(T*5M8b5cG#ZcA<@JIInF?g36<`$cGAA?5tIQ#!?s)$dL*o)t89zq z#7Rl-9t}&>{3_vsT^#AXFmyXaCG8qxeBg2m29AC(6rh$@Z%?GVk`a%{r~lq{8Vs7k^IlzYrVxP1v}%7}|rA zYbAHbU87g#b9;8^BLXa){O?m065jCjI$*}+%{NerpKlRCFl)Jr+#1Uei&E7r&A0+;7=ZXd;ZeWlHS

Yrefhb)b?^fX(iI)b7@$PBDK^(aU?No345&^yLv=Z^9cqpI#Z>2vx zXjNk-J@4@QnUbS}oQ!;i?t{c3aXEvJ^QL>e63o?T==e-7d4E*SaO)x>bShW;hDR#m!fU2f z6eDu2Y_tXK#Q=w>J9(-rt3l_H7zNP9%Ohvn9gpkkrkJ_;3qeH~|Lz~=8B=+IWI=s) zP1I9U!@A$yJiGXiRoI@}@(JNP7k*1|ElZHH)7ESRI>!CYOLtlr>+zWLA?02fIGh`puE?PMdqbKMBXU-0%aes|K5>9 z8I|&pd=1z+kZ&E=s2=-u+vJXGVY2%bgQit@B5rCrIsjVhgjX+GpArmUF7}^_+4%(= zOnC&4{EOk)ryO76^Mm{{nNhxmyBpjlbxRyP8$lJB535+PrqjD<#$OE&K9({&7m|H@q3gPnZ5FQD52U_>9pMyAydIys6da?oi@o$)h!-!h2^&{+2~7uL z(x5yZMtSg`5}0y`=hJhqfldq$4QdH|etMaO_6Y4xBn7xan zTNRc6cmRo537fm*@7rG&?fFv=pRA6Vjoz%Fcam-WO3j)lt|;6Qc8saO2}6tWCwShY zP73n?Q3fw7CNR^IC}p6V@&{NB*Z9;Mp0V^t_@yw(Np;7O2k zBmd6u&5LiECdMwtxM;5!M$)S=|IRWJs;nLQLVd$9R&Il6hFS=Rb?|rPXR$AT@I?h5 zYr2GPUVq^gb9fUp)cHa3F$$g;{+aCzkravlAWi*6Ge03fTF#NbK}-Ic$||Sm8Ych) zFOLL2a0xe&$I52fOMpPwxKn2(fLYuSOc(`1^a?qq)NSD?Ele574GnTP8CrUmt*YIs z5qMScONUKhu;d*8t6 zs$p7+vXNkj&0u4px1^q#z#uMo@)y$Y_%`=;h3-AC$Sp7;R{3kY4dpEWvaB;P;w(0} z5Y~z|p^kXli7AJOR~&&Ef~7AnYYU}s#2MH=IUE{l2yBu7?T1XKex__7kA1EbBzQ$m zDmLB2a#@KGNLGSk4Fsgc%=)g^vgGc2nPjS`i)82yS;)p63oYUy>PppqkWmgNEv#A; zv1+;whFn|j--5;BJdsC=SZ$Kb%O7HcFltF_m~f^IE@d$*X7P)N3RbS(%s9pdH|uP$ zI3$pg)C1D7Gdw%>KWRp8Ff7w%BNY!m94QKn~*~OPRl< zfkdkKHo9TH$(&%tU2yVN&ONb;R}FT?x^UFEq%r=48**7Xf(sb???BGPPRjb(uB`uH zI0xoUF(W>iTP?4Yd4>fA!-@Q*Kjg*BMLar&3U&qMquTJxas^`anfd0&kp8V_YaA8; z`O!X8Ent0q=b4- z>W@eizdy5jwp&>Gveo5_3IX{A92}k*hkQQZxJ(LW=6T)^xaP-#QBJK$?PBH7%x&X? z0I7zy+A^P`Ezxfo)$|SYMJ%3Nq%6-`$t6pFXHYCeV z+Uqy-I~6Ow^OI+zjV4{OfbrL*BqTXLOK`E2VkBZsBo9qUM`!q_nBP-GVhCk^0n`5? z8BSzAf;Q7BLT||UXUlD?I$`Uh&i&e7K&d25ZP~~%EuwQpyns!cNjEM#ib+xWA8u#p}0$hfD4f)hIod(dt4{T_nuM$Nq*FcoMH0#$nq zYYhr1Y%6j;l~%{`99wOZ*w`prQzgKk%`?)T_%hi>pF`#4t0%smPAt)^MSy(FhT<^= zKmpXTS_2M8UV~bI&qdMX=RJiPq#m8svdR!t(&PE z{h<2W-_{10I3;oO6og0Lko<6#zc}W;%O%GG@tb(O)m2@t_*3g(PUP48*HiqU&1gEn z=hN+a^QK^9m|445Y%YsX)$o~r^XIHF*9Xm$IgO44=zBXeM^3SJ?3m}99-kOh`9H&8 z-0~!u!_A*VOeOZ3#AJMeA?xp-zowGT22M#YPv>DWMV7gTmj<6wz|GFBPXXQMXR^-U zEI?r1V<`1YF@9OKB@!{8hC!pVw*mW&p>alZq4T zG9=hdt|{*WCJ|ztiHXM&!!oKug61x7;&0qK&9D$n#js~Kq=Wkjf8x4}_`<<#an3QG z`^*K*y#4&K`2xK&oN<5XAt%0_qHMyJ3WIQ<5!Lm3t9Rd@un{efm@7fCQU7xp=>M%x zi%6_ANAE7G^*Jaz+&GdTs#y~*464-N00W(kw5n&0GdsV;9Xf?^CmxZ#<2U-gt782J z?PqEQO#KwE{^Xl-+}Hhmtc!w-s^*3Fb<7)ROR_rN3>;UUlGA^eUvA_Qzbw&QCbbkI zT@ZVJP@(?&NN8k5lB;%vzK_9xZs4)HHBWf=lh&in>{E`1jpwf%89Z1?#Od5PNI8CW zE9Lym;Zm_*P}4#sZg=1x&#u4a6B}CG?kMDE+Q3NC^pV#2Z^w?-+t1+I#=2)>lCqhw z)Q?L){&#d@|XmO_C5^4qSeg=HJHOpxmgp7R7UwDh(tWao)vnAwE{~I$W-n&{Rip z6Mc@ilV8fQZ!oTdpsAFSVmZ%Y7h@68JCgUHa$2Nb^u3qV)MRDz(K8XPX}ulJDOVdl zax+1BSXt=Exi>v$ZZaPHIDwCMQCK*(nX!0J2o5;A;o@&aj*>a4iN9l(-cZHYd!%r_ zqgk;CZ<%hnLFPpW{xG6;z#xioJNu-8fCLpu=^xX$6&j(Rh!N<2E^lZ-NLsdxgLI{^ zaLD#G28Ke_v8|plKBG+m#lkpXF&f@u4bPMG-tqCSlJbr&oRGY7+<-Kv5ZGxVXDLv` z{X-EoTO>yixC&y6e#f!0iEO_`yp|1SD@7t%heN(Z_d-qr>EG4c<)PaIxRJJ=-N`OFWIPa?VCU-wz~fTGz>(C{@&0QS5Cg%#MZIizQg6SSbIUpRO(P+w4(h>DU9s1G8wdkj^dkIz~)v`@P4J>ct9elDAH&*1Ry;YkRyaDQT3=s?MXo7y@7DK5Xwuv%*7wX}+^Ng^<#@m? zSGW(8P(S>wcJFJ!C$B4R-15CO1)jJ$W7KmzT`F}LC>%xW>@rmTPBRz(&>9=mQ?0;Z z8y9ack>OtVd02qVMP>vACuZhF@F*Fb`JM)J%GstHrt9#8blp1txCMzzMzH{o%8iD`k3c=^!=iL;4U-VEJ^W zwFw~eU*foGxwFCo_q^~{)eT?O^ev-*C0>yW;RCuWf~H8~d{;H|r3(AZ>%HwDFzL#W zEWm0OtQvi7$XlR$9xYIJ>q&Q>-!3B@dXp$pScte;ccNA?|aDEK6DhUhFU27B+vNW&CZt8tQ#FDdB!a8A#B^e;7H=)G(_ zeY{)IQnEKB5>8Se!2Q!b@lifjO*^lH^EI_YR@DQ{mth7C)E~Y7i*E}ncIw?W)DMgS98!ryxO^y=r$L-b{ z;S+L2nvyOw9hOo_8(#@AJ(epcjF2c&Fk0S)XGL-pzK_>MIIujh6F0g^xl`|^6aS#_ zlKnJ%(lF>| z_l6GJJ8$n;9!!|-Ax(~lnjdxy6k`&8Pxcx-HFdd&l}xC)+9otCHzg+zb0Ad+5rp1& zN(BYGmaYA&ArSw2EAWmX`ZsBdj{+ZkWZ9iCN5^v$#MX2x;>|A-N2Z57BHvS~R)tuz z=;5YxYQnBFhMo*P_`%fuo6({Y&#g*|D`LC)#w$zk7;|%vOSLF}7Ttmt6h7|GFTXs! zl6w`pkS3`D-@brpWBaT ztv@Z#tueJ=Xd$zqhJX>gjNx5|3>QlD(8gYhg9rE6+ip8wg8V2IM1w)R%CXOzNuQ{z zmz-Yi1qWM~!G&)-1B|9UG6Ic0vJrn4yqqK>aU|Ug6=dB+En60;q)hmw+ z48qn%jPguQdpsL%NE6r^>*@QpI4?ox$V{j(GiH@~e6SiV7sL2|b**xFUBJNN&{Pfn zk#7)O8JBUW*CxC(?;2bgTmQ8AGq~my=h^iGKX<3zAQx2zH0*+v0G5C3-d`{8(0_|UFsDei4-&v&xDUUMbN9STgJ**l|baWOg2{Cp=*0rtO$ zRw3Jy;lXGp7oV??9N)hEBH|jZza*~negXTXCuWBpg+C!rq^E^wCiD}ms^=ZPh5(Ow zb+iBx9=4+zdl)ax0+403ii6{gn5F_ScH*nn4_QEk2Ro0m(FNrU;&1TL{Ru&wBt&Xr zpsEM28s%K}exE)PRk7v(lWBvaDwG1A0vuizjLyG?VkQDvGT$ktR;rU%jDo@+l3$-p zQVo7QVBp$0_e3yf@)4#8Tww1i&WabH_%2_xKpX$!mK2mmo79s*O z3QyT}Mgbn+PM-yMXv4Z$_!LAKpZYyfDR=wEB4ojZJ)C&Ij?55oIlS$_tvXc>mF+b? zj5?2Jh$0;MbN`;ixHFBSmx@YSdw-NU!nd?B#2uH%bEBWz%ak*{3Oa49K!Bj+$9E^@ z{Djd)1OHWv`D_~&Au}(oqSp-5I-YlI3TfcK?NhUF+jEMDH+nxV(r^<1S`s4Lw3p82``_qHGJ&^;v?wJ{t1pB ze3ioK=0>J7cCx0farV4(|Lzx^l%dy~h~WuR($r3*2dRf*U#R=Ci{cnFgDX9E^z%dY z0E4q#bx)25UxY=a9I5Z4BuXVwK!O>-KT5ly+WrJnJ>$(JmSGhx@Jp665U!l6?Pcd% zIQ$>lqI*X>pOgg#vN-xaj-NNc`|3a=9P9`nK&Bk>m#$m?&$12CDDKpOn`q)1!~2do zr!+D9WHx3($0Qpi7(JD72Gw30bl2cq^RlHdFWfO{%9$xi(J+YDzKSeOZXO0@3LhwR z7#2zFs)&*w2ph_9bt8Cu(+ykl-(uAEJqD={`^C_I{+abKHsF7%L#qlOFoyxIhDe^N z%?InWufp-qv&{9~x%?KA(>S z7HvzOY_6J&JXw1%H2njH`y&V?FV5n2xAQtA$Q=2f^48W(ijXvowwY&46i6N|R>gM7 zJ$n8`*E?a*=GF}5>#5`K=59S`l{e%HYfk_$^j`e3VHx@@78G5!b}`OWPSD;VDa|V; zdoX>T1qpm7COlFaW=kMZ3GQM#A{k3KRj;-&*Xx@PB;21|dh)Rn{sz0%Mt7tsS6gB^ zFT~14d>)g8iMU036HYEZu70)tUxbm^-^M)8z!Vx{uVYi7>&mbS!ERjr4WeLUGCR() z#ss*A+4#CSrepaF;P9mT{FmhUVmD)hWlU0}K=;4Cl3ylv`j=n2%Unw2w$|e8(Qlh? zk|Uh`bmHx-Q|%QXS~kcRM7o*rhUaiJTm25+_zY$kzjV?g+rN^wME5%w%JVy`n1sWB zdB)y7frW=|L>Xi6P_IiOM`=KPV6op@?xir2-5YXP_zu3pMl1NTV|a^^f?%|>RD)Im z-&CWISHZ(@BTwdoUpY)J5!G#by_mL2TKX%ky)zO_*|C;oVk%CE|f1F1A#?miz{w&x5!JpnFec+i_VZx8n6lB5#=ruzN_%#WLZu zKdyuU>K1%H#7oF~=r`KyMPb1O_ZV?ART!`xqnH|TQ*~>S15*w^&fs)eFd?~atuPw> z1ZkU%^}YD)FsMce_ZT_^e0XbOc;?^26@aiQ(nvn-lqu}Wy7Dx^C}13+g^BZ1*#JdR1N#Jq->Ocl^z9$<}(w;P;(U}-@JhQq8Z(? zSJL;7#vcARYVt1zj+1noFg2bBE8VlhCg;eiNiLeQ~WeFtv5T4(1s!ufGh5Xpz?w zAv_E%fdJ@#k-Xcv)b1*80%gZ(^+bzAjyzkH&@JCI6E#(>z*=177s;YX^@K0(^!uid zZj;VOAF>p8#9#fPlICP-MtUBhI|$PQu$|$ujexw zh~Vp20?T~f!CbyC{`&K4%=cp}J#77?OOPLlH5gaG)BEv)-L0-E&#eD)?42^^`V{}N zR@^sSyuJ0&=y&Rk8Ai#ofVK%ZDrH3acY9UDK zK>TxDfHc4!!h?V0%FwdQzf?SYH+oJ!9iJ?Db7yrW2zsIg$&L>&R#rELeL&pA^ z|6`i+Xvi9u)PQ-@s<~!UpS?fiUw>NRo`C_EYr9N*Th>Km8(y0n|2FnnPI9B;EVX4q z*Cvc2HaqOyX1Mp#4;56Z&u;Lw^)W@?!WesnrCF8vb&ONKA>!`rQ~djI<4#63lRc%} z=5KP-wVPo|;(1Ua~LnCHkkn8B&?#oCuI8n4ZMGp8KPwzXFWXB)E&%`hHC8TgM z7WQpqz;ay?0IL4l0Mo$jkxg|T%^Cp!qQ{gxpBn~)nP4h~f!zXdoq}SAg62L96%Sf% zHK5GOS|owl_lZhD;PAp|N$owqYW?YZpU|qVRPAY z?_GfcnFdSewldEM&0Vaq8do3GwgXV~8q~GT5WoPi6RTqPatRXV+NBO`^bDA^rH*uv zi>HGcMDel<#lL3+!4cUnU9iduL_jT!C$_<~I;--+rJ5G2n@2CZGNg}7W~v^Qy5;}U zKYVnrcgEyAUAQ!+Kp4$}!5{|{jL<-?@LS9-FmV~fUfTgpRZoSY zJ_&a}owovPzQWz!FViOO3!&fux3B_W%e)FzU3^7-=dw3FTt1T%yjK8VsNm{U4(mPY zRbbl!2X4)D1uh$9HY8R<--OvqvsacQ%fBxXY z@|T*FQl)Ac4*BWqYfat-RwB6@y)lVNaf*5se73l|&a8RM)7%@gvMTdxFJSWO}@BIuN&T+Of#XnA4sIrha|>f7WargRnoAcuQXwHP2v)guq&fv^pDg}^=>_yI$MQB z&d>YlLEH~sqd19s(^u*^I#^sX$K~u%a^G_)Sh>0+)TVR?gp=UBH+E&SdNmbrjoCHBr1WJ21Rf;u*r zG1$KqGk{ubhfs9{yP;PJUHpEru5t|Lct4Vf#Dt0cGp`LywhNc z#rXQakZ-^Hr7@zzK%l}J^AgE36{pSLn;{Am!L2SuE_29#6n3yo<&~eNJB;~#l`AN| z=!tg|1#_FJpby*=SGC`AdusYjxNK)bK`!F9vuQuxrf=WC0)QhSPQY}`t_U;Y(Ahba zRbGI`Y5yTKXi&h9`3_iS)&TK`kM_WWuTLrPS%Q3<08Ib3e9W#rZ|xZ{2>ymK4B%Nk zIYLd5p5FO@44jyZ{A(Z!u@$#dF`6axB)Jb;h}xRPxxeuFSCisfR zg|zQ~Paa(dO+*s))m$y4w@W{SR7QgmTp5gHA%eiqCo5H{hCU^b)QB4YXO6qfCm(#y z=Z?&JBWds?BX0xQo{yd!3K>)E@-G*Sbi-mG*y4l$4pkM(CW0wy!OFZSn}yVKu0J^f z^Lfy{bBtNXhku!oD+4d0m)6ZzKdzU+6D_#L%r zLQjwl56;J8+}!9Xq7{P;>zub()nY-Yy3opyz}SBu;&xXMMZ72gDh_Ri8qtYwG1a&T z^G|2wXeHpPF@S|}5;C^*?(qp$V%tCw$ow8?O%}9>98}nGv;>o!G7mR{C1t+EGozJe zuIMg)3vy#7WD~Sdz!;Qi*VTQt@hqiZsvr;iwOCWRH{Zh9Fw!b}aD2r82T+cV$OO90 z1*#X9zQD%IASk7{wX19nfxIP9qeCvf7}m_QH>CpH`K*wW0W#G5mcoDiBqMB1K4=qLM)zWcBjO-Fa_QQS1!7P%YiJbaXzgx0#jS z|L9ue_?&MOFG=W2@oU!QntRfOo3h=Csn?O2_o}vqoNbWN(+88$MYoFejB2&DI}VfA zmcZv@h zHn0a<<@1Wq!rjA2NN>frky_Z`}{jWtYSrHr&<1SrE-^I}54zD4eeU z1+n5xH%>Mqxb`sAx$xYtG&sXbU=1-JwzPErXo>Rl(S4>TVY0attV92eUd)OWf6G)O z*~3x4@yFPS6a0Wt(Iz|oOC#>)lQqGQ2=dLA1uuxjYTOo!Z!uq0s5w-1>%BCPQT48TSah^TEQuoqd*2u$`Tx04~iLEx*5ipu+ z;FAb(O8(z{nEX9)qfA%02+oMCg?N3wT(}B3K_?5f$oz*jhb@2*_oP1$zS%0q|B(vc z(lg^6n`tHK#;yH+)6FNgB-kA^Q1qAo=U)RLs95~wrzHP&jhWmNd|h&*7q3}|Qa)$o z+YjxLFX@G(CeMiC*H*BbOg+kht`_A;cm3QZ^YLL4`kM~DD?fV3Wn0j#B#aur+5hAq zP6L{1=ok0eNe?ltBu#m4JHaKA<^GY3#J<((eHQ=G%M#KaW9CK5yqL&mwgF+P20kof z>Drzo_Ck-iQqfhH-%Z=FJbVK=8dYu*yKa@^d;U<_?TN22;bV*g)TrL@#4zVB;OQ^0 z>UR=C@ry9ecG!Bx+-a24r|&&Tbjv#}1`%iOjz;PiwX50OS4jo*4X*+5)Gg266rk4) zN>u0JetCtASs4W8u0=>T_QfrbVN)agTWp;qjR*cQOXaR8(1wJ4GIIlr4NM6fBkv*l# z8<6o0`4cD7i8&BM=~yi0ta=3>o^nIASQv`Tdlm7z;2KbywaBfQ3U-~A`K|)1s%iSp z&q9j$fK*U9lWWbv184%^fFzZ#*9@=@Rc~PBNbIp~)MWwWQ0?E?VdF3N-WTHgH-dkQ zLt|sFb*lu-pt6XL;l68Nf^oJUtaV+K!i66wP0QLMiBYVE*{fZfHn^|&t^VD+?h@i) zVNm4(#{;sV^v$LIcMSO_X1=ze&p0$SN2SjOoT~q$dtr!KNAmlKJ5CPTv7urMiRPi& zvBVPp|C+?qw+-DEo{F-H4kSVXk;yx%Yp{ppxOy$PyhO7Nw}oZ(aab0IY0F;D7;~kn z?@ECU?w$ynWi_k5LibD_{xF4;TUg=<5)Z+MCCUb?6sYgc-0doDPs3iU;G${D%b>w& zSfTjArB|YOfbAonZ6@&_uJf@m8(;xRO}nnR_St7@x9cvi#(gKH3$FFo$S?Yh^I7>2 z?Oe7v&QjNGPZ_}egR{m#tX9ScSFM%i%Y0Qdo(oMBScK3Q3iuH(v5HtxC`T6~97Que z1c$YIDfRG!KRRZA#u^4jiirF8ayASF1`2s0@^`t(SbwUo;X+>j9O`TTUl0egU$G0Y zvS57TqA)hVAaccC0~oJ3;}qub5-dPf0w=&#R^S|sORJz`n~s;Mi*}vb4eU;_I+Vs6p{C63AD`r44!U5rqk^KV_35U$g#2? zcjztA@3c83?bZ|g&b#INUP_P$T~7b+tN!B`j8mvvvNTR!d^-c8b@f`!HO=)i&S$v) zs6ZT6Q>^ib!>(-uZl5m^MB``BV$gLK$<;?3ya*n%G|vp$Q2%wG1ItQkeOrtk4MCh{ z_^)OWh)8>V&ge#+|Bw0{9mH6#Mnu=P$UX=Q<;RyrD2!I-Jz0;-sJfxGIB_{c@QLc} zFL8We=s1Ld;02ZqhcsnDk?;%Hi{O#TjH=)qGVTJzhbiI(?$A?8AY%w){M;nJ$qbujjHK+`I}wCcpenV{*qP7}nwQUtzJUsC~p< z+A+_*R>frtSBjI}xRG{$lS{VQgaNp-hxDA&dHb7cG ze|^{Ep3QK-+tG_JiIoxV8A|64Cp}=D>f@6pVAXFAD#03qw?xYU(TK({J(SAk@lmlv zY6=gdsMNKPz7&^ux}`rwgh@u<=M?dm1Hqhc{6OJP2s&9Eks42)*@SeK1345z>wI`{ ztyiC{ogYgLs4FteJN_1(he4Y(A@zm(Ol%bZz1k zR)dfz7pm4(woFm{#eLWRX{g$LAj@=g?L6)@Sm_5Aj;(a8SyEp{7SX=1u1eWW2=5#( zeB}AWuEi6OkxF!YzNs*YW{c=_xQYPC_rWwUK6wvhj zS>j2v@pOadSq7%^;zf~$&Yj?@ij)8rCutngGE}aImh5ZKNxb3lb*8htjM!9$rcKFs@!EQ%fDT48nU(!?HuQf4T&)%T&Vc6$t zgSNtRjnC@ztJE+CA`feuzh6Gd6?F0<^)bse>4t6Q-=gKZ?yt}CnjQ{s*S8oa$ zXAnFf`F3i?wYNq*xt^qwHNu8`AFwbjTLuo(64N;sQ(0}EBT!JL8q=H zu%ae;?$GNyg2)h|m9WNBSthXxzUwZd-|FtHJ}xi0PAhcBsW4s1vqg8O^EH{U;*2W6 zzU=HU5wa!t0M|!^!zjW>haa(+78TqQeKxn`%B|FK>8OeBs_!@xz`!unzy3zVn~1{z}UKIgTtF zC}u86+)QIG>pd6fC`tT>h+_Tn@#K4}E3A$LHq&S_Kc(str_}j`TbZHp>T98-&x+c| z2OB&jln`ViD(OZ1zj7{qZk|!Zrr+7s!rF0@@ol3o*yL5Vkx#9s*EgRB8`D9JHrRA1 zxv;KyzDPwc>QHd`i;??#Jv5p`=|9%gF~+3)s2S-mp><)AG-bN!i#pGtHOyyf2N=x< zrxB-}v7ujV)J*AO#QKqgSvl3&BLm3)-w_jJ8 zuMlE}{^gwM{^|9a%U|Hw#rxu{j!Gj88)?FIQcr!aYkb-`We^oP7q@YF=@)mz_}MzC z!+TP|O~>B9Qr(Z(n?OY&_MKWRAC!o{DF+-Bu#8Hcqbi>5WvC7F!O{a??UvOKPgZ>A zuHblRx6Wey3qQwaSf~<7_mH^U&`B#YH>}@_M_k-}M}M8oW$Jj8{g@c}!A~V`fCMRAUR+0tLwp?rbl+Ec`z@R9eyB zV?S!Vw{iy6caB=mf7v~$Y8aAMH?FPmoope6Uutn;fT{T>`+XfEKWz^Tn>BYuXRtk+DqzgP6H zrPrJ_TZe3r)>2iPPe#K@DtuMEeM-|R5jg0eIRK{ur*U_;z5FgV56`1FAOoP0#&xTT zbxQ2zQ(W65w=tOR_wfR_c}%JC5g0)cwl~y5vhqNb$wm#Z;Gc>I1~howLdNnlzoo4q zOAXvZmh(<&gU5qv>@6A)k^lsb(*LlP>6-($wU^2wJPbb$_<91NKO$6XMZD`AQMhZSkD&HnE(50ATc8g)P!U++@0^brikhnligZyMSKf=W2SFFn z7cLJP0DCZh=namgJS z9Hrenc(C*cS(03|*U(Nn3)WKcx!b++MrJLo=eWOfd~M)%aLb<@8D1A0#u+86XjaNx zOVppE>%7_P^>Rk?jnX==%+|?BrgJ$!f(M`^qr5L^&J&Nd*M(=!MD7Yq;9{kk;|^nz zsTo^*s#wuQeHFfIvq3;tkg2Q9h2x$Y!}Hmn1S?;*`K%kBsiGt@#6~adygPO`o@%7> zeY3C_>wELt$8x#T`)-4eC9OaG&NBSdC`e&p zQ>Hs6*<>SO#K??*z+47LY7>WFE=#ds>OVMOfNG;eh?3E-sM623j$Qyf4k?Qulzn8y zH@G(P)dL0n66sxahm2!V zK=eWPxJML}DSafjiWKHMN}mOZkpT+R#8sw>1R^YARz=Buy1Ql8IIO2B(<@lBD0JDN zH(xs|y7{$KcmLP~{r#YA=taof~aEn9*zOHoUA#^Z$F{emsvcO#Un4}&HpjKcq z@C~T@oJYE`YLutF5Odg>r~vzx{-3k8s}JNwU|miB#l?W^;EuaDxaiqqX!9`C;c(P$ zy*g?D_TtYjPytV-nIka)NSczsChRV8WbcjOdd4z?JeVYbJ{e4lthkj>OSo*ix z&4sT;*V=;PfB+V*cZg>pS8H0NWY%9P8@OhooZ1i{dAQt}g7J_H-)XTA8w#&=R=yJQ zwjIV@LHq?FD1dokwEgyAHD%NEy!@?i9Th=Z``JUoflR)SIAWBlB5;IaJX zNqyI+M!5$^Hc{ot43=FiCTY9GL#=%220aX0(fyN=bhA@>;-OM#Dj~?vs6XFHg2yJc z$NXY}y8JpP{(b7*OS{MV+9fW3sv58Tp+!#cYekQMRRioylGcb=useeGQJfasm{*eX z>uDBE;twu>^tdua?XQfDHK5~gjWVLYIVE_{|K%Y(ks701lc^C)%SoFQ)FczVzYuyKbqe2=}(p;h4}xeZCC66VE~% zv5eKkWL19O`n@XjxN76_X$$tF=I3MR>4}&vdsych*m}6I8ko^44}s!kPgLrBo%9d6 z+z*D&GI)V}a@WEFoG{K=leUC zVgSJd)g39Lq0b9(nkWSc85X>6q&1+VC+9+&l}%x0YJl|{TaHc!X4sFCB2{4Ns5Ax! zjlr|755$e^O5vdK#DwgM6S%GIK^W$90i%9TGQFFyEv-7@TpDh?qP@YVatU3f)#)e0!o4 zwm18ZpsaB<>D#4qwU=iypDPL(;Md^wGz#%=G?NXup7Ote2x^ywxcR$D{D(c?jr0PJ_C&^DCWpNdkg- zo7nJRS!E*uU`uVg+U70G#(etk_x|FvI{Q(f5R;1yUxHgfpkTJ($YEOGzYy^M!P)rP z{UHuwQ=j8Adn74cfbX=E+Q#7G{As7Q^9L(6dg9R3c&0tGg$0PGvxt1%fAhHbUL&yl zzDdG>7z^Uj3)e-!vqpXc)A7)5Y?OxMakTIFO+Pvmo8=V zF2qtHRRXj7;mcE1__AiOA*eaI!fy7a1VKQQ8o#-~Up~2!Fc4pu z2^;#zC}beH#c%x#8JWIl_m2I(UZCj1NEhvVA%*_NVUE0}Y+l=Y(s**t^^+~OqFRb} zVljs-e1~8o4!fq5!ZJccLpJ5W`8AQ|*|EP}8Ylfhu~2|@>&ocdFfd|#I~HZYevWQL z)xOH&z=Dyu3s!4HfqA~olBdF4cL6BzTmDVRi;%wV;PSqzy`L>!QBmPlJ5E&=uCq)q zL=Fj_Dq$N6qXoAT2Kj-l&%PGCOJq0-JVz@P!Rg6Gcz%sk-uGF+JPZs%6D$@;j^dfMB z2H&l!!8_reW{oKA4O*w*W}xV z;RRhox(A}tjex*lbg8s}NJxs30@5%#L=dGE=@6tFsnLyuNO$~cMhpgw!Mpd<_I-PP z&wXFlc^*fyUfbSU#Lqx~cKy5RJnwUWjUqHEd?<%norj3*?vt%NtsZr2rDfRvdym6{ z07>ThA_B7;zgTsi;)f0@4AcNk=rT`v8*mWu7|^WShqp#gWe0bKJPO^OBFv#dsNO@E zOCJOb`-`5Xo=Y(FcipK4*P0watML1*(I5A5H2}#_B7np#rRBj&jyV?fSaEr4^lJyz z%{O8yV7mWkv^qUre=#T-g}6R|>?X2+-a`OQ#Q0X<& zd@CRI_DlP2u+uKk6d*4lxbYRd_1m)>=;N%(tQoV#6{G}$GX#&#Dst&6n;GY{MlkU{ zOVt3>F#J;g(n~}FkGL_9^b2Oq5o9Wa;uXN;4Tkg_d?5Fm{QtZNIs$A%J4Hj7R9$j` zmZERvnWJ6*lEP{HS0_11NW&S4(1=oT5+-t=s&3V-q!rR)vO1|bbjLeCQawNdc>JLZ z?l<&#z!BCk8+(+z-40bfse|66b*!+*u7P$tnaD@SgtKmBh<28MKptt@a3w{7pdiU= zRntGGyxq2xB#ssfwB#`-#R_azrDld{Lo6Yyg$m)hZK9~^$^?HetbU@igk8)cbSZd~ z+j14!dE1Z=os&ha3H8fg+bhrIrv}|qxmkGcj*Ogt36Tk^KfPU>(3J2OIDX;Ej)Kzl zdlS-~r=4UM;91Tb4^ZJx@I>?r{+ya)#hpI~B4A*;_d-;V8}5xmX#Q=G+m&Xk;-Nz& z@RQeQT_n|A@3*{5sYw&PO2Mp)$tl(u0=u%z_uX%m;@x_7?ji`G&%Y!6PjB8Y+Q#3- zU{)~StR7!z0OZkDMPYby^CZ5HG^s(KoY1mfz9eELc63VrRrsSZe1eo0hH$zj4!a&C zEMK^Mmlc~JM zZBVaM6IHQk2}JjDLSR*IluI}l%uk))^Uy?*)}LZ0&0hZA%rLLWKAC=L;s-sehW&4t zs!0)lnbTaMXvNDXonKkLk8Qslm-?sbl#2=RC>{(t>wG(pA zrTB3iOYL~&A3@Aa-I`+lWX68uMAQELGuS+Aa2(%5kAxe)5Ku=Jgt7muLehC`73hxf&izu8{6N z8cq9O<@c)9@WC_6ufdJE8O&BpAy;x6!U1x}3C`j43inU~BR&PFwcC{5rgfo>N%{>D zG3&b}kwDuL)&OPb(P<^8#Cy&Uje)uXM5KN(xO;$Q@^G{{MZn_nUNP^9(d)p<+7BlFl0=U=!IHT| zuK8w`F+ye$c?7Zk^;jc!Z4I_>hn?Db-z*Sw(OSs1@2ye{Mh40K8vbb!c7v~b{_#pM zL@AsiiGzji)WYNd$927zs{=ND67~lPa#r@1x*B5xS<9aB;#*fV)B}S*XRS76D#uqo zgo(sr$ai_H@5+jw?vaTq6o|}z^{Ra920Z>~0sj;DZP=H$!THQ4J$0pP;&4MOqlN+kGCe!rM)~tAb0q-ua-{=MMT0=UYAO1v1uJFCVcL}`!YOKhp-rQi;QJDAHy$(vlKl62fGDn%>6yQV~ zetkF%R?x9+$53GgE?Tf(xdNN-U^MvfzdR4y&mJ$CS>ov4Tro`SzFhI1Ua4sD+p6Dy z37(7%r&>@KXUux^1jQ|rN0jJ>{KWh3&*3sHB>R^d+|41adLe5V9^?u9ZKexr)XRWG zG;_8~JX6Id>GP8w--?NZ`IFBa z^C&i9YBwP|-@81rh+LD)5cTRDq}T1kML3}QqK61hR+hEr4+`iWh10dNPXfLX&+M1-DmXhFW$~I-KN#S{wtdr zuQ3MsW;5_}uT|W@$Tkas9HW)TUcy{~qQPrHoO{vuaV=`vY_(*GZ5#iH3iqw_M7e4^ zE;Z<<`|U@~HM*Rx-j+w6cpm4o-kG)r&nxS0|1rHhsbf|A9=xG}6+~=2o~?$J{vmJKR=3dK7w1;e zS4~3j(VN|0jl*14Cca;x(2Ib%A<)H5?Aq-KVo5+`ssXyNu8$Ab! zj_zvwWn`8JEDAmE_~O2=A+ZR#G=(98+zeNd!PAqJeJ=aknmV@sXBF=gRx7q90v;UT z?oLo$*45i4l*tn0sJ#^YVFP=8;$nh^4xU{f%BObcIH zt4m|LBmF<|wiSF(tgRP-e;JU@r_8R%arf~zkB75s^n>Jzrg|<(!i9gIOHF0xyl$ME zKUHIJ2+fJj3>6o|G1x!`!uAfx;Yp#_>mKBqlyH~wrF^L$-FiS^#naXPA~=NN%dvu| zb0CB73uqk=a@&f~YT1+Q_T%}?g>dLah}P=Os1E6pfvjdCgMI=-FdkrmBzfh*w%ve<3fU7hG`{?OeQHp zrH$vtQGsU95`wA&jhl~!1Ms;GeU)It5T;0+anl>Xg5UDR;E#vE$^-QJAG1a#svE`- z`C8!$68#!Cs`Sf1Dk&>SiXQvS`T&=?WUgGK?K$W8hk*vK4a=i9b zfD^gcGR37QbDUd0nQZpTvlToi@k=JE*%wO6VQ(0){^StPW9~igG7KeeCVPgah9CaQSeq{#k!ocN54x>JaHnmudzioJNoUg)`uRCr4&PV@90Fst`NZKJPz6p((z58hJ zZ6B_OKV+(g@V9MF8cf0A>~H9;Zx{w|U869r3QfY>5KS-<{@Or%A4hRRNQy5jKQ;%2 z=xdAF#PUcqAHLL0Je_%+t?X5ezNc$*tz4Pt_%nJ}x;&$y>R|83Z~)0gKj=U=ZjB0m zNP`r;Vr2*UBJa0=v|%j)-rt4~+MBVXaB+<%)J&LwRx?9DMZr!Ty~Aoa zTH%)!_|4(}+2}rY)Xj!nPfH5p#osz~Y{X92W^+9K+};tRXuX&{$c%o4xUSv@3#W;1 z63@}^c$2Juc>*KNjQbasqoQ8P5LVzlH)SMmv`=s@x3gSqs}#&g(JVayQGWjY>tNeA zQknnohgO;m#fuct%63)5z8D*HHbX{ECeLQ>>&AgDEr#xn?3J%(6&K(;Jn<=AP`aQr zV6tA4xa9SNEnv-IAsRhYkd1Qd?3a^VO0J{5s<#bQluRXVA$)ESjEG+rS$uM7G{39s zMRr|>*}PZKe+6IQNf~2tOuctXQ!@LCsP0dp)UulrbG-h62pv!_MOfty@+-gW(T2Lx zLZ{wBC(&H{OQ3r|{0l8Z}_Wx?r9nU(m?mVX;rn6tol!FVqi$PVSvNY9n>docI zCRzubNgn#1+jalK^6Ifq?+BDPoyc{(L>x?h?X(kLwr#U9AYhf}x3`A-I+lZ2J!Hpx zmX_s$p%hxTBKpXXG}*uGZm97QejdF!yqx8$riwFZwIEnMOz}N1M7~92e zyQCkzYGMPrL*R`njc}iWtsny0o=;~kLI+SxV{vIp!=lHES3qk{?bEYqz7>i!2%YND zxgdwC>p#vn zLNzmE>i0MsVtCCp;v+9=yQ#CYt{r<@ZDgOvlKDEy2JjCJ_l-Yve*q;8aZ9!>Q-TC( zz1o8$66C=jS~bXUzz;0uaa|0$kkp%*&YGE1?sJs9w0nIU!IvgO_FU}ij`OQdnKzBI zukzk`hxvf+;%a{K5fs-OIX!la_O*9Sqg^avH8<9*=bY*y+8P*B53|sG zDapxHcA9RMV9ogdokxH?2>gm{xzk99XZIl8TaF^&U{qX0e;*sceX!@m(}Ui4d+ zB=Y#OHbRSgspwn=SV_WG+#0#yl?J=z#%HiB5t5Zdmhn<>_-50bKF*!%ul2YP?_^?X5#j{TY1Zrg ziV}+$6WbwX60>;Z`%En!_(dj)3&?qxYw6tz z#X~XKSjy(lNkR7o2US9Af9Q$C{Fi0mCHS7L=dX>d{!fCw$g|o!{ist|f+9l{*>UHQ z4v{tGY9&SZinZ#Rej0idmuQ%^vI_YH5B!n&*sO@z^8tOhPfpo}>$W-T4Qr8;G<1)4dCnz7J4lqe zCL9Yt+R>Z@Pu^q6sbfF8m(uC1&4nwF(S$rwCFRBU)4w~N%iy8u6F~-woSQ5A&Gi8m zKkLeF#LoM<5(@Z;y6mR?r#qlPy@N!er%mf^q$c!u-#xMR0F+mp+9}~G2CX%>=Xhgp zzp#ChCdi1=0esLP681MdKw_^dQC~nEA%6~6iR#mtD?2>Ar-j%zqmOX!;TA+>OZcAI zb5a0Zo);rvLrp~-kx4R!kci_GI!>iodh@00lWP@kC`VJ#v}?%$NAVEod(!IUy%5Rb zlV8_(J9D{>&M!ilA3FFF^REw&`CFU+NH{+uNu4n3dsF9lBh;iFsye13#yqZipPXQ5 zvb)N-DX^7c*~6wxJN~!!Nv1FVl_tgc0$j+(&S{nVY4Ez@Z$V?4XCIT69($yQ3&Hq= zHY$1Uu38kbQVrXFoG$q9-0ESr$u6~W&anndVxvf|N#WtjFBV1P*fjL;KJ*_u`DXdY z?A&Z^;O&VGBht-DVDhRU*+HsPLi>1;R%W;LQ9^(uM$(H|x;EbBk9mWTezc@zVwZz# z7S6ozldt>7hzBYj+$wN-i+IQkdBhdMI%F>lf_b&-!4Y}o@g?9%cc$&)FhW=G)WiQ7 zg&Ang2`(2OKk9NM~D7=%3rwxA=_P1(4CjMvJYkab$bDBsM!U`RG zTq75ak>rR>eHyuJyk-YUI*;x>J_Zx5i43ktG3+5%7iL=sPWPyrL*#Ydhp6ArR$J6k z%z4e$HnJ(amk1jnpAj%V2JU8H_TjvAA+zD;kO0lc(?3NGN^LzH=bFELcXp(_apESG zk4`m)X@?R5@)(D^&q1$5>=>4H^BDbGS8M}0`!Mj{a_C>uV;!C4v8FL6J^6T^9U3WB z-2q*(nt+ummIJ>w?x3BlVk@GgV8;3W1@rs@`DjQ*8Mu-~ z;cX{bcsQdX9vGs*FIELz?kA0-pvZfOs|#?Z`hpHXY(aiw&!$zRgo8LhIV?`=u3D{y z??dOyl82s+My(8z>G?6d$z)R6vH0#%MudH+BUiweVCTu1$l^Z6Hx&gNw|(s zJ({q*mhKoGG3T$laJ!F;4)_;LpIAd|G4%GcH|c;@`7ld*PB4EjulKCQzjDvJI{S(+ z8v6|G3ZqdjEkz~nq11m|%bRcYinAsCpaXoVX~5p@b5%_FlI=~``SQN@G%TI4(`7ljR_gEWo|kZ zp{?+DyD>&RZ|xEZq`yF(W_Do@){~#ztCq7ykq|sTzIPQN^DLus|;bDS`?4%p6=Gg;>)uh;02% zjpb$%GeE~a&SAdECC-tZ*9MurmAD-tGdl;)FXQpIHu4O;Y$X4xYZ0k-gx(h3$y> z<^}2&8XZi}`O|b~-95TAS4KtEtwy;^C%ce)2GGhU1WJN*+3dqwygFO* z_Z#|Xu;zg81L_0XR5kkJPowUc%lUq2>;krf?&tsylseQ&a{`paKkty@tFQz3$>Wqr zw>gs_3yFE;NZh#flVS+k85F|YpaRIbmAYZHt2=hRRl>pPL>?k`QfN?^Vk@VJn-J!Z zt(J{S|4I_X${C(sU9h4IvfhLE7D-3>Q{&^%iJ571K;9{+1aRX|h;8GU+GnA-yn$RA3Z>L)>&%*P@U`bbW6RD4B#a43G}vQ|$@1oN^4aT$K zuUld3%TPD=6F`(Lf~TEvpDZ%`vS`)vTpo9p|?^?i#Yuf(u*Z!1oW3`ShO`Q68v zuPh&xl|LS`%6fXd4y+DQo&&qC2~&NU6UEvU2eM*KO>RlLah#p?R@3M4Z61{NfdY*` zTk6>?XG0eUpRhL|IOI~tIG%!zV4{t3=6-jz+NTEz&LF?Nx9?i^)O-KbMG6FZdDhYj zZdki54d$! zIq_OzpCdav!(-*&uSeqw$qwiW7Rmz-g;Li?*UZt=(-N#3De=K zgD1A+zlc4LW_b@PU680fo?r_}mw)|7YY54R>=Ck-no?!C5e`$Bd2!E54&>6AmEWCZ zEv+E%CoZ3=<|*vT`c2FRX8pu%R!M}A6H;|}9Ki^p?5u5x->#zt3(1f?0#)xmx=e0@ zs@=)o{V4s^yfql51||RNn$-3h6$xdQu3Rwh@;%e&%S*fvL+KDHWO?jgJ3W^^5Lj%` z??cyoDuzk8E%5$CP>i%bv!5P&wQht!I}&1t`9`K`Et)-wvUb=r99Dii{jpev8aa{s zT^JtZ4COZGk$GVp!<;{-36gqfQDcYwI$+uCzNqae3h>5R_z|w_57tZT{>g49#zkTC zc8q|zw?Z{!2W&NB43Vx4xlxvRU?A^#cS(ph`KAEmn~eL`o;UK-<2h=>+^H^x{+l%} z8o~>~Q#FTpW~M)$naSTSV>6f9o#v8s78BjbaiTdVGN9g@l<2CX5+fnI=f|E)X_pvS za3Hl7j>9_&()*DL*J1*p^}vwziOK{&bg5sWrjQXJkTSvey2;~UV}(LFYhC^*)-5tr z=R}FQdSOYrU*Y5vax<1aOyjGn!Nr}Ns8gSxam0Pma}m8z_A`!qUfvq(#nDLF=KGF>M9n`Z%n!ZtCgQn`v!7wi z?*R()f+wfK$sD`?$`1d0uh0?ta&`cIzxm$dfNjSvC2d=16%IysvwLf)dc?kR!74jL zh?mdwH(Znv@~f~A#(xkrN^h|&83>wx2E_+UksgQ>+M;0h?s=HZ)K>F9K0p@zXBeJ# z#aJd@an7QLr)D;r#62J>A^04e8{8LG5vbb=dDKfizB#-?+A6nOgMWa(3V)?(g{{&2 z+z@$Dbyu{*u_sb}_i)$;dHJkU1#vVo%R}JZKIok?Dq5OyFN1?XCr>*^q}Y)Rsu)+ z;SelOE$Y&)!T%$@=h3Aq0=I2M$)sM&K)fgf6ny|7F69Y89$$5?<9fiET&ZakF@GV4 z$B{FGekDH`N51U~yb?6Zr5pbj-E*phgSaUM&PgrcKh|H{gD>3dqnoJmE>}I62T{sI zSdDKu@<~$b3m~>yWE<8%KPFbqzKf@c={n0@XqEIfv}Jpr-5}%#8)}w<%*!n%I`Lu=MCTwTO$V=u>UEH^gD;v!#Bmy3&sTY>llRLuGsoG zk!V@G<=PAI4P)=Hu|oo6)0h;HMWwU?CgC}#th-5B^};znNemF@t8v~ZPZcJp?Y?6Yl0IIka z3-tf5)G)--pOA7AL?P-C-&)je7S^H#weBt9mSpX}o8Vki>&o|iuqu(-M=HvHecVnx z;D+e|a?5Xaz${c0JtE&gD@PJU!%EDI{U-*!$?9qwj_xKtKfKa>$;ru+M-p= z{P;9eBq{tn7QMR6>Q=%?$N7;4e67)aacx8l`DN6=P8wA8*)6bpFyvMk*VWO|36qQ@&e82H ztnQEo2Zx|GG98a6W3QdT)yT;vo_ni`?cJWFx3m7%9MKK+~b>D$2n)|Mqp9TZUr>9_s7`nfMw<&9vkAqn}&xK5&dLbqThH$;te* z3$(fE!is6}Ieg4?CQCo{dZUxhDtBY&m7RYx9iE}EykF!UAbeBY*$i`inMN#o9}w`W z(SPXt-6Jbhcw3QWqxJ=d_angG7yvryW@c$d@ng%kvO|XOavj$Zf!|9<0mA+t09k!uPs7~%&(R`qze6&i8TW`@y$J>q7F5??7bH~VMY z`1o_9WffBdm&fyFMnM4ARG-25)Resh3Tk~Vd;l##4KCwX{|^2xY}6Y;{ujIQJFa31 zeSnRx2HVL7?9b5kKeY~>GiRChMjn}Vb29G`aIV6trij8;SYuXtPv2^i710?7c~7Z4 zSKCiAHmc9$lj-4P$*P<@SMdKvXq^RlalTlo_vqTDud<8#x7JAv(9HRXXOWL22cy$> z*}?BPaG&LlIJT`A0c8qIB zr>nwlwMhGl@6=ZEL05B8?p!;qk`!cyWhPDDR4Od)LlP_8MAt-<97ck8EZI<{jnTI1 zmB3W7~%i*O}Ldf1bF4uNrC8s1m*iJwCm7TNVwb(?nbmDi+PTCpA)ua>W52|Bs zD*TR^x|2}eO*KS}lbfo77X^^pOB~PoOxWE3W)Zuc?l!(cnHVz;V?R}OL)~d*4uT&O zL#`J{?8re{7;%E{ju7T*C3KadnLJ2J<%ZLdOpW^pCFzBA2WGgnLRl<7vcj&>Wp8t^ z;sPq~5^!s{)P1p+5D`Ek^5*butHkc9509lVvu{9N4<}JzgjO1T&yQpC4SK9qt4-MN z<@oHWY*1CHKi-3!#Zz2jzj|Xz)I3JHPjId9m!V=wqu4QP@N@sq9*Q=QgV>HGQN;(6 z%B%?a*+0{gS>GzIcbLW&dUU|}>ZFq(ah$y};mCy-_QsE+X+D>j)1Lwxz+;LbN+Lk`kCW{u;I~yPyp&9-w8xS+krW199L+1o2JlQ<@zZZh}_% zv$n@S_t+(bo*<65l0t3_O6F-wmD}{LfrN$&5%YU&PuWjax4V)&@+@13>Q+02G=wEg z#d{hK2qCO5$WvqFrC5s9CMf(X7uLmJa>E5ePn@JGR8!W z`Rl2uQCXlwBzMM2$BGN5uE#V;{if?Q>D1Vs^W$CJ&BmgDp4)DRvuGk;?vS4B$9uBY zjt|%9$ZEt|4Y4KlBE#>Aa_5&{`y93XS}><-@%hOAAwq*l?gH_KJsZj<#-OVE)q|CS zC`|)SwAnvr`6ahY(&a`{o%ximA~Dt~HO`A%-KMy|^I_Q})u~Ze5?2_%jtOY!{qi9? zL(M~Ks0l+^3Z`7@{|=q($qJwC{@PYbs2)6W8;)&xmWky5 zcuTg%F1kzBds|)VX>lX5TT??2lcLCWcK-s?(k}KNc=sn)F)?8GCq;5FzC^dY8AXh2 zF2gE`F-XA}M)q80g7i0GeDK9 zS=`O_>ExZgwe=k-(yC8g1rzuuHYK{x_1|*qe5p=bWc~LSm4lRPwfKw5!7;Ju%e&R@ z@*gq{=UYAUgS8}`-KNx#S>OyJeRR|W4_871szWAaLut;V$||19%{E?!V&PC)uPKtO zheRb)`Bj6^BYIq849rtP9IK&X0Ts(MbwgajD`M$d7mK^zSr9QkONc0_7)?#wUN01L ze=1mq>Ayb zQG7N->};ITpw5?RzG7LH#x^%%{LlCq72Cl>_xnC7@s(MPoEf*$onJqUFXCnjBzIXaFIc&&or%8sTfs;RPyv`{@*k7xRjG>Vs{J3 z^7^P;%Rd}P1fP9KW7oMV&Z!O<%tHF-!&sSp$ATR4G1p{sdB~F~`_nD5o8hYjy^yLq zvg!~rz#IDx6}a~%@xh1A{T3oD%Kxae<)GyGv&G~rik%O2X3OcTZ8A%GHGVA;gem*P z)M<96#kgg-;8>jF9PCfk;D=wr0UwgC572zndUUHkv6+43eRk#rQY5ol*Mr#OCW9K% zwAg=ydAH#QTFq`JzdQVTfpb4htJIv>Ojcuw7pnc6ZO#_43iNM2?WUC%@pjWGB3zG@ zG?$^fMi}L-7H2~H_vnT#av7U9uS!=n6T$IOl`rN1si8{Z24Z;FzQ-?jZuu+9f6LS7 z?^|W3qpZ?C3hVm?RLr-^EVVy^Q7C|XRfa^g{xuf*N17kAd~i4mw->wZ=(qovcAofE z2=n312C`}DiN=Z~Ogk&n1=8swQGK;oqD(rIhb5BQOmdvo^Ex6sC=~aap>#LNnL^N8O|m;-as<6hpSmcp*MLHM%rJjnt3!2Z(bn_jkB%;7Cna z5>D99k?b8WrtU6!N~gf4VrxSw8Zp@EgxQ`@%`t)X@ql0EWoi)i3=CI)`kx)(l%pn*uJH#AId8?{s&H2&T8Pn6!Tn z*N71;*&He8780ht+*$%Qt48tPSv=w^g9K=qguIURoj z)g3obYn+uj@2~`09W{DkB6=j2Ph5{<30B7gmgq8Oz65D;ygW6{DRJD0rb=!=Jo49c z+{y&nRli1SHz0n|2z|sonlEmMyasA0oc_~K418Q^*9bi%r9n=Ropt?=-CS>-@_!kV zEM}yx6m(6P82$;^?tYo!9ZSc}$sJtFxHJ>u_$^a)A6C~`K^W=nMi{5xjxqH+G72xF z*d1iKuQKrVkzSJBW)!c7AfrbRG0}%{K(W;y?IVs@dSxQ{Cq5mSe~wHbZ_p7gTmCT| z1m|g>lWXBt#bbq;5ti+L4jf!a+x}1`LO>y0>67C#w$-fY#z2Zhdl_;pd45xc%soJI zhglpUe#K$qk_gysfsT{F-!+2pNV0fEnn}dsj-vEsGH~|9<;GqhRQTJ;{&%R=FDdQS zhwv995TFu{H1duEbXq+9>D|xeD)sx{8aZ?2=zYd3`$AKX#5Nzu#V<>|`@IaQcSt!+ zcho4TlvIKZ2!ixPstd{F2Upw25Bv2LF%iVQ0i@4*6E=-$jC?E~!q6}kmK*)NjB zZr?}V-M{WSg`uDOGc(|+LY<_2|3*X9$q*C^jBQVZ)q`t@$4L>%E__Hs@g@K$2F_r~ZzR1NBwPjsym=JdV7YBzfAve^KoR?#j^gZ-*=q64bker&El}Qel zjA{Xh4aosmy(?_SJg0*T{#Ab1r8b7Y9Bxu9S@$2;aa5 z{)A|l%wGux-tK7-n)tIopJ;q=)&lX^?q_;DTneXK^zXC_Xtfsf6hJKljD6{F7=!4ABC3a zZ`^HzkhiFBMbHkOaq@UNf_evk_QQLz_!oWd!{5MU&U4PguFIa=5@PR<(sqD4P>!UZ z6;VF?@KQJatciGBJop`C)rTA*nhGFku@Gzy@$^Z=B8dzD^28mVW&@5tY-Z9lB?>Z} z`sd=)t{*n~>1*D7$Yom5rjD=NUhAj++p5+_dv$#3Ve??~o5khfe$~*5W$5|`s$MtAt9Qrm*bqMe#Q@lwC)~s6) z{d0QRPl?0gZLPN74yj)rLr`$KKWP|$3cn>+&;<w%!26w60kGk0?(cX`iC^`C3u*DP$*RCo;FC+k z8k2Tdb=HYMo=F@>0pP(Bs~{@YcjXNDkHcqzbwk(-0*}9oKC^sy(@61ZH4U$8UCn;V z?WWrK=^dtBf!ctJdDh$}hxL1tl611L@-A6bzU6|2W`wer#S>;&Bp{|o^LVucNFB7HAQ|MhU2rC*pg!ARcRJsJmmh4q`6oE*J+dp19>;Ug_*h;wdAy%p+0mBL2M8+tR)Fu@NOG$pA`)mIo` z7K7Q-sJpqS9>S%(X@=9~y}=?F4L^JosBdXJ(5dlbh`i?@>)gfqxgxLnjgtM9>$S8< zQsiPdPKWH}UiAJq3hLR?xz1afe!I6nU%UPA+xPGM+LnbneAyUpxxi@gIczm9v1sL> z(GMIv;WMv1k+jpEf15BwgavAIVL0&AFy{}T9U%2C&kVUQCywA?g$(gY;33#ew<+(( z;U8w?Apv|ZwDNsb09;TP0=Qu8`!~D7s`L>p!f&oH`)2t{I|jE4v`#jLqrEPgg3q(0 zDPk-ki!tI)jpSaCV!+=ga-F0}@pk#=k^9`|5~Ywzg)ZQ7|KM5FzWIRG=$m{0)PP~L z$as=Mq6m)9RJ;UlhL{p?w$mn?N9++t*G4j{&%FuPM>k~`&18Qcfl8s&1tHu2g`;i+ zCr63-#0|J(jZBs-tZ&PI=phF|?kFDTCyovfNixt^x7FZBNI@%UBga~bpB zmJRZ=upO#Wg`Ng1SoeoNO)%r}pG`0eb(_KYvle<>TOkfSLS;Y`A^Xd=IDPUBNfrl;|C3hOY~!A!ZDIu01F^?!`%7w0MRwFY>c?rA1vWU zrRkH~IQk?i3Ie|pUCN8sfy~;``l%6?m;XhOrzOhWou4pXc8D-jT!TxL$vQ%^utQM1 zsKgVnNR=x6ZL4zMt|NX7#CJ}5|H5Qdu`cSv zot$s`8>*q-{5u3GD38;PIbHOifhA6`~?Q*^q(nJIlE7||Z4k7>E>0eoK%)9PzFE;wWVf?_u#J~jm_rf}GdxW&Hy3fejhFxD@pj`2-iv z)-flHbNHh)>*NY9O!&s+JLow2Odd%6wd4O!(o8R_>kNK_@S-gg5M>gR+9u*kw%GI` zah4YR^tZ=omUQ{jM$x~3<(`0lg!p3EOFj|$@V3BT37>*$><%QF$MWveEPHw-rKpg- zt}n)c4@MdugCha0meeSM;_MyIA%8rqQ40tE0$*CP!XaLk4?p+x36jM=|Jj#Wqqu}R z8tuAurC>lX@B!u=_ST{J(>&a|(n+MFI3MmZmEAFDyf%(iklO)nSfdOuevz={JXd#G zS)i_~ku=3cLk4zyI!VTPE-g&D6@1x!u|Mi9`tv7qkti3FXhYiqzD#Mweieh^{3@n< z;NalHtgviTP}?Q%JKN3e9--+z*O2GQ-r%3#HXGz7c)78xRzD(tM?M}xw|g(Kr#-m( zeMy-WS)C%qLR=}5sZo-kNkCEhhtSa2M@OTq$Xj+oVBV$758Sw-OD6VbdGxJ*qRfXk zzShI9P|vVeTm%f5pbx7-8H6S&#G7KniS>S9Z~BW(ZNG^7PO|+s%rw$}LHR6|FSw8!7`o?5) zaauGtv4XV!uMO+CWk8%+Egil}1f_5Q*t1LSU$eII7AwC(I*di`JHJBu|8jk?1hgGv zojLT|nD6)wbA9#DVMX9_Nuz{5uAjQM^7+#izjKAIu77_S{i5|5&6`~3^AahT_uxJH z1ce_(iC$i;kgj{%{QH$mjO2Ti>UNHQbK}yQq^lHlrjvh76P=H}Y`zCfFDDb?%}b(K z>NdFE4`l{xBnpZ@o5qLnRVxcv2-UZS9PI?6Ck4Fw_ zsMM~E{;~tN=xeL>qPHjE;au*Q-68{an5)+SwD+V8w3O1Kh7`NWx90x_M65 z3L6`Hmm!kJ--10*datO;!?7tE<`u$gEWZltgT`zJ+&OY|B2j1I|H3KQOpjyT2imgP zoSSUeyb|-ybt#xJJHKElEyFRNs9W8*f4w6*V1H-3@uPO?r|a75wh-S%TZh{h=@wRt z_D*-A>uo`XfeX)_rm6xTPmebF9kzdLjkD|Pdd%^(z2sP(#y@dgtJhZi%a+a?R?*(x zFIm$f&$H$4!n;t~0*^#4-6wA#UQVZv@r>pMyl%tk7HjxjG(>QZ@%PW9|5r2%x|)2k z(waz$bJbPkt67|QVIi^Tb3*=^k2{1+A&s-EU-59}fNJJ!{Tv{Wm+2v3+Zec7L}JK} z#ip|=2Xo(eIPSN&&tQ4k9K!c0s=QKEzHaeVE3$C*G_aP^1gu=HNe;rTn{}8tJerLT5`$^;9c@V=snQ)qEMNcJ%w$33&0Pv73)rc=C!42-)MNhieGoIpX z&g2we$Il_RzWyCxrL8TI$3IzAymR#$s^7eypxbze2xNuFizJ#CY05_-sM_ufUsvGc zoa9U7q9ro=Q9H&BC5t+-d5(J96z@5CHCLt}wimt(L;Kv?;sT-!Z1Z=6#O4i2?W=0n zw(vC7_G{xm7YeiS9%(CcIXAy$T>z8aj@!5r2-(ic+B@@s!GB4|j(1J9AlpvNtCcag z=f@h|yOlUwKeOi9le;gEc&!wQvsuRc1xMOK&R#ZQgrWolRix_ zEh#(Ai}Rz>zA1}!qcdinrU3+B4r_g4094;8--}_+?tlg`21=bk@p zcl}qeuRu}u9ZyN1WegNO`HtZk2|@CP&fej_S-@m(qK^pYVnXqB3czu&1}-MlZ-u1_ zYcJ4)ZLzqHs=^k!B_IX=9D`Eu@TLg_I!(uN$7vG7iYH04ZsG4LB7&X(W&CmT1Ie4p zcP6Oc)+s9iR5-~B^LN|zrM9EGyZ7Ds9{RYIR?GW~Ua#&2xoR(Cgx{+H*3ZSLMQC?& zXd15v|4It1=44N#Kd|F?ESj}KkrZxM?${#S zhOzn@Km(IK@>}jAvwfjk0ZW|AQ=W13ZNnw{mX*0DN zZpNv29x#s07rNA{dP^>R-o70|kGu*w3FmvnjTc5>ub_T4upSkYUeP>Tj2Wki=a5rI zZtr9e`eC81&<>iH`f)0Y@E$~K2jTf(%JS{@|2R6!uqMAZjz42`IGQQK=x!8HVl)U! zD^k)8f`T+`G!haLN+Td8-CYWZK`SAQl$0JZ*!JJ=e_eaO7w0<9Ip@Cb@8{DO#F0ln zm>1c?duHyK^i`+uuhSB7pyagLt#F%DifQoSg^I|YE#nAS<&_zKML7k?#c(#U{kz05 z{j@1he`kOn7P&atb>DivOC1?jvKr!m+= zT;_CIpAALnq8xI|*#6!89fcnpVJD>b1jC7!>Fs4*)dW7*uw)eB9q>fNNPG%O^*s>R z4p~dBg*^?_$b9jI!NJSTKUYIU@ai0;9ov{(DtP`%);H267GvH%CnXLcm0V@osR8M( zjNRb|MfhzUz)6e5X#Uo8qX|hJg8D$tVCEfI890RyRH#hj z97d37@N(Ej^+W7ums%s`aqd*$;aeBer-asi&<85C!NxW*M}BA$5p%6+feq{x9S`Y? zKkt$`t721s82C$(1{#5hg=k0i_Jk<>mKr#di<0%KA{iUzx*zk34doT=`J1vGncm7< z-J*g-!Bs;tpEoqR(Kj*AkEuA`5DkXhX#0-aD`t%~t}N1FbxAtPeyW8f0;|k=gM7!+ zR52ves@jNeeDW&awD9ynQ36drQ%Qo<+kH-zxa9Gt5(3mg`65$;dScDcUERZE^`(Wg z2e#gWCqnveJOSBvJJE4hy$aVL+ufCO#LX_Rj^scqQz5bueACj;RKcr?;`P_O!hG*GP=MVv=qy>Rpu)BgxS zFIdMSuUkyVLk)@0r0ST>VYdRl!&r>J;=$dbpCFN^o^@ZWuw&u50iVbsTbsKh2bVu? zazFU2pfaZ`W=M+L8HL?^8PruyO`dsatGrn#3d?Y{agy78zw^Y-`m1K5No7N(V1*Ev zLgBTqP=-lKfI}wV#NC)|HaAtahEj^V#6Fuy(z&1KAHzjK+AsB=0V80Nw7Wu%zTM$@ zky0U?W?O;}+(ZsjuWiCCAeWCtgWp7p`~ed}%x8tYUy^TL{%(92k=9K_kO*I=r{F)0 zV;BM}OsPA&tr|`^k8iinXC5i7>~Jr&YH)mTwkf08A*{{5s3qjfw-jkJb^&=yPRH^c zr++jS6;KqN(Rb1;Xzw(B0e%#ZTNs^499Mj#|0BguOS>%d`p17; z{1sGg-efFZXW!TVG=D-csQ1{SpZ%_-pS4YYq-h`17i8d3Dp`;S*mSbSky2AA+dU-& zweOu|CP{l?pPuF|=y8GtRBk=MY%@+yJ^K=K&fv!6CQ}m7;0l^|kjp@SM$SKOl9cU= zJP>O99Q&ERFt>kqJ6ujx@DAwEn}Su%CE?%gf5RD%b}q5Y!47@C4#Cv#K6Nl09@*|) zyK&(yaTDx8rdpW~*5NZ@>d=dZ^AQ;omAzE<&EH#!Xdh9;?OawY!5UsIJDQu9pQb*h`z_ZrLnQhcDRN znc0@TLJr+bSaTV2K*&C;h#=t@yw2R6+M1*3hBmw{F8pLGQ90Q6rs%Nu#xgQQ>o*qXK;Pbgn(z{kSODq15o_lb!CUTwv`Ei}+J5jX|T z%`_lH-W_)D8TKXX=<8(>VgX;kvj`-{y_MiPl8m_7JHV-?c22}3tm2qf)KndaVPq_r zPNwerOB-aeTW=uLJt<;l5of3<2$aeKYsu<~H<;>J*7|=;j(q?v-D%n-{{9LIz20|K zJ|QyUVnhrsi#|olF`uSpejyI<6uD;S+Gkqk#*@YblU6(P2%HfsjkBV?BDH! zK67#3M7hL;0a0})@Z8S9OZTo_kxUIN=2`{wYL=e^;eyO4bXcHd zWFvCRnRGx3*=b)VgJtwq@sd}+uWtdb)(jEv|!445HXHZmjokp6Xj^?dXN_w|uEGF6jI zoDk59d(BugWTyj4eo#4o5uJIJ_LtuK4+0i~e|n~EP)O3NMJ%M|Y5`q_F<_E+5Tf3& z(a;2+xwcl4F#ckTx5oRPe=Y^a70ENc+eL`kHtBbzf7PwGds^tt=re9m9L6oo6jS*g zS#~D1xJW!|WEiZq9ws zi&t4+e_xY4Z2ya`P#S+pPNg(E=)(~c?ThR4ZMV*C1fWaeolE|<4STy+Zgj(SU*}Zv z13Gj{DUA|9!O(|fAmnsROTbI_P2*|M|5B!+51po!}t zF%55ElwKrTW5IC_K^{5|v)67MNRXIUttounquxfYnpmQ2x>iH2x{nu+OuOCHh&aYd zy^76-bbvR!z0O@L&=e>ofp`UgVlET zuHoI((0Vxg1A^y0myQUyN;vPw-`nMN^f$gh{L*cRw)!YWdq9PFFNGHmw!G?R|J`HF z*|~aOQ02Xlfm=td@$eFRtscvPSwR%+D*?f4-8#DY{Z6tmhx}ywCc2Ww_gtqe`F{O* zk{$ycH?7EL))h&IbQgDvC!aqhTn#;?{r1g-Wf1qy$^lf%W)b0OS{Uz>L z%Fc^HL#;JXn!(Gr+1JO4-TADHO%-#6S|NS^@9oDNns@2t)dT+u1~4Y+CUN||831we znl1TLlN;@TnUBKp6Aj4=SK#n?_q+|R(A2yQz`)pK|ITrrGwN@A&Zm!>2$F^(` zTj8xerTR?xt+s&PhdWO81Rq=gOz8m_Uk5y-7%FJ6nJIB+^3TR(Gy~*!Q=D@np#|HP ztC+-nk{8_#Q742)H2+I`53&@dHP)=|yZdPw%5yT4WaEl5M6@ZuXQ>}lqnVza)~DHZ z3(@sTWvWEu1C5;o%W^9c{M2{c2@yq1> zVi!=W-&-eQR^T~HXr(dV>%AkRV79|S1#&UK%umpiy*P6$jD12q(oi1R(_6S#NB8|_ zrT1DLSMraSS_4bvxra~|BD3~y6K1sY1{etoQN*;C2)9o;7wSyLD%=o2`G z$jXU`2*rLw{44f?{|Nd9kN=~kr<#TL!RM^|z?5iXKIRvc&&GV_I^WtwIUjGxKv!AX zrRNeJU7(0SfG0sKdbjU@rRVh%5l=Y2S4b`yCOxqbTy^>rC#(CHhvd;!~A?iQ*}%viNHD;sknK$ z*BH-pc0{V~c&Md_?B1m>_RFAiNuY4m6*}Zu|9byXvtBU?IwxL<+Mj#pg5F;1YKfk^ z4D|7K)W^<$f>F%$kfNl$$>)q;Qph-6Ct46k8tijLd)^?Rk@`rNHCBkwU-59Elk;^k zi2gL-S-Ltd1CV*Uk6pqeM(0BlUp)wOjhVbY;x?eTpL)C7C-HGZ_g5m++|qSPfbF{d z>QS=++g2ckb`jNMM!>NjNov2{0_mif)&7B19^Q%a5Tv*#eS^WSuf?MO{Za0?jCbPq zfddK!E;1-zU}O~m7N>ZLnK7=}75H-Jv6(KhMm#6a1b9oy9?yPUE#_cz?06856$R=? zw662HajN=&`3_k#!{N|#mUs+`Kt_oQ>ldCR%&Rk$v2^J3Zh?eY>Zaa{^fI)4pA$de z+lp)$3vy*ChZ_V~YZhL-V9xY1@r7QR5LX;UkrWL^epjN-w+@U?LbQ>RPbYtz{InR% z_#46<*h_X<{6@-eI*B9cakT|}&URcz!Ou5g7Qwb>%_(_N&VdKRlIOjn z_|+1eZIxq}i$$p&DB;=Hef9#)OWsl{vlR}MI1}$UZGS!xCD@FfyeufaakOlaImg`K z3hW>-!9R3nuSi>Un#FHQR{c&5t~HEw`3Zj8A@Sgw&^8^p?9oWR?+-Gr+y|?<1Wa21 z+B)(6KqMhNethpUq}SO`B(XLUJ~ZA{Gn=czu6jjCH!_Uq3+)BrSYI!$x1OM_#0^Fu zzk6GDFU1eS`oDGNpHn6TMzQ-tz&ivj!i~e6SyX;}S`s_KQ`1IfFc?DDjRAR?j)>h_ zt`cHktbNAhnjm%O4!mU|Zc{R2|7l~W!Bt6M2T}5EewpYBUPb+cGo!qlPaYb6poTqK zV{kR0(!ji)k$!$Aqkbj!GFmUY#?z*l4AVUp;4kRch|xaucJ_E^LY37r(ZZ!X_2GTs z*auD-uST-;!RF2(%Y_ivOPw!WC4+_DPMmB5L+c!>BO-k>>{3Kbb^SJfDnAtt2o)Dz z7#E4?*zV4gqSQ48p{baPXXrxqorhzw?GzW5)j3b9R?4Bg|30ZFGc*yU9A(-yDApK* z&kebVXe=K+i@6B@WE22Era6^&zaG;nl7?NdAyzO|ORWAg!JSs<{e9DJ;lW*>8i$NG zS*~XR|6SbZ{W={=jT&a$AnLVQV|UM|=Gqc?&)Bn;K_>9(E2Gslx)V}QXz>P;hZNA}J!h|ER_h#kUAE(2H*~`Xuh)4dFmSOJ$ zg`v*>RKhNYf88MH1%)s7DXk??eX_TY`m!Pt3Fg{fXK3t_{Y@xHya4v{$>)h5G5QDX zzxmrPgNePq%`ejQiLSOlP=N{x&8OrFpF_Mwj}7_CAvzW8B^x`1dLz$;XLIG1Y{4gH ztzOyf4ujuISu-?cep*$?pOi20DZD|rZzopT>p&hCC7Wt~4qqa8Wt~);%l@6tr&8Ym z_I!%RSX>ugN%d%3`t9DNasjkGwW-MmuOUNY2-F?|9lTZ>sGlaWvUAR7={ODsK40;j z%5=br8IZ>Fb-Q1L{jvb!D+{pLGs*pd0rc04I)oqjXY@tAw8h@ZQWI(J73TS-kfoUW9JyFsb7^Q!xn`R5^Md6$PP+v*`vh@s;Ki4 z;)b&C;y7dwkO%Or@n0=GUAC2DubdQ=4Pc*r+efo)f4HX=(ZSV3>i3aoN-MIT4is{xrdmj2Dn8p7D*%5R$ z->0N(s}|BKXl2U~!}B>0!cRA6@@(L#o$MH^oD7Tvmm;Q91}yf^KTE%gxsg?B@~o(n z6Ue|gizi8oTF%*d>W8|U3LeEnj2^S7?DeV}=yN?w@kRNsefk1A^ARh?v!77da=etf z8oVG^Zjvt?+CS4h`F=*V=KSQ5k}Kx}Ws>yL5^b0y-~!kgl*4S>{$eSp6b`mT7e0J1 zaVOQhT;MF^@h^v8k@>aQaa53lB-Al;;I@FvHXV!(XDD%@<0Y{nJS#r5hXf!n2f9E@ z2SvjNTf{AGI5^|9GaS(W2io)d^{tSJFehaNK^k%xBBJp7G$Uig$Rwv5z|B5B`3AD( zd+#wKQw1&sp$VsvZ|1-zc|4($Q8lB|k4yutmNjoyX2qLz5sOkEh%z0dx4(54NoNmG ziGXiC`gr@B`UdkAg6FKj@5D`5<*;WSsdWYppsxUpARAjhQ$XEJS}QKUQ0|ucQgzv8 zwvNX;`uS1zUllPs4tMDBS3(`Oe#e9IQ(bNrq&d8eOv|A6WS$=tk?x~MJ}uhHs5qLR zFC(;~Y$H`a&h{_rsjQe8I-5z}qwm-ux)gLp-e=e$zT*e6G$MyHa|9*+f++Ctax?0| zuV}H!{$6MIoGa| zwN!4VCd<%LeNzG7gB+&m+^>^*k})|U$pZ8m0CRe4s$F<5YV+89jBuL13qH4Ab460D zhKkR>ZuQx<%jdxm@r2J{4ckNx&*O2ex6#5Gqyia$3C(PG(G?GtnJbh!-f`t~&5di+ zX&b)1R&Xf`*z;poQ{Dl|#mCOY_VY^0mlWtN=oDQI$viwK)KGQ{;nOGz<)5c9)B*(W z#oP{a6e1Z>lyUw~H#h#pTD4QLWdu=}s8`OTm1tOM8ZqZ> z$~TBs9#QjSi9lT)WVgmKjWPoQVHmDtJO|K4OS@aRO{T6q?U(>n#=ds3m*g_pM0IJ^ zl&#q{3+Oh6>Qr`|@L{0eU-g1$w0DMM&IxL;&e(Mf^Y*r+i4((x?VUZ$UzMVJ%SG2k zbZP7z#|`c+=ZeLJX+NA3oILKCNCsHR&P5LRgyQ>uXqk_At>fS2`z~F*rxQLcRsPjZ z3$_~V6cWhsPrQ1aSKj)~1>WXU0&In|({_o+$NqZz$B@JoAkA_TFc&A>$_e~wlw9c7 zVd=~o)BD&KBFE^^$E@znCW_xN+d8{*wOdy~98J0H<$r1I3>v~|_NG66Tz{3T<{7Jj zouHTB8Tb9M_ujuHK=RYN zej)JN&7C>4W$ZPA_GhpliH%6#1wHZ(J^RXQ-&UvZWJCDmse;(u|G4v==^9GEyJU|l z8}^)OcPO`Z`W_9FcKn`M`)R6F`QWz>IHciLatiL2PGOaTlUrnB$PScooVV2Tu1=F) zW%lGJAJypz(3YEpHG|3XogkC*Neb_*v%k%Pz|@S)^UDB?=x&)PE&#KN201&c9|VY$ zC%xX%vP1N`Fw`yy>?EIyl6*YYev9P{lC!FKN78_FJT&G&BdcZ^!HO+H>$4K3?yaT_ zo{|?Y_?Oq}r0;~zf~`lD2(PvDzd@Jm4RwhK3XC?cg;@@5FTkDRiSLg)#lm<$6i?wA z#lH8VxO&M9pr_AqPnRW|M{HoceMl@F^7JVby54Sui!jsWWzR`0)yrreJ~8^wby4SN zU^r-im2J4Eo}dj9FmV6yul&(O1K=m4naxjg@1I9e(|HNi+>Ish_QPeCZ~EirnZ{Y%6@B-`{BQUXv~VDP|CV;U8G(rG#*KF7BXv$=-u ze4WU*8Vg;F#7SZkVz;d)I_@s#K0OUw(!~eXs9w+?K#FIOCL}9(lMXKu;4F6Ef(IQr z3Et|a0i>#OmW<$jsN#K9C22)biasL6mg7TOz$#+HKjJ#=v!*#p~;o7;S9R-nfIdHv7Wu32CPF!o#u+eaO_r|?VV&wOIL9c znbv({UVoEY`o`c1qIY1w_3thYUXd-8UKOU1n1waw3vsIlE!7M-}%b!Y;6<;x-Mr`FWq7RU@RjRT~jJBYwn1e-meo z$qWK+T#G+zztUhokKJDh_28({0;gg8PmBs=3Gh7}2lhA4`%MGi3p>O_YcQ}+LoSZr zyk}2|)cH6ugwp`*qR42pCHn{^$5KU*K}#1o-vuqED(+mqoJ_*8$C?Pa6o4or7$ zR_CzcECA+9m+a4plVAylEQ&~AP-T$dFLZ(iq@%r4I*3|Bxz1u&gOOOm|IFRfemvq~ zo8rdb7#BIoPC;I^arfP>tn2g5@!`vS_Mk7gGN9<&G7RLIDaA;N=6}FK#<8>M`S1FN z;++CEkky76a%qOS<5DO^-y6Ri6rNhZer$Au=0}$mlmi<|AU%9d zjjuWD^&cooT1Pfl&m_&Pe|nlN2G{Q&InRH^l>kEr{gDc$fsleX*ub1t zy~Fsp!Z7(63UZ0#DQ*I%ID~@07h$^6k2`NyKNZ;vKmBs>0P8>p3|~E_#?kJ|GhOi? z@bdsK<3F<|vk7|4 zz4F{N5E`aU-p51HcTJ0o=^R%5MUb=ijbKt2zOQ`Og_<;c3e>46e%-Adc_jESFgN|h z#+iEkE#w@EsID&mVfWJ?YW;MFQ4)uiW+J3NnD0`zv5(J24dB3pf&LC6&_Xjq%CwCi zd5spQc{))&Nd-`5|2Eo1GeRoGFj60t>>`dH7zjJJ$suN9a(Z=8@JUyZv?Lc(hu=MiyvFZ0RReqaDsSeim64T@9xhP*KSoG4^)o=19>^&?DP4}^(fS3{?Gyi5EQgj3`xI`88> zdbN9fz}@S8fx7vg?ON86Q5$TR^fn1eXh#O(8D8$RoaaUg=r@M;1Z6ikFYarjEPI>M zdJ@?)1^OUo{v8!6{J>DPP~8iZg)Du;_r#9*s#2XVSJR&UL?s|KZk(s_XbjlJlEc&7 zvBf&K&+|#r??b86s3iXtyAQyQ=hXra~C_85Y;{fm{f|9re0fMCVw#OL<1Rzvi zg=tS`toKat+uC@g@D*on$&qNa>VMtsqV&(YO5Y58emb~&FLHNwOnh?p2```3rl4ac zM$cSA;!(-`TLOMBLT>F@%w%`By*k;?`+9gK*-s0bgr*fG-kwn)2#`_wa}@(orQc#> zoih&_`54VZJ(#s)8DY+S4E!;SlQ!voIKuj}qSF9FbB1#3mGv^Cm-qW(#I^8(0~NNy zXoP|^~9uNVq@oYE!Y?36Ua3@yx$xudszh?B3b&uh~PJ7B?Q%R0tH>n0ux%jZ9 zs#Mf?)a7E{`cRN;zPysW&9)P_-(TB2rQ|XFaa}Z6YPP!3gu?kTfthDy!DyTET*OCL za(Zy*?)j5pXWr}xLOA3n!sic3tH*?C2X4as6QXD{+f1e zPnU96a=y+4RV|iQ3CQ*;yjh6&xC>^Qtt|Ss)L?6^c&?Ien>!@asCD0p$@P#vtUuvY zD_X?!hsfJ!vO#OH@4x~@p+rLZOJ4n@%!)%S?dJo{52(6S)m5HhlHM73m^~9o^$Ust z9Z(TpscIfY%%XG*T6a7+o%s7?e@pLLthgedqG96Lnm_*{WaXa!mOm~KuR*+OJ+Z!v z4$ez{VErer1fDj>eWuUo+z>)lk4NNM$#`8P-%l?nK^=uT5*8>mIHHQtU>br$P31HA zy@xt8_^8$=eOnrk5Hq_sH=NVW`>@Ay^s=QPGQjEL142Vn_Ly{}(4i+q<|X%wa# z*xc799&=M)4)RrLmD}0!2hH&25zaWSpmgPwRBamC1D}~Qbk$kcex%MmGZJZ1wBXRB zp%1v|kt<_5tIlc{+>7e`3T6#>EeZ=ggg{wR!pazOHDQP=bsw33hzaT-`CK9AZIYH9 zGQX0{Xn`UO5{^SqU`z6~T=FizS&WSJ;^ilnO4&)NK6#C29IdszQkx77bQmO(&_1tg zNB(>JD)KxU+ElL+{{1CI(_I<^-G%u3qBo4^uZBP$^v(4h= zyXTM2X+u4Jh+p?QcOBw$F9+~Gzo_spVmv_@+@nl=bNr*YQ%PKbaRHOxd+($8*GefK z_e$U^X-uo3fdlQTgb}-{SyVF$KBWV;dbBV)!C51!m~Vu%eDD5jk2>e%7!ylp z$h$A)Hb>d1p_{GVub8-jf7HZzd7vJ)j=K1y4Ck2Wj3=d53lpkrjmj+10URN!H3tr{28m(q40I2dl?YS+52v>t4k8Hwd{;2B9m_JTYz` zr{t<$iPriyW0Cn#p7khLC#E3d761Hj)gzoFalgyiXxEXRZG8K z-@0KA`dcROlr-WTM4+>0O_wT#QP_NW&f-Uap zEUaeG5#t+od-kd+vT9AtVQOnOHc@Qo` z1geO^zlL+V5>$RHH)I;c07kOgG^M4)&{pfw&6!j6{ctB~5h=avB>XrYXnPE4KtyCh zxJwpQ*oWV#oW4N9u4|^Ry8p<~CQ*)K!y*-Usi*!(m=q3I_mXi9=Woh_N+Xs^QISrT zOMVAwMm_i1zq@&GYUERHIIH@ro|vh&DtPHel`l&nE*Gp!En(h=t7ccmytYl2KzGc9={Egb zFQRSg_b8ilVwhVurXU5vqZAk)#C=8DFqQ2sicdP<;xJ4{K2wcu;KClgK+4~j{7Kf3 z zGW&gHh#fA#W1^^*-UY1WFc<@3Vnh;#j0jht!>dqt;6gXtf6UoQRYrNj}f*tu#1XsZ~=&Ci)mGqz)2BA?b$L->Sx@C;IiLLI}pVS z!Uhqg@V!CQ!>FdV)sK`gq4AFA#|ER?f(~+?{A=`{A@JfJ0Mpk3)J6~^>G_!KktccD zo&xz$MSz;>0vbA@e%oJRE%TM?!Kbx(t2!Q=*8F-2XTb&2YxnWrs&D>(_y^B^ZQUqc zsT!2!Y|!y+_18ZP;|wfpI`|hiRT7>|F}dc#j?(V?Mghm<{0?+i6@T>1xV4MK-K^d? z{_ccg1||2u(}%^Y6)*4a@P9X*FZlbY21eSiWRlOV0M!{*_m~qWSYvKF_}N|mB2DRx z4H8;!>aWuuKj%3b+Bnd=^Y3%r=Z)jqT;J`udw@+??Fym`%Sx~fQ+z;3tbE8=6ML$( zPvoN*C03UN@pI6H03+O_St{`plR;_nG8|Plup#|o2lp{#O`pW~zs`vSiyYo`+FC*Y z;0q$bt%gRQu=H$8ptv3X;}QQM_;s_YZ~K3-7}}6}vL6;mhH~T{LMU*ps}xZzH*CDU z2EL<>YXbBIR=Q}^sPmCi7EO8s_&s7C5o(TE%Nc?9R>}af;ZX^y=nWA`*W~G-Vt4k4 zfeek8dsto&0E(8eJqV4wH8$T}%kr((P>A)R@#pE1x%Cf@mD@F>&1k=oUwXECnT!oj zE!udg;@=;U!3ERinbThcEp7Ztu}kyPM{m^ zd;RHKpf`o11}gH8Ro|MsV)MYA^w6nW8jx@Z+XLFC=_;&KP5!ng-|UQDX`ppk)AMr~ z4S_Ph)~_6lhGFTnH`S$DK%Jea^ZY3pJ{>M{M~c+q`Xrj!{jf2`wI2L3Hf+)cyKK`Pk5$sY+Oh~+V`$`xyOXsxXV+YB{S@Zv+&olIal{E zrH#+xw=OKlU&|e8LV{cIy^ibEPuTk<_9TLaY8wo9AdgcS@ru!8g82 zzWT`;6nSe9X;e%Jm>mgM0^WoAy_7vyjkPM7!E>DFX%`X=*4s2POd@Nf!Be3m*Qmz3 z#Xsyg)r7SoVb`m0kKt`RHTZhVdi;*h3MpKbd*2#wSNjn(?dTKqBg?Ir6T+Q+GSLCK zvj+w{+mj1S#-B0U;CF}cYxoX2ddMGK?{n4Q+k%c+I##wid5S521C+yOl|E*v+(VzS zvP!rn#XB@K2Dc!CalI#4s_NLkHMH1_xcirI4Q*tISU##LAuK2(oSq$rQG2fcjZH`HNpT-gtWa^KRto&fbNiF4;S1iSDa0y0#9ovzq7X?sXY-50{ApDI)!9Uo z?DWuq%$Xzj4sXT5VUu77iUVzphO#yo)IIimO_mbo6V?%LK9-tmgH|h0Juf|;ILs=H zJ>|~$NR`LrHX)o-0$Z)|pbwIzC7tIuNqj{#y%hVgI9kbZq(;4$#2iB=hPJV_aq1-l z0zRHweO@`mC9nh6_;$S7b%8_*;o_R`%yd_N^_jL4R2Wumh92R+U^i8~el7L14(l;f z4ExsiC|l`w@fQ~e*(<}?8=7wzW{Qj{A2G6_Z`&V|U2qOOyveP~ULw#&3AGgvQeR=F zq^Efg_eNXBF033kQ+~6mpp}HY?lEubRTUtiec3u z>%}vkPZY0LMq{bPFrUE4l>gQ^iw5Y)#7nfgJP-fA1-l)jrT@GL5-6B75hSvsu~D>53B;4dgQN=46OpC?g=8QgHj^?2RFzoi zUl|h-YlJVme!ORdcynO^wC%sxQYuAF!|YZW-pZt7FJEbmlfHTa3pY~9rloeNjRCPg zP`H+S%FB*X2NiM&D;ZFyj3yj{qlsxj9Qm7Rbg|?>T5#QU{&^-_kpF(2Zr2qZeaXd_ zZMs$9ODHqu#@W=!_?(j;?!!}wVbQIn&6sI+QYG{78UQy))~0*c!{ThcsxTU6%|EcZx3sYfv(<#|`G>@d4@7E^ zShfue5m!AgMivacz3q8wgicS@fl960Wq|>Y@WHy3S{E(y?nf(-Y>OV;d-&oZwE_;4 z$D03T1x9wVfrjIaUaSU>h1U`V%e4>5)Y*F&aD?x>|7@4#AvQup$HLx^Q-{~;smXGo z?^RMG&jq`avJqGMh~QgBccP~10{C)MRboxGv0AmzuN}7*8^Q~8)N#ss_}=u)tF7Ig zFish(?+98L^haRmG2*%a(Ol0q5J>b~nMlt5G$g- zpHl^yby5tnq8$VHQ>;G)|I8oK!rU%c+_1kK3+XosW}oVQ9KSUjE*-<>Y4dDo&2uHNUocHAUF} z(nD!*%DeZ7KMayBg4>hzkm+oKHyYb?_tmxrixp@?1LUIJC6!;+zQRso<-UJkvd03! zM1B1$Dl%tx5mZf|7{2ds&c319^Y(CdBOpx=22Pi#e(mtPp{ueu#)uYz1~vt3PT11U zCT#m4Ie7S*3L7|@h?ENyLENJgRyJXZ+?+^GN}i6j8FdMlJF?IlS0o^uXZ zO*p2Q;t)z1{s6I!Raw1UA{7?aE^ea0IKo%hnN^tOV?Y`3p!hat17#mUmc#NHSnSG+ zu=vv|)-c@6c|JDgWJsnUmp?_fs&~k@YX&Tl^gR1I&BNH+>q=J0gGS9U1-3Jl>j{gI z`3imWSvbllHvD>&IdGtSdBn)|XH_^X%37J%b=IDyaP*Jbrk54xWpNB0UY8};NDq6Z zHiF6z#tq{!TFv|TB(_hmIxGC|1AKz!deJ=U(3b8t?syD{xgVk+LiPQQhs*m=jO+-8 z{iV466Ti8l3$?;gmWvjR95O$hQf0QUGiB~%l|d>S{u!~b*tCF>2%+TkD!mV{P=8yi z*qb?tM-;Q)rKEEIYBGI`Vs3V}#KJBaxmtuoS4%#=-+IvR*rvcd09Jvht%_A^Wn~R=|^gocF~ly)jA4 zVR4&Rc%HUl{YBLM3I`U4k*eR(*SJ>iYSFHC`bS#%`*GvS_hEl2`<=hREMkGORa9+? zbdST$qaS2D(qAAYDy%t`BzQxkr1llYq=S=E#-J*$qvJ!}@_P$pawcm1%qMsLg}R2` zCOy{fZ~P=2{l-)XX0UpxlN{3w-_R7vCMLC9^0~KdYDL9!R9kWz{Q0~uQ@U`M$GIFAmI0DZvu!JDz1DbQ7H(JS2@hYAQTA|w9{tevXU0aQx49on z58JD$+*E%>$-zFN3>{yuJN^cK=c$VEUo}B)v!5eXCda*p1X%<1-+NYv-aRca{QK}v zQkMVEh+LJxfnV@|=w-z4Zzu{3p=m`&%Ysj}rAOux*TDC}A58|azYVtjOz}LRg)WHc zbzN%AFDmkmpQML<5VI`KiQW}wlsEDjNIA7{^xlH(pX?;?d7as^hTy-nD@zt%0YrH( zHAS~V_Mi{CwfQ=CQaI!HEgXmlIo<1e5e%&(fh((#S9z=l@=oI?lC$8){qfu|on5P= z?wc5j*jm`NIwfCsTEtiG_Np7UBQeIY$KW9#=Uc}9l#B++k1)@a3r0W{OLE65Ufj_? z6dvHZ)29LdqyOlj?9&b>Gr)m0Lgb6SEFkKO-d^1N=wD!+0tYFXWrp1zFi@}&(A_Gi z?aTTgZW!Z{750WZqn$CR$-eTuODKhGrHCj#`Od0}Itet9IGb1VrViD>C-@dea>tGE zh*)E0`f|B=fljH8@{p>B&dx|T12pw<(cT`N5nh58%7JNEoO0cGeV|0PsAbb<);Lv{ zhWw+YT~3}#$;eK0kJnj+H$s^x-o(?HE-m|YBU9Uh^S@y`h+~Z(E&HXhFr3BMZRMT^ z;%W_FdIQZ;?)9_|AiPI#);XaL_#^A145MKtQmILdvL1Gnd}pOq4fokeg5<1IcG5bK zC_Y$Ts4udK3COoiwGWRvakh5P(a6wu?eHjUu%i1p0{)N<%rM@)WTJkO)xo@`P#Kek zD<8Sx*81qqJf)f|cJug?dgqe)dCy)-m0sZiOl8$n;hJ**sSYpcT`A%Nh>xS-=`=Cq z+onR1l`aIF#q)}3n6`QNE%e;aW8q|V_;_8e*!H2&84D^ASSGSNwOr6ex_OlR7|{2`x; z_FIh&eThm48@_PdxC6Pqj6LJM9u?_4mmmXfZJ5YW=;Sr)n(&qWl`8ow$6I!zbG>mf zw^C~2jK$q}P*>ArNtTajKW~FSF(?;VU&gf(UYPyyv2Y9AVOhx8SP&^4*unB#U?XgXZsHW++1FOX zz8cAJNWO|3?IVpYUkg$q84@x^1Q}@RT4*lQYAEQD(1*@dk{*}i)GOV7@sc6yEEG1I zZ&gGorxjrm)$TnK>A7*K3E}Vnbw8m7G>UG$AWLhJqvm|OwC%z7Ga_Z=Q=RJ1k#UVV zJK6_p+8sRd!P*FtK87K0UuUdqXpFKyR2ApUzzpEXI-cB~+w_kRk6rL-SyC@iV?D7Q zgt^KcaTDFknD?3ol5(mXB5{w3kz5>2F>r*Au4QDu_K(SR>iLqVQO_ zHI2&VR#Geo>C_XY|9_b$9x`L0ojQkX{vtYVBf9Z=3+mSHM7&W6SX6xvn4{4~H8 zv!h%iR0+{`bY6BfKlKbL?1cs{y~}s*y0y7et@b~P&N~q5|BvIJyR*renVp@Lk-hJ1 zkxfEoM44HccV~|hihQ$)jEHQCBb1cAS7h&b9LJrX-~XRKKkxVZ^?JS@&*!U{AvO+m zNU$&XZE~3!M&DEoXQB(h62xS(<7i!z{Rr-JB=oy{7Bs$mW>Gy*{EpO^tZCP?g zO5d2~-ZHwnI`g?QfO^Rq1B=uv_Wj|6M@VM_*yTe-U}HU6H(nAzI-j3>_&pF?D8kH^ z6vhk`{mV~X`rsRMgFc99837w7Eqn$plG`+~6n+8Lz)$knZj#6Bk$b>CEV(g3&=&T7 zDVq1fW0@Q{l>Hryrw*q0YwGp%K5A4J8=a>+0}1j?WyT0LnZrMNdXY2J2&7(k@KU7` zXh8k-+RtKfTWs_&rw{3+>%q^TsE;_gKRb#LR^T8$(;7$8$i!8yar08ig_X5KUmHx# zaM-iyp&_z4*llIaCIE!jZt-|`B_jcv+03r4GZAl7T+5>H0F@Acz-NP#=IbG4Np zvvps`EyM8K-*c$2qF%&*YMPB3=eqsNc_`3CQ}JN*_OR~oMd}4^H-X5 z&_E?QGLJ!j*$>SyNaeV3;<^S=>Zt2S(RNs+h-b`S3pFj&x#ryHLP)9!YNT0Pz}kA^ zJER#tsJd^sa3MbOUP@y?-OoMZ>3#AMy!(um4Ht6i?S=ULov)HDS4%5I>bcL;Ykq?3 zgT_0mT#PMk0nw43{aBmOpita<;kU$Js-=RsocDhNpM~PeN$cXL$Rmfg>+}yJozcbc zodXvI9gBAE2=m|j%bG6!xDT2#l0XdhLZC+{NwUN34cAH?YZ6tc+VqZbc&V8d#gFiP zY)A~cYB&GckXTJe2RUh*W-ZO$bWT;;8DNLlD2mf!8Ft zIi9lI6LhiK;exe_nhM~c7!t@FR`n2(cx4H_drb5!57kk2h)lW&v;STxyyMM7sP8$ zqAIhOLWG71L(w`UT{9i+Ir5@o?Hk)4>zq_wo`#jh_(U~Rf3;Uy8(&4$%)}>$M{YT^ zeRxNdnb|p(3xY#0RBQ;NVFT*;65_{Y_uj7?P0*@H%*88E*KWC8_p4Dysk-B_m4UYu zfX}wWck&(#zhQjePmG8$XW!f9>#ENg;Nri|=2E}`!;BEN@a*&W@`QZQLo0683@g~5 zI-nyScXpqRh76D;F=1C*kCJp?p97b~eCi&l_q`9TY&5)o+>KC`N5i6T#_6!q?MhyA zihf>crAjmwhf=-KNmh!G+@rs(r09)rZoB~N#Ga^P2?gD2znhBXeAZa$VdZCrpYG+^a zJga)QVl0ZP0kllQb^BoM|Grk(EDRn^^Z9}ba^T)iR5RLTcXb*>qD+3@)&!KTux`k;1bC4`oXo>l-W?@@%AG5t$UvOblLi<&`PS z&-Ir?SG1HUR)#I3%Z^@@A4LxZ6mP=)s`d#I0U{-^Tf4(zeGw&V8l{)i5<$W33auCSIFlk>qw$WARSrx;VpZ= z7OKLIaPRu)>IrsBrwr3;JyAOavq9fC=zLdhib;Ro(~a-Ilc<7#MwR|!hZMQT1Rnet zp-B3f^OHrbxYS#Ug(uP`$0G+gAl0U8sEqmh&DG%-24BhCDIY5KOD; zIxEM9S+q(xLT>;QR2wE>Ac>mUK^08_aFS}M-|;|@f;*IGufw9RYSz9iY|wcd>Xv1cEg_Pnru3@2B!Bnlc0?vUQqiC^l6+cz$yfssFp5W%KIJtYJ)K{=KN zC^{azB25`NU4HhD+Ag{XUE(E_ufHh2v*|6SeENcN=h`0dasrxao~Z13Ple(`tsS9S z3IKY5a*Ua9FeTtHMOC*pT(f<7_ih>d%umc24Tda%0`n(pw@tu^bYd#8OtK}k+j^0? z{D%0ipv8gG=~jn}CIF!&pqFwv!q*9mTNl znq>IWE%acw1+xEV@9V<@-6qH5d*bP5yepKfdVrwK< z+4;gQ$s(O9a3Q}J5_Sp;-)qXe^+Pa@aLTEo@~!^Tz8la-`2ZBzpaLz^s?=59=|T{q zF~FgNwklIsMMv#d^rXbmS#-8qusLeAwGm4>XRVKSCOu3C-y_>-Wv^IJU;5b# z!D`(Stws3+$uV|WOEX}j^OP_8kNWiaZ37dzip#pTyi^InbC75I;?CezZ|wd-gLh@1S6rSuv zjcFM+Ux*AL6V*b@MBSLUih>I)e#{<>c-$CQ68V2LdkMGf%o_q9l~RGEbh9AIrN>yb~b6p&XJd4%))cclD%Wv zNr0!Sg27_rg<*dZ0U#T+*a@|o+xJi6?$&LEMwB+c(89!OlU8k8893C6N03XRvC^Ku z+N4%IeT{b4U(=eCd&UygPyq(VZBcpZMLv1LkDUjn4^`a+gYt9d0)zZ;(}``5_+%Ow zpCP5FBXn$1m<)-&eL7G?ZhF(YyUF1?ZRd|GUwUrX9Sn<%Cc={cAQ&;+6yNiW+!0iu z+6B9=!zY-@__6OrL|n2YhiCy2-Z{n!K0*-*AafPL5S;9Fy8?#B=jnVPZh^STD#O67 zgQv7Oijj*uPE00CC1(f_;x2ckKo~GNJ1Z)C<{y27wCwlb-ALZO zj14!XE$F&~SNuOYxw2c)aCOtWyypHAW8@{upET^vr%Q<>kV2nT3!3v2BW%P2rn6AM$xDbCa_; z3X}{EUG?U=040IM(hZgq-7tqDIF7riVLgf~8yz#F*I#15jLkGpt&S_QI561b1zZd> z({g(uiB<+&y*Ev+enmW*R&2pE@MZ5Pa0(H=4j5(6rR=MTatMRbo_VzZone?TlFldr zDcMdCt8qN-(EhSBRMHad!C=Eiu24tt5As&U71MIgkfhX$)n8s?>gro0+HFv@3f}dx?T$-Cg@G^)IX3~1|b8) zB)K0y=-7V~VJ$;Rf1O3DHhAekCGeM6q^5Jb9aJ2;?A>!Rc)1xGMoqU!%>(8YLR`r zfkGp4yb=>|^(9HP|NQbq4(d%c@ndr17-Rb*!Nm?$(e%Xjw&Yx-h0=uKmRc;)^noqA zmr1T^b&I>;%*pT+s;uNB%KViStcL9;x!Qjj_yxC@Cig>9AAk^YSy%m>K2K*DS=kho ztB}*FC%!r`1t;I@&GQ9bRISF%Ir*aa^})ft!a0NKNHE$07K1gPGsts%w$7%1H6YEX z%zfy_P+G8Njjfv?KQh3p0*Gy4+)aR=K#K?A7Pt;jlW6&55JneX-GIgN`L zw^;=;6YpT#xa(+yz@GP_z5TAK9sJ)x%sEjSMJCyPFD)?AYLqMHzmH*pp2$$ZI^M#fcDcX|<0gZ?6&_*gjjUMc(pZ7*uX5G`$J9Z6 z9N+Gds`tRwFfabbkI99wHHn3=A6x@cUyJ2-_?TEzK^S4PTVQ6&k1-|Re^c({^QjzU z_%lWD?KHrhekFdV^eroL*F>OzZKN}3`RR-tFIG7^e(NCut9U)kW_h$cNN*vRXYry{ zNF#XFOpyD=;V8l;E?R{(eJxiN*Tv%?3}~azzHw(na_G0JncNp*p#Zpq0X>ugOp`+=&rv~po;U+eL56FLS2P#l_iOR0i`By zYOR9T#uOKQo~oh`e)9>M-Hc5jFQM-&iq_G3b0JO-r1mi$cKjB~65D9CNmBD90UV<> zU_dzQSSax6`2&Kxc}Pixg?^xZhS(N|NpgmCsrm8d=A$*UvL z7!a2E4K{nie96{Nf**?~)jR|t-PsPmGK#1&JHvp#r%hca@W)p`j|7z&oomK6 zZdc@)>MF6x19?RH&*_*%;^BO;;T0mOI+Q6FGEr{U1AKzL*u}+ODx~W=A#wrPeT#n6 zg*1Jv<*Fu?hUoP~M;siYf$d@gA-15+A(|sHdLi}rlATQLh^Q7wxE)L8@oc+lDsuN{ zLH&uO728UpBsY8~(Jp84)&isu*9b><8{qsUdtdpilOSMM^h3A}5}Km-5DFQR?qJ7} zL2dQ;q3M6}QPsB-s_!3^yKj?Fl%ZbGax*G^4f)Om&!(ZnDBbHvb)%U#zf{|Q>&WQL zmqdtvB$YdE`{F~bxqB}07WtBDu6+(WFDLi3DmGOxr|?3O#B;pOQ?IX@n{`y@a)iW0cv*rrAE@HpKV z##D9X73sG>;=>_F;*t04Hf8Oc+kA=$+1SAw{Bg3~lTkfJ3`3~pLD|0Ps*@gF?QQhu zSG^yG^WKS1JAEP_nz*aac3EP|B?2dbGGDZGI1Ku4ioGHts=;WUVQ35a5ip6jTmSLG z77*H6_;ePL6Eo*_lG(0%-D6RI#O1O`Ur3N7k&Kc{7;!aAc43Ra=&Mh;)xdqOZ{7P? z$tInsyMe5aybCQs@K?yXGwl=6Cz>G^V2j)D;A52}56|gdfSo!;eDe2;#J~6P^&OYP z)dot`<%C?4>6aoq^I4WfOH&TM0IcGKN80)iz6;ITOh-E&K2c`zgQt) zq&`v``~xjT`6q{&hB>!Pl4NfEf>XhnW^rqTr{9fns82B`^~9dnw(K~A(W*1`^-tla ztvSpK5qsMWgW)Zw(-Mkzj{jI?b+-RwQwv;$KRW*7f!d^I2q3b0F|rC(aydQI}td6mQ&Z;xHD+p0?{ul*{=tB&coi<Yp^`43mDC{aa&h?CjE|Y$SF{ z`SO3bqpK;$7XH1aQhzObdo_HePBx{18oyl0AL&%(F8Qw{JF%LC5Vi+fcQ^9kPPks8hjy zc@6Z0nt@|Cf3eGN{!LMb+V7{2--2XpevBu}9)!WR9J%i9rAae|HQz*eeAyGAe!=Pl z1v|Vx67pZkOoxO&KHuJ|J$(*#EzmwDaWD!SyUw9i zDLfIv0=xg{an#B6h z_0hexI2X=nXtes(`ZMA%8W+NYeMV*NYC>q)6COw@@B1>5!Ja_%KKpS+ep>wIXz%gG zcK&78$u;T#+kA_`{41AmisnFSwBxW`m1U^2NuX;ic$>Y|9Irws+5PzIz~QLwk+!f| z;n9+dj__N*qVt7+g6I?x?v7> za)oq^^Of@N-OWdz|50u(3{Kn4)~RnR&p-AE8<_p{f(O5TEr8@s1%~?J-j~;Mm7cDR zea`tJUn(~1tP7#eG&)_B6)HzG zByI~2fzA9mlz7(FFyz06yvTC+cZ4XgxD83JWhtE8D!6YixW(9`Jr{{AK4XR`2;lW( zxyvrE1yZ5AGkD9Tfibe1t}h=^Jx^d#5>MQZs?*cl`|a=KT^$x>o7o+zKnhrj(Ki?| zGwAI(arcinXoIIx$%-ebla$SbdouL|4(0!=Md8*RKq3_5S}tr+rUndI`P+E{o{QN@ z_vVY;r8L$)-<%Wq0G~9t6r1XM^{JH& z9A0wRcDQZW$?XqW5{Go;)4gC0J<}#uTCBO0S6@ZK3R}u%XlZ`A_S?5`XrT<&{xEp-}!jsJ-F-sav(ybjGN9 z-q4HRsJzu%tgLDU?A7!!@nH`y1@sta()F5Zk3LWgLeN3{LHGDcKyfVBm#urvJ z1l)=fjL8%DLxz(L{oqWlG2sf7oxA=jsuql~Bc!RFT%5qw`3O)dvPtGDs|fRqm!Szv z@lqDYABd28kk8q4KA@FvM4@C>Kbn8e$FVc`W;^eEIm_*;48$2Sp{m*LA(cMv@XXsK z8-blv#htV|ghzA%RhfV8U3|VNvg6pEqT-HeIyOjm1rh`A^#6%J#WRH>vDK$5HMzXa zk~?Zgx#~oJyQMnq#+_dauiAys5{3)>BL^0X@)|>SS2<$+^;{Hia}b8zKqKuUj^TTT zy83s2n=gmKCI!+|wwx7E^(qZEVrQNnvEpBfyNgzugLD6aS6&_!?%&*{3O= zcPO7}QzpMiK;-Vf6mao6P(~C;1`8HnyzhIj?a)ElVg;n)$=m%4Pk;&dN&Nq>UK?tl z``PR9yVZds5vdS>CPCJ$dc^qZkgOXxz;aD${>=Y}h&1A%L0sqY>CQOvQ7TZA7Pgz1 z#H9R4HL}K7b|d83fDc@5r^Kwy>RU_VhNEbmLVZsx7Sil7aB3aPx_R4c0$p^ zxRoMD8+H>SD*5l#8}rB!X4gBkmP~1VX!5f32hDr4di3V$4{bM7Xg`Ai<*zn8gyxhp ze=WUYwnY;Z7-M3mJD8MkF9&jmqt~8hO=qwxBXggDP|-*dSXff@;4@ z_4L48=Zkl@w8E}(nLRHYoLLda`Ch2c|K3k~JXVwY%F&t<(D!#(p#^stbLu+Ve`Ds! z+Tc;(*RTPNZP{YR5r=8+8y7Mc@(6s%kpL?KXGf?yzk25p!A9!bg_$ZmxfT{%;kD<1 zsmz!nK|qL1lBOBU7?FV|BPfe)NE$_+Z3z(cm^tVGtp8T#Xv&NLk|@GTa-a;A)DF?; zJJsH#EBLeOC`rO(UJ@A<$r~93GK&S>AZl4GI+a^g=vDYt1ai+-bX0z-`0Q%W^2LQP z>R^9`&7>il5Ns}d#pCi>*8#L?e065`dZnHp64Ojk9s+E-9b;bE1w`6d!nm0)@>Gvz zVv#P(K~GR*3xrI}+|XLs)pzbrSjTGWeYZy_#)T`cD@tUCd{vX*gN^T=>llV0?VJkn zv|6{sN%;@eUvQrW4(>Ykw)ObAzAK2YghpZly~Q8ySgBL-Z-e9egi+~AfGv!9*j@$r zuTys^e?NKbD1L_|>+E1TcaONm?h@+w)~i7Ww`v+oS}{|sRxJk$SQ)D9oBe=xp@gw#c^fq4mKmWA8-Gi99ceD@ z;DiWb1RAHNRfvz-wg9?Zv@4Dqs5$pphF1Xqwjs$^1iE4m0$`hJsnT%Kf_z2f&d#v0 zIA7S3q_Slki2-xpJ448|4h`kU)=Kw&G)ar(dit7t=TGg`C-3THKiC%<8>Zr>>B6`I zDpD2fJ+P%%QCiW+(}$4C*gyg!=Yy!u_E-&YCLlXo2Y454=R#Kw_hKvLU(qGle}-yC z@|Q#Th3j>Y#l3878zHBkPP)!!k}ELI?tSXdgh*)?f4!MWaYY2QfbOob?EHLKTzK49 zz8@mnt!v2+3m4NnNscY04)xsmVLd6C34v zAhaP@@iuLOI#;_-TRBKCY0hKG%DJayE;?#|<=NsHvB&j7e1MI!Z~gJ3bvImI{(Cqy zS~Q$$^n!*p%=2yv#v!wH(Aq(|Z2w){oG5In<8|`!6vd#HN>NYGh4(8>{P5+v@XuMI zHa7MNgQ4 zAH=(!PugF!7oB3r?iTl#Q=hF^p%UA1-byp(XVWu;HthBq_}d2T`< zPO8OX{jJxdxq8sX%n%EdlUATk4^%z%t%k$IbF#4;{!5h-qxC1b&lOyG)YA}4d(J(2 z2Szrn=7*~@2K)EmP=&a*b?pQgn0`;5Z1XXTJdAM+k^khuFviNkb6#1{wO*kwu=+Jt13t%NgJ| z8y9s=Ay0rDbxxefD4C5Sl_rQgb;E6FOLx=Zm{l~6UCUm+GD555bT?YY>c@M?o{*K< zoAO-lR2>G2c>Ensob3ocVoSmv{ss(kw2tz^m@)71?(=2xsZcsX+B z#Uizw5T@!+Uv1V!`sCWV;{zWEa}zq^y({rD{yvq~r<@X63lIG9KfFGkydmCK$WXVm zjjND;`0@17Qw6ZMAOwPR=@9rR)@`AkY@q7N^yiiotzUwJsB*HwZ`$^*^+2x=FxJoj zkLk#lzj3+w%Dzt4PPF>XSKoKkO4i*@b9JAMe!IW!xT8uwVlee`;-%C_u6l6C>$js{ zJA@O}{+kJ5SYf40L%i}{Wu=|jw>TDCOcwu;?T~Vce6sm*Qb$gcK)J{F&RY$3Cc>Sd z_R3o3%2pF$2<-^7gk)VIKpm*M&sb4oQxsWG+#{zLq~DiWDShF7}+qLVCD} z`@*iD6W}+NY)o7^V#HthaI1wYxLOPp;NzBk6%LO#CrRWVdZ1T2^8aZ@A@x_|Z@hM{ z6MGX{wDR04{L4&ga=S3slX~9%+n9{-c3a9?dlB00*OmgZm$%N84Z|$&)8~m-oYZDm z+uqY>6vFAfMeAv5jnkA==)IOX^6zjFshYBGyfUkq(>ZUs7yaPu&#}NG_uj!G(?Q|6 zx<|k2_AAMOv-M#?$t!!Q5A-m;F?hF~&J>8rpd34T-%UL(+2 z(}F@Ymp!S54q%-AUyYO2``MNQ@wgPA8yXnLL z?K8rWT$rmB9c3Ug%*0?)@2aIZh&42AH%XD-K zz-(oD7d-kq^t4a>Ofebu4OlBR{gxN{+2tDJ@v^v#5^O4i|M{>ySxdfcUaDrxoUX$lNYlb~$2@s(Bp#}?fE@XuZr)?gzo4*$VR?RA%z zkCC@B&+Wz`9VJ@f1#A``6#3+N>I9eU=X0u;1Cxe3yZ$0;lJzps`}fe-KR zRf7SuU~KeB67sU16TC%&wl`5L>h4YGdbA^wt{W5+`Eloz6hH&1&{&a_DA2*L&vI$z zff8W!G3%s}-m0`EP|#Tp=K4blBb(uCs=}H^Z_4YH< z3ZzwEZf!2p@#nJP_*BpXT>1yl2x$M6TuyTEkMS1jz5J_%vk&7=lMvelTZukCTlo7S zA9jrASvfHdrldY?vW%!Ht6#P6xf7;k4rcW1PIs))ceUMScPvPJta-_~Dc-<+)4G@ZKcbSA!sI$k~3{2sa}Ij-vZb8hR(hJ2*j6gIHpp5kxXuh2;MMpO07l4Mm7MAQOJCA8fPW~rnWh5R zWY0Uh=S?o<1sg*F)`g=V-4pU@*Zb3kjkeVm|2<*#ZEt7tgUYqFyDUEky2xHX!e)9Z zz54<~dtE^0aj=fgzc4iUEMlSk)!98ipm(EbOoW?o*=0IxYF*yDdIy4$>d;B_EQaF% z&y1)P8`>TRZ7+tw&Y`gbmh-e1{mRY<+wsKf z8JgONTEP05CkTvace_n|bn;@gJV&AX1w&ByQ>h9G6Zo8~DNP5ozmx5vXLw}2uG!T( zvX2I;>~)FIHo9>+L&@RF{LH&Qr1)8LSaM3w+)z`B(jp$jW}kT{WVfXIeG0qLeQ$_(EoD@N>the4=y5 zgCyh=&*N<>;IV5r=4BJUiBsP3o6gAsmOb*z`NNvv<>8*}-lF7c@tNQ;2s-PWF0_~( z2W0}VcC)sirP5~|JV*|LhNvm~&g53@TwkP%QkjZ^)?Ez|&DeWA1|5k~A{;tQYPk&Q z7YsnRNLx(r#GSAUyPzYIRVEIi#knp#Qw&AsC1fcrS8KM_|9Rxk`lII{qsGmlK7(E5 z@ypa;Aq{u^V6C$IE&$Ix{c0*ZQ9sC|rX?e?LEl0*HY*cX15utV0nyr59X~y)W`_6oflQ`28eW=ZQd+@eG$;IQr?-B|&uI zLQ&@4^clVM`HSN9r!rU8Nn-;;oD3mROSEt$as+wZChIM;IVrNF3nyQDf=8+?zFyZ+a7f~MXrA=wS0j4yZ57#u z3+&jQ`P;C8`#VaqPTK^*diy$=^YJOGXNyd{or#osm-9Y{){ov2O}%H9jXj_C7{OZU z|E4bV#eG{*qc|boQ|GVoe1Di+o$@zOMLKBG(< zEe(Nz?bqYns`VwZqZJBeh6lRyuWZO{8y+G_V(E3ukFC|2eN|W9|5>6}hI;tqMlV*HZZ<$?8t+ zIM2XV)CD8v>1nI*g_e5b_elY-Tb(m62g2N91m(9ZuhQbu4J2xxxvXT;Xv;kc6bO|` z`245Es~@#@yF2+t_vGZ&n=pgQ_?pH`t3Let6a(~dcZ36IuWk%iiLY$PrT{kITA_p< z`dm289sGtbLyq4bKlU&ngGOqa$PKG9FQ7=hUpnYxJTF~@yVcOUZ;4&DGDuF}DtGyx*N@1l05uY3_}le`$M@=~4bcqcfqE?fNQB zXr)Zac}``5dmg+%E_#EDvW7m7IWAeCJ`~XVTJvSl>ZafEz;%pcU*123!Jf@Khp9+u zUO*@VE1FWZuGqhu)gmZOn9B)qi;RYser)#d5}y5-tcRP1P8r!kx4$Exj-D6U?nHxW zdI&4o{iWT1wk)B!%wq(ZN=yKwOIyPbV-hr?iFHvmdbnR4&&Rm{-z)#sD%0i>za(lG zx^7x*uouicr5wo?eDA>}f~XiKqHrxSfGP1I2X_N`DDqW?~iM z5>aBFrnEf|6e{G>O1EO=9m^NiS7cXzMpZaa(-_>7kTj-Y?s5}SyrUsYXRtBO z?rqvc{-KPn6JhjJR{@0OjnRC=FPz464-I+@O9q1{YBByQwNkf^rA?%CmeFHBkfUyAC)3kTbQKng!JOnL{LjxRN zblc;XlC0^^Q6flDAcu4LzZ4vBhwPi;$JK1F#AutoUv_fdK!NuU!Y~~JpZ%y^f z%r=^Nfq?PHp7yRO-KsPVxUas2c{hFP8`Kj10v&}`NON-6RHa@Bd=PDF{LuaC17idx1mFWp{%bl-O(kIM$HVI0}}v7c@dZkJ@bP+x*My z>oNfz9!iQNv>8A`V1*moC;D^FzboE|ye{S}LsD`UsflEOER551VgqN{Fa93LpG?*m zdlPX*sRfc9sxZ$GV*~t+*C(0UYD3t{6J9Vd@B~B444{9hb^&H>9zEZIakv47O;8c_ zl`%dQiQ>$hm7RSyFf8`@f&#GKlUDPxB}Mn=^o%Fn(uwMKzxYssdgb{n&bxSr4`Km3 z@T9snC8xrT)Co=!i<+2SfIEUkfu@jLbRTscz58$gICxEpenAfu5Cs5i$^k`2%A?{? z^>fV%14qQrc##jf7QRs~^=WL&(mJK^k$pH!K?TFfZ5pUu-qfq9MN;O;&a?R4yj59jlJ{9HZedWvn2l`9^9D~tSDLs% zLG`4d3@T8*Lr%@envB!W(nHZT?BE}&5|c`^?-cQ~PRidv{Cb#zW>DMarga(@5Tx(%w4M+XirvlR24ie@XVs(?{;+ptH z-QTb?I;|V4(<7uTrv4>6?^$Px?ym>lO{>TN&H@BBoPcj|G+8!R8E4SRhzVT|IW!AGhc%x>p-fPa=6#8 zkB@B%Qye9051YZ)>n~^^xn%7sBZM0NxK~mxH~-uU&8{cIWdjAQj>7A}8{NoAf6Sa^ zM)g(1i_^b21riq!S`#rrWy02{b!Zl{rAlh6M7vZ;E3iRA%HCr$Z&~$hk$X*dE^NYx z2l{{8&~X|0kgmlmdxe-Jr9>OIsj0LY(jdY?i~hvJvA;(Q3zdu33BF(|=eV~m+sw%gL_%0lcKR$bFR?$ORoTOyT88+>bk0<`Yx_UPOai2 z-F{5=SUta?wNQ0EFmV7m-@@o=`#j-kNzpXP*Y1>@&)g7YCCN(Ix@jYE`Q?`F_b&8` zTHz03Eje5(V|crPhv1Jx0q+#&{!_O!d-hq0y44Trya4OGY^Ew5`e6)F>&Un@o%-(o zj?Gs!5y_as2356DTbgMaK&Kw-fBmSZZjdX)42iP3TzfD$q(lKNXh%-tMBTTLw9#*# zM9sazO;t@*9xN5*k=3-0+^xx@%aZKZ$&x@bcGf*b_E=s5nQL-kH{Qd+c=$Thq zv%%H^ZkjR;pZZc-W+#A{`v6vrp+FPH`+-vQmNG9|ySmstRB<<5KWQtEZR0LmF6}jL z#gLG?WKi;3TGtdiTtCH>&H}G91=gV)QdRGoDNvGHKc8<&iNMn@Uud6&tovOhHGgBd zo&fD1$-jdxhwYgW?$@Sm>C(V^;i93yqg}=>H8!UIMkp!xUQkq6dTo){+_woC=&_F* zXJ7axy=BzXM?M99JZ@n5CZpbN!Jj`>5Q>v;JOSt+bj;0x$Nz92&8-kNP|cV<$TsL~ zPc^bQKh2R#H@KKc1t4rB;z$75;|!6y2K-Y4yw`IQ0q`5zp3YY)$mT6|A#Hoo-9j-MokM z`bJR|yhlbA?KjBMX@&TyiHm+jY3jo)2M%7W&I$b;R2Ht|Ob>o;J)CF}V{xOj?@q*E z?O$y-O+;5Oo?uRMT|m4~vNr?jZq9QN3S*pwHu19az(;4Ln(4f20P}jNng;vw zSY7gR$aP7c9Ac<^ZN4}8!^CWM%zIGuFiD2mUnT(NjcbVt|L=W9st0#gZaRWlUZD=HemXA1ed3yfI3(~k)xj=~P<^hDvxcLUDoiOA~50|7c&gr*^{QzbYS}ie^2&3!m=*(dyttGc5dAZ4l z4K9x*e@LBR0OvZ1GhQJ#l~y(JjW0sZt=wsvAhWAM?3Q1u?_u5GbwPn8{4yRi6b zS?v&ro{n0&S?{}cZq43QPrInDUWmvlc;39``MtSfT!nqXs~ogp3N1z6kqQsNVcH`( zW8}k^urvu8=l_VcG7$Xc?eK(7noH&FGuZsv)tj`@-%+0De)Cl?FY)Xrk4f?8sruXg ztQ4D>`LmsBJw`^AZsCw{zY3MH2Iw zJhnn)S~JNYdZ6R%)O?HN{>~y@YAnbQEN$7f5DI`tQc`^xgrO}vrQ;Ca=Mph=b?b9LnnF5U2Lx*fzCNwC>d$`-qH>?AL9}-&t0s+ak?9`pRT7 zfhUKxya}BJ9TPd8GT5DTdI{GX8p5>+F2z#3)F!Kxl+;TW+Ab7|?F{Mg1v#TCO@njm zf%eZ$rKpV0b(5pjTzLMP+c~1^z5$7Y*rfIL7NTP4LE6M+j)Zg9Btt$U7qpz{5JFa{ zqUaXhb*Q>7q4LD=fO^S|DkFu<2<{y_mcME2DX5U1$^cksat;b4mDaA^q}1;Ir?DH6RNZOi#s(O%c$; zr|IMTX;>Hb%F(Jp@3$-?xDuBkke|+7*6cw3_z5`D@s2szDVkUP|n{02E6@Vs`(RHvX zG6PJIiGt4}dhUuQ<)1egE43XL)m~#B3pA-IOK7Rai`PF!+_!G9{>qlpYXEUw15z6csu_IOYYB>TXgza6WqS{z@8rM zHQGM0sb0Be+@h@@boot+!aZN94)zOZM7nza8|F&`fjX?+~?T{3tM+gWa;Ggc476e43Q;|mL?(Q1Rv*#6D?A;mHIp5#+-1p~$t?`#1 zmr{Gj4?oq%I3KEO1R>=v9NT`;Ynve0vuOyU@c{7oG54c;B zNh7C0CcAXhNI@e=gp`40k&GQU@^TpGxUD-Udne@DaR z2ZK1YGaQj?*s~uM?p1D+at#rlrjG}nr;Ni~alnaHHlxONRhxlZ;*aO3-H$Ht}Ano2oX~0>f$LtD-@U@Vcx}pmU#15pa zW-nH7oqv6fNrnU8zK5cN{xDW{#MV?PpNjad^X8XPFWhPUg8+h}7p$C-aij{a(3gx{>0rOcBgECMf@VnXg~r!UTjOn@G|oLl&0 z$=Ls>_B zD)V;pZ#gz%qM`rUkHsL00BG6#2D?)^QBVu!!tAl+klw;h<)A>1TpKb5e76W9aQ&tL1#Aq58Qm8c#s(NjkCM%>aO4PbW2-dPmYhZE^{LX&@WNI zpJOh#Q_8a(!2!?*rta{L&{1V@MgYPiYppWyV2LY;5;5hv4&==Ajq zJZdcbSe;VcQ!P3p&nZ+-s|Pzg1M1kH_}H&@G6;u!+wCdRzn8*+oWmro2=Aj<<;(gJ z!CEFSfd+7}pT2_B4|A}x5^cvP&oc^tUWr;XRTfLZ{NpqaT-%L${S2LpLM@H=31v#; z9A;Xau^+Lq)`2orOnJt=g>kmZ{W=x-0#(lf&+w1A8yv~#O&J6LV}SyLOwY02@H@n5 zGtrF+fF6P7RTh)xWEPyqrKz;>8U*VR2+|;&heN>*ck127Z@<>(0Xg!D1c1fpIi5N0 z^qXsK?DIvG0QTW2(1U%CL;{S@t7~V^G$aCT7R=}j(~W#9gwD}c`7n7-d^N7+e}V?}uvF`fDWrdxzd~nqdnd{;LZBHR_WDh}eKA z5>RgTFG3>N^DD8O)jHk8KjHjRutm}UC=p;;9@vqEkyOeZ?heh$9)RNS`M7(4M;v2vUU&LFu0c^b%IU}J{L&xJ}` zb4{g2lI*NJE??*Y(K`{3&CGcKkqc*2ak^U>hM=AC*v>fhgIJS-d0+r{;9uWQ=Ulk> zeQn)$FQuE0RWpHZbPq7PeGneZ19bK1JqfBZj=udt?tIx+cfZboYpw|tCotai{_9)w z0}!JM`hXQo!+k+p%%DGW5hYSzs$-KNqGgCyP7tAazVrN8XCRS9zixL+HJNCx1{$K+ zk2e|Jsc&y6cjRTXC25x3#+z7K^~=N{Iz#^FP?*A?JEKIE^LqVcCyJP%z2;8y4f&QC zOsdZI8H$(h=g5sB*R|e70q?>i2ik%Quor_}@PFI3p5k%pQZ@GnsyA~{JTqf|{dlJ? zjqPrS?rW(nDO;HXRWF{fe(L?|Y3_=1u%&?f$G%g=zAwq1--mtF4PPn~+9r2lffzQd zgM{ylo0P{1I0Jvub2;gcgO1B&*$956$9X{}#f<3}&L_F8qw5sb2Z4W7C()dtB0!YT))% zU^0G|zEpz{k`O3DXRTy48g{SVIjfc;iz#p z?80dU1c9vGY1gR{M}9ZvJ|!AXT?XMlFE6$+;B%mzvLqYWdl7EaBk_}`^xD|dqqpbY z5w5VoL9r8brM|fE8ATL)Wb0+k#zpo}hKhzKP2t}fy1P8*yG*$!A zC>-?KHNWE83cIDpaDb2{9=hPTA{EU8AUFLCV1N{WBL%!WSpc^gLeUSgcYGWO@)SZ! zeo^-#$SvOk3IaAst3Ofp-bvu1)lQF8Hx_{Euf}RN6jC$74AbzEY@hwZYyJBzKWBwF zV-6nAC$C_G`2u`$=+-+PO9{LG|PX?~vv4t~9kWf=)%x@_T!@ zQR10-P6QR-L>A#&PwOOcSTIQ1JAhFs5vG?qCNnVJJDjnNxX)VZeGm4{()s)NzDw-o^vaqHdbq9an8=wkcPS4E@T-R#z7N*F zWO)WhM}yfggy$iKcaq7e?ZeLnRZh(c9u_Tc6pzK)5&Hq+%)k(2UPO&AE*RB66=?k5 z3Fyg(yezK|X+-*!>=;2xz!U!=ps}gS^)W^2l*A7hzi|P#`Lo_WEKrDUiL0Rp^2RDT zTI1x{01ZsyI)K!(=Rknb+ChX6;jYh*e@EzniSVUK5^;0Snqe^DpFiu(%uQgQL`%8o zHi(4xC%hwj=$C-BM~Z@z37Drr6~~q6XXgQ*02JSr9RkM^jzOz%vxva|&P^_4FHzy% z$}BAhp(b2A+=8wleYG!!n%bV()sZC2ZPt*u3d~T8Svyn&X&sCELcB6gx@@x*uSEcp9GHWez;&e2(%fsMx{ zS##EqH@KLt2NR0b+J5&9ocaqd%9)73ja?e_Xp!h5YqSaGU%DlV&>ZQj2!w>8fZ&h zli!v62q0d$b9%Nqo+;1TMvP;rGY;23!07Nh=aQi~08kb}ZZmsH`ST?Oe|jT5PY-~( z{AhYe-7SNSAdt5HcA^WrGmD6vXxob;E!oIU7T0L_^&TH^__VUOi%RKCK(&-exENLP zZjFf7l{%Yc_t4t_Slk`QMsgY$@xOj@5Dv49;Rxw9BId;|CkUwmiUWnsq{>U(up<(& zdSK)F$WZ&VdOCs4W~6wc*`;K*=YN@Ul$lM8ZuA+Yvbo2CrAA|q<5+|(sTvN=^yk!J z$M#nO5!LqvXD^e^^c-UpIsw(62dyN_JH)4%mB^@rd$;>LYdmBBTI_n@fw$Mn`kTlr z_2TOb#iw$m62?YH5E29V-bA8gMZLZUXR&$3PjsO(5*pf$$?@8fRzKVot}4+!SbE(*h8ibLD*|G&Cw4@Tq)#$`ch zrrv!#IGfwNVrp0*61Eu`IxSVO0;+zXeTK9ZLQ%Kmq%W6RGG$eQUzVG3{xwG#=O|E-$INBN+?IVJ#+s9uw`qh)}li>o@*YVCfi#)t>u@ zD%v>~M#|W@hu;T*?&%EuFK)6MD#V`JIx+UNQeVaooFD#wTEoQc9cmm+H3aqlpcoqP zx%1Lsc4QJ#l*A}E?XEIRxV9E-ap%_u55j&{sTFA8)LAAXggd^!%&J z3vRH?DUT749=jfJhYDmfw!<`#)SQk-3{cw*&xB1hwrO> zO8uXBy$9h-XIK@|IJ2kcxuS5%AZaa=9z8(>aL9r#Opo1h z1k5gtCJR#06ZQP~e-A)tIduFvQES600Q{`%$z$$mZJR&al&j>IWXC2@x}+Chy)-=Z zn}4OiZ!t*jYFYCmEKo76ua$x$>SzD(AMLPsGQUk3kbb(&@~%;y`1UoA zWM_M}u+*dU$^KlKK3$JtZU<76M#zd~KcTAnc_;ni8cHlQY1=|Bs0;`&_Fga{|NwCBjGzFYNhR&=Pt zY)2-75^(CUutd|SaGC{2JWoZbgi|6$@ORAjhSuCs`7uNR|J9zd@w@|2!mmwgex)4{ z!ANW6$JD1L6nr}47qBwcf~dsc!64>9uTb`|8}MN$;@D=+<+WULu-763Aj(*R&WY&t z9^uY3fgRShz91M*x@G|(`{bk+%#s4h`3S2sSzd}r7T-^*_4v-sAArST0n#gZ95^6? zCosr>iuu0u^iRaG3fpF$r7=uutgx4Y9k&~L+dI1)M_gLxUk8e^J6G|c`h51ny^b)0 z{4uJ^Sa&q3{==dTaGvxT<~VHcFs}!^GsSzV2by5ADPgt>O1lG+q6!IwvIH{4VF^dBC- z4)bBKIQE1{@@pi?@MJ=(!f)umLGbOl;^0DIgO+Uv1UYg=)AlEHMvD8dTWQDlj>3VO8Q{Y7>g@yp26q1&7 zRG0eb#Y0bGsgc*TMY=HQASnTe$l z6$e#fw@r80%jz(m49eLC;cs5oyIh@*Kik6Nf?-p=;}SV{)PQM}r8TH+;d0(2sLJNw zw{H5_<54v~(CV*O`u8J^PD@|$f2yYuqXEPaViJ;KN56oM{~h9q|KZ@2066B&Y%E$m zU=7?-Tzwegp0A4xEJd(ZR7SBz8qsDhvzaWgrIdYOGo+>upNtP~bGI-lKXOYZ!a2^)FF7un25ZjI& zGBGY{VQkPl1BKi~99tGVHXTF~`;hc{==`#Ky_ZOS(R}*w?*+?+*`hrkd^V45)BY{v zK@O_HTq<#D!~N9>u36U5%kU5kp#He*FAV}hbQzHGvk;G)u!``$SsavB5E7t~L5 z--D#Zl<~L-^oANc0o_On5l4JEx}D@o zs^pZPFobB)`gb4%=y2&b|PRk#)y+-og( zEAQXgC^z5DH-Bzkb$WA}MP!9r5vY8=`qjfoW=MBeR;FcRcJgb@tH#%AEVqXTvJf@z zjKPn&e}-6c>`1WT^)RN2Yl+|9O=2x46-SH?2lO-Zi|GqL4`z0HAl=L}9e-Zre6TET zk~opsX4cp&yu&u{3T_wc`8L1gYwt@-8-lUc+0b8CtblPIyt)@|-QIskRD;Jq_;4=v zP=-dAs(#ac28=9A&4wUNz^2G?vU`I^ad{|~AdvlY-||PkWnJJAk{0G{t$P4X2Z&q>5+a-S$v^A`#j2 zG5_2vX3?Tg{te*X!OX~d5ep#{>&|b)vuwp0FdzHsm-g8Eq?srGswAH0`3x4Wjd$=d zHP_tnpoOmZe?%&MI+nU_AY|8G(;<+E&Fe-v;078?jL{)R zIQ2b@e;8Igt0^y8ohBxQ7|V%#J{~M}V>~A}Wd;F`q{=r^Kz~wq=70lyH0W+uXW*wM z|FWzY=sd%^p%KgzMDD|ZIBnE57@le4o>PyqYXJi@*jwDMy8lKR7ii|xXn=iOhl|$W zx_bwovz0L@Fazbn1K#6wR0Mze_i9Cf0leI}Sjh$0JjZ)V1EKEUQ_wrNNKuAOdRzN0 zyiW!1N@mOkk?3F<6>rh)R?{!hDe{x2Tf6qUp&AHoKXf(u0HGxvsxC!!1*|$;UGm7+ z$Ap%zkhuG)J*c-f^mq<$o8YR(C;tQ%p&0QCI3owJOQ`oD!Ex%}KNStn|Co-R2i`K| zl{3<6cQV(PC)Y6LNjkn_@3uzZ#i5c<${^lRSVP zBRJEPsnc3vGVP=u$2q8y^7F;tuh=jd?&|gz(=T*IbTP^W#al}R)g(5>$Evp=S!+GU zLzAqh)WD$5ft*n{`p;a^7ca$GJ>6X?xt}|$Y&ZcDsgnWZZAgm1DDwO9Rb8&@4+O>j zRVz3hp_G=yK{Rwz$;+{Balh64_BrALanL1=dOAv1&i3~%6u@-;{m6|sB{||2A4vZs zf(?wBa{z5p8b=Z8AyE9>gu6=*EQg}#JQMowdPo;Uw*h2ge-345+c62(9Ne6?oQz1N ztO-OFlP>-~cTn)oc|L6-WY3BwmVA`szuFOY-GerQ8ss2K9Uhuk6YD7yV>he^%VraE z2dqkuor7a)>YcHok6zYT_g=Vrn3d{xt?22Q&wkc!A>Wo1Ee(1RWUM`KGr=kYs9^|* zvf!icu$2+zv<+A#S2L88?t7Jbgd}c2=D|6X-vpo{dpT3SiRm;)b>&D z$(M?ElO@gfos;RHD)Z;G%Dtb~m6<(g^@=P$w?7vs^No(~J%lhm zxGa&g2g0;<_U$^eZ-FzwE3@u&^mJcjcJbdAAA!co(7!ZO0zznIIq~Te4)s9Pq5bl{ zMDATD1i-2rvaNy1n_oG!i&k4n6Tf<&knU3F;Kd~cD}DEa0BCg^GpWQ}A_&l86o{Sn zboXUMq=M9}u^rlNOEB;XD6q5!boS-?-c{X)@UatW)hZl{hrMWhBhdRKkzn%c1i?jT zF``6s?V8vP5&jp?X;@%^DM5L1Nn;SF0`?v^}HdIXMSyzlMD zfwvW=dAP!|Da4$Ed0Lhc#z)Bi{MsSRAjuK>VN}(jZYUE4pSJox{`uJ|zt^d#Jk-l6M2igu#1&^QJi2_NxpypPA%hk!SVK!DyFyNV}d%s8O$MmV8Pgqp z8=NyRfExJk0<|Luqogm~W-mq%D?uxzRf2~KC;FVyhp!?*!eA3=0~6_-cR6YNGKa9L zK4rihDjF30ul1y&otfrzx4D=J$$ljbOR#p=qX6QR7N`h>|K0-bu~wu)05^#k1@_w@ z-k+)qoXu)${R zAMfsBZ02br1ZeCxE+&Miui^CVE5}~5PB|Esn2e$691^vPY&r>^Xs+Rf))Y7MgM36a zb2NLthHK*{N0hbqJoxY1)MV_RB;2&w6H+7dyfPcl5og{JSnY;jFBr%Zu#z$P{P*olFIP< zfNmtVy&>ev$Mop?v0~I&X@tD_?IK}IkS|^tY4cO4<9yire{OaQC6*(|{}v4RJf zN#6&5Xk2i5aacAX_XQY^b~jv}0ik&o!t?KOCqmu%hT8xHW3qZ3NoR`=5Gmk>nkaD# z)4FD=3_F@D0)5n^C(>FJ?1VNo>m;kog0o4KoK#nmA;mC-2qZIERrOH3!U=B-GD;;}3VSLh`)?ME>C)X*d;ZRh=#qsMyDd-7#Q(v@;XqrOq~fNpDdH zHlehlqFuzko2B*Hi$QD>^-p7HhaZ<=V~U zT%(=yA-CL2DUluardtSv;H@-VJh$Tq;ZBw}>&-p;vGT}qV5>36>8?CVm0l&<;}fym z!0yH}yY9;a1-PgTW0$CyJ$J$-va>&!s0J#B5$scEp2w|O&68WmDf}whzYZbYI^3J` zo6nN+C(vPRkPOgUB7F&wLnmJ|(k+^MU1a{eUPy1ew-E9>kXe-%_P$`usCX7w*$Klm z6D2)24Q*T+#iqZ576WKBtan@`d6{ki%Jc|c<}0giW#)3<3*}6@uHFa*h92c6g8fB_ zZ}if8>Bi;$_!H+H^d$NC(xc+FFCwUwL~i^G(?mXSaJzgxtJ7KS~e%lCD8fKnYi{`(Spe~;Nux4#)q=91+$#?!Jx(2 z(VI&qohu&`#biO)t}?71P1EN1uqK(&Uj~gw-rAqnEljgEg16D8`YSXv5=2d&b2t1Z z8pk7oA0)>$vA$>PPk8nBW9^K-U`oq|a}FRFu(_A2?4tLt9=}uZM$(^vaK7)j^I-uq zU$ytBk~yfb{(jDuK4uqZ#nZAE07-%<&sxXMmPo%-L(IvC^#stz6uC)3#ERl05Oy758E zs6p2SboLx^gKSi2u&ok10c_Rv8pe#7duvCyyI#|>;-9rMI=L^h(ujDXk^8Dh-8%#4 zl?=B7#SF{-C+B-to+Yg96M>>#l90TL{fd4Pc?<={MSyWtD>(9%VrNME`h?n#oZmSm zU~-p8R0DLJ0Klfz*uCQm=|6p%eJ?IgJk|c0SUnkedLXXjm6ha7rZpcEA}g_8x!<-ce%YJFsfIbO&^}481P=-bwCv|DgI*? z3To@JSSwwkuFL1Llrg646-U8L|ROa_vY0n+|? z%$wR_P-w7_M`brb#&^kJ@fNWY0TviOmaN%)ljZ$n_GF*13wTXsM+4ap^7b(}A>2f< zU*$F>>U$9!nXL2vROu33cP%XocQr|!@vlE_w@af$rj^kw@|cr^l5X-}x=$X%!1gO| z+)SWDy<92(V8j?((NlYI-R~U#!G?x^9}9cXJIMYXnr6Ai33w&1C_#?gImsRLf&j9H zH-s){2d%|`VEb9bN6;po3edBv3y|1*&4k}I4oYoO|9^o2c^s(mR6`+dx$jNz%>2oqaKf3NBq=3CCRN{yR^RnY zn6tlzzW@vn);V2Kl33Kq%MQL$KC;%p3Ps3&AHp7vGWX;Yt&rU7k!NrB9_ihoC2roU z&>r)w%#e9U)7!bh$poaaQh@YQGD6&`&ZK(VuhN8)RMU($m%7-bb22e*gNm>PKWE}F z9Q>3Un!8)ucXGd^!t((hOCXnaLu>+ADF6|E&O_N)@gilh0x){uxvGXF1AbAwY8=G* zPe%9mJ`x|`CU2Af!!D<}FEwgJX&KfPQA7YZrfl+n+W<*qZEB}t6bK)Xm$reZ_)9+; zAYRo#1B(MeM1aHfQy2*#qf2*gCHk_BL|%|FPcJl9pH*B?R!{?H-@lB8@dD?ZdxyAw zxy)X_fn--FY(Nlwh<<0qV(oMw#ji##&JIAhu`%GH;G=w5c%Br{6)O2UZ12~u{19kZ-;D(5)R=>K*ln~O(`Ih{%)1R-7Hp}BY z%U$zlTt^etOX*Ht+DiVSWtka!6)O&B(R@GG~HU z-^RM|35XN?=)vT^__}bU&c1@ecjm`bW=l@j){Dd6@N=;tVo5FYrCl)W%cfjTgmtKD zADnPHE$NwC@=Kcm(XK9KwIbgS8pGNo1a6TUh-Kpm=JR_2 zgytS(`gUp8TCP*EK{lftOi$ooV4l9oWeCR6A1kU1AL2Q?{{7uZ-w^8}iqvR> zqdW6f5^*{=Y{j|~J+(p=va&J{Y1*XF zT~mNh3f~!|4GW}vl^|1U{=}=`?=62k-?8Yni`PDm{=x?rGQtlSCNzoI#ecto)Gl#= zx6GyjbMUPqtkz5q*TJW_0nEyTD+#Kq!hQtC5{Vy)n}U${{hMAl8#kCy0{5bT2ZRu( zt=K(T1JM2CpkkE>D1cel6HEg?Jr&=pF_JX-_IrN*1KN7Z4sqMll>8s@f7ipRk|N*l zcTq`agXN+6G!8z!okIZ`8`I-|8B>u8c1+C_#AxYEyy^aD^@dbhiOn#=Wfcg%QQrG*+F5C`E!7&Yo2r5;NO zq~o3AwV*~gCBWOSqV#}^PRsv#zT5_-WXD)c+um>%R6fY0dVUL%c>g2=`H!iHluuIC z-0lM(fW+{TyIwo=wb_G@XDcZcMis!o-d{*P3MihwYk&FrZbA+SxFUPw+Iz2lpPu2? zbI?;~MmOUYB9p&g%V)@PB%|9Sh5m0k*aiP$>Sb1Hdt2K1vEG677;HKuU4zZwIYYHo%b`{3@A0aMKfl}!;+zTmKPpG6@JnmDG6VpsFS7q@NY|b6Yw9H#$(PiI( ztie^XBr)UCEoRD{&f9SbG~t&w4ojNauPK%(o-Td-;vaF1Thf-OG|>VF7iy(NQ*Gt$p(HVACN>6e*6KuiEFA?}+=LWATx1|Vtv7B}*~0DPZv zXLj#3{Ob5AS3)Y#g)4P!?TJmg;Ceocbsj6&IOUIi7?v{Y(Mt`cki+ZYOvN%D#P0b} zSJN`oT+-sp#)p4J_;%ySz*lc{cIBMiy0c6VKj6Hcn|XBFFYk1VTo3U1TODB29sGj| z@T^BMeOsD^6$B|v6uBRfzeJ`falIqPiv*RIZ4<)Uk6oLJMS(dcl~g+il7pW%E3kv| zCQLIG#&taHb<$rHj)-4YnO%2)!;F(PFUmUF{J_s;&ALN&fh6+LkGe|q$=uq-A?kj* z)eC?7X7Jc=9`{SQyveIY?ZpWw=lz0ma>2T@+XKV<;EU)%zQCS!k#K)Z`0KJKYmJ)^ zR;m-!q=nDdxk?Yrhi&BqXBZBP?`wY?VGe- zYA)Z>lkk|f5;P6I&DK1!WsOM%n%UK&;O+o^ZsjI1Fb}T}ox$OwS*lOG=R9h)JIO)EC=<#xNdOyJDTqOW4BWWwcek4L_XRHy`hqP-AZKWrQh`koFaA-k;BnSX_=>& zkAtRpMafQHRhTU|=E={W@d|!1%Rr~>H-uZ`HyB(J!0@~x#kka|hb8Slx|K|&vt8JO zrzTWA0%AkYqO*1;3a@>zG>;8Q;>{MXb3ArfMn+S56pnSgj%Iz1U*Z=?Cb?w)!{>Nt zK9@&RSJKZK_K2%&W_Gkw$p18hfwO=Up`g6_j~7JWug)0S@)}=Q;JoTAi$3r#{2chj z`#Tufe??e&&sh1w?>8^e!8_adT%hWrF8RUSdG!z|!Ma33?YLZ&8MRX5%yA3O3+l)U zuE_<+gH#3qwvmF;h_`^B`yrm&E*T zh6t0sUl}xc<*WNpOU3J6f6jg4L&$;23Lg$Vk}Cq#?=jL8Cq(Ze+A&+!yei}9M?QXat@{?fCtwnI4%8+=boolCd(nq+lwme9Au3@Y zo@5^z?bE_L+Rf-Q*gGGLQtul#uXo9>)C1Zq!Dtt0=0~jpa<`smp7NpkdhhiND6M`k zu}}7qSem|hWquPR#bDz~6=U~!>Gbgfy4`jEl=CcaC< zS1{=R^3hj(X}Lsgu4ypVvXoN(qXBCtzASr((97u;RP$)iO-cK|L z6PQ?p5tC*s>|%}bZ7mXrn=JwW&h+W|d2cmw2{WxnniL&BFX3mFwM$gr7lNCG?=B2y z_(L4&whaJ%PW0cQZiPr@^~&axweEz#>djBs@AdNd^Ngh}(6)h_f6jSkE^O(Fa=7z_ z9a=c}i;x(0B|9=II08IP2JNMU24@7I*Ur|H8)iLWB~9^f~qLmBhBhIU^mAR5}7 zS20wI4*Udsepf`*p(&K~2b~0e*v{m}vcBhpr2Yd&fMOZa z?_Yj8TO&xW7u!YvNy6IW7Pw!1Yx7Y1SV*g%r9fQ(*0S;R5~m21G)3X%5ebS|&~>GZ z)V{zA1{VM8CwXv^U^E;cMMfXx3TMMMgarrz>J+!)cHeUdKRvMi2+*kJ)xT+y3Na*X zq$JO(_6ntq5$XJ<^>sCPS)xkQW@qgSDyJ3v_@QU!EBsoez_zOpaiiC@=fW%?rh+MM zcdIz(x%ap{nw)^fLNKL5PgHEpWz~#@l^9_iT zR2{J{5K1xCgMNob2z|R9fZbe@j=XgL{ehpNHo*z}5bD}U@uWvOobV588K$xkV{qp7pO`bWPm&L@6jbw(@1 z$bf|{o&AS#eu`(6x+A|#T->408eg@Wl!lXBE)Or4^jxO@w8f$Fd?cN>5L(M*n^pn^ z%s8Ej-cfI%O5oIN^(~QY6m{0=S%0l!ZI~=b)kxH!qlU(&JJz0xjCESC;nhc`ugjl1 z7(BAhu-ZVsGx^aIylH6qULS<}k#^r1bDq%rE20!Hm5KjYGfFN6a*oM; zQjYTbWe0xs0 zQwbQoD`}knqV5ppcoD|RHQsOU#~j~wsEdxh5p8J2Noyh75+VUPuZdU+F=Gc*@znA3 zWr2}5Z^Ri21Q8Wa;eZ-1E&@PF`tD^`9x7I)KYlnKEPvW z_)ziPGkvd&?Yv`_PJXtgDjf}2`x9OXMyS}$bG%gy-V0q;KxgK2!BhZvJ67u@SqL|? z8&cLP09mnEJyC-c>oPSf_S@jgI7L&_UR|InbE9}X+SLJ+l2VK!2?Z-^619}UC!m;_ zy?#Q7x_yjY^SCYE;JCc6H>@M8vv~jbbRnx@V|i3g$70j3joY#&=xs{Mmrk8)V`AN0 z%@^%PMtBQi^Q;jXi81zY7Kl5^GC9U%aPmhyft!TpS?8lJ(2Y>zKlbBEWE^JcD^3Ms z)9~)AcI))D{J?9O^MvP?mp)p4Qozg@UPf6;$_9x77W5`I?TQJbUn5SHXK4ZO`{2)tD|g!Q?yGnV6PaWH^-Rjpa*TTJbsFBfoomk@)r`7N z|B~KLvveYBh%+>U<_M5L{R)-jgZ!}|^;B(rfaZNyexZw_g1_ix3K)%~L0A7~>50Hr zMY(^MC~lU&1XNpTR;p%*?~%RpXBwNG<1G0~TNDW5pF_qkA15BH{&@!ox)#?ySWBq5 zG4yBAlPub`{!!A*@I7-ZQx^mA&H4_^o*n#L0Gro5qW3jrn1Jy8T1^bAfK?M0-io$Lg_P z5_*XWxf~v<9$MbiSawvHJAS{>Eh|-nrkYUBgNy-YME>i-R_EU18;~)Ez_Syvpl&9n zbA0x7AYgHXNxtPjIJ`0AfEX}xxIU0qXpZ2PII{t)a;dofeD)ilZX2$aOMJ$%^%P>s zg3a zRDA#e#6eiZ;!$_H#q9`zJ}`{brvQ4~#1Dxb<=$sS%o1rh2N^(t;(M_lt>qO6Uob#3 zb>UQT-OfRR(@i@3`Eg|_Gdj-dT{gPJGw3x1u(`V#C{Te_BkX6<2>N1P18NQd$&zJ) zIT~%977XAJu81!C^h^k(S=GlqyobZAgE1@an7>zH^H|U!KZg}oPy&59^yR&X=R(BC z@@a?gDe>$oiHGK&WbYXcM`gcVhsDqzABncPaeh73w+x0@g%VXcp-<4gXzSu%fX^IQ(v4+HN!8NfaR&;0_HVGtEYM zf?YK{i;43p3>Fk{683Pzh$a6uvWSdq6Rhb~8KmdZa?gE2ians6A^DSdD zT^87%IKh0FpYerNjj%nqy&1IEHL?_95;5)HZ|&OqUjQF9;L2sV&rE#^Oz*pBr<%WL zD8JeGFm3b#YCdj_jtl;U(uryQ5&O*Q?(}0rVS|XzhL7zpyG9EdP3C2B5#R_Ed$Nx>nBwQ0vF;exvBVfS|kL+u88;$0g{|Zq67=eiU?5+**Z$6e;su-~8D5nbg7~6b&Y|C-vnb`UofS`}uUAOCpfIPMI z7se{Ky{7qLO%(T1#fI^0?|41^z%^wJ(g`a!fW<|nM=4P0K`E30B2N%`a<&Sctx$_b zZG71v3VMJ;b^@%<*xJ_C8YFI&Wp@x&0DvuLcrtUWoQ>jxWwB=wIs_UNikRUk#kAZ3 zfrPPyikWf_fiXaLpyBrv+@*k**CsK{?_p`}I_W5{Mu#p@1 zaBOFT@8pBlJu}Bxe-eFZy+l8xpF-c^8XNpsj69nAy~DWum0*DeYL;w0IYkive6GY8`?!{*&dt7v*u$@0`t0;?sS)vhmrC$bo{#Q zmB9V9n^(CNkFmlAXEbXa=qf%0n`X8%%Elp`LY(4o)&a_NgoHp&&Z6s^~=k2Qes5_|dcn;SZ*FotuH;m#vru)qnT7^1>Ey%2K_?>&)n`%b=z`Yh?hr zN7dWO!5>WmGb-3azp-6SMMMI%?iB{c?mh`dw!SsGjV#@AV#($=muw!U{u2G{$pb!& z0c^b2z>oGmu?b#Ce?)>M8ykHWuw%I=ppKVjJ`72`4x#eW`Y^(eJbdn#V?Iaq^zXu3 zUkVmzWFL$|@!P}q2mp}vXzT|$0EqDdIs_1Ofjd+RK8P!f6Rz^TLq=em_Z6Z;kQuxC zHN*f&d=Ka%cs3Z;53qs)DMzW|sA38OLWh;y2XG45()!Dj<=bKdTl7i9{S-H+J2!3* zCy(tt1z?O9xK(w&k6r8b9-M`(F(pbr!JToa)SWggYm1TKoZWX8%~hB6L`&||p*%<> zyWy$m`jubcFf{#Y3GKcr7y}MnFt|GqnBmJo4SNE^dny$c0N9)-Qt&V?r{gJGmk2XG z?9<~{e#rg=Ziv6@&HO}e^foIq&0FD=@*du zlRyDreETT{fRU}}2b?gKZ}h#wW(bh8=>c%(ADhKp!oi$li#PYff&R^}Ew#ZPNk4LIU2VGA^juV;ugc{-jojYtE3?W4G!!|sMsK70AxeY3JLN#O; z%J5MIYY0+Z!uY`rE%d;%Hn;HSpY6pveD@CgIOKLNbptj~WP+4uCkRcK%1iDu%1}9l zPJL@!)9tlxZCvXcB`rWek%sOE)BDfiLNn@d7|}eV*Fm!DPwibD^~bAV@W7wwM3FQK zG>AsW!$vwp{XqnPe)ga_ua%u-a<`B6jPE%&$+mx6`U~g*SODPLyK0Hg@AK+{`*YIn zbeHiR=mDH&MA^kxgH7O&YTgf2+vWlZY=1jgLczeqy{`2c6MYW=3JJD==XD#$x5?W9 zEJh~)IbmBrfG`-?`+jNo@DU;bM8D|)Y_!+G-*wEUaJh_UvDWjzcKn53f*U_rhDEm8 z2WZSEf`JEs#T#D%i|%@P@upW7ZKl0=Q^Uwzua4aHiYAH&F=lhf3KdH?zRD<~K!O;6 zZbXU)nGr}f)PLJ+Ts5SDqpioQJQ?V_2A9xN=>?~380xjEcOu3&AD`TcNN{vwYY*c# zi9N=rn&04)&*cZ^J|1`5Yu-xRAHXj|z6?_WXjG)YqITlpBL!eju?>RlbC%v+5&&lzpn%ionR%~!OeyuwKS`d;?b%|)vMasw(t3unvC}NO_~|2mocA2ay0R{pM7r1cr${ z=OpVn0O%d=jpDZ%-Nn~0Ij!PLbUp66q}p-OYU3S{TrI;!?)U8ycb=R2GGusC54srjR7rTGuBMt?#ZruH(S z(8841^aHU?Rc({)Hi{@X)!yo>59>*I4y!U8Dr_8j1E=U2#+qO=r?^1_BK*{>IMH4-%ynPNB?rP z!~S3<=E)>vdN^t2esP{N;?S8utA8&S^$T5dE=T zO3#c?{VDV#J5G)4IGw@SB7pKb0u)F#p;eibs?wn%3y)eAJgT1mf90B|+5Pyy9L z3OiK(zXJY=4n7uC+UWx+9h5*HJ_~n6l_6{x2n>UmM7o!7P0nB@kmdm3e+7SH04xb8 z2J8!}|2Mq{U4a7ng?1{HG<+ET6|@`Jdf0V(oT=X5d&)-e*%ldvg13Ojz}f95$Ix{S zwxgeYDj3~v>R+7FbirYw(!dXj`cvi8ppZuYrSC5KS;3Lo0!_5yVQ9by?fRkv9VGY4 z=4}OAF!ow@j#~*ZR8fvoE6y6ZrT?h=u-BI>{q}*JP4%zFs`}z1<(Y5K}h}831CyXz4L@$$f|l_6Cr4$RQ*?Pq1{;tKC7T2n-4X{c!x}n5GZ{4*(sc z;EoA>pl3jWac@B=iXbI+p>+bYxETg~!nsU7>xCgPq|@ax0l;)@;w;_Qip()-UdDZGQ@|e;EP2|zo+T_bh1^jrH{X>)G&#+KScQ+41Y2h zzMbtLB4z+>jjk?WFPu_>tVhZx7IP~Bha|R8G?-bhYO#&`Pb$SJ7-FpK=HCJ0eqywF z<7`9ahdsIjQqDg8nDy+7*1u()g95_<8~BiEZ_i2Vx4yoF&*GX)r#5>azcMVg8C|#g zkm$FtRsWi~U94Sw%0U03yZB~D&ggP*{}rGOoTcd%()#EPNY*Q%@1ll3fcmos5OPc@ zPwJe3wkmah~o&kXtHwG@(k^{e%O!zX^H>By~774t8e_4v? z>;BSxr6f%o{-|R`?y2>LtN#@c*Jf~YhKF*u%YnaJLB9|BH&37O0kpi5k}sU1 z;p(3WPZ(M9xaz5>S)^QDXrJFSd$|&kne=6Ikek!T#a`J zhYiQITLUFNp4TmFb%0OO8s6~Nye``8kaEbniT;v1gf--Ee|70?uV@jcJQY;{kkCuw zE7b5Oun_=4SgF??neGKS6&Qwd6* z`PHSHUjZXqULDzj7(kUTq7Pt8zMKAm9ok5dKidD} zqBUAB`g@?!Co|(+jW%fLuXEHTbBed+N3yve`?RBwQCm-mlq>7a2xovpslS&rD-W_W zD2*thuE-qrO!{u8WazB-ss|J8^e){DV^$?uH_PL=s;aWvysqu;b#{qu(bLlRhU10u z8ZQ}lIm!l=H+{FqbVa&}c4FYdd~5sL3jH!}V=1?5zT@S^w-X8al3QP13X0YTI8+8+ zWog6%9IDCRD86xR!;4@6TkEFOR~rCyVOIQ#%bvxp;!v6%0>Oxt3lO83K4oG|#HgZ5CG+|4(AYBME{GNJiwk3n>x zGsY{rRDUI=Lo}Mhh808KWr+UrFy2rD+1q-22yw}Qr!P7q%tP>I^1@C`z!iG z{G92b!Z=ih3av)t=eHMW1q>JgN((xj+)2MFOSxRhma@x^!yr5=%qpKjI2+V#=QLXb z>Kd!w787??*w^x=o4Q0cGt|=mnk}(^ZFD14<(R!?#kO2#!~A$5Y%|KzOa%bfbOi%0 z-Qj~;q4hyD|AF@a?tInngTtcQ67;;Of zv`hAfe9Io>l;^Tk9ShE8YoOT(2@6khQW-Ur3?qqnCHf7@;KJHZ{f9yXaTtb(Qy^kS zd{p^m$aA&Czb3*C1FEgRB10)I^kMWemO`4bb>KZk+$S>7woX8RhJWOQt=?xYMVVYI zRrbOYhak7vMaX^EIkFk;Ews9P!+ox~aBQb#g@?A{qaOg`uu^|uYwbDLDn{&ey8`NL z;lUMOu_2sN_6P%EAOK{!2YM*CF${$P-FfK-pRM%(U~T`>{I?}N>KwF1KcmLJbYG$$ zNd3V--1Pi+5E5>B0RaFkx`{9VXr}kd$y3BbK+e$mUMkTKaA7_LDEG;TetstqCqvEu(Jh<+fR1;@j-0miD=_$okf_w3Z&kC< zqROE9BReXUDp;;&VN1~E%tU@=LyBzKDbFCJ?_E2PN2j3XO0SkIfX{N;{)h8Ahd33oPUwjH6bjoq3@6VLNQv zhwj5@fPZuoU%uD`0OU8-MkJ7?D-6g`Du3qM^2n|>Zx9*+DqHllhIC|Sr1Ei?+xxwu zpLJir^A9&O04%)mg+(_#59wccdzi1`l_5K zu*lI9S~460V#@qMi3E+I@*PzOz(CzlB@C1yY^9Q8oeFSDwM&OGlry7S^^!AaDk4s~ z)6OcJ!oas6#Bn&2yQPMZDmH8(>(s~HCp)s9fg%q>J%FO-9soj=%inDswwyG2Na|dZ zZ_DZ>%Al#0(*7%06f9VL`-{d4!&ziV*3r5T ziSM$c#Y;=?e0eE)1F2^qO|MiwtA8&!a|8oQKMrxZ;TcLu;HuC9_o!%)(+p+{k29hj z@m%hY+Z3@PfyV&(%RAes``(>Y7uCZ6x~Lo`;vEVW29WBh=3{(Y!dxY)wUQ5&}--BsO#~IBdkJ3SDlCK+Ncn z^)X1w=UA5aK zJ#4W13~h@+{(>!Vbaa2Ie6ele{Pwp-z}+X78c=Q6%9ekN@*{R&d0^ogu?3X8g8>6~ z=8s+&(|QEEo2{qx{^(_)nm2oGw&?D@pdxL&IkM^a2rC*>vu#A)fM*o-&YY}+)>Bv>(QPjGl6a(nK4lS9~gyenC&+UZ~Bb|fL~wp z&%e6npMSkz>FqCG7D%~^dl}mSA?O(h{m30HM(=oe^v;(@aTA0Y>k~kuD?#r?eFqa3ACH=?fHo!=-${d z*v4U}wKUtyQsth?W6uq+jqj!3lpTjTPlfJjb$DPloI;+^R%MPupqnU9F_3}HY7IEB zDY}~r1V?wCT)N{e;Q9jDMFYI36?f?x0JE8ZN@tNd7Ot3RwuH4pmxPfIr*@tt+LK`C z*~x?*o~fMlYxjbkXDYvgv+N~6`f-1B2p9mO6|cjRfggis_ne)!?T-Da*Mx0#aK^X2 z+!I1xBRhM*ZMka#ABJ+Yg${Q|=#sWZsA=cK<+{(r zjx*Y5A#JgMay5`q+iugIxorV|?8n)Qvgr}r%#zJ-2rj+nhHI}a*=T#{Bjx66XvGt5 zwu4}>0jd{w)Yoz#(X61A|ipB^BDyI&w0}ei*9^T^SVf{wNZY- z`Q9H3AN(<%U-;wSfCZIa_mbQ1RQZlH2O}Fy?z<=e+<6rNpaPeK#21;a$pHXHC_B)p zu#qB9wO( zA1>!Q_p-ba&eZ76DD#Z%Lsy-m`0hP3v*&z;zk-VIpE@_S^GvfTO~IqZK+f3wvj>1Y zZJ?Jnm&0bzpQ~%^TB)`+Jh(!1iG_h4#-QeWAU2f!6v!Dwvv)#abjK;*GspGXfd_Jo z^xJ`J<5s$l^j$!&^I=9>4?C1A_^5ddCR0D!o613>70pdp=14gl9$ z09bf4Q2>DFS~T{VzCh_0G5Q|?V99MS05?@JVBMHpyAm5!p;Y zjMR&ft<6KlHhJ5O+E*Y9XzZRtB@SBjXv@DI1E%m;RB)k}QVV&IO2K<|ydtty{tWcD zHNR?#oIQQ`D~r9s3#H!cdjV0n(K>eTrkh=Zg7^ zVFT?2dX6n=_LxI3IJzXSFVYUC+iVk|j_wBt0A=LIv84;%3D7=q76wqP;Y2z1`K}-$ zK`8sxqC14G6k$M~UKA&~P6;=m0a>xPuqHVCT|kg%J>Jy@&FUO5aN(Gxlu;^aPa7Y= zY5A>{jr&0TQvg6qoUfX)oyfo>-#OJlXWF?8=+)%xC_w1TDWI-HAvQY2U&m)){4?F> zhu5c)`ZwT%*$_~r-B(UM43)9bDid*}XC`)@(LQL@TsZS7J8K?dV_YpPCohB?OR~QO zA?SnrQ0=e-sd?7}r0gQJJ_sj>VIM;=0CLa08C?AhzDW_XZULZw2#_mhP6Yse^$M^dAe-skz*fITelqr~FX3X)D+>Tw>x1YDTyqO202bd^xambG zJ47FL8u<~v`qQB23g!TAgT6BWK;jh!8gBp1qT7D67^iOg%@Vfp889UJ3IMp=qsXl< z;H;uWDEn}-$}0c>__M^rlT}`aIYUt8tXR77l|_PkS)-J254$CsIX{dZfQEfy=wC(| z9YUX=9{dS7MQoZLyNGB404&`CyAgT|11Q|IbqSDD0YEw)#+9R+1mO>5uMT_htG3(3 zqmA`LzBSNq^Q%UC?4GM%+QU9T{SOA1loO2a=EeiIwZKURgS}_KcpG>o)oIv6TN$`J zLQ!5s4z!@h4H!0V(=f%hJYP`)@_Pp-xMwOdcqr;p?oa7&(}XBw^M6F&jCE_`PyC(P zX=bcIguYASD`PN-0i~9eg`SdWj|4WCYnv(` zTf70XPQy6rbG(Tq_IehdxsAOXNYms83-3gG-$r{Z=e2ft%^fda0|meH)rEJU)lcq5cX0@QWo3031FFeOE9TLMT2`9qPg%vL1N^4uk5#iluki=0!?z zA2==5$|62tP!7c?hYoGC>rJ*8bTErKC~3RJG7Ny&Ug~&R`QUPI8}FKbO7jEWDr_xEr)Gmr$f7ybt$-dhwC>O7((mmm)!TkJ+`<8(dK(4b35!MO@_?? zyMNrKm;DdL&J!cMP9QC79LwUCKE^wqK#Pc+@(B%1Ut54F0v$UX zfLL1z_+fT7so&XFTPlOu^r!p#2iE$?rQ6?J$~BkG_hvgjT@lQjev2Wfqn3DUZ0OYM z5Gob^TNp8wZw{^HzQkl2sSe{T&}^_;@oLB;oANK-^u}V&>vGdgITvkweGzPH^}{Ip zaRP3~wh*eV-PV#!t7)&+I$Gt{Hm5*yA*Qv{7a|)nK+t`e=Fx>3wcl{`8ff}mZE#jO zaW^&aHPplW)c0%de5us~DALdIpF3Zoai^{GQH%bTx?L|#AC441a{8iA$o-F9?n@7V zhu8kIaU)lJ_OieQ`j+RH-txj?1Oq5_0;ft0p!6dW5E>3HS=tUEl70||?2+3c%545C zia^stsX-iu)<;?y)azo$q3d{&`$6auQTa8|t&QyvOL)T)R3gA=yhsMpaSH&8x4gP| zbB8VLC?uMmgi=EW(tv*JSdf@$7%i{EgrSk8L5$cF@b(y53pwpE0&Y*#!k#S%X{I_S zb-;CvvdwQxC*Lw!0iXq9K8}`1WM+g|yZ({vF777Wh_s{XuDV^kZu)UnjPpnPmnANqf$Tgrhqp|-yNBM8EvHp^V%2%!a%z{O?JP#J%@;v3<#&aqZ0LtS6HA`-D-G|Cc z{X_S|9{~U&JLsXa&}C|Id&~!f%10id3?cng?QexHk2^^2Wgzu=f-;jVaLDOmPBE32 zptXUNL-eKTS=K@E^ph_!#b*GBNKngni`QMvQwH-Ja)tKSIUCq-@rD@NXrz-=MogQnsTZ_JZ2=YT)(0w(}19%%w|mM- zTGQ*{C=XJ=eVcU^?teSlxP;Q*wy(YQ#cOY4lRvb(3II}b85!oe<%NYd!MpG#7%wcc z30_<1sofoFWf!*Ce;5n^Q2#}@K99CM8s-bzEV|vtb{xokMLHY0AF1MjV*vnqtsVdf z9nVgnkRV0IDWxJjfet0y!x@wf{;cfqNbX@|XT@m52?T~dmLLFZW~)C$AM%}%#AibK zg`CoTVH?Q}=mAjud)od&fq^?olmO7A+FqigHGeyj9%+*TfJ?uf_3+aRpEB(@fo)MR zgft$~k55IE-SGl0#zoo%vflGobQYHcCv{K#Yx-M~e*OD@FZ6xti_O3pn%+y5zk-Ft z+VX7CTPITTou$0X@Kbg-&k*7s85-I{rvAvJt>z@?VN?D6?BY#F1DvvV>c)T%AJJNf zz+eWkR=;+3Q&d=Vr$vH09I8M3knFVin(e>#%97h(F1s-g#4{tO0b4g&cq=5nVG)26 z@-5FTFW?Y@{tr-R(|Xtc7!I|IK|K5PAD zuL{_Jz=c~%W5Vn)V{{8k4wRZwUXZe@Xvxkf{~+ukUOu%umKfz0o^&FOLx0d$!#JG9 z^*tE!gsq?^kOS~C;7q_gXC%*1AA z2JyfQ&sOkwKm{**GpMm|9zP7A{xoiR;X9D~@FQ~EjP|}Q@x1wk#oEg4$86nS`(whE zKGRzIEyJ&`{paVe`6q+{82wlP;NZ_d*hRSYH$GCN2?JAk*+Poni)Xl%<3(k)Xe$gz zyDJ-MWE)P_aa}%$M!}1|2XTjdN_KX7&p`TaprxNDhcn0@a{wXqq6z@8v*HW*L;8Vs zi?dy$2hRZ@47jq_Nc~k~tx9KX=k%f!Ay}1cT{*qihVySEi*J`I^M5J&bNR0ak)c9< z2(FB8f$MepVuJ_(m0!f!3OX48(7Yh!33jIAn+Zw?G##Rk_PY$%ant&201nq!HW|G@ zwPVG7h5QTvb|~qW=nG^RxhsJiDkRvF9uD*yZ05H|{1FsI2qCxHs-fFoxc1iP(W^iTTipu)DVOexY!7EpFsSIB zmC6epB9+!>!ifys001cWNIT$EY)Hr9pk_kYQ5eWXNC*%K6aZw{Ce?bltOs-sSRpsP z?vxf8oKSffIV7||3@8#)35U(-a{BkmXx$<1*?-&0a|PrZ_EP2lFJ;JI%%@~$50H+3 z3*z$TRt5~IIcW5KE8rWi;Eeg+UXZ71OpI*hKAPj`1ehE%wLNoOrXj>8_dZ5n(71dU zy+Em<*Oc-O^6O845cfcy44kp19?13~AQ*SNvJmP|&nrOFwu?A_E9~$Nh`4KdM)EBq zR_QFfwb6HZWM@w(yO1-V%IXii;jbP5n*X5van5!6owDbV_D1u^h z3`AIiV4wtrNJ;*ng2O?f6KF}#44+DTL?6Emm$U3Dz@&nHF z#`CwFyyYQtioUU(vQD4jVc~5r0gc^rBG-8dwDjGa%_*N)f2h9& z0A6(~p%HxrF7h#o)=T|!0J!#_`^R}g_toAP-}$FsednKk{SQC-6|%9+rV7Xt%lodUgyw1M3{nsbIh!2~SnL zu!P%aytHJ)OQR5F=ru&Vs5$hVH#A$ZLAFlmETmi>X}yrAlJA@Whq{~WO!N$TK>%P6 z;I5a|8(@c^wDhyYcZt}boFV;W%TaD?%IC}aQ84gE9GmmLd+cZnQwEITF=GJi zPv9ooPOt@PniYa2HPo~-bP#8?WKdCxyE#I?klDtjY;$amLF>aPgA1G;cCkU^ZwW7N z|0CvkT}|xx6Tn*51rtyP2`PK9?ad`yxYALF_K-)xI%N2#^4|*Z?eTxKkr~{dcP=-F zhk0O^%z42jKNy9(HU17i)E}oL_gZ+fn6s(=Jc=Je%^52EDhN7K?x>7wA;;WA!AIc@ zj=YbUL48NsY4TI()eJXWi0`sixoLW6xBN1gv)BN6Y;~rc(uIsSo+sZyEqVw#BLT73 zr`*AJX?3CE7kxL0Z-)5A;#)H>fkkMB-}1sjs5x4(w?ftg{5mCGf>JMpUPGg4_|81w znj7WeaviSNF+0x2jy>0a(iQm5&BckPJuWv9FGK%6?4vfmMFPlrOX4fk*l%DAa27z{ zMWy>0Mu-6cAS1b#Av;nA28#jK6?i^Ymj<@v)WQSqfSw1P_Jo05Vm5af!c`;MVU#J6 zf`br!MFJd3!C@oSVWrWaSH$HMxC$v6{z$cbkZl&!9I9K;g{LxnW_TbP@TrKi8U+HJ z3D|mUbkniX%{|ytX)&XK%Wbg8)E1Oi2j{gb37l;c7hWM_TbrA7UtZ6b7?-p112$bv zo}^ziY^l=J-EV=3ZEpY=lYzZwjq6Y&R<+S>|Bm3ElGprKv;jDPw(6`7dy%9%rm%48 zhPt+kE^KUz8NARG*}t)3*pw+#Q|dCsT0l7A=J+KH2rj$Kp9)aZdAY7q8+`OGGP2>= z5FpuXttWIXL|Myk*x28|UDYVxY?vFjzY;IN5;zobXC2}cG6%{WHg1(OSR>sLxv0E>i|aLg+V(fs{us@6 zF6Fed@Y4ez_tJeLk2meD72jUZqZpvBKrQiYTEPJ6j~Xr?02ZUWanr9C-on8j1OSZ! z1tIzp`sx%?ol*c;dJFf2Sd2@oe*il`0I=9#y$2hNT72{KAjYN$f&njl5&0PvbYW@Z zOZ~%ED(TlL1cprHRJK4dARp#5PE+wEj1S3^x~4TK>~~lIJvU^My{;4#q}?uZEk}t2KUwB z!W$_*q<h--2xp7#+O10LUqjjJ}T6^}4bhT{dDma;cs`?!&m!R1?1 zyGS*>Q^Eiv!H?L?50xJR0>HKDa6wUdOA!fv?D4>Ds6S5K%(j08^gTjc=i5N+fy9zRTDM}DhYo4Z8@bOPMI-Bz8JCO_0RR2~MzJ+v4|>m~ZK<&O&k9ZI!f zHws?R+1^tb4-W=C8|?{UXg$+c7Hw+1Xg(97uc%O+9oc+TTb6b~&p>&~9njF@T4}IU zGY2t096*dL04SSHBpmmpAT3#pIN>X@ffkH=2EN?J&yH+`lX4tq%S{^X!?n$j2r^h` zK{o0*QftzR$<1#}kk`kleaE?lMj!S8YA2{ab`fq8PQ$(>eo^`=>`<4OzL!s~^h%J< z4rEj7uM3^hwG!wI2i#rgq1QFGv++@UMS*!VR?~VoHfVFI0R~MmmKS10A4XiJ@1HjL*bsQ>1&PwjP`Yt;>NHxx$fvij4g|s*hGaQ70h+6=t zKr@`e9Xsmyu4)VbK*}pSbM1|EGC~-3$@2;Tx|~DycA*p;Xb~qu%QdM{uT%OfoYdt= zjLr1MdLG3FwdJAgR~7(3jQ@ZI3xE9Y;F^E_Kanm%KY*W}V?G@A(V+zZsd*PjGg2vX z$aEaagEW*K#G$f`7TR<@~wdMUDVC+ zg3fC6Ux#d%n}XXr$okj(MEgAm=@)ztfjwx}WyCg|0Lm%t3k9@^0oU{fD6c(6plJht zq(GilHlD3eqZ_UJRtApR`&H0Nr82^)*w72K56x$dZ;Wqz4JZsi&TK*wkqH5#*3BWr zzwcy?4rgI8gtAZqK+0djfMoiz0%reW#go1b3Cj3bK;WB%=SOGwy4~7JQEbfDlxGT*@MmL1v65q)nbe7G37!YXW zgnYA&rr6L72PqA$59wDt5OInDV2izurbBf}OU)_njS4rE05G05hmc454s1Twv$ZGz zfcp#aF|+`F4*TH#aT5&$S!TD>*v^~dC7aM^)BLBNP=yflUr;=YU^4)H4A}|*&8akO zr8h9gsR#qQq+AHmw+|EXsoevpgg$^@m&AoZan=zCJs53+BcRc^N$9m09ssi9SD^Qh z?<3%$^dl`j0Jh@A1`hH>d%qVB0o{3Q^!8UrZ$FB3}) zA^{ZNmfqI^Kq4-c4?@<%&Q2Y&$)CauJd1C6iGw^E0xDrZhk?ub;2r=_9*+SX1qZT& zq|_b~x&c7Wa;gEI@BH(xzJsfh=p&V(+k$JX;SbUOhkyL{;9JIkfBN5Sg;V>k{}6z+sSw4 z%y;h0o!^~x@BBBgdhxzhyPm!4*;3U-#cFFRVPjBWz`?;`t0>Fs!ok5`|Nfw%z*_K9 z)o0=00A2oihMu}0A39fe7i&952%V>&D})Z>YiA7y=et&)XP0H#HgEO>CZa*;l;)>u zcJM=?>AjU9QDbB}?t#22(=(bpFo(l;dkr^V@btB>;pfW-yK4FD`qp{1tA{a(Taml+ z`XH(oCnkL1F2dE1Y#LRW^D#;f7?ZirKyg}C=Ze7uYEHldC=W{Y7g?`)$OfRoC6^2CaZ(Df9J~= zf`tJCXSU>dOwZlK;k`-@+0B=v7P0MZyg4Z`A>8yvtokM9&keXoKnuv-@nL0kG;^X4RXfbH)eJ*kBa@{&>^}c0! zhA~obX~pZ>C0z%nmX$f(i78{d7Qq#NN{JgJ5o7M2o8w!WvJ#ew9iAyT%Ucfm@D$;&_X2xMF>l~JChoc@?=Y~T<^W1tU>(pW7CeLXu-a1eOQ9tA;E33M{DK;$Ju+jAmQCx zxwqe6aI9c&WS5NCwr`?Xg4TrvV|AwFfoMx1C)pT69Enk@$+Ajt zcO=qkZ}5&A>sv)rVNz!3G61pqyCzAt)lraUm<~91{W$LD4+>2?u{T{*akgogEG7oJ z+T{12PwXke8)9GbF^R8Z_iSdyXc(_YWm1mCq)$vD%@*+5w7Q}hw%41QLjop9>bFCdnH| zasD$A{aCAxLL&VQmMFUe*3bOMrz#V|W6iuoi zl`Y!yP1EapwodnZ>=!+H?C z=yA79KT|DvN2W?|r7rLuZVYo*_4K|>5yAYu-wSO^gO#)btBiL5>lGI&`O1}D|1dom9nM6kOqxs;`yub zPcLaVKbf!U9kCxK5`IOYCLM$COnkqam@A&!BrpH{=qJLA2Y02E4j5CV>V#}O@m)_(*r`zjK<@Beg$JZpvW^#oqr?hHj zITAU~4~IwWFBl&b^-``gV$zovd`0AFbv_*q9%B+SwoxB*%MBLWCs_lzBF9%maYj+ z_vNb)9PM9hW+i5dG3v~da|Sa`FWku{&m*@h!D*Al;p<%#qzIuYeT;6)?tI+P4!LCO zyxq-~>1cUHJhn$X_*T-Ed0}}iH)LNYD*J3K*(M1GF!1yaHo3`ZlOlVFdwP18*Kku+aT`97o?Apd?Xx`OgyED!RF zBYKSun36>4#N*ebCy3THiAbZ|splUC?pPZSu>4*NTv_+B1z|hD+h0wczX8Qs@D9BU)Hf?Q6XJaxvgP>+m zoAT{ZgzMK72|qo)8oX35TrZ6nzlDhUgerhg%%!-)kF(L62+P_!LOF&UG<^P8s7{FH zGuqL-)lT9kt~|+P4@IRz+Op7rd3>5RyPxJYAyD=C_P1r7U;^9(nT>0La5n&9$TMH$ zvxBNP)6bHX&}oU4roydAEcCLvdAl{0i?`z7Ls@Cl)H94kN;ks%d7VEG0E$Cc1g5O@ z4`ic53PAWV0~5%lqRL(=O*5LI+~Ll$;+de%E6K*_CaH`j6Qjw#UuD3A`C_jbtM z1yIDckcc24|4k$0kXWAqgHJd-hq{KGpjOr@4hR>iQv#o`Sr4NeH{!f}9HEnqT#Y{J zg!W3R!fTmV(`O}1qqAy!e1VZ4s|n6zwV>M)9`$&%)CtaZxq|F7nrxAL-swg?-~5?K zf@pIg$-DyrC!b5|7@>xnKDP?uMlQu z7P^ITgaf3pvxwJN#^IeQGcv>`t)3ARwQf!9tYmjp9qeVe{(~+A94XQbf;Jc&FRYPH zvYypOu7WakHsmJV;`Tn8N(r+>TjpU=cg;RM%3x3{V5$Tq@&@*lkGXPKVPlb!6{Pg8Df#{YB#bVW%C#Yd?LNb8dOHbPfK<%pWR52dPm7Kp^L>!6K9 z+aK{9enIkb`%#9!|Ls01K`qYpD@S+H2GpELzA(cim-f>BrO-T!IkM2EA9aVZVz?2M zB2~Gip@glcX(KPZ@m6FwOPY#`lSbv!OLCIGjg`^h%i@GS2FjMv%t@--`hDu4B@Z4& zRjtfmQ%jZRz-ZiA>kwgA`hn>cI|>~X8}9)SbQ(F5GXU+aK6t#rPQN-Iu7n^d39`Q# zKpGur!r2$Uj}`+vT^z`QTQ$7)hPkD)LVwA81a&g0-A^Dlo?2mTk^G8RR)-RPz`NXM z#lEa6II7}|?-@u(d*CWfO)|yt?orW3_D!&~w$=jso1&3yC@XZ&l=l{6_M8U?BSvw9 zpNhy?W#hU$crjcoI^d)0t_$XsL2X?fl{FXh-4lAu=uWw9xnIBp#bZ44(27HLX-WlO@*=N|H45>{Z-`Zydv1b>zSr zkUfQ?-K3a~53Dkw-V}1?>gXM{l9z!9ST-sc`uE$R#Po1h&PdrH+4r%QINMa3?_Kb% zWNmE(eZ`@}{;_?4G;iJ2@wtjt6t$tR#Ic41H&VWVeCqNFL)4*!(RT_Lrm{w^KoEVv zoEM`cz$G%w_nd~kw2a{VnT!Btg&dw<%LO_(g?@eF-6;|&Qv@QEj=&VZCtrr`O7a(u z2|`r{Gg8#gPwwXA_~ojy*u}H`Y8>**rD>T2M(V=fE)4o!T0t5~lg-|ch~!FtYLgf+ zF2#-8^J?sWy-z{)c}xtcK9hM4(4U}M9pxFG!~P7X^aGW^kNDdnEUrMm_2GEetAxIg zO;qB0JL|LVE$0~>7nF}-(Y1yqaYT9aTOnKUb6vj3BkP^}?|{r5p$_|_{Z7R$Hu68J z2q`FTRmiXS=qnaS;DS`t>M$SXKkV@3WMQ9=ETksCl%>A4Mq`$YOF(J2K&tu5fyH1i zSb$a_!=7L@T%BdEY%Zg8bgmQ2-jApw9Q!wobHzEF<=jTDRSXzHxm@-529=#k3<&YSagEj%N zH4XSOov-TM4pH)JVtWDJ^6~>Sr3L=B3EAWt5`!4j;i36AR^A(W&xC5R;uTQ@`+ut6 zHiz6@Bg#;=r>CZZ!WE`SqN-@n1a>DzEmI}W(KZTx7J0!7^D+}p*MQk}ieq~W2|gsK zEHeTy3VDy@A>z-HxAvo79%O|0Zv2AYpuJ2VP1S0Ono=`HP#8<5`hIX-=27V}ZTH23n)-#+$^l`EpV)QlsBj%MLau+>g5tr@Rzn!P9O z4PX6yB%@ALB%fhboE|UwCGnu0x%$Q1A8$|$cI0#ElB5tq7%?2v{np~0g8~LP2yQVg zeHgdgdFf7K3_wOgQQ|aqot#cn&@(|_-4u+lLMVI5wIdd0k9$^6`lbo$vzh`~-C(?H zqzQeeXh#dFTEqsD1aC>@sE2$ph0Wn-*>{uC$7??^7IxWX^9X|2Gb-Ye+@EE?XSB?j zk<#(A1c4TXNKUqqE*f!x^ziwj*Lj~t$AEoyW_53t41P6DY>kw2Orn$F@DSL{zR8{* zKofPIollR#KAO^|t1Z+CdylY~CCz z`;NQF&yRoUWq%C7odg<_;1<|=oN3N8gl3L>d_jBMvZDiJkSEru#2zL=UrDhrJTRud z@2kxCCbO?$fl0%=hJ6qE*sSIWkY3+%s=>Zh-|Qj)z!M77s? zRvo4lyiP+i&zH}FiM;Z9I*R9&I3?tV&@hh`&5~f|$5C~Jow-3}bEjpU4EIE8MqxVU zaAssYB^+5)I7X%jqtzhjK?^@$G9^N?6`qW+JS}ZlTOJ105U$$pxhoc0$>TluS^1Wp z?s`U@c45qh!=#94@dMdfIHp8xZW;r4_&&HbKG2D&CLlrb0)I=c<(k)7C`hJp_eV2P zB>;e@YI59|LGOfgS=xS`F1(#ot*{S?*(D5O#Kouubh@3T?9mSa<@bSfy8Y?i%~uvK zTJHdULKhOqJsP~<;CUOf*7);RSe?9rwBYtI-5sJqcfk<_^$xZe znnmrAiG_;H)k{R?D3tsRs4|Ul!X=7}Ch~}EL;Y0u_R=DaicpfrxbAS2xnE-7BK5SO zaMU_#tBpuR@*6Hts2;c*%I$u<$D*uX9x#>?aIupY{CqvC9iPyNde;&cAD}<(t zRx!?xwb5&=OD;QyBgnkJjW)hM!1ny~y_?uiWqd)89IrBD4l}Eil#?~jG+&CfT=exD zt*s5KErDp_av%6D7xEo#dHA69n(Oz{4RK8GmrIi=#HC)wKdQMGiR6?cQ9@&FKU@rc z+z{GRAi2WTN|P*C6mpS2gg4P?vG$1v{v0y&=`eIB5!43ob&_h4tCggDN~)qMSZ!xF zi#e12Wmv^w&Mga(f7eN9qRDUWl{MxwTGqQ+UADGd!thHwi~v*)YFQM5(NGB=QTSGv z&e%y{bKu3cw$*U5n1~~q@vQDVN$`S@9Lg`$>CmBA4K6%1GW$(h$C$i-{b;k019V>Bz46{T5i?7nO~ zW>=|`uBO)uvzn_b-yA+S>Zi)HMeFjI#`tRe%WGZbt$aTx?7~Ga-||sst9+oF zj1YqjOP~OOzsgT*;;TABraaF`W}7lNqjBX_rsmB%AqtD4m6^2hows~uIEOUe3? zzIzYP89v!Ld&IZik}TKb?N+Wc|GNH|ru#J=?tNMSx*umG6hQM{qkwW!Erh()2Z(HN z66Q|F5(4QDUc=LtlW3DJ(GMZ3JtYjmtBt!jGdbK>Ty_o&t5O+Fbv))FEM^G@cq=|2 zmE{U8R!`qEY8*vXeuxg$XRR({P_7!P;nZoqoI3w8;x3|W6=0D$?nhY*oJCqE)-$)> zX&(9z)MB5=dSCj;88v17O=!%?05K%d^GE?(sP9IOsR7%*An&j+N&Y=gI~_^+Uq$57|m~uDHI4YylXyqf!!oewq^J(Jz|I2cebNY#0Wy3R0+SKgmjpXtuT>M zgi!Lmg*a~Z`nsKe#ml+#Oa(Hyvu)Kp7)|Zsl3ge2=h=O=Y$W1v(*c+Lb4!gWQj@B| z!SqdAv1|+bnH9BvNSwFm7vLp{8=RZ)j#PQ6xepO_(1knzE_f?e-iF~dUYM@2w0<@4 z$Cu6+F>3)rG(^!aNWW=c9l;O?dZltQS!P-!QR~& zR?x}zyuMtkJwNIYD`C;pN7W%AYKmB`P@#fuE(8y_ZJ;S%3U?~8WNUTMe6=;zdY`~h z@QlE%Ney))pPdxEvr0(XEdCf$f66+fIu_w=^NgGO-Sf|Z3-cZL1z7~@0xgg{AvIT_lM5-`9WN*w9nYywqsjwY_D|7-bQV?|2!}H6NY1 zsnu*6T(M0jt!aBy8g3&uGL{l$>cJ3XUw&5$Tp%*Tj7zRXzo|d2*p9zjjK>^(8oRuA zYh!lB?zvB}7iz|l8@WDF#*j2sLW){=*AQ~2vmT%-$5$YZU<(&AfLZWNvjF!lpdj$Y znD)#q&?+-YGlkU1?YznH^h6r|xB{^SeHK9(3Rf2%eM!tXFhZZ%Pv+DT;6+VUHb$ZI zZu0WDUGG)ba?JVw#pL&vEBCpYn-Kgz$%K zNS~J!%|7_*Dztpbs4I#dIlRu+CNf<~Lb;%8CrC-Na zlh&SJW^t3DF3$5RK?6*xiilPjRum3jUvZNlkcZ^EAA*ic#qmYEr>y`RM%W^Hd8I&{ zw#9>d^+E7k0X)4X99cUbpLB@$4~9ue*`{~Qh)C;DevAqarDhuwr!T}i!A^O~&kp2y zWkRtRkJn_iEL!~^sV%et&*k!H&X*V}IR^?~ zmSpZ7(quUWb^S6E<#??Fln!e2ncr!dey?3h;+lR^Mn5kU*~LxXhKt)lIldA=XYj=h zCAUZ~pg!++MOoEBK=Ckj!UuQk*sT{KtI4~YH5a`kEJ*mMtW3i0dt_pUB$Db;+@7J_<+*nqosEQVMSe6U>YD!IFGB1EO0 znC-jTUC&WqmH<3MrO{<8Nen5lPit6XTfCBu3*!5UnLW~mi`rXkehHB7Nebu&t7#!; zlz{7IEWagf&0GW$ZdIIIny;jO#5NvuGhvB*-P(}Euk$GOX&Pa;SSVJApq)J)**KlG zVsBS%(@v0flxe=S8WjE-iBDMj-O}s_*U-rvDX(!aNf%IBb$(+0P^?pdJWm1cNP*=r zfaf+zQP#6wDgW%lBB-gc3<26ET%Dx&F$b~x8(R;=0hrm`qShm6l3wy_RGvWyue5;0 zyn}=KTM~J*q{!v_>2Ac*z$R1H>{CA#ZN{?tQW7Rul!9_=KI&@you%Jf5%)&J=_{ z-74MQD=6uFAF5c8Be!?cZ>F@~vyYvFJ65&f&|;3?d9?%d&<=Ct3VHSwXODeD#c+rJ zXEd*KJ_lCJE4C&>e7zsFsdUGo^Y@|@Gx&g2bx5@q4w5YK%7G^e49&T0K4>cjGwI-_K4*Al8a z=J@hQ**o`=_|VH7D~zw=&QfDo0TDLd(B{$$zmT6AEHV!a!qjr5$5ydILTR&E!*$1; zb?7vuZAZIjYji1epTp~r{J1c}XoNf)!-ZINP!B!$e2z!`0DBcemr2v0;h7`jk!VFl zs;oA(aA~Nb@9?V8bK>U~^rK5@a0=##glRt>LrjG11JP)Y#OP(+OKcesZbN||u`%Hd_)&f~JEXME7HPh`sWan?VKX_w7B_*a zQt);>r%4k>(DZVOCzj@AT42qw?d$0mDQt3|%RF*gAbKQ4nO>u^bBoDs@gb6moCsfV zpmV}n7KoR6i4jz{k2O6kdyfh}vR!*P-j4c!cP z+`)^*ff4LNUfPO)@Jzj+5Qk_TR;dCgCAPj*|IKVKWMEH>ZzY|cU|oe1STLMqdl1?_ zboevmcvez(79NF7qy6-7Tf)8N3tHp%FwJqz5wvTQHomF!Fdm0~ha4QOYP$*{qd@_A zFKl|@f}I5eOd7iG3;&Pu@*P{>hiDnG&pq&~N~R=ctZV|r41t`UD#V_Y*VpPn^wY|! z=_{92l@%A-f{V(RDg3=SV+Kk#?X~e34kp0Q$F&rB%95&x%?fX)l-i(OI_!+=b1G|V zrq0^Tu2Cc_iDT?p#QD99SZw|EOZn<@HS#alrOlhJMKfeuYym%(bvM_w%XzE=bj7$S^^_L{vpIBq7P5F{d)Hq zaai@E(hMVs!a(IK&sF;7SMNNZHcNj#lr(VHx**s$zt#6TUwhne_ci+T?C5x9uLMm+ zF_~EqAF3mbOw8cx?clk^>=@Q*vSfanL_1<2d-`J>9$t|Uifd?eN^`8u;mKHhkC%q5 z0^CfCU*dhi%6KH7G-4j!ms3_TWy=UJaPx*3tMUY~GKs(W5$?T+0X*MQWf_d; zad))p1f#rR`%&`NB9KxcK3N`ntk9azsFHXoHbW zYgx8}%$3+R>D#_eQt8=g0mng0uOvQQ!OGXO8uQiFfZJE_cW_H^d(_Jpusbagc5-ss zDspoFc8vvgc_lw6RYG}CnlN&k+_muW!55zhC}pwv1lI z&5g(u6X%gbpV~Cv%gdD*sh<{4SeQkDl<+CAm{~Fx2!Z0J$v5=b%{)~$$pQZ&B2k|F0z^H-rFw}x^c{ifq*OV1?+g=a9{6* zdC?2PXd?yPn3-4>Ec=wZ)RXrUF8LEUnBx47`+VrTJnsz8K^@UXL)x)6F{N=eI=v}A zMmya@M)iV*m0FJj7CI(l?gjQlDjf8Y0`jA_MR61yA8c2Yc~gtOwnxAEa8)_RlwOOW zn1sAoEbIQT{pvEH)!NQ~Tmxe`^pSM^kqi-`OS;Q${nJS6rX>8s3^a53`-%y4gXL)I zEr|qz>^gMF%OL$WWP+ORx=bxNu!CJIGSpB9f?b@rKvpi65H4RQSJ<^9 zI5=@BUsn*=0pdw#39+$rmH=FI^#SPYtRw&if*L#;u5u7tJ7s@&$a8;9J+Qw6Skww2 zC5a*K3xokUK|DcpzD|zL9zb6Sz#m*7?D=;yH-PSsh^K=Dz)(Y*PR_+0LMOl_z{SI< z;A`j22av>|6L+_=2I|Ty{sRK`CIPVZ^mGMsbNl%CaQX0axwzYK^NNa!a`W(U^YL-Q zBse|%oIOFloX#E$zajp@kcW7H-R)dG?OdGceq(|xUA#Oc003A&-9Nx#yo z+)hsaGQz`C!TaC!{ih>5^kBEUxOE{OE?(|nh=Mo7*^}Y#N?jejJpQiJ%LDSe>5tux zR@U4wqyA|A`xqq^4efu9`AwsZos;XI5x=GXE@=h+r<|*oyW<}jD=;_25#j_h!~=%R z`!DjIcGmyypnsXq@1Fl)2u$5S@&85o-}w4t%O75W@-ASn-%3^FB>=ze1zNd)?W}-* z9{B}9mV8#iU{0`*H9w~Sk2RPRBxEVb2@bD<>JH<06s3Be{a!t z1bJG!xI4kp%g))##mD2{yY%dwAkRHPziHzY;uYW(;o%bz72@X==KC9>KE&Mv7UsWE zd3m__`2GyH0s|Fcm>^iV**Sr1Al$CbHh(&PI|c}w4vbpR@9cmf|7nNK1}Nta0eQN( z>$$i%N&tRaOZOY{Pg2o||07+1>Mr0v1OEs^tbS+QKXOhMWW)XERh;|368;ZJ&uv|N zod4h9{JZEsQDodbeO%lfwA{5U?IB>#{~G5%6aFWXF09&lc)I(k{2wOuf0GmcD`1sj zV_n?+{-*yq#O<%QzeC)eAPD>y{T?81h}9n#zLv?LU(3 z-`ZivYyTmTd4#~Cyn@1_oYo+IFf96DX&@>B26KuCi(13}T8ao;L;e-r!^PUu2jmWs zv4O<^%+Ijo`r~Ih)_=qw+rP^D*g}2>7Y`pVCl4(xfn!9bPYdG_4P)z#4s;{LB;@Na4H{~-6b@&8D{ z|I_fli~TiP&c)RaR-|k_wS1iaOZESW;9m&pc3_CJhs%Fu`rk$VqUCSvEX?M=+F^ToMGEqAJ}=p+a2K<*jWRLrJ9mF+|%#hqMpihSPQzVvXKWI946uK z4?NuKd@@)gs;7#E0_tZ3GF&p=p6Z29aBy^RD)KUVzH6riJ~@yVxecFMZnjto$CuEN z92wEl)Rpm!^>`*PV??5^*puQC4-faqMsfp`8VI}1m6ViFT|KeLlnPGZ00d&Q8jWLg z009Bd^YIrf7meq%0i>Qa$IoAU$p`KP6jlLCwa&W{H_fl>K1PNIt!W1uJ2=7wxL&)B&DNa6aUH+iU zD~xvRJ9@!qVG>_qi_;e3!qFn9Vc#))W$lgLeTlYOH=X*${tw2$2J(ZV z?gu~GxF>}>n(oxdGAtgPz*GW*s;{|eTJnE*sZx}og<7WhfbP&hUP3nbPe8~vI@Zhb z54elS?bqn&e>9;IUJ9}jH7Hz}ZzB|SsaQtBt3n%e9`M=~dvuf5|6)d)>Egq{XJ4#r zga&v`+j+7b#PyJ;flESmN8JWu8PeZAIMjpx6f;~&+Kx0B3GJxAHb7klx6>5TmVSJz zk3Y)79+QF@HY*C~ePfPgQ+fv%^|gA&SQU zEu5LtC777^0c*-YTfBNY>-G!G;Mv1%e@qGmg7CQW#yZOQm4IO;BzDE~Ys_<>sfSdZ!ikU)MkG7@Q}g(`BF8rl{5 z1@TJmfrPf5w4El~CB<64M;0NKr#6H(ai;*f9$IQQ<@n8b55Yv|foyQ;nCg*L4ge$C z1M_X$eB{s&>lQ-YZU^{VO>h9?;R>idi%jZ;jdqF-@tXp=6YUJ;^lj>hg0bmZ%k$YkcY-MGpem+Lsi?I^$Vq4ovfkXmZye4u#p zBvcsg0w2C1j^L7T+xXg44dtb3F9u+Lpts*k0_Qat-IqUV2?`Q?WOy@hDM2I!*d}jL zrBLzQpfZueiUyIo9-{qf;PGlO`h^!Y zgXpl-fi=N0%j;2xIk>I^%Pz&`uLDsTmyZw6R!tK}BK*s{rC^~bCI}r4;n#WKygWvE zoRf5wg0BsYUSwy#wrf74iZ2zRzWdayY4%N@;G39sAkH9#S5m$^I$*z`>4%G4sbT2u zOKsE9-S5tC5Q4*ZU6!8tnM&pgy#@@k)u+2om*a2J4X zRITwUfqXb$ygP1zPue#8%)h^4WLxqmLWk zJRGTw5T+Hr7TYF$AFJX3f}s~77sdC9z&tl{0-1>JrLdDt+qnFs-I-9~1!JusgaN+H zPpHmx76rykVOyRar}#vJ`H8zuP!kc~BXf}3NA!o+As4!q>HYu+NOIuC$U3piuNv7hjkOSHe?OTKtuy~M!3%mTpC3|;!% zIYO=5fJ)1$bh2Ai{YxVQJCM|nmq@ElKlc2|9%)GmM=m4$nIO6F*spEcEWuZ4RYem+ zPU0g#c+x>HXts`{?w3)wE;`v>-+nJ@3}2e4EWaPzr!JxJ7VhcA)6xJ;}>knpdEQCAtcXcew>1{g~<#dZat>8y{~LobdU zulb(Yi?ruyx0hYskwr}uvDxFGwXnb`@|eOu@MoEtz(VaiZsWUAx7r?Wzz#9RqPT_{IUF5&isele(#^cR=y&)uE91Xw zi^IG3=g{I0bB1-ylO&QV6$1%KP@!#9=**I+IIEoQ5LFj3>X0f27pggCx3*&Wo*z~_ z`JH6c&!vvtS!Dd{4%C%(Z4nNouh!yQ*xhvF`mt`h1XdjIr<3 zY^IhXtOOxTexyVPyv;^4lw%%*akU{3WxhOZN}?UY)mJFXIp0mKXrO5ze4Mj)K)aQa zqLC;N>E{E`VKBi}ebw79&owzw5mZ9fUGlzIKJRCUjP5Vek;aITV;S>ZEI8%x_F%j8 zFM5e&T1BX`T~5|Jfz&7D&_U2{R`6`(EDTK)W7LpmC)5GT4JP&uF@bU=;d#;~+4z8Y zM5wv9F%S*!KJe)HQwGGghiRfcJMbprQnI{*yHwnk$IE6EU^MkYbWyk>*zTJI2D5un zMI@14+Zk+~KwUqJ${i(vH4;jn?cI$H>X=QqdQo96fRfbqcnh*|@GL+#1f0laV<* zCGCVX`t41_ZhH3}cXS30#RN&5JEBXzrX+{zOR$A}k0Mp)Y_Tj(%6q7e_LdKM)h^fR zE|#-HD8)l%`jv>cfuqPzP>Fi>Q-Jvf_U4GrB#p|XX2|;+_nI?? zZ4|UwG65QzE1Tq5+=rIp{FW=&B@L#~UDs_yqz7akI(k`Kpo~E`W;@y-?j%Joj-yU3 z>%^=l7j<4;dNUnwJO?G>-YdGEUnHkvXBmje7!^)Q?*~j=5|icNX(@%-Gx!OOY*!}X zznWwi4Z>Z1fhLkkvH`dRM9GP=OYgDcZeO>2d)<=OMK8MrVlA40Kf$9ByVPdLQ+EEz z=?xNwcv7AQaIY;IOa>~oiGt)sPOCc zvYH|^zse4GY@=FW(Dk@s*9GI%WTMD9Qb!iYwY|=8aYTQG?CT;`yYrGWF3_YCYtx%_ z*A5}})O%URTn$^ZB&Dn89L{`gZm&BuHagJ!Pi+QHFj^u@WlQ=tg%XAz3ND$88EF9Zm5>@vg*` zmX%wM(rNo)GbclCwt?Ms&YWt5Vy2|is%x0TOsS<2QW2xYu4>g}-GeH%D#X1hDY;Y! zL5`C`pu-HlzC{o3n+YzlYrqhlrrAGTUM)kQ@hE4<>WfjcL=xf7|# z7#Zha8s!*yA+%gl=*b!=AZbCkEnn^A@l>}L;j>rIwcWOD;o-KJd-~$+>N)xVy}<=K zv=UuprEaf_%vt*jAF~{T>{iwXPs1xGO<74!0+bhlC|%9OeSugq?&!=t5g2iXgB}|% zzGXOeE^FKY1B1EyI0x9BIZubD!VMGLCF52~+M9c(Ns)@#-$NZjvrF%ajYb;CcND$w zd$AJH{7gEp6OcRnG+n@r%ug|ypJcabqO(n-2xqoS8z}%Z#fZ2i^VJluLdUpbl}Al< zpetao;TV#OViSdks&)J9jwfO^;ze2VD_E4n$15#h5bK*bPRO z_<_gvlUM!eh$wxttmSK6#)|oDW%?P%_}zy~`^#Kl{gK%eg}`g9OB5_xWKB|3F0=pz z%tM(gh3x{e$Dz7C12X2N4!~i3f~U*cdPL1^<_bbX*jmq<(`iBN>E}q@{9$ZN z1?|#ABLd@k+Y(;?w?+SQPj|7rj_ZeE(sT;g}(NwX?uH+fwJcf7LkO`8@l!Y@-Y?}VX$KFO= zE3UxTgV1*?5?598(9mD39Fq9tNYYs|=#G=V@p@zCwU54b&|5s)X&LKqPl-8-lq zh&Oa};2n%(T0EU~ZV%Y!j3eI6)bd??K+Z+i&UEA8j%L%qzc3FiG!B)SnSI)*h@n@M z=I}SIIBrb{cpadaV-i~`N_{5+0<7!i)`w|-J$pDpszaIwgYOCUB8=r;k3#lY< zh!9y<*s}RAwTJ3OGB@w4sjPlnrH3PhM8b1)9~KTWm&z0bxhGVd2|5cEkAAfzazWj~ z5XbF6C5`asLwjIz1xT*s3O>Naqb@yjgV@TCnxJU-8l2~b-*7aL2O~Qi8la3YyR(JcG$EEE@UMEJ`mUA(odOft!YgYUMz*3Mt7y@Zm- z8etY%x!mgK>>}do{79rUGp}LugLs`?yx) zhD7+Q_g#L`^CD!nz8xkqcsI$!IB`u^cT8Lcc@FpaKc2TvKw&#k2^GvVN%ecD4xT~b zJq>T+^Nb_%j@nR?{W}jZ^6pogjc|`C!@r|NaoY- zFG(1X2Nnb9v(G%9zt#ZZSkgE|^S~1o_J3?b7i=ZRkPC3qM3TvQ zIxvTAH;Ul)=Q0{^s~O2jPgr2t_o3w6mh&mLOq(6+pwWP%jdAf?4FTb26Q z15t!uT)R?8ahMj(=-&2)L7PmA{staBskLwuMOwYMJ=^O(rJSu5@JRio#8dxeAXx|Q z%?9!i2(2unj85;_Hi}$G@hA~IA*0P{RgSzpdt6G}TuAlh+is?J4D>F$TASh#yZ2Oz z3E#G?=^UmWksA5n#a8;yu|e>(47UeiJYARu8KZmrvfj&CqZAa{tj4gfbR6i^%%GMs zN|#+^yK|-?#_XoF!nujBHy^`o58v(@mx^GRG(;A6G5ByPg@!dYV79U9et^UgTA_;afMem|ynflBncfS7P!O!L?f1*ovMI-EP zl0%el>MnZ3vu9A{a-aszI8O_?Dnp7SmcM?S94TS!UWn%-Hc*)KvXDYS8?g`_R1?`%79VD8J=>Cc5+ry8L6jOQ+ z1KR+2+)v>F(m2r=uPu`Bq4>gFvRm*X?2a|MSFZz=w$z?m<-dmQ*=9AM0q=YfUAIiz zNHGpu2ll>qe2cw}PgzGOmq=lg^~TG0-fuUvRl>i$L}vx-L?OOOdl6o7^qTGWw<534 zD5_KbAI@iq(4DO%Bof?lT+uK6NiNa2Aadd%1LDIQ&!BgR+LO{Nu_=e}EDT{>&9Nb< zA@Rjn8KjLtE@|!bup~x`hq;|Vde+ghD3pdoaBs1WDRP%}n-A+E?okm_((16aAG<~^ zP>k#|71k#55%H`S)`#~Nv55G_&!LD2MDScp9C4W6=CfkjVLO?$Ic+^{TOs=7CR-oV z14Sa0_ZV_QAPb-xE{y1ZPWSa8C@`5$U}SAsuSy$KhEtzRpv3W#}0DRu{fuq6G6xcrxg znK8H07?h4-XHQKhGI9-wgYiq6{P>*Xij&lXZmdZ*sknRjhm2*1D_Sqrx=I@)QY10> z!i&IwLs>HDuqGN0q#JZpOW-%01t}ooFFLEBweT7vo zIx^ZaPKSy)hETxwz_zR_7tJp|_C7OXF0hc%2JVS)$gn2}GK_ICapLl1ryjQ6970Or z*UBqQwPaz?OKOhq?Hpm_jAc?W?pDq-n8Yv5JH{%+U`4^Tz0hUO_6U&ON;~<2@qf{D zl~HjtO&51t+}#}#++BjZy99S91lQm$!QI^gWLew-1PSi$ZXx8GJn#8_?m4s5H9g(c zRkv>43B2ct4EYyoUc$H;gY;1F7TkHs=C|Qu9&=^!*-0p)0J~`mudAa zEYtJPns>q2YS0FI7g0ID15@K_+uyCyw(6t+?~sa}$ep!LLU(@T5-?#(wh* zeT`Ymd-Q=mCgh=tpcL37V-??6>gff+kj5qjTc<@S6G=h$GCpp-8$w4M+x+;BHtnqU?d@wo@OOj__*VqN`h~|Idgg!6 za<-)GqQV%*UWBn)F?IS$)oC5T#C zgPfAs4GO#VFkMP`RF`^Dq9}nNU#=@JGy4r$1Bpk;iD-jxxf9fN?Wj-jr>{@-$f9Bk zdG&z>z4Nq=-{8HSkcNzPtWjlDQHNS&JY%S>qx0Etg|j-5UN? z7jn!4;BQ(B54qM5dnh{L^xo+Tv}!FKXiYQ7uTFmNmk^)mMQpHMQ`R?+TecZt5x*c> z@?ubW3PuTvaIFSbys2!^5+N6Rj(WF-Ew2a&sRiH2*ILb!xyPC(g#sh_L%nd z6xPs!K{>0FMu8A;tJ>yYt-zSZ0EwOt&1u-T@z;MhP zy~2R8)aVn?tjC|+!}^A~uqFjZQ%{$s`|9HqonNDL%_wx^hplerbhGix@-obA%%C{w3A~SPdBQAB3yBj{PprO2r zF%Ahv*&Q6iiE|!awH&mVfn>sjBKf7FzH}_b4os^V!M%!Hp@lmOTSs81hghR=krvuE zY&s(0ERQFXWLnKS@k~2SoW<1B|+DvQ|>+H8qAxvHBJZcBI7RP@V z<~!xz!!Mh)ctaQ7qPm4{FVu_Mv3O5VW^OkomyU-*B165RefcMEk^s7)i{%#&@Z4~n zn%la5gg*sF+2MuewmsywakkXK<)L;l&DA@@07PBxbEs(};?taKNpuj8W|^W|lm^I_ zJ-vBcK5kKOc~Ln?EsOV|H!v~dsC7}q_9ltor+hN25OidZfN^#WL5b8LnJ_Z?7X!?2 zPWodLCV;QlGu)BGgfR)Qjm=cC$P>Nz0$~BLD~xahDG;9@;dYIBzCyef*Um>_xLUoG2bXQM z&F^MwFO~CNOOLGHfo@5K1Tjb^8=JQa{vO4VjqhUA0H2EBchvD$3d+*#RXr<^^hGuFOmXFOeuP>Hg^X^Qt3p3k3O5{oMKp2@YhJ{a zxh2_>>lJ258C=!8my?)<5qIl}5`vg~FQE7TTmY%Z0nP8BukDQ3ttYiw0e}8HLhX7k zrfZQZxZ(g6QRP7wOS;o%+$(xB@eIS;Z#O-dCKb`a(7OZq0boBE1eKWXnksy3N#(3H zP6gWsf-LqS=0)*vqe{0xqYo6Z9ncie3E{j|gbNpO5>a@YkwUdZG1o=hC{mA-#hBf? zlC|H){{F<@7LiYA{4QR>i>wF$L)3e1=+S&;se4n5Z>0Q~qvMsW7_pq!PDWlyESso4 z)Gy3Z?oi#j!9QkvRLoZeExmA^b>7!b8>I$6|hy z44u<=#_Ye&ulQb{oz#|~(+{OikfLoR_yjGKHh-xS$mB@cvnYML?nDctlob68Z5lPq z5Vnm@$d32}AKHcDeKLy=)sgEgC$y4dqAJj(41gQL{gd_#;1y?u{a}L8Bx)a^E7HrB zU)X}PY(qi;glN%8(^7-KQq9AsVgAAnZ8v`r3@eRnh1bl_JGoG$LtOn8I*0zj6hXy1 zBvjOR#@YaE4aQVaTI&QJlMmWd3VW)+{E5?*+WZ*4D9q81o_YNwwvn_`G_dkZ|BqFw zF5DMYII`RlBvn-3{6>W&hFw9Llr4H-P%3dJ%E$?pnLtusrbbEGvUdv7Wribr`QDL3 zFeZR20<%Pnxh~TbAKFJP(H0@)!fFpj=<*TKSpZ8SDnTJvZBGENz^B*`)fLYD%f@vn z8{@MYniZ>!kq|VlkGLIy7K*t<_GI&@TkapYJo!%J`~U)WQb8b(>86wniWYCp-(_k< z88U?#8DB!08!QjVh)dF?JJL*)+IDkQ4c{}<`u^z!X??#dl->P{C?B6mi7t#34rfZs zkpfG`{b&?`E1pOkdgDTW0%sGl!<#VtHZkYb%_rb$K%pQEfV&|&5`e}QF{?mwD2el) zbYsxZt<-Wi|BbYE7d((7r`dBToCsvHaY-Gu!3`4X5;(5RS0-B;AXoFn2yD7OJeaQW zuc+yyXI&)zFNK%Vr6-WU^(UC&DZ(t~ekV*j(LbN!V#cu(#F!JfgsdF_ghy-eSJ9Qe zWZole0rbyNkxn&u1=OF=dwVKADva>s_@DxU>j;@+a5&7H*k{MAe22BzDAR+5eH1Hu+Tmae3MeQi z{aT25=QX?j74j&OJpe$MhY+W&Dd7QA7!D~A4v64DidE2}Af-m82;>}?fb2XB#u3_P zICZMuEcLTu=~Op~vRw6ie87&=hYLXhss=SkPi!9@RiNKce9;@V;EePt6;9e9#Ah1a z+La`8@#+WQ3*^o>AN-ELCfBz@@6-R(2WPPAOA3>J*13vLR%4Xhn1l(!%n_d83`84Z zh7_}hs^PGbb>Ap3tuxQZ5o#aeD&gUgkZU^zYTYiJ;?{pI6cQlYhpRJC(U*>bqoIk|K=yo)HY=t~6)76YTUFMg(KV0g85}`ckhVVkT5 z#U_g(6jIxUC;|GwTMhty$P6}>%5eSx*~ez?A&+wwkakfwpakooH1mE5_}S9a4bLp4 zcF%r|MI=l|;QfX9V!ihHgeNB;KTl+4XxB}n3Rkh}lPJcsJOD1T$Ogh02Zg3CxCfD+ z(#1gh+r3*R7Z*{jWDJTOZ8PEhD8{)*!VP5i2GM>S!deRWKG_Y~ezx-1Og%};l`q4c z(5(eG*TUlGfGU96gw|8#je-CCcIhk5*idHDEOA6e_DV`;T@&;iO1C4S7u~gTs^Jki-pd0%IN0cdiIs z(i{qcu?VGduz-VSeey|RdW~xhksM0Bttv3?Oe4(rq!omEy&Yh_r@)xFZF!}xu_s>{ zpEW|ui{jB=T#lZ`HbMPW#d5VJ;Co>BJS#uW+?dqN1zke6xW9Q)oaQvZkAN{S#ounR=hpVrH-cI1O$f~D&N396>VQW-u z4O~4vIKF&&TEMj5TCTa;6EdTKgQ2UWulWdjq=LQLlrZ=M9nK^_?@TM=*`lTq;~OPu zis$*dFLiRI%Amx}C*bOlblY0aPMvoxEHrwCE~O5>IDN=j*gou&KCs!%R;1zj(F=?5 z6+i8MW-Hx08<7G&fCZG;98moZM8!SA{vP;#23ZOqw8l`bbk2t>Ec3Ly9Vi5Mcsa@k0wzS#yZ(sc6ix(ybHVJ1mKqb+KPRq{?7Ir^SJFih z$3t*qKo5}WI+Bi7o8@L}#A9b~MlxOZRv>-M!C)#XV8MPz@XEb7pQ=VY#GzWm{Rgf| zG_Y!ueKN8H^81EKbyzYhAGa;OdtF3i(4xujkIC%yLy;xYjKN$_ z%wAL2BDmiB)5TfCQc#v3G7~~=BE?4@H$+HEnzx$#5>)>W0~WyM>F!`nG)H%#?K^aS z{RwBXwNqjh#zGoh1|dbCy}_{U&i9YMp=X^a?r6A7x}Uf8*!`caVvg~fPxx}tmp$1_ zqhVUF)Yg`zCMm9z#yjH?ME>#9nO59>qZc+UC|%GW4oU%MjQwkS^~S(FxT-?Ixi@)m zsYDBDbfOje!DpZ7H+bc*WK`ERv5etr`|mz9KEL%dbJ5+ ztSC^P%MU@^O#~IJgwB12NuN}N8FMBmSFmHWAARP=CzC@j$Ym-!y0|E>sd2dAV&*Kh zgctSecm5&fm(oddECL)VUrH#Q2CV8@V$h%H?ykc4Emix$s7&;_TV{>>U}Gb{VU*QV z1a1OBMCGh49U4AoeSG6(R)gtt5_|0fshUW?btarUYsJC+-hVaJPIsZeUBmi;^7mJ2 z{e)-FWKDzHNBJd;L%*9i`Kgb-evpic4;i^C>*`m8v1C&sK*Vn>hx@7=8_7> zi#FyTQQ8v)p7eV5qBYJ%k9^N{yj@&4sUlwURYb-!3jrKYz^d4B zCoqh+ez>qaACkQS^NAs4np6Set@beu$=`PCh*Qo|-srhl!+pDoYUr19TbGM%#^cv2 z2b#UuoCjiE#o5(WA07)^+x0_Zx@&=B@UTbTVwo?`CYvYujS=aNe#{t_Nsnyu-iT3Q4e8${6KF~-qDo7Gr~?PY#^mBep|p3_<5iZYW(&SS4A>i&Xw6xjj8b62hK5QVKTX~-?tCN6OpmWgB=C}S9O-L zQSJ6YzZ-Wp`HsTcPg_~z9C^RF?1EaLSd1s&_)20Ju=Ca`I$wLt6G%ySm-gWmYyduG zO*16iCnEu-Lm@#>8S0rKhM>c&nPcz+M?bAUbVLTt;{#<8Rh7&r!MHDo_j2*otmNPu z2~WDdia-X3q3mSUHcc`yudKrGHf+NK;n~JmH4ciN!G$qE`V%le-yhGldxnFy-vQ-S ziEZ5%L}u!R?$RQOularJym80EXTUy?ErOBKjAe0YdAg9D@^yPKrAQnZvudgQL!6lP z%>G2o(0N}9yWxF+tJr=Viu4TQJ*b?gO!~AWmD_ku+n=desuP;s!S}CeC2mlb9qQ z3?dfX8%b1I?S|-l-(X)Zsj}a=TFt1I6*C$aZC!5fJ%u>wMJ5OaTSks>AQ!m{;1G|TktL95QLTX&1TXS(xi zlF#1M&TvIRsuT2s+V6l&g{>fnt^EkpI(H_(*`7&%ly(XH!y8jQ9L!V{NkJ{z2D{+# zzHEN`BMj)yhRPx8nMi}Sd}S)p0*D-=lw5Ohcusiw%znLU?G4^~y?U(mzJ0L{M|N{J z0T>cGu=oO{-V^k^(B1Fh2K>(Uu$X&c_}bEfgF6FH;#if!1>EKk+8r(`GBBD3+;t zoCsUk)FYMXuQ8*sZ~wV>rEQEa#uE5z4zcQ#$l;h(1>CHQ+LQt3aht8$J<5`1Zjms? zzJiiBQ{UGiGpl-5dcY4b&=J|`v@Wum_K{BH{0^ul%Jc|KUhDkubP!-jyA=m~!T^iB z<8R27@{2|xgA#6(=W2@*Ojz zEkxVZ$=2K*0E%1uAfkJ?>VP+vje6}yvpoX`9oJpT7gH3@ zG2yLi`3m{nKF`SP(CU zGS7-Oh`edUdV#!?ysf;VBFdMwy%BmNSGI54H3YQf1Ex`C-U)Ew4@~)T&9p@T9lVer##X~%l>yJuXwLdFjr{q>qr0>qF$+Ej&?mlu4-ofC0BlglRF zaG&Yi;DAaOUjI*byYIIuz#Lq8HlmCjhd^9=l+=B8rEkEn{jD+?QC`#o_Id|n{HVNh z=rl_pvl-I2FKLwr*!C@8V|oe~n&X6QN4esQ@Z;wdwZ}(dzpbLth{U<^^m1HR=t!W3 z<0LwHJ82r*4$wQYSX6y>$mfG~!7HCIo9Z0gm@W6zHMW{S^2dvxfM~s{`P*_3T;$sm z|K?L_#nZ1ipGJ4^xF!JrsIzFBC%%n)tfj66orhHT)*&h9U>yI_J-E%tsv+mkAPm5XGSj0L!eQs1LRxfJQ;>TB##KIr0$f@H_>Bp@4|no8imIvs}7>(-2mJi20ZN zK~~r@8a@+LM4*Cg-yF}Gtjv!xz`)rP6a1O8rc26r2#1>$7(s3vg`^r+W^%ySrg4~J zD90CV?lX48{5I`5Ap(yk5u15EKXag==&{#g$(tju&}nivus0JP{5{95u_XLGJN>Wc_KO3 zg?3V&K%iRC$f6T%DSSvQ>v0a~G)d@TYAojKlQA`VR!SS567Sz7Kzy(;qJrIm=E22y1) z>9%8ppY6MMZ2BmS@2y4eZWK)X2Wg|cRA=68I^I5!u0Ii)2h18%<~Okyy@Sq6+?K{| z-~yIztfSgy>OV$m++LCf{+_Uan^36`rdruLf4aF+RrJDK1O?xXmQ%eRGpkCPONRp) ztfG+Bx8Gw+k-FqOW2YvZ;|eUYM3d}lycSxSOz-i!t}2o(?_Nqm4$XbVA~mCLNjwBL z&126`>_MKO8-zJqALrx6XRN2n*WJMhrdJbK5uTiY#a$Nd7$Z@2n-*p}s3S6YM7J#53puVg7JjMt2)Y zCE>e*XSnbPqjovd<{*OAqVL2*%i#M1*jJpZte1=)vCRO%j<6y!_er9AQP zOxOG6`P37U^+RlubB2$%Hbbt!HQ^`RQ(N;_9oVE z?YBS9tg`vW;g+^R-x^&*p~gkg4%MXKwU&pYrNl;ebY>hTNcui`^UYmO-6s2rN$Zgq z$9A_08Z-Vt3>xG#<*E@VoAZ7r8Ro5|fG$8MV__e0T4aPbyp9Tc)daDoDyfT(S#)%czygz4*_&pFkq#h0bh zak8|aj^RSQXWfs;ZN5AXnW=US@FkHa!O9{7oJMZGl`KM6!N^QA-)#LUFWP6-2Q_su z=jpm!(mKAZQVg8raN<)xsAm}a&TwMgtQ2TV^O>D4Y!r*Hf`_5}%7nZIn*8wwbI z>cHPM_x`4AE4AbR)#FC99fRhWQ^y%1ih&jF@Vtm?&pV^KFh14TnRK5ugrD)GPdMS} z%hF&W-ZK~q*=y9ThA9R;8_}O86|=zp6CXx_H?AxJz2kfzvSA`k;0R;s)BY}zMczws zCl~r1iZGh6ik{8FsBY9}CbS98SF+tT@qE6V$BslH0+&uCy;+h?ksFh?izo849O;pr ztbSaHH5o7a^9OcxI{0+n&7XVT;0yG|`S1ugICC+T;>4PxeT!Rx3$+4=KaRxN2XWFX zq2U;-Dk%3A8nWk=h1kRycf0!VXDX*9xN}8~@m=R%YtIcEKf$!87m6W|(3LRzq!eu| zGomTPvLYtfAV*$!SK%;8_I>m~dC3Ft;rCj0Xv5;OQDl~SOoa@}W`1AyXXfy6!(2&M zURn^C$BfNBYYsj;Q)U!4cHK-piA5NgUK}Ks6`SbIFOZJ}w7t z9A0ZUq&7mG-eAco0Y{sN)q6beUD9o#RxmO_2CZ2aj)@C0S+Gn5dm(Brv>DWTLbf?D zsb}K>POh-W`hA26by^bXu#Eai&79eUTg5AXka(-89Qw5oohU^pWCOF@sR%IPjjK1> z+JZBW`{^U;@sNG_oN_nVBsbvdg`GoKR4wu4W_o9u@W@=*(ua3`Z?=en>#>z>uw!+W_jkcS&_A(6DOkOypMcuW}XV){QCJ}LrAH)r0?-f>#K*L zk81w%*LZ_{G;9Q#K;&V{&=}r!ln|O}Q==Tu7C;xvgz^1@+vGMg*GOOc6 z)H02=0LbBSDwir&6_Y#+t~9!+Q65HQfopd5H$l+iPLM0toSa5pI7L zMjcv(HsfA54&v8=9Y9ZXGIP5~U`anX4-vZ_$iy6Bx?T0f^J;Vm7Er}qmgNGw?D`6}Bndf+A6D4;E2w`dS5hmfnb1VT(8 z-IJ;P2mvZC$JUjcq3v5AK-C!#EqH1TyjznkC|9hbVD4}0-kpB6tu63vHIgPHx}v6$ z>*PwZW=?34q@cTlbN=h2V#!EC-ok{N)o$h;k=pT0TFDIy=^6|>m7lW)UJ@a2uEge) zQ%J7S?Hhhf5Qncs9m+Lm)hF~d(5RiK16H6|@<|y8irYGNR(HzfB2AboaTMW?cW_41 zKnh0ZRh+Y%dt2t|FoEh%F{5oPK3rq!^zwrne=J-*ke)8CR>%7C)%g3+Np8f}8%cD)&+Uw_S|Zdn#QGr+!rz=T9HEA| zx51r#_}SIhE}HxQ6)h64L26P2vP}5r;%#J8)*(L zWxFq2jiudo``rZ=B=(+PmB+>(1U~CCqOVUC#o7I-9SL(1VtKoLoPE}n`B!fiuJno! zQih568?Hs3VtWwYZIA7nDAN(P?ncm^*7x#YV=$>x>PRV47~4cp?)+Ov@W)$TrD5;N;)81_=2;qYcL`?I`Pb&;uwxcx(Wl%EgP@yK^-&lG#b#RC{J5_^Wt! zJHK<{F{iBM4~MWs61#XjRwT1W(z6z`55&BR-76Zrmh7-;3aQnKw_`GaQ1UU+Ko7 zHX;bkj`aPQ%yfkW#DNv$2D)i?-^ZVHU{uVmdZ0sbtn9uE3l}(IGrD7_9x$7zDvaPu z;xcG{wNb0v>{}1ZXUxOKEGWC$Q<<$G$wb5F#qdu~al|PPi2Y|0iya9w`;*1QDnVRh z%s^GR_Kr8~TfXWVEv>mR z{6_?u@CpO!9iZT#RBdq1#!+UtS`Z<_1g)e)_M(Iuq=XLCLw8#CTZJVI_f`pSwceh4 zQ2&4kzeFIuuZa%kXSbs+ZERG}qU-4;G`I3s%)kJ!GuM3`J;@g8`kr^upz^AFt)W2d z(e_2TFZR*8e93HW7H!+rJo=$yC{~~JaF8cc?B@JUqK+n>hqsR=(j0(kf z!e2u%rz9*2FhX`AbI6x}@!@)9PC8bZS+O-8(^vjhpA4^RL%|YeW6mus4h^8VA+Ng) z38V2w@{1C|VDrywz>68I@6lUlV^LjlYUmK&=_G?Yeppa(w0AHcAR^yR8;Xk%0K0F; zfI(6Zs(k8NWT_x5nLCdx$^>!uqt+_PT_baDQ4|UjseP|Zv=(2xn?5nXgiVT$OpDAG z|2ZqTSmXi~8S-hNLE~0TqXRvnaIEbwkJq{LB8o5Gl1@lvM1VKSH;&N7^XdcqNOE`2 zR9QBuP@3@m!dX1xHL^YUa9)GM)b@Gr3tv_<)T=-=m?%6*ZY=P912j zEu}5L@DMlUz~gm{r6~ip+A_9_m>}OpL-X4>@wTz-60Rd7_}_1Kog8t7@7dTnQ4-k| z9jOZo_L|#hb?~l-e@$U>t|@_8LcYyM9Yx0}3K+OZ72^*EGGVtc>wUyX22>o^U?+La zi`0_lIe&fOA~07NGEEEBqR6{(mpICbQJ#(qHoJNc7)Mgy; zh5Hj@v2>%_#D)buvZXXJ$Bs|Q6O1U|4p-G%Y=XiO9+k^TW_jUAh=%^fxGp-XPZTnw z=T;>A@sNcd%HNR@QO!U>qL%GIAdkWAXcg@m4b21%(K%#|KWovXZ$a|SZ)|`WF6#~H zO$~QKU0F2RgK;)a3N7vk`?fz(E{9XFw~4xsI;%dTorHW7_aJP9v-Da4_`E_<6}1VBGvXUg97px{kLlw; z+X|#?`1E}`%I)|Bv~T>H1T(RQs`2Vf3q1F!Ss)UI zFw%3_R>zxp4NBmAJ!ueRI4?)qO9*QL-R%%s9flju6ZZbc(GRT=$3G4*TEXgDw}hfK zZz)v^t++@|j+5PWoFl(d!5!K?B{WK!hNx#S!*)k*0mTMcjK4laSkgm+jq|Aw(XZtg12CFcBS2BY>x~7VQA253##JV>d zi279d0`*O2H{ayrAwlK9w;KT_Y#Qkh4`h!S3TR?a%v$8z@wjSmt5tSv}H#uwqEtu03WXncxV8?!+ zziatc{kgg#KYI@Lu!oGdm-HbBariUHo5KP2&J+2QEs+ajg5bNDwm}=#NJ5#&=&ht; z_2ld9YMh2mRm@-!#*L^cvXkl2I(sPw7SrD)HJ~#*RiiG~@S1Z3c9`MdUh%#@OLlXq z`i)7d?Bl#fO!KAJVv*u_Nr!x02Kr(r>Msa_%0TL1kXbZ^Ye#` z>OwJz_2N~U1?I5Sds2^U!HF@+@Xud{WaMi&{{~)3$~Q!f$!%}(nFW>KZ?;3blk0Q^ zXL}$c>^?cr@SkMo(q(Sg+cq=i$m%?uRRt{^ni4|yCG4<(F}&b!xyJzC{v(~CwoxzY zW7id0W^Q%&ESh=E{2$hULPp3#ce`%IH}|-g^>~if==fg;SLu<1P>x4ocLrJhq!@vS zuaXlKT{ViqIgFO>qTN^ncQQ!bPOR4IMX1W8GR6HcDhm-lzxGX;3ZIZ&0YM6FT~3Bn zgYX{9u_g1Sgj#?MlEB~$s~wd?r8J)%x{q7!2^2Cpb@UIA?;z`a^#hlL0+NGu)^Ggi zA{CO42BWnhO;Kh$-dAyTf7?HhFK!4QG{RR@Aiw-FKSM)U-?C|}K7nL%>_EG+Tv@(1 zH4~XT@!2CZ4iCgj(vz1<-DW3EL^8+8FoEno{1I>Rjv}8kk7~VfCw+tPfZcUlr0?&F zez4>!0^8r1q{u1#6({F2?Xda1 z3CI<6qbN}OG4fW$>Q zz-;$}*_H6p-*<$)B+MmFAj}G#J<}sP;p-}N*h$#G@jE!+uP3nXnv9ZtDC*&$Gk6N4 zE^xNhfchFgaxhcg?ja10;51aRaFK) zy_8-UqrrOrr2I|&;sY%g+TnLZ_kiu>2Jw33p9?4KJFpAr=1wr87_Ve9eWh#uq#`5*AfuhBP0HB5JG6Sogv@#2cKQq<&So zUL%^na4r4e$)eDgq24fUiOvj81bH(CNd9T-bOpI@AD@0j`7M{Iy{C`Fa|Txu{(RUQ z-tUU2H}4iuzM^KWP9mQ>^f>oZ8(>5P71%e~miH?_zCynu0rQIERtCk?e9Sx(2vDWxUmH_nB3HoCR+ zg{PibO9(g}p~vCMUaibKRWhIDvQ%uIs9nRlL*=YAVv2@!Yq1*yP~;= zrj3#XW}==&)gUSJ;-jR<@ov?Lt148BYD|~*`KYe2MkSgm{LkYOxV8H3S2(uMn0l+0EP8q zu#~BkZ&VPHZyv+k0yf|qZ~H;cBExeZn%GNI%RYLdCT95Ch}cRz9ygvM^m!d23a{3< zDo%)oAaM>_aHG+y87YG|YglxAK3}EFPSEI)NqhRPRqd%G#e-pZVKTAEzZq`tq%=wT zywNt%=;SMDQ*zgM&}rc3UHe~Lc17z#UD66CIE#QxJv2ZZu%_Uv8p5~Cmk&Tj;a+53 zTE{6Ok3}qtwge}P$&XI0!+(LM&7~)AM?zam+p#hY$8*xUxlI|#K{E6qL{%d0ZdpZ( z&tL?TA5ATg#24=>2rXa%Pd+n>o>K0#2C2ed*92p)-st01bu;o3%nRD+8pW{fjFfN1 z#fV?ZM;U|6w$>R_yJ}6ktHa?UuDD;_p4ZbT~>|vM4r`E=R zqLx38+;%Y+9MYp*!n4zzssGaB9>H~!%1XT|0Cx}`(BI?hivrSY<=<}Hq4WV#VQpAM7i<`CY&30I%6P=_iJK_;%<5`YnWeDg_1{DgpzT-XvXi z8H;e)=n$I3qWqq{AmObCiu{RngKHracEtRF9m5~27rpml${=R2`POZxQPmrP<_$eC zDn5;kgkZ2h3>V&s%)N@yN3@1OLW0hVL)+_uTl~+?xR=^RLZ&7NixKF<|#x|JCnxLl-XCBSX-heezn@?uVu)$gLMqX zPg9I0;^7njQm6=CutpMyj^IRsw2Q=4f_*)*%abG(`c-WB>J+@tI2cI_L?F6^TcTU{ zV3GyI<*GNWn||PL4_xZJ{M;zv`}NdBrH*DezWd&Epeh<*B8#WFgs=_EFoSogrAC((hP3S`JQ_IpP!$B^;Z=K(nIr1IgFa@}&w&JZE{ z;h06&g0ha;DM8Zekne-z~04&|_5XT{g{ z+HH<)#pUS2gI#+ox$V*=alS|YY_n{1Cc7k&b9rShT1GmC2r!ui6FT8Rn6W^xtg+@R zOD*MKlThs+(dhG5iF5n&qtYOdEt`qM<4UjH7D~xj1XVxiIgZ$0F za0T*h6hG{*B4qH;!CAl|x8JSF&-AaI19or~!eY;OAy zWO>RVQ#e4i@}u<*aWOYHoZId{RmfI`*=iPoaNDGVBi_tVN?=y%!bx1uG>D=nN(tZ*8yoKRX_zU^d~X z!^v9U{+Y0X3GXQf#y0HpM+~0VBmh(!i+Y%YHv0LVd3NP$ zsLUGg-@{5!_Aq2pb2-Us0Ua{H6%f!Jzu82zJq2IBc!K&rL$UH9>PbnU1@ghck-TiZ!gNB^5b34RPBxBa>g($fD)o^I3Xi7Q6+ zPDJ2)?*1hF_m<$dt!Qf-t72zDosD?){<|xK*FHdbd8(M92D+vJ<6YdOxedNlhNj`h z*pcy(XX;Y}gVn`CgFoMV56^JhXZmn4ZfFs=8h=OZqRf1Q1UyVI6AX+u)L$6Vv|lTd zvT8ufRyaL2Fy97=c3UK1?>inQ6i54+HhDUzUMi^1QGZOU9r-7v>)#UX4SSH zuFDys`oSDYT?`wV>BEr2I3tMmla`#&x#KZLClv%9^u&>~z9IPHxmL5TO{(JI zfkTbe&PMgTIc!_0VAa`IGCXpfX}Y8lpuLI18ow7^IaK0(0pyApvSeI@y!R>Vqm=GO zhGad`+?2o78V`u#Ngy)OqEkjh?g9dExb~4>0vbaOv9I_DP3Rl?1QxAY(S~?$X`Gn$ zFgSzv;`v-E#S(UQ!3!tL)LW94`aJOF^WV3Nx^RY0gF9eWlRew;HhH`ilC#qNW)wiz z^vJ&C1Dn|U*jsVbJE8nHvQzDKOL~srLuGIQ)WGPU=U6UvFUeuo!${E`Glm3v(iD9od{Bakes0JM* zo~7db&e~?4&iy9?(2pWDGw0ib?@M!qLTg;Nt@Mp^l|H|$E?iiFaEsm;mspu-3Hep% z%JhWVdz=OYfYcH*f3uXqr(8X-{2xgDVV#tqG@S$B`r6`?POC{WojyusBAbObGOuPX z$gD9WN&`!3D7!oTHuw>`fgTXw{5%JB(4#gfGkPkW(^%d;c_Pb&v4;BkPTg7aa9jlF z7u#5|2qHomY6d(pS2a&C4Gu>g!p97BK&9#k4uW)_NcJ*82h0YN4&0wCr|l;L zEA*W1$PJeR;SJL@vh((Zn*!|H)piTDDmnVCCc((YvsVBr5MjV}VaRIV4XG|V`sbLp zpsqTcv!n-dtQb7Tm}V6P7d)~;ssTB#c~Wk)uLx6NXC%sW8CWbX8g*w>Xy~g8UF@1tA^>bQioAeO2%Zk+fab&ItjVXDQU*wnKl1M{WpQX^BKRzQ z(PMSTZfnVYfs5Q30lA6~S-pScNecH-pS-tD!9KxXPpD&L0UcA_fJZ*C?l=c3sAjMiq4VIu$cbdtD9}$AN9eR40f0{?a!UbLNHpP*95zkIbsF)b zC%v_YE<#wvx0d~#FH;&)O|vn4ppG$Z^3lI0Z)%CpnCifG9T0Ej;lqD+OC>%FQVfn2WWupv-NASNny?;7HoWm^M` zNOdZY^vlYz@lGZQtz^iywQ+PrmOr)F0|XmQTi5o-Zj(`Od3%9_%t(Q<<+UL7S)Y>L z-u?zUPW=Ojckg~Xchqa7ZSo*UwqR;z`te^YNzFlVl(4Z2ihpM)FX?Rt~3Gi)=!HeIdKziv>UVD4-_HZYh z4k#o^1Mf~lWO_>=faPE}>B1o#3JsH*~LkOXQiZaUg z_lgG>6;(yR0h!wyo<3xmGGR>t%xlunJ#}{8Y(55{4=CiDgtJ*hzpW44d=A`)`C{Wl zgXcyc(LH$WpmKs(3JS*?#bPcwrzDmQ+NSa#h3E%P1;nXdLhn#j6$k+&F&*A6>N#W} zmxI@yI6l}<2SYt%f62R@H$~85!5IPI?{#=;-0sfT!jKNDG*B16O0$|M7Yhzsdw=_F)ZVdz5lU4`a?=ptdb-(~ z|BvI)4}XH#ks+iR)D~>Apszlf5VP&JL-r_yR!og2TD}MtzI;;))6sE$b0RDezSrR2 zhaeaHOHx$H)Q$bGYVfK``ba|QSVWg%`YYCQGC-voVd)Z>F61El;-r*#KpOB4tBC|Y zX~`1ZkU^^z$ni z(Sg;6@9)X=;0jcmxYitMy^*2+_yg_(;RUsfl-Uu|#SYXj|E60Q@ZmlRh-Ctmh3?jJh+6y|R2um0;q$oI_y@EmrI**mgJ1o(zyLZTq+ zFV05`7OG`XuEWb-eL&c2-tuVbRlhJAnd-olfuH*Joe7OAm@SZB;w#sN)TRm3jC)vX zf$?seXjX(8Mi(_$7gI~Blo3+t|H4ecdzEoHhFmopRtFT@oq%$`4;L+VLoBBi#<>`5 zs9ILnJ@Q@&t7f!hmCCD;u&@>q+QI2zGPQ?*4|uc=csFJ{O8q}_p6^Kfqe$glEXPM@ zArnam7b}^I;D>%0FmoeBCGBCMu{wXhEE+yijgBuIdfnFUBt9y@&;u6(a^mwJnyyowEl*{!Ju;bHkDStE z?C@v&>|o(znBD`pLB6R}!eJF0fUm<9Ft);r+W0@eqqOLkZNaH(0IP$)VgmZ|lST+Y zgFBz9!fy9;2l|tA@7rCnrj;RHJ{(*n)qd@w%bpL`ple@$XC6G;g!Bn8hWMxHckM&nnb@RY)j+$&8m;x{NA_Dh;6!ist{NOqN zuow$(iH(*Hp}#xY9R(uuvn-N%F?@PcvacQmB5q;pPA1-|_I(&-8YinTJV${#GB?Q> z(;Ik#L~e}D@Gjo?hc5K`OV!Jd9Yi4tthG>UkzgC=xv@R~(Hj9cFHPl+&_$Jh_~`BIOfua*)-*I?a(6;EKKOvp^)a&EKY{CHwdukg!K&-u zcak1zv3)3T?QNo%OzVNW)z3uDr9!iX|4oy@$U$R-+!mZC&+U8@kC_f=beoGb#WAc~ z`d%bcX{Wsdb+=uU)ODob4HmouQP|desmzW0c#>X_beJjHls#}}s;-E`IUE<|_UEq# z1KePFJxK3T7z{(e>h>s;D2^#W1N<|d?Nti-J{IHexw01Jy3~HchNOfAMa*fc#QbcK?!;4z-h3pv3g=0SqamP=mj8{0AgMc}t^5j6N5e{yeM zs$-zs=u3Ue*Lmc;DRPA%nt&dv+pwMUz zOa!k8F}8vuy`xa8-{OeEB0sU{FKbJuKscj1MB1a_%wqihQ^K2fz=)_7xpM~brr-^# zY627?rH4T@S#zwAt=s|_hfASE3Qt5t_d0-tj!9lU$|uCp^#0_a!ECq9WkjrR-??E3 zZQT(g;-ea%3JHJ>=9Ygql3X>Q6&glN=SB*l|CjJBu*@@K9h%V#xZ%|RNA)v?fmvFO z?zaGr{(yZnwM;CPV*1QqXfJ~_5KBl=17V>-|COX`t9VRw`@Ix-#n|5LGZH?iLmVzl zLp8Ykk z`m9KMgMSTPFeHE5p7P7dsO;#|l2Izko(>ea&$V*;(v?S0X+ zTb4`*!(G%z7m7DSEkJ>sg!%~gV@n%*hBjC);QjF>*xgD(I~0@iD{GCe?7AG`ChA)} z?6h-Ya1^2lG2DaFEsVaDoA(beS5Q@h7H#eE-+?-k9c|>tkXm~AFGnHhRv2Kv(Ep~9 zO7p?~;g<~zSV&y;j>{JJ+GFtcU(pbiNgfsV#B=qM; z;xJA_b@nMg{M~s6jE$>&Aoj)1E1xH*|M=od{-`J#CfDsk6JePAN^%FvO~nsGETaZ$ z6j=dDpy0I`-D!5HVb%UDF|J1Zwv9PU5y!P4gs?xJs{eO(7*XgB+H z*`dhXaBuk6BF?A-$)5Tl6Ckq3ZpCCQV2fq4r*>RkCMn(=+B3QPxT3 z6oK9^NQ7N~;8s&EW}@fPeAubyB!dLE`l9|jAr(%^Jla!}TxW+1M+QijU*FLD^S;J& zCvL8tTbbo+Dh10ji}MRb(M!cjW69yaOx;0#?qAHMrjm+_xw^Zp8fTw0_5TepuuQ@j za&RD(&bUdnLxCl?{O>2<73bD$|C7vXznRL=#)R!|WGB$w&(p)ua-{i(3 zZCL?ID?>cHMH_c|TQE^mfR}G-Y=Hc;W*T6bW^u~#sHNi`)W9R77hDT?#vjh+rnzu{ zpUQ-1q)%bK39{QsJ7w5|E#7F>0voe&+Ng(AeOq#43~b@ai$B5~VUJuh9=LuDr?RTxVHZAq4KIwO_%_p6!!$K0=_@{%Va^aEULhdJtlWT$xun)yl5FYp`VdcXqJez5LW z=X-0*0;Prg?}9RHT*!hB>!vJ_H|Fn}vZ{|8Qx$jLNSix83H%#n#Fd@jRq2wvYT-n? z=!2;8&kHk!CWA@D)JFvJZufvoOo#B9cdWVkS}T|617xH8!t5Z`!AI3P`*2tc^ukP) zfr?vdX3i7H`>_gOO)g|!HjPiTjaL~O?FsiPhz?QeciPoIplCzbJ0(CZ_B%=mv3i); zg;?E|hOk4QZDyRVjS{DypQX~MsFgTj8s9JcS|h$);d;%{3Otq?9WlSw#o!W#9k3$D zpUJMB1S#7y6~#*{Mal24qm^aODL&g<0`6A6&wWM5L2cDdIEHEp#;o8{%3jos>(FaK z(Cd)BqX)3LDK(j8K7-<*8!~D)69h+8{#AQc5MH@KWBN9`1e=wUhGjj)cvB?$=4ke* zj)?V`Ie%;!r9J1WsAXHy#3(Dqdz?F&O_3(YT`9+NMfChjO?h0v-30tJCr>*a3)-r8 z%j%V0g=hX_*`H(9Sr`?W9-dQ<$^){&UUGhn&RB42OmdwDQuh`-_cQs}z`Ef)5n&EK zg|wB|lKT8?BM=#^O)G~0-t~HJTF#*9$F%U2H?W9f>(Cz@I4UP>`Fe4QBx5G1#K9jA zQr}^EY&dRs;U35@AX5>+z_d)q$f$e^bLy@{fU>EF?N(OygnHSrhQw9i4i9#$Y&tfR zr%80peodzL_oaKUlBpVFnh^GC%9I+ES+XO!gi8Q0pIwXSN*Iya6TMMjikQ@B7!f!& zt-S^B#?5BX`mFBl66vCi{)Mc;gV%>>$=|Iuk%!w5IZ6Kc{U8x@Ti5A)ZBho(E3E9h zZzMf}^;2;|fC<3n>0{_=sm27-s>Td){co=HGztDi#B{fHB!LxW!}g;P6Usjy+b^3V z%%1gkPn(D2?+6nyZ{Nm7Cr%It$-#j9DR3>Ss8H70ziGqAsj~^Za!1t9Y78$UaSaW~ zs_o4_a8ve=RpoodxdyTGbw=0AzPVGP;k`{Q#EDWjA{cNYmr}Ki$;FrQqo2Tf=rL&Z zq^L|dV|$pWkd^X;DEfrZ{K2!Iq(sX2>!+Ixa1KH4q}oW(>G0iVe`{92 z?jVYz?6{irO9CpR(n-5za3NcPUskl)KYrIt!s=IuYhZ~{!Qk$5A}^uytD%2_Fy0zT zYV2%Ro3HOwsWGv(Xyf}6tsl^jIyD1QfAA-LIQr~kw1eV$7)QPPKZJ#pKClswUH&ky zN)TI4&M>Af)gPr=`Z7c79E5Z!$EZNveqe&;PCJidMZ(H26I;$o#U34<)6p}J zeY^m$p5N^$5_GtYUllfR*(FYS>scE(`={G?r-dh#A zmvA~EBNWXCt$Q~nktR5TNFjtbjpr*eX#g=necZd#;cr4~=nqwvl>TmM*RuB6dC8ge ztAY;t!}1Xcp|aGfJ2cN589mFhp$Hg>3CybW(?zPYA%xJdF@3H-86S$B_>Sx962+M9 z9`woAZvS^-6d!&)ghMw-eK2rG8$a9C+a<+lr~M_L=e8)@C(Lc#h9-I{=W6+O--|{9 z&Ub>Qb#LbPgjaisP>Yup4FOx>xdXGJQC@X6faM`qO@&!*A;%Z(0#8@wJVdEliEFd^ zuw)4!eCAtsX(!@!+pYk>^TKw76}081gvb~US;~<6xUreOpO@cDU5PF{LC`@4GDTj7 z4H%7(ElpktqNsq@ZD6YC9Npg((ldsbvym`b^OT=b@9TCP(qt@NtUYoZKsVzLjKyHx z6#o;8*j-h0KW?tt_Luw&()u;vfWe-xZ{2I+Jxi)Tw$nrsMur^(*~v7-z{q;R%IwkB zNbr2r?RDW-S-}T#=c9Mr0_KyL`YMx{HgR+Z$lx0Ukh#|FY}&@(OEh-UkeHhr*iUp2 zGit+9xf32wA#xHw&F%t=>46ZUGHr;X)ERL+(R)>Uh-r-Lr0p=BVaq?dX|0KjGV5wY%-gMqyL z#_J_#S>`hazt)o5G1g4PRv2|5q63?8sc4A=is}WFV@d8B0n_lfNlA;M_zSp4>Wbww zy`=bzm0dPmyEI3;){`rd}Swmu>3ihcGHdg5z<&+~d! z!a-v5O`FcOO9}ai|=rIC3q14Ge}%9z8{C` zQ*t+&Y`q(7atH{_#yRe?yVqYnxkP&iQB-+qv5!n|SdRKBu8=G0+x*U)LR;^5v zDWHJ2KD-x*kF((gm2uUb)io1ZyMB`Hf*PZ8FG4pfus*P0MT?=R(y{ChB+t=}`v zbeZx^pq9=JG?3<_16td9J^{|41h2@}v=z@|lztVO=-Mmk_J4q1@X&V_k>1Q+|&P=WYCISiqaC-WYYaxH}@UdOVie5T6c@TnQ_q}^GAt_)$iqTPtd)> zLnl?{pqb-6;~W{-&i^vO#?!o@P>OH}jy7Vee~Znx8+BhTNUsB$GXNdP?1W(r`%q(b zY22T_aeN10P{c2=8BL#Yzqc3OKopuNvgeI9rctYvy#4;zrc#Acf;mLd181%vcnwAo z0skgEmFuQNIadB+b?2eQP-CW~*U~499q;tkOFM0(LQ#rP_bs)_)QfV+)f1~F(ne@w z)Z5)<7=<~F2@PmHqLgYm#NkL0gO6Wg66P0RkryVw9Cz7`=k&|dn&k;E@A4dT_qmLJ zG&9tJwX$i}?TZ~85e@y(d2cNx+x2J1vs?1KC$mK75!Iyz zTlKviSJsIs#TCUP7LTjVzf}O`mT^`MSJjJuVSF(#_`=6PNx@8 zb?MHAqZN2#=&?eehXQ@F#7|2v!*YCA2ibR1)VUn_&P<3%b8w0XPI{NH5a~CYNRc|{ z*~Xs^nM;l#YBR<9v$)#yw({*8NDs^ml6w)y6P`E4IB$bjkM|1w(v0_mXbSncvga09 z@@3k$W#O)#_r-KP2DUDf)PFQQBqoZHRQ%%O%rX4|>`%ub7Ck70^AB_i>U06nyY(kX zF~NDgf%;&g`ea3fK@-h}wQPJuvBB$tF`XmjwW%Y#biXJj-D9<~=A-Fs<^FgoJazOo zh8@9Z!!NupmZc#)FN}iK+0UMhL=!d{GomPw^v`c>Nlp4Um}>8yZmpL?-$VpQAo)x} z+ZX?d#b`ILTj>7)AktgM*_EKAHQ+vsrrx)Z}dxNI$)X77%0z%G9Sp9oVCi zz&jjD2bZ9}w}EkOJV^iHA&y3Jy>S0X;u{-7z~DrLbwSsN4=$+@C@2A89*k3rm{W1P zJ4u@LPAG~kvBv(HOl=W;R5K2)PT2B&EY@!4LkQuh$ldFjz~wP3TE4c)*WTy*;C-9o zkB(h%sTg8|4cTsCRr9i<6f75`a%(a@F@muBt-?PyvSRs@8!29oJsUqOzgR{!ceI7- z0JzGHAo;{3eL}=btV2-)!w>#ng@!&-96IB$qw-ST)k>8lW7GH2P7|9W+t>KZYwf{( zX%h{f0%`e)m-@frjdi|(t-x{pE`UN&PqjqzZ! zV%Fo~fxF}`hh_Jt75Q<4i*u!(4rm3ZQyj!DUuC{)5}gaVDvwfF7m|)sR^}S9mF=eV zZl|JouRtHu6MV=0KFq~hm4v_$wT5dgFYdy$G&iMnCFEkk#{S#^5fTWI^0 z(l=&Ra9}Q>iB(mM)%s2NZQw}vE?{cY{AS!8AlvgSu0YpC_v zX0OT#YoQRxo5gnBO$z@uL{qh0ZF)S#BkBwE1C9*);d$7#)s{F*Kr|!7CX?7oca)&% z%lXdpUg7)pO@YjHXI|g&Zv)iWqy^1Cy@u|q_LB)ypW9yF_K_p}HRC}TU`8r0nBaNl z&nBW1xFLT`4MG{mq3=I_)#pR*4Qm)@gk^AUp zrE2m~v(}%R(Z6&veEL`WOlgM7S(QS67&1UY{RvxN8W9GTaBZj-n^b$N@AdIki?RP4t>4 z{yE=u9VV~%j;?Id64lAS7Qt7zXE6GZce4nbNge|$V#VRH(*DWaqOQEZd`lNkE6F*~ z_2Coi?avQU_uTNhu2MO;iLhZZ1pD_H)qj0W_RWVVhR_@&Ei1C~v|ai=wC$+c?}EAN zb^*c4u>_Ss3#yAjubct9}VuxN@aVuWkgL?MMBrZ(oi)I zn6MsB*4djN;~!<~ac1Y@;F^8vv4a#|qv29ad3K-rPy7$yR`THA)4zJRe|~!Z=)Q}q#-TM#A3|UUXj zKp{wLWH5&+PoXfzT@fhcSuM6*?)zM{)rF{69;Nq{F9|A(9Uvw&bW&n!)F;DVOi?eV zHE0WCJy`rWme@;&Am%sFtL%WUJujfz*?00*4#%m{Zc zFcw#kF(;*_1suDiUawO^ThFz`ak&3dKjOoLXAL@)FA`n)MZ0rS0&G>1_G?5$VZ|pr z-h{>f1lzo7TIts%81<3mxVH_Y&jla_C_6s_6uj0R-#<@UFNYUhyA)$){(B_0b&!R# z+&@he7gWHI(0%FG6g)JPFYnZT45?!&RiLJfv7<#u3tMM?>EU2q+KgpdPOY93LAg)E^s5hG5Ea^Dsha>w0;?bL|WfAChCDF}NXFU)*EZk*H*zla7@q+8vANRJvc2{$KR zta#YD6`_(Zsd17Pm3cu;K$Wq0NpT#yWzJoyCs)FTvhx<$=oI1_oDtpkez6(yC;S%^ zaQs)G1mlMoY9j^*1IZDd!~~iXI?BFD+8B+)c!Dh6i{~Qav65Vm)5d3OKz+z=hvbi| z-6Jg})-CY^EMmy6!W|FU+Bc#PUf?~7lL7l}r5H2>M2shh*uL_Kqe7=tOf6cvJ9B;W1(acpPgc1Z$|KesPz zZ;XQev-b#yFL;*SGb$(Dz`J@Pw;=q4fLg7M*PU91{C6CU zIh-QXc{tNALdRJxD4&p>j5(eS-8X=3mj=XtImiXtV_eMyAm5Om>IhElgATqcmE60h z?jm9kmBln(n&wA;V5s=rD%{5a46}!sm7_aSiz$4yop^2BW%R6!3I`V9Q%BVM@OI00 zEb9NJ03~E6TmFf|FGb^=RtbLQyCkW?{`G%6>sS59A4znXg@eBtHJuj@Q@i~I;unO* z59pSeLol7B_K?9b{66ixuMDp~0{NCH$yENA2~vZDGQ`?IoiJLVTpD*`0kn{tTrT%8 zwEeu#st>Oxg(TfPX8!QKn_-a}7n6%Um^rYb?EHgvt%%|K9%}1-#`9C~%L3Juag;5C z7sFI4qSu@_LPjL`XDlz@>)28Z&r|M&@@l0jz8mCISj5i~ z6pj8=t`k?LHT;DKk9qLV52jGG;IAC0>%nSZ=Oz#HMiw7drB+e0JAlZG@AIl_BRl)0 z>oRStnrjE+_0VsC2m}P?kGQaa5(tk?b|60J`MjvtkeTaUv}AYoo)?8d+}ur5o6Mps zluOOAkm>GQ2S}U>80<-IE@Yb|)J#|-LnD8Q{Q0Snr+i*<9$s{`sX(JE$N>GafqBO- z=UNaK@nuhtTA!Ms&+Ef09b=A+aGZT7g45K^r-*KvZra3=fghFC&QeT^x9ds#m5JAX z`FVdG!(YHad4y5n8=#2)*3~nx1z}p1RR#);fP8!>UrlWIG*h3|IHdi$H+!p@s}CKB z>vb~S@-26rs%shQI@Om|t?$w(XIe5_xEfiMNdx?5aghk36#|*8j3S6kyh!XP&^7Wf zYbJE8+Dq%JQ!A4FB$O}x55DGGpn3x#9Ez-q$#rahNlm+K$0#K%kvxOxA>$OlviC?Z zVG%-vE_4vteD@eZbWj1zF?K3CYjuB9gap4Xp(I_t)(pDbbaGlxRDEXAT{*aYH!F&c zQXn5e=BJ^WF9CZGh1EKoTak9yX9U^-t(P^kn{6d+a3u|oZGRnxvoGA|c#^QT1g_R? zv12|~6qFQL)4-DovNq`v6t+j91cv)(!ad{kTF(e zdDufCY#6kSGHrH0*fEFJO5!SMSbNQ*i)|qS>9_z!WgeMbC-6a)QXZ}Z#S%?vFx!(Q z_TVVU<)opNS4PxctqXeA{~==o(qZyf4N31EaRR|EzgE-%#_ZS2df8*(yTs;_D$D!* zWLXIG=xFRl^XuNn1!XZoW}?z{X6@_yg4(v8cmYrGj4IKo0P$Tn6Nn4EzcK8PIU0a7 zgneeYztS165sR;R3B@SG)Zq}QPJi45=rF8C5nJ^FGg@k|pK=(2+*ba@V3waA-j?uZ zJwTHb9-1WgUEiz+Ra>hqDe^RNEfpuNWycNH#b6D?JSJYsd5z)!HyvME@MBttSqYmD zM*|{hVFnZ8`lp}R_nMOox{0hzX<-D1eAF2THSU{_tv0On`^ClCgew`EM68y@w5v3| zx|&d5`g9EbMw%TiF!3Q$e8h#ESR=laVKs~f5nsDw+tTfJ5I;hdFjoBw<}C|r_Z;T1 zFaeog)KXqA*jRu_P0m}(`)E^}35-{);)a(x!@!_0RPQQ`5=of9Nl!+Y1Vno_KZ(1w z`A~2WM2lukSldA>f(ID5C}dYZ_KGML1JRj=!$ou8c@vsIRxevTFNqI3+TIt(0sdzS zf>P)z?e?cr=nA<#j&g;x9uj!uhOy%B?s~Mh@glP7sxR))kk>9o1KKi;pSQD+sv?=> z!h85wRToOY7?I0?ERm$Y1ret>fuohBEJV%p`nqmaKlO_(+Hxhg#ZtlFyoC2vI zi`>{CWr_UMm5eAj1PC!!tr|>q$MlvM)LH>t@(m0**F?9`K6>Vo@Kex~oRc-*5TYfS zRj-z}7!Fu?PS6=nZho#sIT;ieV(Q@`{Xn6Jn5L%8X|5;UO0Do2}vyKifavKCLS? zK{|~C{f9|PNC1Txa@6X6y58M9c`nBGqawtU-ge{}Z79HZK3FZzmv!;+(YSGxJ>Pjn zn)Lg97`Yx@1Tt8A5X{|rniyyQ)42QH7qS(S5eILdG*Szv`#A~&B+)eKi02Szjv}3Y z+3CjHa(O-&Xb(S)l#6{?yioSD zyQ9TyS>vNg{<<-2pd&=@(_;{1T}-UM5uMeN4hHq{C`GWJe}d%8rAl$zze)Hd;^qEw zk*{-QZ_1PFi?zIRuRhWRr;K5Loa#wecnS&Wb*l^?z}MP}f0LK^-N?5vZFe-z^1ABg z{NIE?&^(gCg!G>)k0>lYlk6-W!kx9d>^u9vl3AE&fcQacj{6x9n z*GPR0FSm-BZNe7GDOVrX+NpdZsXNt^dr+7M#p)sfhSlN>6_(HPXH+BwX}p_!WzOgLVEZ(uSiGeVkxQ*g0f(&I}0j4Bqt4hf&QcBsj)lO)OH-;4zXcRGj)-$76uw<7F#XWuz(1vM8b#+P)yn z8O*u3+h%0KS$VRoXIJP{PxzNxTJ3OlFIX=|gD={@du-F!w`jIkVogCVOZebYj-G$o`R-O3y0|fXJ7m*RJ64VR$AG3GRlK=n! diff --git a/android/app/src/main/res/drawable/flag_eu.xml b/android/app/src/main/res/drawable/flag_eu.xml deleted file mode 100644 index e545a72a2..000000000 --- a/android/app/src/main/res/drawable/flag_eu.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/drawable/flag_jp.xml b/android/app/src/main/res/drawable/flag_jp.xml deleted file mode 100644 index f7b9fe6bd..000000000 --- a/android/app/src/main/res/drawable/flag_jp.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/flag_us.xml b/android/app/src/main/res/drawable/flag_us.xml deleted file mode 100644 index d83fcb9e2..000000000 --- a/android/app/src/main/res/drawable/flag_us.xml +++ /dev/null @@ -1,539 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_album_24.xml b/android/app/src/main/res/drawable/ic_baseline_album_24.xml deleted file mode 100644 index 58c0ba697..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_album_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml deleted file mode 100644 index 2a31b2ef3..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_category_24.xml b/android/app/src/main/res/drawable/ic_baseline_category_24.xml deleted file mode 100644 index dead295df..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_category_24.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_close_24.xml b/android/app/src/main/res/drawable/ic_baseline_close_24.xml deleted file mode 100644 index 16d6d37dd..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_close_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_create_new_folder_24.xml b/android/app/src/main/res/drawable/ic_baseline_create_new_folder_24.xml deleted file mode 100644 index 78c162283..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_create_new_folder_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_delete_24.xml b/android/app/src/main/res/drawable/ic_baseline_delete_24.xml deleted file mode 100644 index 3c4030b03..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_delete_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml b/android/app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml deleted file mode 100644 index 22560a4f9..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml b/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml deleted file mode 100644 index ed42b700b..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml b/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml deleted file mode 100644 index e3f30c6c7..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml b/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml deleted file mode 100644 index 81f79b322..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_folder_24.xml b/android/app/src/main/res/drawable/ic_baseline_folder_24.xml deleted file mode 100644 index bbfe9c931..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_folder_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml b/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml deleted file mode 100644 index f58b501e3..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml b/android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml deleted file mode 100644 index f4a8a8711..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_grid_view_24.xml b/android/app/src/main/res/drawable/ic_baseline_grid_view_24.xml deleted file mode 100644 index 6d4f4a563..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_grid_view_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_help_24.xml b/android/app/src/main/res/drawable/ic_baseline_help_24.xml deleted file mode 100644 index c0c92681d..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_help_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_import_contacts_24.xml b/android/app/src/main/res/drawable/ic_baseline_import_contacts_24.xml deleted file mode 100644 index 99a23c4db..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_import_contacts_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml b/android/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml deleted file mode 100644 index 18b306228..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_library_music_24.xml b/android/app/src/main/res/drawable/ic_baseline_library_music_24.xml deleted file mode 100644 index 2ba294bd7..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_library_music_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_lock_24.xml b/android/app/src/main/res/drawable/ic_baseline_lock_24.xml deleted file mode 100644 index d6191026a..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_lock_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_lock_open_24.xml b/android/app/src/main/res/drawable/ic_baseline_lock_open_24.xml deleted file mode 100644 index a11b70e62..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_lock_open_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_menu_24.xml b/android/app/src/main/res/drawable/ic_baseline_menu_24.xml deleted file mode 100644 index 4350ba96a..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_menu_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_not_interested_60.xml b/android/app/src/main/res/drawable/ic_baseline_not_interested_60.xml deleted file mode 100644 index f5f7da3ae..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_not_interested_60.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml deleted file mode 100644 index 41f4a52ed..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml b/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml deleted file mode 100644 index 67f9b5735..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_radio_button_checked_24.xml b/android/app/src/main/res/drawable/ic_baseline_radio_button_checked_24.xml deleted file mode 100644 index 05a669ab2..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_radio_button_checked_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_radio_button_unchecked_24.xml b/android/app/src/main/res/drawable/ic_baseline_radio_button_unchecked_24.xml deleted file mode 100644 index 2d5445cbe..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_radio_button_unchecked_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml b/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml deleted file mode 100644 index 2f9f24ca9..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_save_24.xml b/android/app/src/main/res/drawable/ic_baseline_save_24.xml deleted file mode 100644 index 955858d7d..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_save_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_sd_card_24.xml b/android/app/src/main/res/drawable/ic_baseline_sd_card_24.xml deleted file mode 100644 index 84f6ea6ac..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_sd_card_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_settings_24.xml b/android/app/src/main/res/drawable/ic_baseline_settings_24.xml deleted file mode 100644 index d99a8ae40..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_settings_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml b/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml deleted file mode 100644 index 93d588228..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml b/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml deleted file mode 100644 index 27f451658..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml b/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml deleted file mode 100644 index dbf6358f8..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml b/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml deleted file mode 100644 index a9600b74e..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_view_list_24.xml b/android/app/src/main/res/drawable/ic_baseline_view_list_24.xml deleted file mode 100644 index 9884aeb83..000000000 --- a/android/app/src/main/res/drawable/ic_baseline_view_list_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_a_button.xml b/android/app/src/main/res/drawable/ic_controller_a_button.xml deleted file mode 100644 index 74124b194..000000000 --- a/android/app/src/main/res/drawable/ic_controller_a_button.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_a_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_a_button_pressed.xml deleted file mode 100644 index 302d83cce..000000000 --- a/android/app/src/main/res/drawable/ic_controller_a_button_pressed.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_analog_base.xml b/android/app/src/main/res/drawable/ic_controller_analog_base.xml deleted file mode 100644 index 85168053b..000000000 --- a/android/app/src/main/res/drawable/ic_controller_analog_base.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_analog_button.xml b/android/app/src/main/res/drawable/ic_controller_analog_button.xml deleted file mode 100644 index fec0a0a6e..000000000 --- a/android/app/src/main/res/drawable/ic_controller_analog_button.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_analog_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_analog_button_pressed.xml deleted file mode 100644 index b08bd5e16..000000000 --- a/android/app/src/main/res/drawable/ic_controller_analog_button_pressed.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_analog_stick_pressed.xml b/android/app/src/main/res/drawable/ic_controller_analog_stick_pressed.xml deleted file mode 100644 index 7304ff569..000000000 --- a/android/app/src/main/res/drawable/ic_controller_analog_stick_pressed.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_analog_stick_unpressed.xml b/android/app/src/main/res/drawable/ic_controller_analog_stick_unpressed.xml deleted file mode 100644 index 121c376ba..000000000 --- a/android/app/src/main/res/drawable/ic_controller_analog_stick_unpressed.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_b_button.xml b/android/app/src/main/res/drawable/ic_controller_b_button.xml deleted file mode 100644 index 8f25810d9..000000000 --- a/android/app/src/main/res/drawable/ic_controller_b_button.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_b_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_b_button_pressed.xml deleted file mode 100644 index 353d7e5f2..000000000 --- a/android/app/src/main/res/drawable/ic_controller_b_button_pressed.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_circle_button.xml b/android/app/src/main/res/drawable/ic_controller_circle_button.xml deleted file mode 100644 index d9fa460c6..000000000 --- a/android/app/src/main/res/drawable/ic_controller_circle_button.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml deleted file mode 100644 index 47953d0c1..000000000 --- a/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_cross_button.xml b/android/app/src/main/res/drawable/ic_controller_cross_button.xml deleted file mode 100644 index b133bd616..000000000 --- a/android/app/src/main/res/drawable/ic_controller_cross_button.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml deleted file mode 100644 index b00988473..000000000 --- a/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_down_button.xml b/android/app/src/main/res/drawable/ic_controller_down_button.xml deleted file mode 100644 index 898f2c54f..000000000 --- a/android/app/src/main/res/drawable/ic_controller_down_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml deleted file mode 100644 index 4e7d5a35e..000000000 --- a/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_fast_forward.xml b/android/app/src/main/res/drawable/ic_controller_fast_forward.xml deleted file mode 100644 index 37e89e29b..000000000 --- a/android/app/src/main/res/drawable/ic_controller_fast_forward.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_fast_forward_pressed.xml b/android/app/src/main/res/drawable/ic_controller_fast_forward_pressed.xml deleted file mode 100644 index 2a505aad8..000000000 --- a/android/app/src/main/res/drawable/ic_controller_fast_forward_pressed.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_l1_button.xml b/android/app/src/main/res/drawable/ic_controller_l1_button.xml deleted file mode 100644 index 9f3ab7240..000000000 --- a/android/app/src/main/res/drawable/ic_controller_l1_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml deleted file mode 100644 index 06d625fe5..000000000 --- a/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_l2_button.xml b/android/app/src/main/res/drawable/ic_controller_l2_button.xml deleted file mode 100644 index 3853d103f..000000000 --- a/android/app/src/main/res/drawable/ic_controller_l2_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml deleted file mode 100644 index eeaefb18b..000000000 --- a/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_left_button.xml b/android/app/src/main/res/drawable/ic_controller_left_button.xml deleted file mode 100644 index ccd46c3cc..000000000 --- a/android/app/src/main/res/drawable/ic_controller_left_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml deleted file mode 100644 index 39c1de9e1..000000000 --- a/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_pause_button.xml b/android/app/src/main/res/drawable/ic_controller_pause_button.xml deleted file mode 100644 index 9b64f42b0..000000000 --- a/android/app/src/main/res/drawable/ic_controller_pause_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_quick_load_button.xml b/android/app/src/main/res/drawable/ic_controller_quick_load_button.xml deleted file mode 100644 index f4197876c..000000000 --- a/android/app/src/main/res/drawable/ic_controller_quick_load_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_quick_save_button.xml b/android/app/src/main/res/drawable/ic_controller_quick_save_button.xml deleted file mode 100644 index 2a81bac25..000000000 --- a/android/app/src/main/res/drawable/ic_controller_quick_save_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_r1_button.xml b/android/app/src/main/res/drawable/ic_controller_r1_button.xml deleted file mode 100644 index 3130def38..000000000 --- a/android/app/src/main/res/drawable/ic_controller_r1_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml deleted file mode 100644 index 352ddbc6a..000000000 --- a/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_r2_button.xml b/android/app/src/main/res/drawable/ic_controller_r2_button.xml deleted file mode 100644 index 195fbe85d..000000000 --- a/android/app/src/main/res/drawable/ic_controller_r2_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml deleted file mode 100644 index 640c36863..000000000 --- a/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_right_button.xml b/android/app/src/main/res/drawable/ic_controller_right_button.xml deleted file mode 100644 index 8545a61ce..000000000 --- a/android/app/src/main/res/drawable/ic_controller_right_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml deleted file mode 100644 index f0cff05ba..000000000 --- a/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_select_button.xml b/android/app/src/main/res/drawable/ic_controller_select_button.xml deleted file mode 100644 index bea56389c..000000000 --- a/android/app/src/main/res/drawable/ic_controller_select_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml deleted file mode 100644 index a34a925d4..000000000 --- a/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_square_button.xml b/android/app/src/main/res/drawable/ic_controller_square_button.xml deleted file mode 100644 index 0da658c01..000000000 --- a/android/app/src/main/res/drawable/ic_controller_square_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml deleted file mode 100644 index 248a1d400..000000000 --- a/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_start_button.xml b/android/app/src/main/res/drawable/ic_controller_start_button.xml deleted file mode 100644 index 7247bbcd7..000000000 --- a/android/app/src/main/res/drawable/ic_controller_start_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml deleted file mode 100644 index 5a8255577..000000000 --- a/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t1_button.xml b/android/app/src/main/res/drawable/ic_controller_t1_button.xml deleted file mode 100644 index 2f342f01c..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t1_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t1_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_t1_button_pressed.xml deleted file mode 100644 index bf3ffaf35..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t1_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t2_button.xml b/android/app/src/main/res/drawable/ic_controller_t2_button.xml deleted file mode 100644 index abddf7071..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t2_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t2_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_t2_button_pressed.xml deleted file mode 100644 index 2b07da75f..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t2_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t3_button.xml b/android/app/src/main/res/drawable/ic_controller_t3_button.xml deleted file mode 100644 index d9b001f00..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t3_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t3_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_t3_button_pressed.xml deleted file mode 100644 index 78465f7fd..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t3_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t4_button.xml b/android/app/src/main/res/drawable/ic_controller_t4_button.xml deleted file mode 100644 index 87f5e6726..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t4_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_t4_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_t4_button_pressed.xml deleted file mode 100644 index b7da1f1b2..000000000 --- a/android/app/src/main/res/drawable/ic_controller_t4_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_triangle_button.xml b/android/app/src/main/res/drawable/ic_controller_triangle_button.xml deleted file mode 100644 index 9a1392988..000000000 --- a/android/app/src/main/res/drawable/ic_controller_triangle_button.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml deleted file mode 100644 index be10c7a85..000000000 --- a/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_controller_up_button.xml b/android/app/src/main/res/drawable/ic_controller_up_button.xml deleted file mode 100644 index e8b5a4064..000000000 --- a/android/app/src/main/res/drawable/ic_controller_up_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml deleted file mode 100644 index d17013321..000000000 --- a/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/ic_emblem_system.xml b/android/app/src/main/res/drawable/ic_emblem_system.xml deleted file mode 100644 index 4be483cfc..000000000 --- a/android/app/src/main/res/drawable/ic_emblem_system.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 0d025f9bf..000000000 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_media_cdrom.xml b/android/app/src/main/res/drawable/ic_media_cdrom.xml deleted file mode 100644 index 9269ea16f..000000000 --- a/android/app/src/main/res/drawable/ic_media_cdrom.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_star_0.png b/android/app/src/main/res/drawable/ic_star_0.png deleted file mode 100644 index e5b56db70af52fbc1629cf6131d3b6d5eebdffc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5534 zcmV;P6=CX$P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3&sawIv9M*s5^a|CW6z~gW=n;XpW=fT6JtX#UP zdu*i4loaxCCxLGRGo1hV?;HM$k6N;YDr&8_7Wt^X_B{B;;^*V@HO2b;`+T(bSN#0b z>mhx;Df3d{YkA*r-s=ai%a1o`eP5qHzaDhG_p$dz?=QY?Sp3PEZ{F+dy-~@Z*VFrR z=>v-Qi=R){*?0hJNPU7n@Z!NR65_+qWoJ* z`g85V_qV?k?`wg-)-I0sit=+Q!gm|mFEx5!Px9y5Jr(+?YhT~ypMSA)yz#Fef34l! zcJKM@Zq!DlT6i}V-&;K2aUtcb<#}7%H;RG#`ipW(Z$KR&#% zg}cd9W@CdR&d)8Tq#xKSPoE3dNvz>TUef*EfCcevVKJ%Df!I=!)Kb)(H6>RJ`ZHL0 zE;Ua&kVNuM;nJG|6Es`)XY(FS&$U_d=VhdYMo4Lp#GEcPt5yL&S`7>hDN=)oG*L0> zGFoZ1No!4;Yt+!9RYjYsnsyylteRLeHM4G`=U$L7>8)$%)_Wg=hYoH%c=zCnF=n1+ z)+w`1oi_U%3;3+MWVNNsR$pW1T{iUJZEM)J`yMA9pmgey(~cfH{frCNZn^c!ZC9_| ze#dv$-md=Kwe(Bp{=RGJ?V5M2KG*4wuJN+!*DaFEiFnWGSgwhVt9JlEXYX0GRBQIm zdCy|ckU}zDOXAJh$vZ{|>q%K|`tIG2&i!rQg4F-0Z~0$!E_mzyKROq@b#Lc>@7o`` zw#VZrVK9mC4i7Hy-SWwXgVoOXpxS4B1l0xA5#B>hX!%7fJ;AC*0m z&S6cgC!v9<$Fpi$F4I72H;aWGvuK2FE+8Z0Y~*p*6cAA_eX?bWi5+02GnO-D-Cb)# z+XdQFDVerMv;34{re__E7i8qMM>zYIO*f9wgw&B&&6R3qQ1~u_-wg_-TBR$}acE@k zJME6SqUazgS)dT-L>(gQsj)9NNzc^yvf4k_7_}O_dG#uD%UPxNFbiGvhtJgIu(7eV0MtY!A>xvREGYP>^hA&S|Taa);4_JOB@1?c3Mw-cwmh&H_i{QtiwBZ8rMW_X#q!&)dNFR74TwSaa#oIm@*M?ogSQ z#!x~{9XJM1LU1ziWzZ1q?To@TD5RITcl4&5bk!>B-whjfRwTG#1gX>zAe;sbE9vmo zJ13c(hh9UTw&-ygSX%Kp!0rtK~&qPb;G%t1OdRZkH>y zB1m4p9%78bjmA>J4^|RU?XI)x?iYB*)@hlQ$SR%HTo?tKV6N@~ z=m1AAcW($~OK5~rVx%l=Dndj#qxM~>NjgGZcPS9V+khTV$`3boS#4&h0ps9As`yi; z!S{?V=+QtHKs@Hg)0C$}?Xz1snjc<_1uu}NYzX(ui!}KT;5aS%=~Hb5*5D7WPbKbL zMOcY-T1*Kx?zElvhUj*wdzOlOyWW<*SL_Qaz(I%+(BlHcAXh01c%%%iXTv@h3Wd~c ze+2S*i!H_D7TSMiM)Z=~Rns*lfC%+CFo<=NuZacIY(AF)K^!?Z(CwR8OP~G$HE-zE zg;0RMTPOJ4&2N;kZJe3*Vf2f3@Xd{5ZhGiAybz)i8z}by+L?t;ZFf2VOb8R;?}khR z;v~FxY~#3SQ5a)dlzWf>B&)?ssSRpuWDdO0l7h@9g@{;J1K1TAShl z{|HKYFn7D6sHsqVpV|iDETRimj5aHKFl;hROhT@6G<+Ev_-MxEwnCbrkaC}D?B zh~$Vwy8t;Z3c?@V41X5R-*vMgl#zDL54{zdvtWYD^bw}Fka;gv%gF_fI(-CA$8G

SfiygqJZbx2IknOAC&b(% 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 diff --git a/android/app/src/main/res/drawable/ic_star_3.png b/android/app/src/main/res/drawable/ic_star_3.png deleted file mode 100644 index 330aefbaca17c6dab21b9e1076e6238eaa269ba3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6526 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 diff --git a/android/app/src/main/res/drawable/ic_star_4.png b/android/app/src/main/res/drawable/ic_star_4.png deleted file mode 100644 index 4e9f58dfa3fcc1b8243282b1beb14112b3f64606..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6312 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 eR8!6ClK%mVDD4Shew>d00000laD@ diff --git a/android/app/src/main/res/drawable/ic_star_5.png b/android/app/src/main/res/drawable/ic_star_5.png deleted file mode 100644 index aa5707ea750c04a2e1ad86be3e45c34856301def..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5990 zcmV-s7n$gZP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*tavM7mg#Y6da|G-K90zN}++dDBUp6WES+d_U zF;-ZjDY6@=%E|?_JOBCjb^qc|wi;b7)@rZS^C$P*3b23ntw;^5Fft_P$Vxujj-2 z->$#kH}W60^Zt8Re12W#>sMd@osDAb!u3*g@uZME-<$4|UVjIF3*SSkyeOp3$)~J|BK5-p2yJ*3OUjE7H%UaG!0cUuyJzK9pbA?y1mEUHf<#fBr?w;p45}Yj=0M z_q=vDa-~w$y*K6Gmv|oWLdu!b^R~*D@wf2Up0CbVV~dN9ZMr%6S}ybusrC&y>@dO& z=lQw9Vu?APX#9+E#q>PalEofZQVX)a!i`2Vwxbs@ExML?I)1JtJooL-{T86zeg|F| z0~ZUtk%Jq@+ch>~hDTEn=|tegw@vE0DWD5+#uN~y(4 zE4_@GYObZ$YE>LHELpZ<)y$f88!ffmN~_IUYrTygdxFABuiad4z4tM=>EP0XcMo1M z#*8z~Jj<-pW}AJE1$pIrZ3Sr=M}b+D$j# za_hC*ZolKRYwxc9+_mtfbAR5o@a~#AmY>(zkFN2w*~cY<=%lD;bS&mT$4wmo&`~|} zE#w^4IrYr<$WW9>*FsTqc2LLYU_K$18$NsYqjP`SH&^mM>RbF>=bT#i|LB}k>)xIF z-nUWh1ht(X(P5FO&W0yzs(laFC&y5%gz@%(q(;HK2yh&TV#ph zbS7!^z|uhW+~us4`%K(1P8P6tsCD~snIWpv_U=dx{6<|vi@BZSZBWTVy>>ovl&~ES zON-BnSdxC6#x!2v_pWo+d<~RLelv2bDTdwX8%R0J-?3>OOBxp3seR5EM&4%`6)BcM zh_!sqEPJvAT187k0UDwEvvODx0Km$fvs->KLX1cbX|uCDfNQ92Cf;J>*6Gu#G(Yt2eCKr_ zEHar-`C=)j&t@~{E7h8Fx4*Gv_XQbcMJ9w6wWCMtS$Zj2yw={zYwcE7KWBK>4giq9 z>T;SQc2NFm0ORp)=x0CcMo|9>7w)&f_=<~pM~SDmZF!|q?yT1Fe`ntr&)#kW$287i zuWenrbnYDaF0|0E!D`|P*2g?4mV9oni4o435nuJx1m>;Y+PXV~vh>-(b@#T?xqh@8 z>Y0`{>|$$%uqW^6ez()sNE^FiT)M%3%h;T)Vh-q{YRq4_9lE8nW;M|yx}EMmW8+S^ z!K@usv_M-~*u0=A6@WI|g}HZhvmBOPVZRZfm0e-n6Cx^LfTlQ`VYXuaC54*cBc26K}A5@nVN!cP_(SJXF0{39ZF0VI0$739JRS zXL$gUc52KD;*r61E$}K_&y(OkFajK_?5#ZgK4wL2=|C$G1xpK9e z#&z-Tk~f5v;1HU-2ZFWH4_=lgm^urUIhVIowKc0_52~m%Yy+y$jnLeUz4_DoEzhX6 zwu{z7h%1ORn3-!As_1Fl)PUH9?+^5JEM#W#0CJ<)wF3jj1uHNx@MXffx3BX|>C}`? zqsMvNS+1tE0!FsKHKgEkKSOEw-UCP=f&>E^Xc=9T~hZ*UkP=Fc&Z38vK(fE=DM&i63L4?d`dHb!?DXU@jykRjcTC&dmesyMuvY5s@Kvs zr8W~)Cj5|7T=lJ#N%P~yI9Z-h99)Y4XLVWni4`N6)s0^Ra|cR=|82=^&}>GrIk*YW z#T3$Q)Db$w`XzbFPPbXWNtMKxkX+1%CG5udqXfLbb+&tHz&8oFb>}GzK|rOTCpEPW zS01zyg{$`~ySqQ5cgm3aZJOa;7(k2^wX;?6L=+P|vo98o4ot^p7(~zJCw{_gGauq_ zf6FqWxdaL@rG`ZYk8oYGBXtN3t2=R|=g|`P5|h8w5@--O5j5rwbA>Y6Uf5Qt`&|HT zIx=n^psj=szeU{imh{rGr&PFHoXh}}59`&ii|j8Pn+Ng?f)151>~U5v+H8K2dSUa$ zA{egY?+g>dsPtdMi()V^%V1o9B$7bm@*#7>G=7@9G!mU+Pm~tJg2TMbbvX7=0<|uY zMgg;N5+=`Mp-^j#(}G~Jr+a6SwL_h73s4VyPQBnW*=89o`Tc`x=CS5JSj4_sBVjDo zl?DB?!tt5X$)~vsD%#qbf>m*IP(2L3Xi009{jeFK+DktuAr15!ZKD(Rq>5eRqT{p? zIf5>5Q&hG=%8&*GNsur5$U24OmSToV;3M({AuCwX0uR0aDtP4O5~*>J3pwjbYUDD51idpPWb@iw57!j2`-ZAcZdn^6h$3~9>+)1>Rm_GuOelh) z_*-P@JjP+13b$g&U-z1U4d?2$OWZEZjfRhEj>7EWa&P_X!sDynJB2YSMr*wku} z6TUg2-phfiy{Tr!)Gh4!U?u3U+{XQ#c&M!3e>dkJh*5nM2LWf zGf!LbtF}>dCz8Mc$tO4h*e@-DI}MWoLHzNxes9fkuvq2iQ*gv3JOEDplBXDO0tKT& zyux7Ojfqyo*`WsagM>gDJEYYlsWJEvh$%$*Wp4I26dPP_D#g6vnZ!X}SjIjXF_Hlx z^2ulr$9bfaC}LYj1fH0paC}JI7%HZVO2{#6JVavZVMv2+>G`eCTUpsXm~TDq4X;rk zcpmSR2!4=~J*Tj7KvHC7(jqn*p$`-SVPM!n!J-rt#oFFWsIFu2@0|r$= zwMs?6v_OoI1hh_Y0l^Syx(W!d4FM&i&{GCvhoWPPr(eVh>WalHbgfr;M3}vfJ@uA3 zu*y(2poho%K-LUHum{S=-jbMOC$c?a-&sz?b|1$bh_*f=b&MigQ29^{l&=lw9>;*+ zqI~ztqZj{P7YPR^K!+D@0z2=!^7Z_vF zhfauK6%kHBX)RzP*zyd6v6iq2!yZeHwFjF^#UR?F|N2eA&f~6F8)h5RUMfpwqMYHc z@knfrc|y++D$~G9s}Mxsia{cA$Z^Wuk$2I&wXv3vDa}9W$$Jhm;pT9A#Z;gzM<)-P zRD~<^=oy9xXN`q~zR9FT4o*XPz+XFX2;ASW4-D=5H0Ut_C<&v=Gy@ytklG;1WI^V1 z5D)QA1DRk}8%vVzg;Jj-V*M~LU|wbl8fr{TBa(G85YvLEA9dmB*UK@+>H~dXDwWe1 zefZ8}@Ucmb_F*p5!$M%1oVZUmCO*c@J3Vt3^-%{z4G!)?*H5q9t_Tqr`d(Xr=`o`c zwDEu)x)qn#AS?Izypj7yL$W@7V=)@Eu#>s3~ zJVm4c4S&@FtpEijeJ|#3YRNFB%-l~0pCRjw$cFpn^5k$9hq@y4xN(#fw*n5)dMq4+ zol~u&L*hC&Qiwrlnt6vU=C#;Ga7j6~W;E%b5+{k{^iT{z90=n|=JHQQE_qc`szEd= zML#}H*C<<#c#k(#J+|-O7#4+j@|Hp&`Hk#2V28#=JU!-f!H1cv@a}vALY#V|00k9g)m;Pm96{wGAIyTzV>kL7 z9>XrbK|hrJ-`gWfLrpL*&7-x{WD{cXS(wxg2~1#Ep>A@NXhdMhuF_IDU7?|G6`V*m zJ`NR^n2A&ZO~G27Y{3qf08T){M(`tpEi1!ST7vly!DDVw9P&n@i@GO#p9HyJm|1I> zJ{b>v^rr;|>=n^!hR4BC>w64s@{a;D=y1`!`kcrArx=~%LZ z6jBS)Ly4~o;bak0o^fPK4J)B-qLE5C#*vUSNZI4t?#6Q}2MJRO6vAPxs!l-^Ry*K? zvhf8vXD1)SR9PohKssT#SB4iLh_w=NTGqXY!(`p@knke!EtSMN^l>H5mX~4UCQ>-A zLBDAZw?7{=)3UFAj5RL0%w1a9Il zhKKy8&N|o-Z>}zTtn_YE`~^wrz{w}X>lv4&kFVn6`!OP@3dz9my9`HqP9Ueu^aXSa z97Wlx5%iI_*T**n*fLI}o~4TU*KxWkC^OOik%kp`t(Nx*EkxvmdTZUg6&yOIlV;7MoA38o8F00X*o7ytHSnTqt`#tdT@C^UC0>3w54Myt2ijxVb)S4#3e)OBt=N5 z4lo7FO%^+tK6nJoyWfNU*O&(#&qpP&Od-{Z+JH{Egky?#pKkf8%t_*9n=sPCpJa$~ zqTO&G`h%Lo@PLOvh?fCsgcs$DVB0Z>GdH9_vqSDkL>u@)XFZJE}zfvFLT)OGanchnEf7iR`!tF+@yA>;&2so}<=luTElXbU>2T zNi@q7!(ei0I-6o0+qt3`QS#FO|*jdAck0A zc>?!eGuj8c5^uw(00006VoOIv00000008+zyMF)x010qNS#tmYE+YT{E+YYWr9XB6 z000McNlirujw1CL2WK~!ko?blCC(@-1-@b7B{3T`H_R%T2T7q)Dv zL5T)lZQF+k+N5UzOU;MsUEdN6tbLqtf#WCuizo{XCsMxtQxB6LMr z?epNK+h|MwMKArHUf+KCyk@`k?G^Cj$B&=?7S>|jSpe+--g~g7Z`llK<=HpedG<8` zvYlr=vs!r;F1#uW09<$*EEk@x<>vmnMsI}a=t!6XxG*{rcGG}r>qZzZyebO-TzDHS z7oM)=)FVQOeYLufpsNcB%7@rj00X*;2ByDJY7o&v2ft&S^!|=F`Iy? zbf0jbkz>%vF&OL>4gg3E_6klKplg-Jo>>k&T`S!uz=5Z00f1eP0N4>~X6IJ&393pW zRV9(G?pT1T1)#EL9R=!Lw}DdgU+i5`Y-Xl4!VfOEey> zYmY~{!1>cba^hH^YNhFCGR7aT(zm(qg?G8)htErD_UBLf9zYJjJpkRlWzD*Jx#bpN zes4mV+c$R}=}K)wLp@__FIZovcrgAAm#%!uXd03XgGt}AX00!Yo#iJW5ZKC$Pu|~s z{P4jhTj1&-BKk{F1JsX3)M&~r_V zME1&>8Ecc98i^;CDv9#USes{-Dv6$1wWlCOS)-|B_x!|6rm!hClPMtCou5$FXo};A zqO4JPVNE}5QvzD`Qn0U6VfgT2d7+_HLDos6$ z5TM4ZvH+mcGqcL`!~KGxrRN!0< - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_emulation.xml b/android/app/src/main/res/layout/activity_emulation.xml deleted file mode 100644 index 12493cbca..000000000 --- a/android/app/src/main/res/layout/activity_emulation.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_game_directories.xml b/android/app/src/main/res/layout/activity_game_directories.xml deleted file mode 100644 index 54bfc2181..000000000 --- a/android/app/src/main/res/layout/activity_game_directories.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index c9dc2d100..000000000 --- a/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_memory_card_editor.xml b/android/app/src/main/res/layout/activity_memory_card_editor.xml deleted file mode 100644 index fa471163a..000000000 --- a/android/app/src/main/res/layout/activity_memory_card_editor.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_achievement_list.xml b/android/app/src/main/res/layout/fragment_achievement_list.xml deleted file mode 100644 index d1a64a0de..000000000 --- a/android/app/src/main/res/layout/fragment_achievement_list.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_achievements_login.xml b/android/app/src/main/res/layout/fragment_achievements_login.xml deleted file mode 100644 index 5ab32fd3c..000000000 --- a/android/app/src/main/res/layout/fragment_achievements_login.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - -