diff --git a/.gitmodules b/.gitmodules index 4ae9400cff..09ed6b807a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -54,3 +54,6 @@ [submodule "Externals/rcheevos/rcheevos"] path = Externals/rcheevos/rcheevos url = https://github.com/RetroAchievements/rcheevos.git +[submodule "Externals/libadrenotools"] + path = Externals/libadrenotools + url = https://github.com/bylaws/libadrenotools.git diff --git a/CMake/CheckAndAddFlag.cmake b/CMake/CheckAndAddFlag.cmake index 226e570833..4fe7d5be18 100644 --- a/CMake/CheckAndAddFlag.cmake +++ b/CMake/CheckAndAddFlag.cmake @@ -21,6 +21,8 @@ function(check_and_add_flag var flag) set(genexp_config_test "1") if(ARGV2 STREQUAL "DEBUG_ONLY") set(genexp_config_test "$") + elseif(ARGV2 STREQUAL "NO_DEBINFO_ONLY") + set(genexp_config_test "$,$>>") elseif(ARGV2 STREQUAL "RELEASE_ONLY") set(genexp_config_test "$>") elseif(ARGV2) diff --git a/CMake/FindMBEDTLS.cmake b/CMake/FindMBEDTLS.cmake index 687994806f..6059dc9639 100644 --- a/CMake/FindMBEDTLS.cmake +++ b/CMake/FindMBEDTLS.cmake @@ -1,8 +1,8 @@ -find_path(MBEDTLS_INCLUDE_DIR mbedtls/ssl.h) +find_path(MBEDTLS_INCLUDE_DIR mbedtls/ssl.h PATH_SUFFIXES mbedtls2) -find_library(MBEDTLS_LIBRARY mbedtls) -find_library(MBEDX509_LIBRARY mbedx509) -find_library(MBEDCRYPTO_LIBRARY mbedcrypto) +find_library(MBEDTLS_LIBRARY mbedtls PATH_SUFFIXES mbedtls2) +find_library(MBEDX509_LIBRARY mbedx509 PATH_SUFFIXES mbedtls2) +find_library(MBEDCRYPTO_LIBRARY mbedcrypto PATH_SUFFIXES mbedtls2) set(MBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) set(MBEDTLS_LIBRARIES ${MBEDTLS_LIBRARY} ${MBEDX509_LIBRARY} ${MBEDCRYPTO_LIBRARY}) @@ -15,6 +15,7 @@ check_cxx_source_compiles(" #endif int main() {}" MBEDTLS_VERSION_OK) +unset(CMAKE_REQUIRED_INCLUDES) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MBEDTLS DEFAULT_MSG diff --git a/CMakeLists.txt b/CMakeLists.txt index cfd4dbf6e8..ddec28b082 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,7 +372,7 @@ else() check_and_add_flag(VISIBILITY_INLINES_HIDDEN -fvisibility-inlines-hidden) check_and_add_flag(VISIBILITY_HIDDEN -fvisibility=hidden) - check_and_add_flag(FOMIT_FRAME_POINTER -fomit-frame-pointer RELEASE_ONLY) + check_and_add_flag(FOMIT_FRAME_POINTER -fomit-frame-pointer NO_DEBINFO_ONLY) dolphin_compile_definitions(_DEBUG DEBUG_ONLY) check_and_add_flag(GGDB -ggdb DEBUG_ONLY) @@ -728,6 +728,11 @@ if(ENABLE_VULKAN) if(APPLE AND USE_BUNDLED_MOLTENVK) add_subdirectory(Externals/MoltenVK) endif() + + + if (ANDROID AND _M_ARM_64) + add_subdirectory(Externals/libadrenotools) + endif() endif() if(NOT WIN32 OR (NOT (CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64"))) @@ -1027,10 +1032,16 @@ include_directories("${PROJECT_BINARY_DIR}/Source/Core") # Unit testing. # if(ENABLE_TESTS) - message(STATUS "Using static gtest from Externals") + find_package(GTest) + if (GTEST_FOUND) + message(STATUS "Using the system gtest") + include_directories(${GTEST_INCLUDE_DIRS}) + else() + message(STATUS "Using static gtest from Externals") + add_subdirectory(Externals/gtest EXCLUDE_FROM_ALL) + endif() # Force gtest to link the C runtime dynamically on Windows in order to avoid runtime mismatches. set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - add_subdirectory(Externals/gtest EXCLUDE_FROM_ALL) else() message(STATUS "Unit tests are disabled") endif() diff --git a/Data/Sys/GameSettings/GIA.ini b/Data/Sys/GameSettings/GIA.ini index c89a24083c..f9627e9d6b 100644 --- a/Data/Sys/GameSettings/GIA.ini +++ b/Data/Sys/GameSettings/GIA.ini @@ -14,3 +14,4 @@ [Video_Hacks] EFBToTextureEnable = False +DeferEFBCopies = False diff --git a/Data/Sys/GameSettings/RWA.ini b/Data/Sys/GameSettings/RWA.ini index 530dd555de..bcf5e356d3 100644 --- a/Data/Sys/GameSettings/RWA.ini +++ b/Data/Sys/GameSettings/RWA.ini @@ -2,3 +2,4 @@ [Video_Settings] SuggestedAspectRatio = 2 +SafeTextureCacheColorSamples = 0 diff --git a/Data/Sys/Load/GraphicMods/Super Mario Sunshine/metadata.json b/Data/Sys/Load/GraphicMods/Super Mario Sunshine/metadata.json index 931602a373..805f3fac91 100644 --- a/Data/Sys/Load/GraphicMods/Super Mario Sunshine/metadata.json +++ b/Data/Sys/Load/GraphicMods/Super Mario Sunshine/metadata.json @@ -49,6 +49,10 @@ { "type": "efb", "texture_filename": "efb1_n000000_256x256_1" + }, + { + "type": "efb", + "texture_filename": "efb1_n000000_512x512_1" } ] } diff --git a/Data/Sys/Shaders/default_pre_post_process.glsl b/Data/Sys/Shaders/default_pre_post_process.glsl new file mode 100644 index 0000000000..d802f10bff --- /dev/null +++ b/Data/Sys/Shaders/default_pre_post_process.glsl @@ -0,0 +1,86 @@ +// References: +// https://www.unravel.com.au/understanding-color-spaces + +// SMPTE 170M - BT.601 (NTSC-M) -> BT.709 +mat3 from_NTSCM = transpose(mat3( + 0.939497225737661, 0.0502268452914346, 0.0102759289709032, + 0.0177558637510127, 0.965824605885027, 0.0164195303639603, + -0.00162163209967010, -0.00437400622653655, 1.00599563832621)); + +// ARIB TR-B9 (9300K+27MPCD with chromatic adaptation) (NTSC-J) -> BT.709 +mat3 from_NTSCJ = transpose(mat3( + 0.823613036967492, -0.0943227111084757, 0.00799341532931119, + 0.0289258355537324, 1.02310733489462, 0.00243547111576797, + -0.00569501554980891, 0.0161828357559315, 1.22328453915712)); + +// EBU - BT.470BG/BT.601 (PAL) -> BT.709 +mat3 from_PAL = transpose(mat3( + 1.04408168421813, -0.0440816842181253, 0.000000000000000, + 0.000000000000000, 1.00000000000000, 0.000000000000000, + 0.000000000000000, 0.0118044782106489, 0.988195521789351)); + +float3 LinearTosRGBGamma(float3 color) +{ + float a = 0.055; + + for (int i = 0; i < 3; ++i) + { + float x = color[i]; + if (x <= 0.0031308) + x = x * 12.92; + else + x = (1.0 + a) * pow(x, 1.0 / 2.4) - a; + color[i] = x; + } + + return color; +} + +void main() +{ + // Note: sampling in gamma space is "wrong" if the source + // and target resolution don't match exactly. + // Fortunately at the moment here they always should but to do this correctly, + // we'd need to sample from 4 pixels, de-apply the gamma from each of these, + // and then do linear sampling on their corrected value. + float4 color = Sample(); + + // Convert to linear space to do any other kind of operation + color.rgb = pow(color.rgb, game_gamma.xxx); + + if (OptionEnabled(correct_color_space)) + { + if (game_color_space == 0) + color.rgb = color.rgb * from_NTSCM; + else if (game_color_space == 1) + color.rgb = color.rgb * from_NTSCJ; + else if (game_color_space == 2) + color.rgb = color.rgb * from_PAL; + } + + if (OptionEnabled(hdr_output)) + { + const float hdr_paper_white = hdr_paper_white_nits / hdr_sdr_white_nits; + color.rgb *= hdr_paper_white; + } + + if (OptionEnabled(linear_space_output)) + { + // Nothing to do here + } + // Correct the SDR gamma for sRGB (PC/Monitor) or ~2.2 (Common TV gamma) + else if (OptionEnabled(correct_gamma)) + { + if (OptionEnabled(sdr_display_gamma_sRGB)) + color.rgb = LinearTosRGBGamma(color.rgb); + else + color.rgb = pow(color.rgb, (1.0 / sdr_display_custom_gamma).xxx); + } + // Restore the original gamma without changes + else + { + color.rgb = pow(color.rgb, (1.0 / game_gamma).xxx); + } + + SetOutput(color); +} \ No newline at end of file diff --git a/Data/Sys/Shaders/lens_distortion.glsl b/Data/Sys/Shaders/lens_distortion.glsl index f36f232524..b0c872c45e 100644 --- a/Data/Sys/Shaders/lens_distortion.glsl +++ b/Data/Sys/Shaders/lens_distortion.glsl @@ -81,7 +81,7 @@ void main() float2 uv = (widenedRadial/2.0f) + float2(0.5f, 0.5f) + float2(offsetAdd, 0.0f); // Sample the texture at the source location - if(any(clamp(uv, 0.0, 1.0) != uv)) + if (clamp(uv, 0.0, 1.0) != uv) { // black if beyond bounds SetOutput(float4(0.0, 0.0, 0.0, 0.0)); diff --git a/Externals/Qt b/Externals/Qt index 376baafde6..495517af2b 160000 --- a/Externals/Qt +++ b/Externals/Qt @@ -1 +1 @@ -Subproject commit 376baafde6cce2f8892c34c17ed397afa6c46d08 +Subproject commit 495517af2b922c10c24f543e0fd6ea3ddf774e50 diff --git a/Externals/libadrenotools b/Externals/libadrenotools new file mode 160000 index 0000000000..f4ce3c9618 --- /dev/null +++ b/Externals/libadrenotools @@ -0,0 +1 @@ +Subproject commit f4ce3c9618e7ecfcdd238b17dad9a0b888f5de90 diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index 36ed3baf02..c9c9942f88 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' version "1.7.20" } task copyProfile (type: Copy) { @@ -110,6 +111,7 @@ android { externalNativeBuild { cmake { path "../../../CMakeLists.txt" + version "3.22.1+" } } namespace 'org.dolphinemu.dolphinemu' @@ -122,10 +124,14 @@ android { abiFilters "arm64-v8a", "x86_64" //, "armeabi-v7a", "x86" // Remove the line below if you want to build the C++ unit tests - targets "main" + //targets "main", "hook_impl", "main_hook", "gsl_alloc_hook", "file_redirect_hook" } } } + + packagingOptions { + jniLibs.useLegacyPackaging = true + } } dependencies { @@ -160,6 +166,9 @@ dependencies { // For loading game covers from disk and GameTDB implementation 'io.coil-kt:coil:2.2.2' + // For loading custom GPU drivers + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" + implementation 'com.nononsenseapps:filepicker:4.2.1' } diff --git a/Source/Android/app/proguard-rules.pro b/Source/Android/app/proguard-rules.pro index 985cc25961..5ed4a21dce 100644 --- a/Source/Android/app/proguard-rules.pro +++ b/Source/Android/app/proguard-rules.pro @@ -1,3 +1,43 @@ # Being able to get sensible stack traces from users is more important # than the space savings obfuscation could give us -dontobfuscate + +# +# Kotlin Serialization +# + +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes +# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900 +-dontnote kotlinx.serialization.** + +# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes. +# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning. +# However, since in this case they will not be used, we can disable these warnings +-dontwarn kotlinx.serialization.internal.ClassValueReferences diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index f470bafee7..6d33d6158a 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -41,7 +41,8 @@ android:supportsRtl="true" android:isGame="true" android:banner="@drawable/banner_tv" - android:hasFragileUserData="true"> + android:hasFragileUserData="true" + android:extractNativeLibs="true"> diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java index 68fdfee277..b83a489256 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -297,6 +297,8 @@ public final class NativeLibrary public static native void SetCacheDirectory(String directory); + public static native String GetCacheDirectory(); + public static native int DefaultCPUCore(); public static native String GetDefaultGraphicsBackendName(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.java deleted file mode 100644 index 924b5911c0..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.activities; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import androidx.fragment.app.FragmentActivity; - -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.main.TvMainActivity; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.AppLinkHelper; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; - -/** - * Linker between leanback homescreen and app - */ -public class AppLinkActivity extends FragmentActivity -{ - private static final String TAG = "AppLinkActivity"; - - private AppLinkHelper.PlayAction playAction; - private AfterDirectoryInitializationRunner mAfterDirectoryInitializationRunner; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - Intent intent = getIntent(); - Uri uri = intent.getData(); - - Log.v(TAG, uri.toString()); - - if (uri.getPathSegments().isEmpty()) - { - Log.e(TAG, "Invalid uri " + uri); - finish(); - return; - } - - AppLinkHelper.AppLinkAction action = AppLinkHelper.extractAction(uri); - switch (action.getAction()) - { - case AppLinkHelper.PLAY: - playAction = (AppLinkHelper.PlayAction) action; - initResources(); - break; - case AppLinkHelper.BROWSE: - browse(); - break; - default: - throw new IllegalArgumentException("Invalid Action " + action); - } - } - - /** - * Need to init these since they usually occur in the main activity. - */ - private void initResources() - { - mAfterDirectoryInitializationRunner = new AfterDirectoryInitializationRunner(); - mAfterDirectoryInitializationRunner.runWithLifecycle(this, () -> tryPlay(playAction)); - - GameFileCacheManager.isLoading().observe(this, (isLoading) -> - { - if (!isLoading && DirectoryInitialization.areDolphinDirectoriesReady()) - { - tryPlay(playAction); - } - }); - - DirectoryInitialization.start(this); - GameFileCacheManager.startLoad(); - } - - /** - * Action if channel icon is selected - */ - private void browse() - { - Intent openApp = new Intent(this, TvMainActivity.class); - startActivity(openApp); - - finish(); - } - - private void tryPlay(AppLinkHelper.PlayAction action) - { - // TODO: This approach of getting the game from the game file cache without rescanning the - // library means that we can fail to launch games if the cache file has been deleted. - - GameFile game = GameFileCacheManager.getGameFileByGameId(action.getGameId()); - - // If game == null and the load isn't done, wait for the next GameFileCacheService broadcast. - // If game == null and the load is done, call play with a null game, making us exit in failure. - if (game != null || !GameFileCacheManager.isLoading().getValue()) - { - play(action, game); - } - } - - /** - * Action if program(game) is selected - */ - private void play(AppLinkHelper.PlayAction action, GameFile game) - { - Log.d(TAG, "Playing game " - + action.getGameId() - + " from channel " - + action.getChannelId()); - - if (game == null) - Log.e(TAG, "Invalid Game: " + action.getGameId()); - else - startGame(game); - finish(); - } - - private void startGame(GameFile game) - { - mAfterDirectoryInitializationRunner.cancel(); - EmulationActivity.launch(this, GameFileCacheManager.findSecondDiscAndGetPaths(game), false); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.kt new file mode 100644 index 0000000000..f3f318202e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AppLinkActivity.kt @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.FragmentActivity +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.main.TvMainActivity +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.AppLinkHelper +import org.dolphinemu.dolphinemu.utils.AppLinkHelper.PlayAction +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization + +/** + * Linker between leanback homescreen and app + */ +class AppLinkActivity : FragmentActivity() { + private lateinit var playAction: PlayAction + private lateinit var afterDirectoryInitializationRunner: AfterDirectoryInitializationRunner + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val uri = intent.data!! + + Log.v(TAG, uri.toString()) + + if (uri.pathSegments.isEmpty()) { + Log.e(TAG, "Invalid uri $uri") + finish() + return + } + + val action = AppLinkHelper.extractAction(uri) + when (action.action) { + AppLinkHelper.PLAY -> { + playAction = action as PlayAction + initResources() + } + + AppLinkHelper.BROWSE -> browse() + else -> throw IllegalArgumentException("Invalid Action $action") + } + } + + /** + * Need to init these since they usually occur in the main activity. + */ + private fun initResources() { + afterDirectoryInitializationRunner = AfterDirectoryInitializationRunner() + afterDirectoryInitializationRunner.runWithLifecycle(this) { tryPlay(playAction) } + + GameFileCacheManager.isLoading().observe(this) { isLoading: Boolean? -> + if (!isLoading!! && DirectoryInitialization.areDolphinDirectoriesReady()) { + tryPlay(playAction) + } + } + + DirectoryInitialization.start(this) + GameFileCacheManager.startLoad() + } + + /** + * Action if channel icon is selected + */ + private fun browse() { + val openApp = Intent(this, TvMainActivity::class.java) + startActivity(openApp) + + finish() + } + + private fun tryPlay(action: PlayAction) { + // TODO: This approach of getting the game from the game file cache without rescanning the + // library means that we can fail to launch games if the cache file has been deleted. + val game = GameFileCacheManager.getGameFileByGameId(action.gameId) + + // If game == null and the load isn't done, wait for the next GameFileCacheService broadcast. + // If game == null and the load is done, call play with a null game, making us exit in failure. + if (game != null || !GameFileCacheManager.isLoading().value!!) { + play(action, game) + } + } + + /** + * Action if program(game) is selected + */ + private fun play(action: PlayAction, game: GameFile?) { + Log.d(TAG, "Playing game ${action.gameId} from channel ${action.channelId}") + game?.let { startGame(it) } ?: Log.e(TAG, "Invalid Game: " + action.gameId) + finish() + } + + private fun startGame(game: GameFile) { + afterDirectoryInitializationRunner.cancel() + EmulationActivity.launch(this, GameFileCacheManager.findSecondDiscAndGetPaths(game), false) + } + + companion object { + private const val TAG = "AppLinkActivity" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.java deleted file mode 100644 index 29a968d50c..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Environment; - -import androidx.annotation.Nullable; - -import com.nononsenseapps.filepicker.AbstractFilePickerFragment; -import com.nononsenseapps.filepicker.FilePickerActivity; - -import org.dolphinemu.dolphinemu.fragments.CustomFilePickerFragment; - -import java.io.File; -import java.util.HashSet; - -public class CustomFilePickerActivity extends FilePickerActivity -{ - public static final String EXTRA_EXTENSIONS = "dolphinemu.org.filepicker.extensions"; - - private HashSet mExtensions; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - if (intent != null) - { - mExtensions = (HashSet) intent.getSerializableExtra(EXTRA_EXTENSIONS); - } - } - - @Override - protected AbstractFilePickerFragment getFragment( - @Nullable final String startPath, final int mode, final boolean allowMultiple, - final boolean allowCreateDir, final boolean allowExistingFile, - final boolean singleClick) - { - CustomFilePickerFragment fragment = new CustomFilePickerFragment(); - // startPath is allowed to be null. In that case, default folder should be SD-card and not "/" - fragment.setArgs( - startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), - mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); - fragment.setExtensions(mExtensions); - return fragment; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.kt new file mode 100644 index 0000000000..3b48c62f12 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/CustomFilePickerActivity.kt @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities + +import android.os.Bundle +import android.os.Environment +import com.nononsenseapps.filepicker.AbstractFilePickerFragment +import com.nononsenseapps.filepicker.FilePickerActivity +import org.dolphinemu.dolphinemu.fragments.CustomFilePickerFragment +import java.io.File + +class CustomFilePickerActivity : FilePickerActivity() { + private var extensions: HashSet? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent != null) { + extensions = intent.getSerializableExtra(EXTRA_EXTENSIONS) as HashSet? + } + } + + override fun getFragment( + startPath: String?, + mode: Int, + allowMultiple: Boolean, + allowCreateDir: Boolean, + allowExistingFile: Boolean, + singleClick: Boolean + ): AbstractFilePickerFragment { + val fragment = CustomFilePickerFragment() + // startPath is allowed to be null. In that case, default folder should be SD-card and not "/" + fragment.setArgs( + startPath ?: Environment.getExternalStorageDirectory().path, + mode, + allowMultiple, + allowCreateDir, + allowExistingFile, + singleClick + ) + fragment.setExtensions(extensions) + return fragment + } + + companion object { + const val EXTRA_EXTENSIONS = "dolphinemu.org.filepicker.extensions" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java deleted file mode 100644 index b9a9eab46e..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ /dev/null @@ -1,1178 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.activities; - -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Rect; -import android.os.Build; -import android.os.Bundle; -import android.util.Pair; -import android.util.SparseIntArray; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.PopupMenu; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.LinearLayoutManager; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding; -import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding; -import org.dolphinemu.dolphinemu.databinding.DialogSkylandersManagerBinding; -import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface; -import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.features.settings.model.StringSetting; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; -import org.dolphinemu.dolphinemu.features.skylanders.SkylanderConfig; -import org.dolphinemu.dolphinemu.features.skylanders.model.Skylander; -import org.dolphinemu.dolphinemu.features.skylanders.ui.SkylanderSlot; -import org.dolphinemu.dolphinemu.features.skylanders.ui.SkylanderSlotAdapter; -import org.dolphinemu.dolphinemu.fragments.EmulationFragment; -import org.dolphinemu.dolphinemu.fragments.MenuFragment; -import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment; -import org.dolphinemu.dolphinemu.overlay.InputOverlay; -import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer; -import org.dolphinemu.dolphinemu.ui.main.MainPresenter; -import org.dolphinemu.dolphinemu.ui.main.ThemeProvider; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; - -import java.lang.annotation.Retention; -import java.util.ArrayList; -import java.util.List; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.slider.Slider; - -public final class EmulationActivity extends AppCompatActivity implements ThemeProvider -{ - private static final String BACKSTACK_NAME_MENU = "menu"; - private static final String BACKSTACK_NAME_SUBMENU = "submenu"; - public static final int REQUEST_CHANGE_DISC = 1; - public static final int REQUEST_SKYLANDER_FILE = 2; - public static final int REQUEST_CREATE_SKYLANDER = 3; - - private EmulationFragment mEmulationFragment; - - private SharedPreferences mPreferences; - - private Settings mSettings; - - private int mThemeId; - - private boolean mMenuVisible; - - private static boolean sIgnoreLaunchRequests = false; - - private boolean activityRecreated; - private String[] mPaths; - private boolean mRiivolution; - private boolean mLaunchSystemMenu; - private static boolean sUserPausedEmulation; - private boolean mMenuToastShown; - - public static final String EXTRA_SELECTED_GAMES = "SelectedGames"; - public static final String EXTRA_RIIVOLUTION = "Riivolution"; - public static final String EXTRA_SYSTEM_MENU = "SystemMenu"; - public static final String EXTRA_USER_PAUSED_EMULATION = "sUserPausedEmulation"; - public static final String EXTRA_MENU_TOAST_SHOWN = "MenuToastShown"; - public static final String EXTRA_SKYLANDER_SLOT = "SkylanderSlot"; - public static final String EXTRA_SKYLANDER_ID = "SkylanderId"; - public static final String EXTRA_SKYLANDER_VAR = "SkylanderVar"; - public static final String EXTRA_SKYLANDER_NAME = "SkylanderName"; - - @Retention(SOURCE) - @IntDef( - {MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE, - MENU_ACTION_CHOOSE_CONTROLLER, MENU_ACTION_REFRESH_WIIMOTES, MENU_ACTION_TAKE_SCREENSHOT, - MENU_ACTION_QUICK_SAVE, MENU_ACTION_QUICK_LOAD, MENU_ACTION_SAVE_ROOT, - MENU_ACTION_LOAD_ROOT, MENU_ACTION_SAVE_SLOT1, MENU_ACTION_SAVE_SLOT2, - MENU_ACTION_SAVE_SLOT3, MENU_ACTION_SAVE_SLOT4, MENU_ACTION_SAVE_SLOT5, - MENU_ACTION_SAVE_SLOT6, MENU_ACTION_LOAD_SLOT1, MENU_ACTION_LOAD_SLOT2, - MENU_ACTION_LOAD_SLOT3, MENU_ACTION_LOAD_SLOT4, MENU_ACTION_LOAD_SLOT5, - MENU_ACTION_LOAD_SLOT6, MENU_ACTION_EXIT, MENU_ACTION_CHANGE_DISC, - MENU_ACTION_RESET_OVERLAY, MENU_SET_IR_RECENTER, MENU_SET_IR_MODE, - MENU_ACTION_CHOOSE_DOUBLETAP, MENU_ACTION_PAUSE_EMULATION, - MENU_ACTION_UNPAUSE_EMULATION, MENU_ACTION_OVERLAY_CONTROLS, MENU_ACTION_SETTINGS, - MENU_ACTION_SKYLANDERS}) - public @interface MenuAction - { - } - - public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0; - public static final int MENU_ACTION_TOGGLE_CONTROLS = 1; - public static final int MENU_ACTION_ADJUST_SCALE = 2; - public static final int MENU_ACTION_CHOOSE_CONTROLLER = 3; - public static final int MENU_ACTION_REFRESH_WIIMOTES = 4; - public static final int MENU_ACTION_TAKE_SCREENSHOT = 5; - public static final int MENU_ACTION_QUICK_SAVE = 6; - public static final int MENU_ACTION_QUICK_LOAD = 7; - public static final int MENU_ACTION_SAVE_ROOT = 8; - public static final int MENU_ACTION_LOAD_ROOT = 9; - public static final int MENU_ACTION_SAVE_SLOT1 = 10; - public static final int MENU_ACTION_SAVE_SLOT2 = 11; - public static final int MENU_ACTION_SAVE_SLOT3 = 12; - public static final int MENU_ACTION_SAVE_SLOT4 = 13; - public static final int MENU_ACTION_SAVE_SLOT5 = 14; - public static final int MENU_ACTION_SAVE_SLOT6 = 15; - public static final int MENU_ACTION_LOAD_SLOT1 = 16; - public static final int MENU_ACTION_LOAD_SLOT2 = 17; - public static final int MENU_ACTION_LOAD_SLOT3 = 18; - public static final int MENU_ACTION_LOAD_SLOT4 = 19; - public static final int MENU_ACTION_LOAD_SLOT5 = 20; - public static final int MENU_ACTION_LOAD_SLOT6 = 21; - public static final int MENU_ACTION_EXIT = 22; - public static final int MENU_ACTION_CHANGE_DISC = 23; - public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 24; - public static final int MENU_ACTION_RESET_OVERLAY = 26; - public static final int MENU_SET_IR_RECENTER = 27; - public static final int MENU_SET_IR_MODE = 28; - public static final int MENU_ACTION_CHOOSE_DOUBLETAP = 30; - public static final int MENU_ACTION_PAUSE_EMULATION = 32; - public static final int MENU_ACTION_UNPAUSE_EMULATION = 33; - public static final int MENU_ACTION_OVERLAY_CONTROLS = 34; - public static final int MENU_ACTION_SETTINGS = 35; - public static final int MENU_ACTION_SKYLANDERS = 36; - - private Skylander mSkylanderData = new Skylander(-1, -1, "Slot"); - - private int mSkylanderSlot = -1; - - private DialogSkylandersManagerBinding mSkylandersBinding; - - private static List sSkylanderSlots = new ArrayList<>(); - - private static final SparseIntArray buttonsActionsMap = new SparseIntArray(); - - static - { - buttonsActionsMap.append(R.id.menu_emulation_edit_layout, - EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT); - buttonsActionsMap.append(R.id.menu_emulation_toggle_controls, - EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS); - buttonsActionsMap - .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE); - buttonsActionsMap.append(R.id.menu_emulation_choose_controller, - EmulationActivity.MENU_ACTION_CHOOSE_CONTROLLER); - buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center, - EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER); - buttonsActionsMap - .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY); - buttonsActionsMap.append(R.id.menu_emulation_ir_recenter, - EmulationActivity.MENU_SET_IR_RECENTER); - buttonsActionsMap.append(R.id.menu_emulation_set_ir_mode, - EmulationActivity.MENU_SET_IR_MODE); - buttonsActionsMap.append(R.id.menu_emulation_choose_doubletap, - EmulationActivity.MENU_ACTION_CHOOSE_DOUBLETAP); - } - - public static void launch(FragmentActivity activity, String filePath, boolean riivolution) - { - launch(activity, new String[]{filePath}, riivolution); - } - - private static void performLaunchChecks(FragmentActivity activity, - Runnable continueCallback) - { - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, () -> - { - if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) || - !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) || - !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DUMP_PATH) || - !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_LOAD_PATH) || - !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_RESOURCEPACK_PATH)) - { - new MaterialAlertDialogBuilder(activity) - .setMessage(R.string.unavailable_paths) - .setPositiveButton(R.string.yes, - (dialogInterface, i) -> SettingsActivity.launch(activity, - MenuTag.CONFIG_PATHS)) - .setNeutralButton(R.string.continue_anyway, - (dialogInterface, i) -> continueCallback.run()) - .show(); - } - else if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_WII_SD_CARD_IMAGE_PATH) || - !FileBrowserHelper.isPathEmptyOrValid( - StringSetting.MAIN_WII_SD_CARD_SYNC_FOLDER_PATH)) - { - new MaterialAlertDialogBuilder(activity) - .setMessage(R.string.unavailable_paths) - .setPositiveButton(R.string.yes, - (dialogInterface, i) -> SettingsActivity.launch(activity, - MenuTag.CONFIG_WII)) - .setNeutralButton(R.string.continue_anyway, - (dialogInterface, i) -> continueCallback.run()) - .show(); - } - else - { - continueCallback.run(); - } - }); - } - - public static void launchSystemMenu(FragmentActivity activity) - { - if (sIgnoreLaunchRequests) - return; - - performLaunchChecks(activity, () -> - { - launchSystemMenuWithoutChecks(activity); - }); - } - - public static void launch(FragmentActivity activity, String[] filePaths, boolean riivolution) - { - if (sIgnoreLaunchRequests) - return; - - performLaunchChecks(activity, () -> - { - launchWithoutChecks(activity, filePaths, riivolution); - }); - } - - private static void launchSystemMenuWithoutChecks(FragmentActivity activity) - { - sIgnoreLaunchRequests = true; - - Intent launcher = new Intent(activity, EmulationActivity.class); - launcher.putExtra(EmulationActivity.EXTRA_SYSTEM_MENU, true); - activity.startActivity(launcher); - } - - private static void launchWithoutChecks(FragmentActivity activity, String[] filePaths, - boolean riivolution) - { - sIgnoreLaunchRequests = true; - - Intent launcher = new Intent(activity, EmulationActivity.class); - launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths); - launcher.putExtra(EXTRA_RIIVOLUTION, riivolution); - - activity.startActivity(launcher); - } - - public static void stopIgnoringLaunchRequests() - { - sIgnoreLaunchRequests = false; - } - - @Override - protected void onCreate(Bundle savedInstanceState) - { - ThemeHelper.setTheme(this); - - super.onCreate(savedInstanceState); - - MainPresenter.skipRescanningLibrary(); - - if (savedInstanceState == null) - { - // Get params we were passed - Intent gameToEmulate = getIntent(); - mPaths = gameToEmulate.getStringArrayExtra(EXTRA_SELECTED_GAMES); - mRiivolution = gameToEmulate.getBooleanExtra(EXTRA_RIIVOLUTION, false); - mLaunchSystemMenu = gameToEmulate.getBooleanExtra(EXTRA_SYSTEM_MENU, false); - sUserPausedEmulation = gameToEmulate.getBooleanExtra(EXTRA_USER_PAUSED_EMULATION, false); - mMenuToastShown = false; - activityRecreated = false; - } - else - { - activityRecreated = true; - restoreState(savedInstanceState); - } - - mPreferences = PreferenceManager.getDefaultSharedPreferences(this); - - mSettings = new Settings(); - mSettings.loadSettings(); - - updateOrientation(); - - // Set these options now so that the SurfaceView the game renders into is the right size. - enableFullscreenImmersive(); - - ActivityEmulationBinding binding = ActivityEmulationBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setInsets(binding.frameMenu); - - // Find or create the EmulationFragment - mEmulationFragment = (EmulationFragment) getSupportFragmentManager() - .findFragmentById(R.id.frame_emulation_fragment); - if (mEmulationFragment == null) - { - mEmulationFragment = EmulationFragment.newInstance(mPaths, mRiivolution, mLaunchSystemMenu); - getSupportFragmentManager().beginTransaction() - .add(R.id.frame_emulation_fragment, mEmulationFragment) - .commit(); - } - - if (NativeLibrary.IsGameMetadataValid()) - setTitle(NativeLibrary.GetCurrentTitleDescription()); - - if (sSkylanderSlots.isEmpty()) - { - for (int i = 0; i < 8; i++) - { - sSkylanderSlots.add(new SkylanderSlot(getString(R.string.skylander_slot, i + 1), i)); - } - } - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) - { - if (!isChangingConfigurations()) - { - mEmulationFragment.saveTemporaryState(); - } - outState.putStringArray(EXTRA_SELECTED_GAMES, mPaths); - outState.putBoolean(EXTRA_USER_PAUSED_EMULATION, sUserPausedEmulation); - outState.putBoolean(EXTRA_MENU_TOAST_SHOWN, mMenuToastShown); - outState.putInt(EXTRA_SKYLANDER_SLOT, mSkylanderSlot); - outState.putInt(EXTRA_SKYLANDER_ID, mSkylanderData.getId()); - outState.putInt(EXTRA_SKYLANDER_VAR, mSkylanderData.getVariant()); - outState.putString(EXTRA_SKYLANDER_NAME, mSkylanderData.getName()); - super.onSaveInstanceState(outState); - } - - protected void restoreState(Bundle savedInstanceState) - { - mPaths = savedInstanceState.getStringArray(EXTRA_SELECTED_GAMES); - sUserPausedEmulation = savedInstanceState.getBoolean(EXTRA_USER_PAUSED_EMULATION); - mMenuToastShown = savedInstanceState.getBoolean(EXTRA_MENU_TOAST_SHOWN); - mSkylanderSlot = savedInstanceState.getInt(EXTRA_SKYLANDER_SLOT); - mSkylanderData = new Skylander(savedInstanceState.getInt(EXTRA_SKYLANDER_ID), - savedInstanceState.getInt(EXTRA_SKYLANDER_VAR), - savedInstanceState.getString(EXTRA_SKYLANDER_NAME)); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) - { - if (hasFocus) - { - enableFullscreenImmersive(); - } - } - - @Override - protected void onResume() - { - ThemeHelper.setCorrectTheme(this); - - super.onResume(); - - // Only android 9+ support this feature. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - { - WindowManager.LayoutParams attributes = getWindow().getAttributes(); - - attributes.layoutInDisplayCutoutMode = - BooleanSetting.MAIN_EXPAND_TO_CUTOUT_AREA.getBoolean() ? - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; - - getWindow().setAttributes(attributes); - } - - updateOrientation(); - - DolphinSensorEventListener.setDeviceRotation( - getWindowManager().getDefaultDisplay().getRotation()); - } - - @Override - protected void onPause() - { - super.onPause(); - } - - @Override - protected void onStop() - { - super.onStop(); - mSettings.saveSettings(null); - } - - public void onTitleChanged() - { - if (!mMenuToastShown) - { - // The reason why this doesn't run earlier is because we want to be sure the boot succeeded. - Toast.makeText(this, R.string.emulation_menu_help, Toast.LENGTH_LONG).show(); - mMenuToastShown = true; - } - - setTitle(NativeLibrary.GetCurrentTitleDescription()); - - mEmulationFragment.refreshInputOverlay(); - } - - @Override - protected void onDestroy() - { - super.onDestroy(); - mSettings.close(); - } - - @Override - public void onBackPressed() - { - if (!closeSubmenu()) - { - toggleMenu(); - } - } - - @Override - public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) - { - if (keyCode == KeyEvent.KEYCODE_BACK) - { - mEmulationFragment.stopEmulation(); - return true; - } - return super.onKeyLongPress(keyCode, event); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) - { - super.onActivityResult(requestCode, resultCode, result); - // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) - { - if (requestCode == REQUEST_CHANGE_DISC) - { - NativeLibrary.ChangeDisc(result.getData().toString()); - } - else if (requestCode == REQUEST_SKYLANDER_FILE) - { - Pair slot = - SkylanderConfig.loadSkylander(sSkylanderSlots.get(mSkylanderSlot).getPortalSlot(), - result.getData().toString()); - clearSkylander(mSkylanderSlot); - sSkylanderSlots.get(mSkylanderSlot).setPortalSlot(slot.first); - sSkylanderSlots.get(mSkylanderSlot).setLabel(slot.second); - mSkylandersBinding.skylandersManager.getAdapter().notifyItemChanged(mSkylanderSlot); - mSkylanderSlot = -1; - mSkylanderData = Skylander.BLANK_SKYLANDER; - } - else if (requestCode == REQUEST_CREATE_SKYLANDER) - { - if (!(mSkylanderData.getId() == -1) && !(mSkylanderData.getVariant() == -1)) - { - Pair slot = SkylanderConfig.createSkylander(mSkylanderData.getId(), - mSkylanderData.getVariant(), - result.getData().toString(), sSkylanderSlots.get(mSkylanderSlot).getPortalSlot()); - clearSkylander(mSkylanderSlot); - sSkylanderSlots.get(mSkylanderSlot).setPortalSlot(slot.first); - sSkylanderSlots.get(mSkylanderSlot).setLabel(slot.second); - mSkylandersBinding.skylandersManager.getAdapter().notifyItemChanged(mSkylanderSlot); - mSkylanderSlot = -1; - mSkylanderData = Skylander.BLANK_SKYLANDER; - } - } - } - } - - 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); - } - - private void updateOrientation() - { - setRequestedOrientation(IntSetting.MAIN_EMULATION_ORIENTATION.getInt()); - } - - private boolean closeSubmenu() - { - return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_SUBMENU, - FragmentManager.POP_BACK_STACK_INCLUSIVE); - } - - private boolean closeMenu() - { - mMenuVisible = false; - return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_MENU, - FragmentManager.POP_BACK_STACK_INCLUSIVE); - } - - private void toggleMenu() - { - if (!closeMenu()) - { - // Removing the menu failed, so that means it wasn't visible. Add it. - Fragment fragment = MenuFragment.newInstance(); - getSupportFragmentManager().beginTransaction() - .setCustomAnimations( - R.animator.menu_slide_in_from_start, - R.animator.menu_slide_out_to_start, - R.animator.menu_slide_in_from_start, - R.animator.menu_slide_out_to_start) - .add(R.id.frame_menu, fragment) - .addToBackStack(BACKSTACK_NAME_MENU) - .commit(); - mMenuVisible = true; - } - } - - private void setInsets(View view) - { - ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> - { - Insets cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()); - ViewGroup.MarginLayoutParams mlpMenu = - (ViewGroup.MarginLayoutParams) v.getLayoutParams(); - int menuWidth = getResources().getDimensionPixelSize(R.dimen.menu_width); - if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) - { - mlpMenu.width = cutInsets.left + menuWidth; - } - else - { - mlpMenu.width = cutInsets.right + menuWidth; - } - NativeLibrary.SetObscuredPixelsTop(cutInsets.top); - NativeLibrary.SetObscuredPixelsLeft(cutInsets.left); - return windowInsets; - }); - } - - public void showOverlayControlsMenu(@NonNull View anchor) - { - PopupMenu popup = new PopupMenu(this, anchor); - Menu menu = popup.getMenu(); - - boolean wii = NativeLibrary.IsEmulatingWii(); - int id = wii ? R.menu.menu_overlay_controls_wii : R.menu.menu_overlay_controls_gc; - popup.getMenuInflater().inflate(id, menu); - - // Populate the switch value for joystick center on touch - menu.findItem(R.id.menu_emulation_joystick_rel_center) - .setChecked(BooleanSetting.MAIN_JOYSTICK_REL_CENTER.getBoolean()); - if (wii) - { - menu.findItem(R.id.menu_emulation_ir_recenter) - .setChecked(BooleanSetting.MAIN_IR_ALWAYS_RECENTER.getBoolean()); - } - - popup.setOnMenuItemClickListener(this::onOptionsItemSelected); - - popup.show(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - int action = buttonsActionsMap.get(item.getItemId(), -1); - if (action >= 0) - { - if (item.isCheckable()) - { - // Need to pass a reference to the item to set the switch state, since it is not done automatically - handleCheckableMenuAction(action, item); - } - else - { - handleMenuAction(action); - } - } - return true; - } - - public void handleCheckableMenuAction(@MenuAction int menuAction, MenuItem item) - { - //noinspection SwitchIntDef - switch (menuAction) - { - case MENU_ACTION_JOYSTICK_REL_CENTER: - item.setChecked(!item.isChecked()); - toggleJoystickRelCenter(item.isChecked()); - break; - case MENU_SET_IR_RECENTER: - item.setChecked(!item.isChecked()); - toggleRecenter(item.isChecked()); - break; - } - } - - public void handleMenuAction(@MenuAction int menuAction) - { - //noinspection SwitchIntDef - switch (menuAction) - { - // Edit the placement of the controls - case MENU_ACTION_EDIT_CONTROLS_PLACEMENT: - editControlsPlacement(); - break; - - // Reset overlay placement - case MENU_ACTION_RESET_OVERLAY: - resetOverlay(); - break; - - // Enable/Disable specific buttons or the entire input overlay. - case MENU_ACTION_TOGGLE_CONTROLS: - toggleControls(); - break; - - // Adjust the scale and opacity of the overlay controls. - case MENU_ACTION_ADJUST_SCALE: - adjustScale(); - break; - - // Change the controller for the input overlay. - case MENU_ACTION_CHOOSE_CONTROLLER: - chooseController(); - break; - - case MENU_ACTION_REFRESH_WIIMOTES: - NativeLibrary.RefreshWiimotes(); - break; - - case MENU_ACTION_PAUSE_EMULATION: - sUserPausedEmulation = true; - NativeLibrary.PauseEmulation(); - break; - - case MENU_ACTION_UNPAUSE_EMULATION: - sUserPausedEmulation = false; - NativeLibrary.UnPauseEmulation(); - break; - - // Screenshot capturing - case MENU_ACTION_TAKE_SCREENSHOT: - NativeLibrary.SaveScreenShot(); - break; - - // Quick save / load - case MENU_ACTION_QUICK_SAVE: - NativeLibrary.SaveState(9, false); - break; - - case MENU_ACTION_QUICK_LOAD: - NativeLibrary.LoadState(9); - break; - - case MENU_ACTION_SAVE_ROOT: - showSubMenu(SaveLoadStateFragment.SaveOrLoad.SAVE); - break; - - case MENU_ACTION_LOAD_ROOT: - showSubMenu(SaveLoadStateFragment.SaveOrLoad.LOAD); - break; - - // Save state slots - case MENU_ACTION_SAVE_SLOT1: - NativeLibrary.SaveState(0, false); - break; - - case MENU_ACTION_SAVE_SLOT2: - NativeLibrary.SaveState(1, false); - break; - - case MENU_ACTION_SAVE_SLOT3: - NativeLibrary.SaveState(2, false); - break; - - case MENU_ACTION_SAVE_SLOT4: - NativeLibrary.SaveState(3, false); - break; - - case MENU_ACTION_SAVE_SLOT5: - NativeLibrary.SaveState(4, false); - break; - - case MENU_ACTION_SAVE_SLOT6: - NativeLibrary.SaveState(5, false); - break; - - // Load state slots - case MENU_ACTION_LOAD_SLOT1: - NativeLibrary.LoadState(0); - break; - - case MENU_ACTION_LOAD_SLOT2: - NativeLibrary.LoadState(1); - break; - - case MENU_ACTION_LOAD_SLOT3: - NativeLibrary.LoadState(2); - break; - - case MENU_ACTION_LOAD_SLOT4: - NativeLibrary.LoadState(3); - break; - - case MENU_ACTION_LOAD_SLOT5: - NativeLibrary.LoadState(4); - break; - - case MENU_ACTION_LOAD_SLOT6: - NativeLibrary.LoadState(5); - break; - - case MENU_ACTION_CHANGE_DISC: - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, REQUEST_CHANGE_DISC); - break; - - case MENU_SET_IR_MODE: - setIRMode(); - break; - - case MENU_ACTION_CHOOSE_DOUBLETAP: - chooseDoubleTapButton(); - break; - - case MENU_ACTION_SETTINGS: - SettingsActivity.launch(this, MenuTag.SETTINGS); - break; - - case MENU_ACTION_SKYLANDERS: - showSkylanderPortalSettings(); - break; - - case MENU_ACTION_EXIT: - mEmulationFragment.stopEmulation(); - break; - } - } - - public static boolean getHasUserPausedEmulation() - { - return sUserPausedEmulation; - } - - private void toggleJoystickRelCenter(boolean state) - { - BooleanSetting.MAIN_JOYSTICK_REL_CENTER.setBoolean(mSettings, state); - } - - private void toggleRecenter(boolean state) - { - BooleanSetting.MAIN_IR_ALWAYS_RECENTER.setBoolean(mSettings, state); - mEmulationFragment.refreshOverlayPointer(); - } - - private void editControlsPlacement() - { - if (mEmulationFragment.isConfiguringControls()) - { - mEmulationFragment.stopConfiguringControls(); - } - else - { - closeSubmenu(); - closeMenu(); - mEmulationFragment.startConfiguringControls(); - } - } - - // Gets button presses - @Override - public boolean dispatchKeyEvent(KeyEvent event) - { - if (!mMenuVisible) - { - if (ControllerInterface.dispatchKeyEvent(event)) - { - return true; - } - } - - return super.dispatchKeyEvent(event); - } - - private void toggleControls() - { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.emulation_toggle_controls); - - int currentController = InputOverlay.getConfiguredControllerType(); - - if (currentController == InputOverlay.OVERLAY_GAMECUBE) - { - boolean[] gcEnabledButtons = new boolean[11]; - String gcSettingBase = "MAIN_BUTTON_TOGGLE_GC_"; - - for (int i = 0; i < gcEnabledButtons.length; i++) - { - gcEnabledButtons[i] = BooleanSetting.valueOf(gcSettingBase + i).getBoolean(); - } - builder.setMultiChoiceItems(R.array.gcpadButtons, gcEnabledButtons, - (dialog, indexSelected, isChecked) -> BooleanSetting - .valueOf(gcSettingBase + indexSelected).setBoolean(mSettings, isChecked)); - } - else if (currentController == InputOverlay.OVERLAY_WIIMOTE_CLASSIC) - { - boolean[] wiiClassicEnabledButtons = new boolean[14]; - String classicSettingBase = "MAIN_BUTTON_TOGGLE_CLASSIC_"; - - for (int i = 0; i < wiiClassicEnabledButtons.length; i++) - { - wiiClassicEnabledButtons[i] = - BooleanSetting.valueOf(classicSettingBase + i).getBoolean(); - } - builder.setMultiChoiceItems(R.array.classicButtons, wiiClassicEnabledButtons, - (dialog, indexSelected, isChecked) -> BooleanSetting - .valueOf(classicSettingBase + indexSelected) - .setBoolean(mSettings, isChecked)); - } - else - { - boolean[] wiiEnabledButtons = new boolean[11]; - String wiiSettingBase = "MAIN_BUTTON_TOGGLE_WII_"; - - for (int i = 0; i < wiiEnabledButtons.length; i++) - { - wiiEnabledButtons[i] = BooleanSetting.valueOf(wiiSettingBase + i).getBoolean(); - } - if (currentController == InputOverlay.OVERLAY_WIIMOTE_NUNCHUK) - { - builder.setMultiChoiceItems(R.array.nunchukButtons, wiiEnabledButtons, - (dialog, indexSelected, isChecked) -> BooleanSetting - .valueOf(wiiSettingBase + indexSelected).setBoolean(mSettings, isChecked)); - } - else - { - builder.setMultiChoiceItems(R.array.wiimoteButtons, wiiEnabledButtons, - (dialog, indexSelected, isChecked) -> BooleanSetting - .valueOf(wiiSettingBase + indexSelected).setBoolean(mSettings, isChecked)); - } - } - - builder.setNeutralButton(R.string.emulation_toggle_all, - (dialogInterface, i) -> mEmulationFragment.toggleInputOverlayVisibility(mSettings)) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - mEmulationFragment.refreshInputOverlay()) - .show(); - } - - public void chooseDoubleTapButton() - { - int currentValue = IntSetting.MAIN_DOUBLE_TAP_BUTTON.getInt(); - - int buttonList = InputOverlay.getConfiguredControllerType() == - InputOverlay.OVERLAY_WIIMOTE_CLASSIC ? R.array.doubleTapWithClassic : R.array.doubleTap; - - int checkedItem = -1; - int itemCount = getResources().getStringArray(buttonList).length; - for (int i = 0; i < itemCount; i++) - { - if (InputOverlayPointer.DOUBLE_TAP_OPTIONS.get(i) == currentValue) - { - checkedItem = i; - break; - } - } - - new MaterialAlertDialogBuilder(this) - .setSingleChoiceItems(buttonList, checkedItem, - (DialogInterface dialog, int which) -> IntSetting.MAIN_DOUBLE_TAP_BUTTON.setInt( - mSettings, InputOverlayPointer.DOUBLE_TAP_OPTIONS.get(which))) - .setPositiveButton(R.string.ok, - (dialogInterface, i) -> mEmulationFragment.initInputPointer()) - .show(); - } - - private void adjustScale() - { - DialogInputAdjustBinding dialogBinding = DialogInputAdjustBinding.inflate(getLayoutInflater()); - - final Slider scaleSlider = dialogBinding.inputScaleSlider; - final TextView scaleValue = dialogBinding.inputScaleValue; - scaleSlider.setValueTo(150); - scaleSlider.setValue(IntSetting.MAIN_CONTROL_SCALE.getInt()); - scaleSlider.setStepSize(1); - scaleSlider.addOnChangeListener( - (slider, progress, fromUser) -> scaleValue.setText(((int) progress + 50) + "%")); - scaleValue.setText(((int) scaleSlider.getValue() + 50) + "%"); - - // alpha - final Slider sliderOpacity = dialogBinding.inputOpacitySlider; - final TextView valueOpacity = dialogBinding.inputOpacityValue; - sliderOpacity.setValueTo(100); - sliderOpacity.setValue(IntSetting.MAIN_CONTROL_OPACITY.getInt()); - sliderOpacity.setStepSize(1); - sliderOpacity.addOnChangeListener( - (slider, progress, fromUser) -> valueOpacity.setText(((int) progress) + "%")); - valueOpacity.setText(((int) sliderOpacity.getValue()) + "%"); - - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.emulation_control_adjustments) - .setView(dialogBinding.getRoot()) - .setPositiveButton(R.string.ok, (dialog, which) -> - { - IntSetting.MAIN_CONTROL_SCALE.setInt(mSettings, (int) scaleSlider.getValue()); - IntSetting.MAIN_CONTROL_OPACITY.setInt(mSettings, (int) sliderOpacity.getValue()); - mEmulationFragment.refreshInputOverlay(); - }) - .setNeutralButton(R.string.default_values, (dialog, which) -> - { - IntSetting.MAIN_CONTROL_SCALE.delete(mSettings); - IntSetting.MAIN_CONTROL_OPACITY.delete(mSettings); - mEmulationFragment.refreshInputOverlay(); - }) - .show(); - } - - private void addControllerIfNotNone(List entries, List values, - IntSetting controller, int entry, int value) - { - if (controller.getInt() != 0) - { - entries.add(getString(entry)); - values.add(value); - } - } - - private void chooseController() - { - ArrayList entries = new ArrayList<>(); - ArrayList values = new ArrayList<>(); - - entries.add(getString(R.string.none)); - values.add(-1); - - addControllerIfNotNone(entries, values, IntSetting.MAIN_SI_DEVICE_0, R.string.controller_0, 0); - addControllerIfNotNone(entries, values, IntSetting.MAIN_SI_DEVICE_1, R.string.controller_1, 1); - addControllerIfNotNone(entries, values, IntSetting.MAIN_SI_DEVICE_2, R.string.controller_2, 2); - addControllerIfNotNone(entries, values, IntSetting.MAIN_SI_DEVICE_3, R.string.controller_3, 3); - - if (NativeLibrary.IsEmulatingWii()) - { - addControllerIfNotNone(entries, values, IntSetting.WIIMOTE_1_SOURCE, R.string.wiimote_0, 4); - addControllerIfNotNone(entries, values, IntSetting.WIIMOTE_2_SOURCE, R.string.wiimote_1, 5); - addControllerIfNotNone(entries, values, IntSetting.WIIMOTE_3_SOURCE, R.string.wiimote_2, 6); - addControllerIfNotNone(entries, values, IntSetting.WIIMOTE_4_SOURCE, R.string.wiimote_3, 7); - } - - IntSetting controllerSetting = NativeLibrary.IsEmulatingWii() ? - IntSetting.MAIN_OVERLAY_WII_CONTROLLER : IntSetting.MAIN_OVERLAY_GC_CONTROLLER; - int currentValue = controllerSetting.getInt(); - - int checkedItem = -1; - for (int i = 0; i < values.size(); i++) - { - if (values.get(i) == currentValue) - { - checkedItem = i; - break; - } - } - - final SharedPreferences.Editor editor = mPreferences.edit(); - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.emulation_choose_controller) - .setSingleChoiceItems(entries.toArray(new CharSequence[]{}), checkedItem, - (dialog, indexSelected) -> - controllerSetting.setInt(mSettings, values.get(indexSelected))) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - { - editor.apply(); - mEmulationFragment.refreshInputOverlay(); - }) - .setNeutralButton(R.string.emulation_more_controller_settings, - (dialogInterface, i) -> SettingsActivity.launch(this, MenuTag.SETTINGS)) - .show(); - } - - private void setIRMode() - { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.emulation_ir_mode) - .setSingleChoiceItems(R.array.irModeEntries, - IntSetting.MAIN_IR_MODE.getInt(), - (dialog, indexSelected) -> - IntSetting.MAIN_IR_MODE.setInt(mSettings, indexSelected)) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - mEmulationFragment.refreshOverlayPointer()) - .show(); - } - - private void showSkylanderPortalSettings() - { - mSkylandersBinding = - DialogSkylandersManagerBinding.inflate(getLayoutInflater()); - mSkylandersBinding.skylandersManager.setLayoutManager(new LinearLayoutManager(this)); - - mSkylandersBinding.skylandersManager.setAdapter( - new SkylanderSlotAdapter(sSkylanderSlots, this)); - - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.skylanders_manager) - .setView(mSkylandersBinding.getRoot()) - .show(); - } - - public void setSkylanderData(int id, int var, String name, int slot) - { - mSkylanderData = new Skylander(id, var, name); - mSkylanderSlot = slot; - } - - public void clearSkylander(int slot) - { - sSkylanderSlots.get(slot).setLabel(getString(R.string.skylander_slot, slot + 1)); - mSkylandersBinding.skylandersManager.getAdapter().notifyItemChanged(slot); - } - - private void resetOverlay() - { - new MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.emulation_touch_overlay_reset)) - .setPositiveButton(R.string.yes, - (dialogInterface, i) -> mEmulationFragment.resetInputOverlay()) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - private static boolean areCoordinatesOutside(@Nullable View view, float x, float y) - { - if (view == null) - { - return true; - } - - Rect viewBounds = new Rect(); - view.getGlobalVisibleRect(viewBounds); - return !viewBounds.contains(Math.round(x), Math.round(y)); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent event) - { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) - { - boolean anyMenuClosed = false; - - Fragment submenu = getSupportFragmentManager().findFragmentById(R.id.frame_submenu); - if (submenu != null && areCoordinatesOutside(submenu.getView(), event.getX(), event.getY())) - { - closeSubmenu(); - submenu = null; - anyMenuClosed = true; - } - - if (submenu == null) - { - Fragment menu = getSupportFragmentManager().findFragmentById(R.id.frame_menu); - if (menu != null && areCoordinatesOutside(menu.getView(), event.getX(), event.getY())) - { - closeMenu(); - anyMenuClosed = true; - } - } - - if (anyMenuClosed) - { - return true; - } - } - - return super.dispatchTouchEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) - { - if (!mMenuVisible) - { - if (ControllerInterface.dispatchGenericMotionEvent(event)) - { - return true; - } - } - - return super.dispatchGenericMotionEvent(event); - } - - private void showSubMenu(SaveLoadStateFragment.SaveOrLoad saveOrLoad) - { - // Get rid of any visible submenu - getSupportFragmentManager().popBackStack( - BACKSTACK_NAME_SUBMENU, FragmentManager.POP_BACK_STACK_INCLUSIVE); - - Fragment fragment = SaveLoadStateFragment.newInstance(saveOrLoad); - getSupportFragmentManager().beginTransaction() - .setCustomAnimations( - R.animator.menu_slide_in_from_end, - R.animator.menu_slide_out_to_end, - R.animator.menu_slide_in_from_end, - R.animator.menu_slide_out_to_end) - .replace(R.id.frame_submenu, fragment) - .addToBackStack(BACKSTACK_NAME_SUBMENU) - .commit(); - } - - public boolean isActivityRecreated() - { - return activityRecreated; - } - - public Settings getSettings() - { - return mSettings; - } - - public void initInputPointer() - { - mEmulationFragment.initInputPointer(); - } - - @Override - public void setTheme(int themeId) - { - super.setTheme(themeId); - this.mThemeId = themeId; - } - - @Override - public int getThemeId() - { - return mThemeId; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt new file mode 100644 index 0000000000..9d189eac32 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -0,0 +1,1069 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.util.SparseIntArray +import android.view.KeyEvent +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import android.view.WindowManager +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.slider.Slider +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding +import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding +import org.dolphinemu.dolphinemu.databinding.DialogNfcFiguresManagerBinding +import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig +import org.dolphinemu.dolphinemu.features.infinitybase.model.Figure +import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlot +import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter +import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface +import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity +import org.dolphinemu.dolphinemu.features.skylanders.SkylanderConfig +import org.dolphinemu.dolphinemu.features.skylanders.model.Skylander +import org.dolphinemu.dolphinemu.features.skylanders.ui.SkylanderSlot +import org.dolphinemu.dolphinemu.features.skylanders.ui.SkylanderSlotAdapter +import org.dolphinemu.dolphinemu.fragments.EmulationFragment +import org.dolphinemu.dolphinemu.fragments.MenuFragment +import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment +import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment.SaveOrLoad +import org.dolphinemu.dolphinemu.overlay.InputOverlay +import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer +import org.dolphinemu.dolphinemu.ui.main.MainPresenter +import org.dolphinemu.dolphinemu.ui.main.ThemeProvider +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.ThemeHelper +import kotlin.math.roundToInt + +class EmulationActivity : AppCompatActivity(), ThemeProvider { + private var emulationFragment: EmulationFragment? = null + + private lateinit var settings: Settings + + override var themeId = 0 + + private var menuVisible = false + + var isActivityRecreated = false + private var paths: Array? = null + private var riivolution = false + private var launchSystemMenu = false + private var menuToastShown = false + + private var skylanderData = Skylander(-1, -1, "Slot") + private var infinityFigureData = Figure(-1, "Position") + private var skylanderSlot = -1 + private var infinityPosition = -1 + private var infinityListPosition = -1 + private lateinit var skylandersBinding: DialogNfcFiguresManagerBinding + private lateinit var infinityBinding: DialogNfcFiguresManagerBinding + + private lateinit var binding: ActivityEmulationBinding + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.setTheme(this) + + super.onCreate(savedInstanceState) + + MainPresenter.skipRescanningLibrary() + + if (savedInstanceState == null) { + // Get params we were passed + paths = intent.getStringArrayExtra(EXTRA_SELECTED_GAMES) + riivolution = intent.getBooleanExtra(EXTRA_RIIVOLUTION, false) + launchSystemMenu = intent.getBooleanExtra(EXTRA_SYSTEM_MENU, false) + hasUserPausedEmulation = + intent.getBooleanExtra(EXTRA_USER_PAUSED_EMULATION, false) + menuToastShown = false + isActivityRecreated = false + } else { + isActivityRecreated = true + restoreState(savedInstanceState) + } + + settings = Settings() + settings.loadSettings() + + updateOrientation() + + // Set these options now so that the SurfaceView the game renders into is the right size. + enableFullscreenImmersive() + + binding = ActivityEmulationBinding.inflate(layoutInflater) + setContentView(binding.root) + + setInsets() + + // Find or create the EmulationFragment + emulationFragment = supportFragmentManager + .findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment? + if (emulationFragment == null) { + emulationFragment = EmulationFragment.newInstance(paths, riivolution, launchSystemMenu) + supportFragmentManager.beginTransaction() + .add(R.id.frame_emulation_fragment, emulationFragment!!) + .commit() + } + + if (NativeLibrary.IsGameMetadataValid()) + title = NativeLibrary.GetCurrentTitleDescription() + + if (skylanderSlots.isEmpty()) { + for (i in 0..7) { + skylanderSlots.add(SkylanderSlot(getString(R.string.skylander_slot, i + 1), i)) + } + } + + if (infinityFigures.isEmpty()) { + infinityFigures.apply { + add(FigureSlot(getString(R.string.infinity_hexagon_label), 0)) + add(FigureSlot(getString(R.string.infinity_p1_label), 1)) + add(FigureSlot(getString(R.string.infinity_p1a1_label), 3)) + add(FigureSlot(getString(R.string.infinity_p1a2_label), 5)) + add(FigureSlot(getString(R.string.infinity_p2_label), 2)) + add(FigureSlot(getString(R.string.infinity_p2a1_label), 4)) + add(FigureSlot(getString(R.string.infinity_p2a2_label), 6)) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + if (!isChangingConfigurations) { + emulationFragment!!.saveTemporaryState() + } + + outState.apply { + putStringArray(EXTRA_SELECTED_GAMES, paths) + putBoolean(EXTRA_USER_PAUSED_EMULATION, hasUserPausedEmulation) + putBoolean(EXTRA_MENU_TOAST_SHOWN, menuToastShown) + putInt(EXTRA_SKYLANDER_SLOT, skylanderSlot) + putInt(EXTRA_SKYLANDER_ID, skylanderData.id) + putInt(EXTRA_SKYLANDER_VAR, skylanderData.variant) + putString(EXTRA_SKYLANDER_NAME, skylanderData.name) + putInt(EXTRA_INFINITY_POSITION, infinityPosition) + putInt(EXTRA_INFINITY_LIST_POSITION, infinityListPosition) + putLong(EXTRA_INFINITY_NUM, infinityFigureData.number) + putString(EXTRA_INFINITY_NAME, infinityFigureData.name) + } + super.onSaveInstanceState(outState) + } + + fun restoreState(savedInstanceState: Bundle) { + savedInstanceState.apply { + paths = getStringArray(EXTRA_SELECTED_GAMES) + hasUserPausedEmulation = getBoolean(EXTRA_USER_PAUSED_EMULATION) + menuToastShown = savedInstanceState.getBoolean(EXTRA_MENU_TOAST_SHOWN) + skylanderSlot = savedInstanceState.getInt(EXTRA_SKYLANDER_SLOT) + skylanderData = Skylander( + savedInstanceState.getInt(EXTRA_SKYLANDER_ID), + savedInstanceState.getInt(EXTRA_SKYLANDER_VAR), + savedInstanceState.getString(EXTRA_SKYLANDER_NAME)!! + ) + infinityPosition = savedInstanceState.getInt(EXTRA_INFINITY_POSITION) + infinityListPosition = savedInstanceState.getInt(EXTRA_INFINITY_LIST_POSITION) + infinityFigureData = Figure( + savedInstanceState.getLong(EXTRA_INFINITY_NUM), + savedInstanceState.getString(EXTRA_INFINITY_NAME)!! + ) + } + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + enableFullscreenImmersive() + } + } + + override fun onResume() { + ThemeHelper.setCorrectTheme(this) + + super.onResume() + + // Only android 9+ support this feature. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val attributes = window.attributes + + attributes.layoutInDisplayCutoutMode = + if (BooleanSetting.MAIN_EXPAND_TO_CUTOUT_AREA.boolean) { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } else { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + + window.attributes = attributes + } + + updateOrientation() + + DolphinSensorEventListener.setDeviceRotation(windowManager.defaultDisplay.rotation) + } + + override fun onStop() { + super.onStop() + settings.saveSettings(null) + } + + fun onTitleChanged() { + if (!menuToastShown) { + // The reason why this doesn't run earlier is because we want to be sure the boot succeeded. + Toast.makeText(this, R.string.emulation_menu_help, Toast.LENGTH_LONG).show() + menuToastShown = true + } + + title = NativeLibrary.GetCurrentTitleDescription() + + emulationFragment?.refreshInputOverlay() + } + + override fun onDestroy() { + super.onDestroy() + settings.close() + } + + override fun onBackPressed() { + if (!closeSubmenu()) { + toggleMenu() + } + } + + override fun onKeyLongPress(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + emulationFragment!!.stopEmulation() + return true + } + return super.onKeyLongPress(keyCode, event) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + super.onActivityResult(requestCode, resultCode, result) + // If the user picked a file, as opposed to just backing out. + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CHANGE_DISC) { + NativeLibrary.ChangeDisc(result!!.data.toString()) + } else if (requestCode == REQUEST_SKYLANDER_FILE) { + val slot = SkylanderConfig.loadSkylander( + skylanderSlots[skylanderSlot].portalSlot, + result!!.data.toString() + )!! + clearSkylander(skylanderSlot) + skylanderSlots[skylanderSlot].portalSlot = slot.first!! + skylanderSlots[skylanderSlot].label = slot.second!! + skylandersBinding.figureManager.adapter!!.notifyItemChanged(skylanderSlot) + skylanderSlot = -1 + skylanderData = Skylander.BLANK_SKYLANDER + } else if (requestCode == REQUEST_CREATE_SKYLANDER) { + if (skylanderData.id != -1 && skylanderData.variant != -1) { + val slot = SkylanderConfig.createSkylander( + skylanderData.id, + skylanderData.variant, + result!!.data.toString(), + skylanderSlots[skylanderSlot].portalSlot + ) + clearSkylander(skylanderSlot) + skylanderSlots[skylanderSlot].portalSlot = slot.first + skylanderSlots[skylanderSlot].label = slot.second + skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot) + skylanderSlot = -1 + skylanderData = Skylander.BLANK_SKYLANDER + } + } else if (requestCode == REQUEST_INFINITY_FIGURE_FILE) { + val label = InfinityConfig.loadFigure(infinityPosition, result!!.data.toString()) + if (label != null && label != "Unknown Figure") { + clearInfinityFigure(infinityListPosition) + infinityFigures[infinityListPosition].label = label + infinityBinding.figureManager.adapter?.notifyItemChanged(infinityListPosition) + infinityPosition = -1 + infinityListPosition = -1 + infinityFigureData = Figure.BLANK_FIGURE + } else { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.incompatible_figure_selected) + .setMessage(R.string.select_compatible_figure) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } else if (requestCode == REQUEST_CREATE_INFINITY_FIGURE) { + if (infinityFigureData.number != -1L) { + val label = InfinityConfig.createFigure( + infinityFigureData.number, + result!!.data.toString(), + infinityPosition + ) + clearInfinityFigure(infinityListPosition) + infinityFigures[infinityListPosition].label = label!! + infinityBinding.figureManager.adapter?.notifyItemChanged(infinityListPosition) + infinityPosition = -1 + infinityListPosition = -1 + infinityFigureData = Figure.BLANK_FIGURE + } + } + } + } + + private fun enableFullscreenImmersive() { + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_FULLSCREEN or + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + } + + private fun updateOrientation() { + requestedOrientation = IntSetting.MAIN_EMULATION_ORIENTATION.int + } + + private fun closeSubmenu(): Boolean = + supportFragmentManager.popBackStackImmediate( + BACKSTACK_NAME_SUBMENU, + FragmentManager.POP_BACK_STACK_INCLUSIVE + ) + + private fun closeMenu(): Boolean { + menuVisible = false + return supportFragmentManager.popBackStackImmediate( + BACKSTACK_NAME_MENU, + FragmentManager.POP_BACK_STACK_INCLUSIVE + ) + } + + private fun toggleMenu() { + if (!closeMenu()) { + // Removing the menu failed, so that means it wasn't visible. Add it. + val fragment: Fragment = MenuFragment.newInstance() + supportFragmentManager.beginTransaction() + .setCustomAnimations( + R.animator.menu_slide_in_from_start, + R.animator.menu_slide_out_to_start, + R.animator.menu_slide_in_from_start, + R.animator.menu_slide_out_to_start + ) + .add(R.id.frame_menu, fragment) + .addToBackStack(BACKSTACK_NAME_MENU) + .commit() + menuVisible = true + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.frameMenu) { v: View, windowInsets: WindowInsetsCompat -> + val cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + val mlpMenu = v.layoutParams as MarginLayoutParams + val menuWidth = resources.getDimensionPixelSize(R.dimen.menu_width) + if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { + mlpMenu.width = cutInsets.left + menuWidth + } else { + mlpMenu.width = cutInsets.right + menuWidth + } + NativeLibrary.SetObscuredPixelsTop(cutInsets.top) + NativeLibrary.SetObscuredPixelsLeft(cutInsets.left) + windowInsets + } + + fun showOverlayControlsMenu(anchor: View) { + val popup = PopupMenu(this, anchor) + val menu = popup.menu + val wii = NativeLibrary.IsEmulatingWii() + val id = if (wii) R.menu.menu_overlay_controls_wii else R.menu.menu_overlay_controls_gc + popup.menuInflater.inflate(id, menu) + + // Populate the switch value for joystick center on touch + menu.findItem(R.id.menu_emulation_joystick_rel_center).isChecked = + BooleanSetting.MAIN_JOYSTICK_REL_CENTER.boolean + if (wii) { + menu.findItem(R.id.menu_emulation_ir_recenter).isChecked = + BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean + } + popup.setOnMenuItemClickListener { item: MenuItem -> onOptionsItemSelected(item) } + popup.show() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val action = buttonsActionsMap[item.itemId, -1] + if (action >= 0) { + if (item.isCheckable) { + // Need to pass a reference to the item to set the switch state, since it is not done automatically + handleCheckableMenuAction(action, item) + } else { + handleMenuAction(action) + } + } + return true + } + + private fun handleCheckableMenuAction(menuAction: Int, item: MenuItem) { + when (menuAction) { + MENU_ACTION_JOYSTICK_REL_CENTER -> { + item.isChecked = !item.isChecked + toggleJoystickRelCenter(item.isChecked) + } + + MENU_SET_IR_RECENTER -> { + item.isChecked = !item.isChecked + toggleRecenter(item.isChecked) + } + } + } + + fun handleMenuAction(menuAction: Int) { + when (menuAction) { + MENU_ACTION_EDIT_CONTROLS_PLACEMENT -> editControlsPlacement() + MENU_ACTION_RESET_OVERLAY -> resetOverlay() + MENU_ACTION_TOGGLE_CONTROLS -> toggleControls() + MENU_ACTION_ADJUST_SCALE -> adjustScale() + MENU_ACTION_CHOOSE_CONTROLLER -> chooseController() + MENU_ACTION_REFRESH_WIIMOTES -> NativeLibrary.RefreshWiimotes() + MENU_ACTION_PAUSE_EMULATION -> { + hasUserPausedEmulation = true + NativeLibrary.PauseEmulation() + } + + MENU_ACTION_UNPAUSE_EMULATION -> { + hasUserPausedEmulation = false + NativeLibrary.UnPauseEmulation() + } + + MENU_ACTION_TAKE_SCREENSHOT -> NativeLibrary.SaveScreenShot() + MENU_ACTION_QUICK_SAVE -> NativeLibrary.SaveState(9, false) + MENU_ACTION_QUICK_LOAD -> NativeLibrary.LoadState(9) + MENU_ACTION_SAVE_ROOT -> showSubMenu(SaveOrLoad.SAVE) + MENU_ACTION_LOAD_ROOT -> showSubMenu(SaveOrLoad.LOAD) + MENU_ACTION_SAVE_SLOT1 -> NativeLibrary.SaveState(0, false) + MENU_ACTION_SAVE_SLOT2 -> NativeLibrary.SaveState(1, false) + MENU_ACTION_SAVE_SLOT3 -> NativeLibrary.SaveState(2, false) + MENU_ACTION_SAVE_SLOT4 -> NativeLibrary.SaveState(3, false) + MENU_ACTION_SAVE_SLOT5 -> NativeLibrary.SaveState(4, false) + MENU_ACTION_SAVE_SLOT6 -> NativeLibrary.SaveState(5, false) + MENU_ACTION_LOAD_SLOT1 -> NativeLibrary.LoadState(0) + MENU_ACTION_LOAD_SLOT2 -> NativeLibrary.LoadState(1) + MENU_ACTION_LOAD_SLOT3 -> NativeLibrary.LoadState(2) + MENU_ACTION_LOAD_SLOT4 -> NativeLibrary.LoadState(3) + MENU_ACTION_LOAD_SLOT5 -> NativeLibrary.LoadState(4) + MENU_ACTION_LOAD_SLOT6 -> NativeLibrary.LoadState(5) + MENU_ACTION_CHANGE_DISC -> { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + startActivityForResult(intent, REQUEST_CHANGE_DISC) + } + + MENU_SET_IR_MODE -> setIRMode() + MENU_ACTION_CHOOSE_DOUBLETAP -> chooseDoubleTapButton() + MENU_ACTION_SETTINGS -> SettingsActivity.launch(this, MenuTag.SETTINGS) + MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings() + MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings() + MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation() + } + } + + private fun toggleJoystickRelCenter(state: Boolean) { + BooleanSetting.MAIN_JOYSTICK_REL_CENTER.setBoolean(settings, state) + } + + private fun toggleRecenter(state: Boolean) { + BooleanSetting.MAIN_IR_ALWAYS_RECENTER.setBoolean(settings, state) + emulationFragment?.refreshOverlayPointer() + } + + private fun editControlsPlacement() { + if (emulationFragment!!.isConfiguringControls) { + emulationFragment?.stopConfiguringControls() + } else { + closeSubmenu() + closeMenu() + emulationFragment?.startConfiguringControls() + } + } + + // Gets button presses + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (!menuVisible) { + if (ControllerInterface.dispatchKeyEvent(event)) { + return true + } + } + return super.dispatchKeyEvent(event) + } + + private fun toggleControls() { + val builder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.emulation_toggle_controls) + + val currentController = InputOverlay.configuredControllerType + + if (currentController == InputOverlay.OVERLAY_GAMECUBE) { + val gcEnabledButtons = BooleanArray(11) + val gcSettingBase = "MAIN_BUTTON_TOGGLE_GC_" + + for (i in gcEnabledButtons.indices) { + gcEnabledButtons[i] = BooleanSetting.valueOf(gcSettingBase + i).boolean + } + builder.setMultiChoiceItems( + R.array.gcpadButtons, gcEnabledButtons + ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> + BooleanSetting + .valueOf(gcSettingBase + indexSelected).setBoolean(settings, isChecked) + } + } else if (currentController == InputOverlay.OVERLAY_WIIMOTE_CLASSIC) { + val wiiClassicEnabledButtons = BooleanArray(14) + val classicSettingBase = "MAIN_BUTTON_TOGGLE_CLASSIC_" + + for (i in wiiClassicEnabledButtons.indices) { + wiiClassicEnabledButtons[i] = BooleanSetting.valueOf(classicSettingBase + i).boolean + } + builder.setMultiChoiceItems( + R.array.classicButtons, wiiClassicEnabledButtons + ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> + BooleanSetting.valueOf(classicSettingBase + indexSelected) + .setBoolean(settings, isChecked) + } + } else { + val wiiEnabledButtons = BooleanArray(11) + val wiiSettingBase = "MAIN_BUTTON_TOGGLE_WII_" + + for (i in wiiEnabledButtons.indices) { + wiiEnabledButtons[i] = BooleanSetting.valueOf(wiiSettingBase + i).boolean + } + if (currentController == InputOverlay.OVERLAY_WIIMOTE_NUNCHUK) { + builder.setMultiChoiceItems( + R.array.nunchukButtons, wiiEnabledButtons + ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> + BooleanSetting + .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + } + } else { + builder.setMultiChoiceItems( + R.array.wiimoteButtons, wiiEnabledButtons + ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> + BooleanSetting + .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + } + } + } + + builder + .setNeutralButton(R.string.emulation_toggle_all) { _: DialogInterface?, _: Int -> + emulationFragment!!.toggleInputOverlayVisibility(settings) + } + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshInputOverlay() } + .show() + } + + private fun chooseDoubleTapButton() { + val currentValue = IntSetting.MAIN_DOUBLE_TAP_BUTTON.int + + val buttonList = + if (InputOverlay.configuredControllerType == InputOverlay.OVERLAY_WIIMOTE_CLASSIC) R.array.doubleTapWithClassic else R.array.doubleTap + + var checkedItem = -1 + val itemCount = resources.getStringArray(buttonList).size + for (i in 0 until itemCount) { + if (InputOverlayPointer.DOUBLE_TAP_OPTIONS[i] == currentValue) { + checkedItem = i + break + } + } + + MaterialAlertDialogBuilder(this) + .setSingleChoiceItems(buttonList, checkedItem) { _: DialogInterface?, which: Int -> + IntSetting.MAIN_DOUBLE_TAP_BUTTON.setInt( + settings, + InputOverlayPointer.DOUBLE_TAP_OPTIONS[which] + ) + } + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + emulationFragment?.initInputPointer() + } + .show() + } + + private fun adjustScale() { + val dialogBinding = DialogInputAdjustBinding.inflate(layoutInflater) + dialogBinding.apply { + inputScaleSlider.apply { + valueTo = 150f + value = IntSetting.MAIN_CONTROL_SCALE.int.toFloat() + stepSize = 1f + addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> + dialogBinding.inputScaleValue.text = "${(progress.toInt() + 50)}%" + }) + } + inputScaleValue.text = + "${(dialogBinding.inputScaleSlider.value.toInt() + 50)}%" + + inputOpacitySlider.apply { + valueTo = 100f + value = IntSetting.MAIN_CONTROL_OPACITY.int.toFloat() + stepSize = 1f + addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> + inputOpacityValue.text = progress.toInt().toString() + "%" + }) + } + inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%" + } + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.emulation_control_adjustments) + .setView(dialogBinding.root) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + IntSetting.MAIN_CONTROL_SCALE.setInt( + settings, + dialogBinding.inputScaleSlider.value.toInt() + ) + IntSetting.MAIN_CONTROL_OPACITY.setInt( + settings, + dialogBinding.inputOpacitySlider.value.toInt() + ) + emulationFragment?.refreshInputOverlay() + } + .setNeutralButton(R.string.default_values) { _: DialogInterface?, _: Int -> + IntSetting.MAIN_CONTROL_SCALE.delete(settings) + IntSetting.MAIN_CONTROL_OPACITY.delete(settings) + emulationFragment?.refreshInputOverlay() + } + .show() + } + + private fun addControllerIfNotNone( + entries: MutableList, + values: MutableList, + controller: IntSetting, + entry: Int, + value: Int + ) { + if (controller.int != 0) { + entries.add(getString(entry)) + values.add(value) + } + } + + private fun chooseController() { + val entries = ArrayList() + val values = ArrayList() + + entries.add(getString(R.string.none)) + values.add(-1) + + addControllerIfNotNone( + entries, + values, + IntSetting.MAIN_SI_DEVICE_0, + R.string.controller_0, + 0 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.MAIN_SI_DEVICE_1, + R.string.controller_1, + 1 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.MAIN_SI_DEVICE_2, + R.string.controller_2, + 2 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.MAIN_SI_DEVICE_3, + R.string.controller_3, + 3 + ) + + if (NativeLibrary.IsEmulatingWii()) { + addControllerIfNotNone( + entries, + values, + IntSetting.WIIMOTE_1_SOURCE, + R.string.wiimote_0, + 4 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.WIIMOTE_2_SOURCE, + R.string.wiimote_1, + 5 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.WIIMOTE_3_SOURCE, + R.string.wiimote_2, + 6 + ) + addControllerIfNotNone( + entries, + values, + IntSetting.WIIMOTE_4_SOURCE, + R.string.wiimote_3, + 7 + ) + } + + val controllerSetting = + if (NativeLibrary.IsEmulatingWii()) IntSetting.MAIN_OVERLAY_WII_CONTROLLER else IntSetting.MAIN_OVERLAY_GC_CONTROLLER + val currentValue = controllerSetting.int + var checkedItem = -1 + for (i in values.indices) { + if (values[i] == currentValue) { + checkedItem = i + break + } + } + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.emulation_choose_controller) + .setSingleChoiceItems( + entries.toArray(arrayOf()), checkedItem + ) { _: DialogInterface?, indexSelected: Int -> + controllerSetting.setInt(settings, values[indexSelected]) + } + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + emulationFragment?.refreshInputOverlay() + } + .setNeutralButton( + R.string.emulation_more_controller_settings + ) { _: DialogInterface?, _: Int -> SettingsActivity.launch(this, MenuTag.SETTINGS) } + .show() + } + + private fun setIRMode() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.emulation_ir_mode) + .setSingleChoiceItems( + R.array.irModeEntries, + IntSetting.MAIN_IR_MODE.int + ) { _: DialogInterface?, indexSelected: Int -> + IntSetting.MAIN_IR_MODE.setInt(settings, indexSelected) + } + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + emulationFragment?.refreshOverlayPointer() + } + .show() + } + + private fun showSkylanderPortalSettings() { + skylandersBinding = DialogNfcFiguresManagerBinding.inflate(layoutInflater) + skylandersBinding.figureManager.apply { + layoutManager = LinearLayoutManager(this@EmulationActivity) + adapter = SkylanderSlotAdapter(skylanderSlots, this@EmulationActivity) + } + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.skylanders_manager) + .setView(skylandersBinding.root) + .show() + } + + private fun showInfinityBaseSettings() { + infinityBinding = DialogNfcFiguresManagerBinding.inflate(layoutInflater) + infinityBinding.figureManager.apply { + layoutManager = LinearLayoutManager(this@EmulationActivity) + adapter = FigureSlotAdapter(infinityFigures, this@EmulationActivity) + } + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.infinity_manager) + .setView(infinityBinding.root) + .show() + } + + fun setSkylanderData(id: Int, variant: Int, name: String, slot: Int) { + skylanderData = Skylander(id, variant, name) + skylanderSlot = slot + } + + fun clearSkylander(slot: Int) { + skylanderSlots[slot].label = getString(R.string.skylander_slot, slot + 1) + skylandersBinding.figureManager.adapter?.notifyItemChanged(slot) + } + + fun setInfinityFigureData(num: Long, name: String, position: Int, listPosition: Int) { + infinityFigureData = Figure(num, name) + infinityPosition = position + infinityListPosition = listPosition + } + + fun clearInfinityFigure(position: Int) { + when (position) { + 0 -> infinityFigures[position].label = getString(R.string.infinity_hexagon_label) + 1 -> infinityFigures[position].label = getString(R.string.infinity_p1_label) + 2 -> infinityFigures[position].label = getString(R.string.infinity_p1a1_label) + 3 -> infinityFigures[position].label = getString(R.string.infinity_p1a2_label) + 4 -> infinityFigures[position].label = getString(R.string.infinity_p2_label) + 5 -> infinityFigures[position].label = getString(R.string.infinity_p2a1_label) + 6 -> infinityFigures[position].label = getString(R.string.infinity_p2a2_label) + } + infinityBinding.figureManager.adapter?.notifyItemChanged(position) + } + + private fun resetOverlay() { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.emulation_touch_overlay_reset)) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + emulationFragment?.resetInputOverlay() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + var anyMenuClosed = false + + var submenu = supportFragmentManager.findFragmentById(R.id.frame_submenu) + if (submenu != null && areCoordinatesOutside(submenu.view, event.x, event.y)) { + closeSubmenu() + submenu = null + anyMenuClosed = true + } + + if (submenu == null) { + val menu = supportFragmentManager.findFragmentById(R.id.frame_menu) + if (menu != null && areCoordinatesOutside(menu.view, event.x, event.y)) { + closeMenu() + anyMenuClosed = true + } + } + + if (anyMenuClosed) + return true + } + return super.dispatchTouchEvent(event) + } + + override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { + if (!menuVisible) { + if (ControllerInterface.dispatchGenericMotionEvent(event)) { + return true + } + } + return super.dispatchGenericMotionEvent(event) + } + + private fun showSubMenu(saveOrLoad: SaveOrLoad) { + // Get rid of any visible submenu + supportFragmentManager.popBackStack( + BACKSTACK_NAME_SUBMENU, + FragmentManager.POP_BACK_STACK_INCLUSIVE + ) + + val fragment: Fragment = SaveLoadStateFragment.newInstance(saveOrLoad) + supportFragmentManager.beginTransaction() + .setCustomAnimations( + R.animator.menu_slide_in_from_end, + R.animator.menu_slide_out_to_end, + R.animator.menu_slide_in_from_end, + R.animator.menu_slide_out_to_end + ) + .replace(R.id.frame_submenu, fragment) + .addToBackStack(BACKSTACK_NAME_SUBMENU) + .commit() + } + + fun initInputPointer() { + emulationFragment?.initInputPointer() + } + + override fun setTheme(themeId: Int) { + super.setTheme(themeId) + this.themeId = themeId + } + + companion object { + private const val BACKSTACK_NAME_MENU = "menu" + private const val BACKSTACK_NAME_SUBMENU = "submenu" + const val REQUEST_CHANGE_DISC = 1 + const val REQUEST_SKYLANDER_FILE = 2 + const val REQUEST_CREATE_SKYLANDER = 3 + const val REQUEST_INFINITY_FIGURE_FILE = 4 + const val REQUEST_CREATE_INFINITY_FIGURE = 5 + + private var ignoreLaunchRequests = false + + var hasUserPausedEmulation = false + + private val skylanderSlots: MutableList = ArrayList() + private val infinityFigures: MutableList = ArrayList() + private val buttonsActionsMap = SparseIntArray() + + const val EXTRA_SELECTED_GAMES = "SelectedGames" + const val EXTRA_RIIVOLUTION = "Riivolution" + const val EXTRA_SYSTEM_MENU = "SystemMenu" + const val EXTRA_USER_PAUSED_EMULATION = "sUserPausedEmulation" + const val EXTRA_MENU_TOAST_SHOWN = "MenuToastShown" + const val EXTRA_SKYLANDER_SLOT = "SkylanderSlot" + const val EXTRA_SKYLANDER_ID = "SkylanderId" + const val EXTRA_SKYLANDER_VAR = "SkylanderVar" + const val EXTRA_SKYLANDER_NAME = "SkylanderName" + const val EXTRA_INFINITY_POSITION = "FigurePosition" + const val EXTRA_INFINITY_LIST_POSITION = "FigureListPosition" + const val EXTRA_INFINITY_NUM = "FigureNum" + const val EXTRA_INFINITY_NAME = "FigureName" + const val MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0 + const val MENU_ACTION_TOGGLE_CONTROLS = 1 + const val MENU_ACTION_ADJUST_SCALE = 2 + const val MENU_ACTION_CHOOSE_CONTROLLER = 3 + const val MENU_ACTION_REFRESH_WIIMOTES = 4 + const val MENU_ACTION_TAKE_SCREENSHOT = 5 + const val MENU_ACTION_QUICK_SAVE = 6 + const val MENU_ACTION_QUICK_LOAD = 7 + const val MENU_ACTION_SAVE_ROOT = 8 + const val MENU_ACTION_LOAD_ROOT = 9 + const val MENU_ACTION_SAVE_SLOT1 = 10 + const val MENU_ACTION_SAVE_SLOT2 = 11 + const val MENU_ACTION_SAVE_SLOT3 = 12 + const val MENU_ACTION_SAVE_SLOT4 = 13 + const val MENU_ACTION_SAVE_SLOT5 = 14 + const val MENU_ACTION_SAVE_SLOT6 = 15 + const val MENU_ACTION_LOAD_SLOT1 = 16 + const val MENU_ACTION_LOAD_SLOT2 = 17 + const val MENU_ACTION_LOAD_SLOT3 = 18 + const val MENU_ACTION_LOAD_SLOT4 = 19 + const val MENU_ACTION_LOAD_SLOT5 = 20 + const val MENU_ACTION_LOAD_SLOT6 = 21 + const val MENU_ACTION_EXIT = 22 + const val MENU_ACTION_CHANGE_DISC = 23 + const val MENU_ACTION_JOYSTICK_REL_CENTER = 24 + const val MENU_ACTION_RESET_OVERLAY = 26 + const val MENU_SET_IR_RECENTER = 27 + const val MENU_SET_IR_MODE = 28 + const val MENU_ACTION_CHOOSE_DOUBLETAP = 30 + const val MENU_ACTION_PAUSE_EMULATION = 32 + const val MENU_ACTION_UNPAUSE_EMULATION = 33 + const val MENU_ACTION_OVERLAY_CONTROLS = 34 + const val MENU_ACTION_SETTINGS = 35 + const val MENU_ACTION_SKYLANDERS = 36 + const val MENU_ACTION_INFINITY_BASE = 37 + + init { + buttonsActionsMap.apply { + append(R.id.menu_emulation_edit_layout, MENU_ACTION_EDIT_CONTROLS_PLACEMENT) + append(R.id.menu_emulation_toggle_controls, MENU_ACTION_TOGGLE_CONTROLS) + append(R.id.menu_emulation_adjust_scale, MENU_ACTION_ADJUST_SCALE) + append(R.id.menu_emulation_choose_controller, MENU_ACTION_CHOOSE_CONTROLLER) + append(R.id.menu_emulation_joystick_rel_center, MENU_ACTION_JOYSTICK_REL_CENTER) + append(R.id.menu_emulation_reset_overlay, MENU_ACTION_RESET_OVERLAY) + append(R.id.menu_emulation_ir_recenter, MENU_SET_IR_RECENTER) + append(R.id.menu_emulation_set_ir_mode, MENU_SET_IR_MODE) + append(R.id.menu_emulation_choose_doubletap, MENU_ACTION_CHOOSE_DOUBLETAP) + } + } + + @JvmStatic + fun launch(activity: FragmentActivity, filePaths: Array, riivolution: Boolean) { + if (ignoreLaunchRequests) + return + + performLaunchChecks(activity) { launchWithoutChecks(activity, filePaths, riivolution) } + } + + @JvmStatic + fun launch(activity: FragmentActivity, filePath: String, riivolution: Boolean) = + launch(activity, arrayOf(filePath), riivolution) + + private fun launchWithoutChecks( + activity: FragmentActivity, + filePaths: Array, + riivolution: Boolean + ) { + ignoreLaunchRequests = true + val launcher = Intent(activity, EmulationActivity::class.java) + launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths) + launcher.putExtra(EXTRA_RIIVOLUTION, riivolution) + activity.startActivity(launcher) + } + + private fun performLaunchChecks(activity: FragmentActivity, continueCallback: Runnable) { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { + if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) || + !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) || + !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DUMP_PATH) || + !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_LOAD_PATH) || + !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_RESOURCEPACK_PATH) + ) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.unavailable_paths) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + SettingsActivity.launch(activity, MenuTag.CONFIG_PATHS) + } + .setNeutralButton(R.string.continue_anyway) { _: DialogInterface?, _: Int -> + continueCallback.run() + } + .show() + } else if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_WII_SD_CARD_IMAGE_PATH) || + !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_WII_SD_CARD_SYNC_FOLDER_PATH) + ) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.unavailable_paths) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + SettingsActivity.launch(activity, MenuTag.CONFIG_WII) + } + .setNeutralButton(R.string.continue_anyway) { _: DialogInterface?, _: Int -> + continueCallback.run() + } + .show() + } else { + continueCallback.run() + } + } + } + + @JvmStatic + fun launchSystemMenu(activity: FragmentActivity) { + if (ignoreLaunchRequests) + return + + performLaunchChecks(activity) { launchSystemMenuWithoutChecks(activity) } + } + + private fun launchSystemMenuWithoutChecks(activity: FragmentActivity) { + ignoreLaunchRequests = true + val launcher = Intent(activity, EmulationActivity::class.java) + launcher.putExtra(EXTRA_SYSTEM_MENU, true) + activity.startActivity(launcher) + } + + @JvmStatic + fun stopIgnoringLaunchRequests() { + ignoreLaunchRequests = false + } + + private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean { + if (view == null) + return true + + val viewBounds = Rect() + view.getGlobalVisibleRect(viewBounds) + return !viewBounds.contains(x.roundToInt(), y.roundToInt()) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.java deleted file mode 100644 index 68d46a6b57..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.adapters; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesFragment; - -public class PlatformPagerAdapter extends FragmentPagerAdapter -{ - private SwipeRefreshLayout.OnRefreshListener mOnRefreshListener; - - public final static int[] TAB_ICONS = - { - R.drawable.ic_gamecube, - R.drawable.ic_wii, - R.drawable.ic_folder - }; - - public PlatformPagerAdapter(FragmentManager fm, - SwipeRefreshLayout.OnRefreshListener onRefreshListener) - { - super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - mOnRefreshListener = onRefreshListener; - } - - @NonNull - @Override - public Fragment getItem(int position) - { - Platform platform = Platform.fromPosition(position); - - PlatformGamesFragment fragment = PlatformGamesFragment.newInstance(platform); - fragment.setOnRefreshListener(mOnRefreshListener); - return fragment; - } - - @Override - public int getCount() - { - return TAB_ICONS.length; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.kt new file mode 100644 index 0000000000..27bb122a0f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/PlatformPagerAdapter.kt @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.adapters + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesFragment + +class PlatformPagerAdapter( + fm: FragmentManager, + private val onRefreshListener: OnRefreshListener +) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getItem(position: Int): Fragment { + val platform = Platform.fromPosition(position) + val fragment = PlatformGamesFragment.newInstance(platform) + fragment.setOnRefreshListener(onRefreshListener) + return fragment + } + + override fun getCount(): Int = TAB_ICONS.size + + companion object { + @JvmField + val TAB_ICONS = intArrayOf( + R.drawable.ic_gamecube, + R.drawable.ic_wii, + R.drawable.ic_folder + ) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java deleted file mode 100644 index f05cffb983..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.adapters; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.view.ViewGroup; - -import androidx.core.content.ContextCompat; -import androidx.leanback.widget.ImageCardView; -import androidx.leanback.widget.Presenter; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.model.TvSettingsItem; -import org.dolphinemu.dolphinemu.viewholders.TvSettingsViewHolder; - -public final class SettingsRowPresenter extends Presenter -{ - public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) - { - // Create a new view. - ImageCardView settingsCard = new ImageCardView(parent.getContext()); - - settingsCard.setMainImageAdjustViewBounds(true); - settingsCard.setMainImageDimensions(192, 160); - - settingsCard.setFocusable(true); - settingsCard.setFocusableInTouchMode(true); - - // Use that view to create a ViewHolder. - return new TvSettingsViewHolder(settingsCard); - } - - public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) - { - TvSettingsViewHolder holder = (TvSettingsViewHolder) viewHolder; - Context context = holder.cardParent.getContext(); - TvSettingsItem settingsItem = (TvSettingsItem) item; - - Resources resources = holder.cardParent.getResources(); - - holder.itemId = settingsItem.getItemId(); - - holder.cardParent.setTitleText(resources.getString(settingsItem.getLabelId())); - holder.cardParent.setMainImage(resources.getDrawable(settingsItem.getIconId())); - - // Set the background color of the card - Drawable background = ContextCompat.getDrawable(context, R.drawable.tv_card_background); - holder.cardParent.setInfoAreaBackground(background); - } - - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) - { - // no op - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt new file mode 100644 index 0000000000..c52039c5b2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.adapters + +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.leanback.widget.ImageCardView +import androidx.leanback.widget.Presenter +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.model.TvSettingsItem +import org.dolphinemu.dolphinemu.viewholders.TvSettingsViewHolder + +class SettingsRowPresenter : Presenter() { + override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { + // Create a new view. + val settingsCard = ImageCardView(parent.context) + settingsCard.apply { + setMainImageAdjustViewBounds(true) + setMainImageDimensions(192, 160) + isFocusable = true + isFocusableInTouchMode = true + } + + // Use that view to create a ViewHolder. + return TvSettingsViewHolder(settingsCard) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { + val holder = viewHolder as TvSettingsViewHolder + val context = holder.cardParent.context + val settingsItem = item as TvSettingsItem + val resources = holder.cardParent.resources + holder.apply { + itemId = settingsItem.itemId + cardParent.titleText = resources.getString(settingsItem.labelId) + cardParent.mainImage = ContextCompat.getDrawable(context, settingsItem.iconId) + + // Set the background color of the card + val background = ContextCompat.getDrawable(context, R.drawable.tv_card_background) + cardParent.infoAreaBackground = background + } + } + + override fun onUnbindViewHolder(viewHolder: ViewHolder) { + // no op + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/InfinityConfig.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/InfinityConfig.kt new file mode 100644 index 0000000000..0e2e54acb5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/InfinityConfig.kt @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.infinitybase + +object InfinityConfig { + var LIST_FIGURES: Map = getFigureMap() + var REVERSE_LIST_FIGURES: Map = getInverseFigureMap() + + private external fun getFigureMap(): Map + private external fun getInverseFigureMap(): Map + + @JvmStatic + external fun removeFigure(position: Int) + + @JvmStatic + external fun loadFigure(position: Int, fileName: String): String? + + @JvmStatic + external fun createFigure( + figureNumber: Long, + fileName: String, + position: Int + ): String? +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/model/Figure.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/model/Figure.kt new file mode 100644 index 0000000000..b65b978e37 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/model/Figure.kt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.infinitybase.model + +data class Figure(var number: Long, var name: String) { + + companion object { + @JvmField + val BLANK_FIGURE = Figure(-1, "Blank") + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlot.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlot.kt new file mode 100644 index 0000000000..7823fe1a92 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlot.kt @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.infinitybase.ui + +data class FigureSlot(var label: String, val position: Int) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt new file mode 100644 index 0000000000..8d88c37ccb --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.infinitybase.ui + +import android.app.AlertDialog +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.DialogCreateInfinityFigureBinding +import org.dolphinemu.dolphinemu.databinding.ListItemNfcFigureSlotBinding +import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig +import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig.removeFigure + +class FigureSlotAdapter( + private val figures: List, + private val activity: EmulationActivity +) : RecyclerView.Adapter(), + OnItemClickListener { + + class ViewHolder(var binding: ListItemNfcFigureSlotBinding) : + RecyclerView.ViewHolder(binding.getRoot()) + + private lateinit var binding: DialogCreateInfinityFigureBinding + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ListItemNfcFigureSlotBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val figure = figures[position] + holder.binding.textFigureName.text = figure.label + + holder.binding.buttonClearFigure.setOnClickListener { + removeFigure(figure.position) + activity.clearInfinityFigure(position) + } + + holder.binding.buttonLoadFigure.setOnClickListener { + val loadFigure = Intent(Intent.ACTION_OPEN_DOCUMENT) + loadFigure.addCategory(Intent.CATEGORY_OPENABLE) + loadFigure.type = "*/*" + activity.setInfinityFigureData(0, "", figure.position, position) + activity.startActivityForResult( + loadFigure, + EmulationActivity.REQUEST_INFINITY_FIGURE_FILE + ) + } + + val inflater = LayoutInflater.from(activity) + binding = DialogCreateInfinityFigureBinding.inflate(inflater) + + binding.infinityDropdown.onItemClickListener = this + + holder.binding.buttonCreateFigure.setOnClickListener { + var validFigures = InfinityConfig.REVERSE_LIST_FIGURES + // Filter adapter list by position, either Hexagon Pieces, Characters or Abilities + validFigures = when (figure.position) { + 0 -> { + // Hexagon Pieces + validFigures.filter { (_, value) -> value in 2000000..2999999 || value in 4000000..4999999 } + } + + 1, 2 -> { + // Characters + validFigures.filter { (_, value) -> value in 1000000..1999999 } + } + + else -> { + // Abilities + validFigures.filter { (_, value) -> value in 3000000..3999999 } + } + } + val figureListKeys = validFigures.keys.toMutableList() + figureListKeys.sort() + val figureNames: ArrayList = ArrayList(figureListKeys) + binding.infinityDropdown.setAdapter( + ArrayAdapter( + activity, R.layout.support_simple_spinner_dropdown_item, + figureNames + ) + ) + + if (binding.getRoot().parent != null) { + (binding.getRoot().parent as ViewGroup).removeAllViews() + } + val createDialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.create_figure_title) + .setView(binding.getRoot()) + .setPositiveButton(R.string.create_figure, null) + .setNegativeButton(R.string.cancel, null) + .show() + createDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + if (binding.infinityNum.text.toString().isNotBlank()) { + val createFigure = Intent(Intent.ACTION_CREATE_DOCUMENT) + createFigure.addCategory(Intent.CATEGORY_OPENABLE) + createFigure.type = "*/*" + val num = binding.infinityNum.text.toString().toLong() + val name = InfinityConfig.LIST_FIGURES[num] + if (name != null) { + createFigure.putExtra( + Intent.EXTRA_TITLE, + "$name.bin" + ) + activity.setInfinityFigureData(num, name, figure.position, position) + } else { + createFigure.putExtra( + Intent.EXTRA_TITLE, + "Unknown(Number: $num).bin" + ) + activity.setInfinityFigureData(num, "Unknown", figure.position, position) + } + activity.startActivityForResult( + createFigure, + EmulationActivity.REQUEST_CREATE_INFINITY_FIGURE + ) + createDialog.dismiss() + } else { + Toast.makeText( + activity, R.string.invalid_infinity_figure, + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + override fun getItemCount(): Int { + return figures.size + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val figureNumber = InfinityConfig.REVERSE_LIST_FIGURES[parent.getItemAtPosition(position)] + binding.infinityNum.setText(figureNumber.toString()) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.kt index f98af62592..d38f282f64 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.kt @@ -50,7 +50,7 @@ class RiivolutionBootActivity : AppCompatActivity() { binding.textSdRoot.text = getString(R.string.riivolution_sd_root, "$loadPath/Riivolution") binding.buttonStart.setOnClickListener { if (patches != null) patches!!.saveConfig() - EmulationActivity.launch(this, path, true) + EmulationActivity.launch(this, path!!, true) } lifecycleScope.launch { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index e59cb8a097..80060f61dd 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -217,6 +217,12 @@ enum class BooleanSetting( "EmulateSkylanderPortal", false ), + MAIN_EMULATE_INFINITY_BASE( + Settings.FILE_DOLPHIN, + Settings.SECTION_EMULATED_USB_DEVICES, + "EmulateInfinityBase", + false + ), MAIN_SHOW_GAME_TITLES( Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, @@ -719,7 +725,8 @@ enum class BooleanSetting( MAIN_RAM_OVERRIDE_ENABLE, MAIN_CUSTOM_RTC_ENABLE, MAIN_DSP_JIT, - MAIN_EMULATE_SKYLANDER_PORTAL + MAIN_EMULATE_SKYLANDER_PORTAL, + MAIN_EMULATE_INFINITY_BASE ) private val NOT_RUNTIME_EDITABLE: Set = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index bb479267c9..7833e83542 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -62,6 +62,12 @@ enum class StringSetting( Settings.SECTION_GFX_ENHANCEMENTS, "PostProcessingShader", "" + ), + GFX_DRIVER_LIB_NAME( + Settings.FILE_GFX, + Settings.SECTION_GFX_SETTINGS, + "DriverLibName", + "" ); override val isOverridden: Boolean diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt index 38c162d6fd..6b4def552c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt @@ -49,7 +49,8 @@ enum class MenuTag { WIIMOTE_MOTION_INPUT_1("wiimote_motion_input", 0), WIIMOTE_MOTION_INPUT_2("wiimote_motion_input", 1), WIIMOTE_MOTION_INPUT_3("wiimote_motion_input", 2), - WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3); + WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3), + GPU_DRIVERS("gpu_drivers"); var tag: String private set @@ -85,6 +86,14 @@ enum class MenuTag { val isWiimoteMenu: Boolean get() = this == WIIMOTE_1 || this == WIIMOTE_2 || this == WIIMOTE_3 || this == WIIMOTE_4 + val isWiimoteSubmenu: Boolean + get() = this == WIIMOTE_GENERAL_1 || this == WIIMOTE_GENERAL_2 || + this == WIIMOTE_GENERAL_3 || this == WIIMOTE_GENERAL_4 || + this == WIIMOTE_MOTION_SIMULATION_1 || this == WIIMOTE_MOTION_SIMULATION_2 || + this == WIIMOTE_MOTION_SIMULATION_3 || this == WIIMOTE_MOTION_SIMULATION_4 || + this == WIIMOTE_MOTION_INPUT_1 || this == WIIMOTE_MOTION_INPUT_2 || + this == WIIMOTE_MOTION_INPUT_3 || this == WIIMOTE_MOTION_INPUT_4 + val isWiimoteExtensionMenu: Boolean get() = this == WIIMOTE_EXTENSION_1 || this == WIIMOTE_EXTENSION_2 || this == WIIMOTE_EXTENSION_3 || this == WIIMOTE_EXTENSION_4 diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt index 4d950d9933..cae9f4e840 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt @@ -1,5 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// GPU driver implementation partially based on: +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.settings.ui import android.content.Context @@ -20,6 +24,7 @@ import androidx.lifecycle.ViewModelProvider import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding @@ -27,6 +32,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.InsetsHelper import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint @@ -165,8 +171,21 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { super.onActivityResult(requestCode, resultCode, result) // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) { - if (requestCode != MainPresenter.REQUEST_DIRECTORY) { + if (resultCode != RESULT_OK) { + return + } + + when (requestCode) { + MainPresenter.REQUEST_DIRECTORY -> { + val path = FileBrowserHelper.getSelectedPath(result) + fragment!!.adapter!!.onFilePickerConfirmation(path!!) + } + + MainPresenter.REQUEST_GAME_FILE + or MainPresenter.REQUEST_SD_FILE + or MainPresenter.REQUEST_WAD_FILE + or MainPresenter.REQUEST_WII_SAVE_FILE + or MainPresenter.REQUEST_NAND_BIN_FILE -> { val uri = canonicalizeIfPossible(result!!.data!!) val validExtensions: Set = if (requestCode == MainPresenter.REQUEST_GAME_FILE) FileBrowserHelper.GAME_EXTENSIONS else FileBrowserHelper.RAW_EXTENSION @@ -178,9 +197,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { contentResolver.takePersistableUriPermission(uri, takeFlags) fragment!!.adapter!!.onFilePickerConfirmation(uri.toString()) } - } else { - val path = FileBrowserHelper.getSelectedPath(result) - fragment!!.adapter!!.onFilePickerConfirmation(path!!) } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index d9ffd9113f..3b67dbd9e8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -3,10 +3,15 @@ package org.dolphinemu.dolphinemu.features.settings.ui import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -14,10 +19,15 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem +import org.dolphinemu.dolphinemu.ui.main.MainActivity +import org.dolphinemu.dolphinemu.ui.main.MainPresenter +import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable import java.util.* import kotlin.collections.ArrayList @@ -111,6 +121,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } override fun loadSubMenu(menuKey: MenuTag) { + if (menuKey == MenuTag.GPU_DRIVERS) { + showGpuDriverDialog() + return + } + activityView!!.showSettingsFragment( menuKey, null, @@ -170,6 +185,74 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } } + override fun showGpuDriverDialog() { + if (presenter.gpuDriver == null) { + return + } + val msg = "${presenter!!.gpuDriver!!.name} ${presenter!!.gpuDriver!!.driverVersion}" + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.gpu_driver_dialog_title)) + .setMessage(msg) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int -> + presenter.useSystemDriver() + } + .setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int -> + askForDriverFile() + } + .show() + } + + private fun askForDriverFile() { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + type = "application/zip" + } + startActivityForResult(intent, MainPresenter.REQUEST_GPU_DRIVER) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + // If the user picked a file, as opposed to just backing out. + if (resultCode != AppCompatActivity.RESULT_OK) { + return + } + + if (requestCode != MainPresenter.REQUEST_GPU_DRIVER) { + return + } + + val uri = data?.data ?: return + presenter.installDriver(uri) + } + + override fun onDriverInstallDone(result: GpuDriverInstallResult) { + val view = binding?.root ?: return + Snackbar + .make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG) + .show() + } + + override fun onDriverUninstallDone() { + Toast.makeText( + requireContext(), + R.string.gpu_driver_dialog_uninstall_done, + Toast.LENGTH_SHORT + ).show() + } + + private fun resolveInstallResultString(result: GpuDriverInstallResult) = when (result) { + GpuDriverInstallResult.Success -> getString(R.string.gpu_driver_install_success) + GpuDriverInstallResult.InvalidArchive -> getString(R.string.gpu_driver_install_invalid_archive) + GpuDriverInstallResult.MissingMetadata -> getString(R.string.gpu_driver_install_missing_metadata) + GpuDriverInstallResult.InvalidMetadata -> getString(R.string.gpu_driver_install_invalid_metadata) + GpuDriverInstallResult.UnsupportedAndroidVersion -> getString(R.string.gpu_driver_install_unsupported_android_version) + GpuDriverInstallResult.AlreadyInstalled -> getString(R.string.gpu_driver_install_already_installed) + GpuDriverInstallResult.FileNotFound -> getString(R.string.gpu_driver_install_file_not_found) + } + companion object { private const val ARGUMENT_MENU_TAG = "menu_tag" private const val ARGUMENT_GAME_ID = "game_id" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 72649a39fd..5ed4238caf 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -4,11 +4,16 @@ package org.dolphinemu.dolphinemu.features.settings.ui import android.content.Context import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.text.TextUtils import androidx.appcompat.app.AppCompatActivity import androidx.collection.ArraySet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.UserDataActivity @@ -25,6 +30,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter import org.dolphinemu.dolphinemu.features.settings.model.* import org.dolphinemu.dolphinemu.features.settings.model.view.* +import org.dolphinemu.dolphinemu.model.GpuDriverMetadata import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.utils.* import java.util.* @@ -45,6 +51,9 @@ class SettingsFragmentPresenter( private var controllerNumber = 0 private var controllerType = 0 + var gpuDriver: GpuDriverMetadata? = null + private val libNameSetting: StringSetting = StringSetting.GFX_DRIVER_LIB_NAME + fun onCreate(menuTag: MenuTag, gameId: String?, extras: Bundle) { this.gameId = gameId this.menuTag = menuTag @@ -52,10 +61,15 @@ class SettingsFragmentPresenter( if (menuTag.isGCPadMenu || menuTag.isWiimoteExtensionMenu) { controllerNumber = menuTag.subType controllerType = extras.getInt(ARG_CONTROLLER_TYPE) - } else if (menuTag.isWiimoteMenu) { + } else if (menuTag.isWiimoteMenu || menuTag.isWiimoteSubmenu) { controllerNumber = menuTag.subType } else if (menuTag.isSerialPort1Menu) { serialPort1Type = extras.getInt(ARG_SERIALPORT1_TYPE) + } else if (menuTag == MenuTag.GRAPHICS) { + this.gpuDriver = + GpuDriverHelper.getInstalledDriverMetadata() ?: GpuDriverHelper.getSystemDriverMetadata( + context.applicationContext + ) } } @@ -115,6 +129,7 @@ class SettingsFragmentPresenter( controllerNumber, controllerType ) + MenuTag.WIIMOTE_1, MenuTag.WIIMOTE_2, MenuTag.WIIMOTE_3, @@ -122,6 +137,7 @@ class SettingsFragmentPresenter( sl, controllerNumber ) + MenuTag.WIIMOTE_EXTENSION_1, MenuTag.WIIMOTE_EXTENSION_2, MenuTag.WIIMOTE_EXTENSION_3, @@ -130,6 +146,7 @@ class SettingsFragmentPresenter( controllerNumber, controllerType ) + MenuTag.WIIMOTE_GENERAL_1, MenuTag.WIIMOTE_GENERAL_2, MenuTag.WIIMOTE_GENERAL_3, @@ -137,6 +154,7 @@ class SettingsFragmentPresenter( sl, controllerNumber ) + MenuTag.WIIMOTE_MOTION_SIMULATION_1, MenuTag.WIIMOTE_MOTION_SIMULATION_2, MenuTag.WIIMOTE_MOTION_SIMULATION_3, @@ -144,6 +162,7 @@ class SettingsFragmentPresenter( sl, controllerNumber ) + MenuTag.WIIMOTE_MOTION_INPUT_1, MenuTag.WIIMOTE_MOTION_INPUT_2, MenuTag.WIIMOTE_MOTION_INPUT_3, @@ -151,6 +170,7 @@ class SettingsFragmentPresenter( sl, controllerNumber ) + else -> throw UnsupportedOperationException("Unimplemented menu") } @@ -454,10 +474,12 @@ class SettingsFragmentPresenter( BooleanSetting.MAIN_DSP_HLE.setBoolean(settings, true) BooleanSetting.MAIN_DSP_JIT.setBoolean(settings, true) } + DSP_LLE_RECOMPILER -> { BooleanSetting.MAIN_DSP_HLE.setBoolean(settings, false) BooleanSetting.MAIN_DSP_JIT.setBoolean(settings, true) } + DSP_LLE_INTERPRETER -> { BooleanSetting.MAIN_DSP_HLE.setBoolean(settings, false) BooleanSetting.MAIN_DSP_JIT.setBoolean(settings, false) @@ -834,6 +856,14 @@ class SettingsFragmentPresenter( 0 ) ) + sl.add( + SwitchSetting( + context, + BooleanSetting.MAIN_EMULATE_INFINITY_BASE, + R.string.emulate_infinity_base, + 0 + ) + ) } private fun addAdvancedSettings(sl: ArrayList) { @@ -856,10 +886,12 @@ class SettingsFragmentPresenter( BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.setBoolean(settings, false) BooleanSetting.MAIN_SYNC_GPU.setBoolean(settings, false) } + SYNC_GPU_ON_IDLE_SKIP -> { BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.setBoolean(settings, true) BooleanSetting.MAIN_SYNC_GPU.setBoolean(settings, false) } + SYNC_GPU_ALWAYS -> { BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.setBoolean(settings, true) BooleanSetting.MAIN_SYNC_GPU.setBoolean(settings, true) @@ -893,10 +925,12 @@ class SettingsFragmentPresenter( emuCoresEntries = R.array.emuCoresEntriesX86_64 emuCoresValues = R.array.emuCoresValuesX86_64 } + 4 -> { emuCoresEntries = R.array.emuCoresEntriesARM64 emuCoresValues = R.array.emuCoresValuesARM64 } + else -> { emuCoresEntries = R.array.emuCoresEntriesGeneric emuCoresValues = R.array.emuCoresValuesGeneric @@ -1230,6 +1264,15 @@ class SettingsFragmentPresenter( MenuTag.ADVANCED_GRAPHICS ) ) + + if (GpuDriverHelper.supportsCustomDriverLoading() && this.gpuDriver != null) { + sl.add( + SubmenuSetting( + context, + R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS + ) + ) + } } private fun addEnhanceSettings(sl: ArrayList) { @@ -2093,7 +2136,7 @@ class SettingsFragmentPresenter( profileString: String, controllerNumber: Int ) { - val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false) + val profiles = ProfileDialogPresenter(menuTag!!).getProfileNames(false) val profileKey = profileString + "Profile" + (controllerNumber + 1) sl.add( StringSingleChoiceSetting( @@ -2246,6 +2289,7 @@ class SettingsFragmentPresenter( setting.uiSuffix ) ) + NumericSetting.TYPE_BOOLEAN -> sl.add( SwitchSetting( InputMappingBooleanSetting(setting), @@ -2303,6 +2347,45 @@ class SettingsFragmentPresenter( ) } + fun installDriver(uri: Uri) { + val context = this.context.applicationContext + CoroutineScope(Dispatchers.IO).launch { + val stream = context.contentResolver.openInputStream(uri) + if (stream == null) { + GpuDriverHelper.uninstallDriver() + withContext(Dispatchers.Main) { + fragmentView.onDriverInstallDone(GpuDriverInstallResult.FileNotFound) + } + return@launch + } + + val result = GpuDriverHelper.installDriver(stream) + withContext(Dispatchers.Main) { + with(this@SettingsFragmentPresenter) { + this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata() + ?: GpuDriverHelper.getSystemDriverMetadata(context) ?: return@withContext + this.libNameSetting.setString(this.settings!!, this.gpuDriver!!.libraryName) + } + fragmentView.onDriverInstallDone(result) + } + } + } + + fun useSystemDriver() { + CoroutineScope(Dispatchers.IO).launch { + GpuDriverHelper.uninstallDriver() + withContext(Dispatchers.Main) { + with(this@SettingsFragmentPresenter) { + this.gpuDriver = + GpuDriverHelper.getInstalledDriverMetadata() + ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext) + this.libNameSetting.setString(this.settings!!, "") + } + fragmentView.onDriverUninstallDone() + } + } + } + companion object { private val LOG_TYPE_NAMES = NativeLibrary.GetLogTypeNames() const val ARG_CONTROLLER_TYPE = "controller_type" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt index fa861153dd..896b316883 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem +import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult /** * Abstraction for a screen showing a list of settings. Instances of @@ -111,4 +112,21 @@ interface SettingsFragmentView { * @param visible Whether the warning should be visible. */ fun setOldControllerSettingsWarningVisibility(visible: Boolean) + + /** + * Called when the driver installation is finished + * + * @param result The result of the driver installation + */ + fun onDriverInstallDone(result: GpuDriverInstallResult) + + /** + * Called when the driver uninstall process is finished + */ + fun onDriverUninstallDone() + + /** + * Shows a dialog asking the user to install or uninstall a GPU driver + */ + fun showGpuDriverDialog() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt index bc23ca3f37..aa4157a54b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt @@ -16,7 +16,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.DialogCreateSkylanderBinding -import org.dolphinemu.dolphinemu.databinding.ListItemSkylanderSlotBinding +import org.dolphinemu.dolphinemu.databinding.ListItemNfcFigureSlotBinding import org.dolphinemu.dolphinemu.features.skylanders.SkylanderConfig import org.dolphinemu.dolphinemu.features.skylanders.SkylanderConfig.removeSkylander import org.dolphinemu.dolphinemu.features.skylanders.model.SkylanderPair @@ -25,27 +25,27 @@ class SkylanderSlotAdapter( private val slots: List, private val activity: EmulationActivity ) : RecyclerView.Adapter(), OnItemClickListener { - class ViewHolder(var binding: ListItemSkylanderSlotBinding) : + class ViewHolder(var binding: ListItemNfcFigureSlotBinding) : RecyclerView.ViewHolder(binding.getRoot()) private lateinit var binding: DialogCreateSkylanderBinding override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) - val binding = ListItemSkylanderSlotBinding.inflate(inflater, parent, false) + val binding = ListItemNfcFigureSlotBinding.inflate(inflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val slot = slots[position] - holder.binding.textSkylanderName.text = slot.label + holder.binding.textFigureName.text = slot.label - holder.binding.buttonClearSkylander.setOnClickListener { + holder.binding.buttonClearFigure.setOnClickListener { removeSkylander(slot.portalSlot) activity.clearSkylander(slot.slotNum) } - holder.binding.buttonLoadSkylander.setOnClickListener { + holder.binding.buttonLoadFigure.setOnClickListener { val loadSkylander = Intent(Intent.ACTION_OPEN_DOCUMENT) loadSkylander.addCategory(Intent.CATEGORY_OPENABLE) loadSkylander.type = "*/*" @@ -71,14 +71,14 @@ class SkylanderSlotAdapter( ) binding.skylanderDropdown.onItemClickListener = this - holder.binding.buttonCreateSkylander.setOnClickListener { + holder.binding.buttonCreateFigure.setOnClickListener { if (binding.getRoot().parent != null) { (binding.getRoot().parent as ViewGroup).removeAllViews() } val createDialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.create_skylander_title) .setView(binding.getRoot()) - .setPositiveButton(R.string.create_skylander, null) + .setPositiveButton(R.string.create_figure, null) .setNegativeButton(R.string.cancel, null) .show() createDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java deleted file mode 100644 index 933b4ad2ff..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - -import com.nononsenseapps.filepicker.FilePickerFragment; - -import org.dolphinemu.dolphinemu.R; - -import java.io.File; -import java.util.HashSet; - -public class CustomFilePickerFragment extends FilePickerFragment -{ - public static final String KEY_EXTENSIONS = "KEY_EXTENSIONS"; - - private HashSet mExtensions; - - public void setExtensions(HashSet extensions) - { - Bundle b = getArguments(); - if (b == null) - b = new Bundle(); - - b.putSerializable(KEY_EXTENSIONS, extensions); - setArguments(b); - } - - @NonNull - @Override - public Uri toUri(@NonNull final File file) - { - return FileProvider - .getUriForFile(getContext(), - getContext().getApplicationContext().getPackageName() + ".filesprovider", - file); - } - - @Override public void onActivityCreated(Bundle savedInstanceState) - { - super.onActivityCreated(savedInstanceState); - - mExtensions = (HashSet) getArguments().getSerializable(KEY_EXTENSIONS); - - if (mode == MODE_DIR) - { - TextView ok = getActivity().findViewById(R.id.nnf_button_ok); - ok.setText(R.string.select_dir); - - TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); - cancel.setVisibility(View.GONE); - } - } - - @Override - protected boolean isItemVisible(@NonNull final File file) - { - // Some users jump to the conclusion that Dolphin isn't able to detect their - // files if the files don't show up in the file picker when mode == MODE_DIR. - // To avoid this, show files even when the user needs to select a directory. - - return (showHiddenItems || !file.isHidden()) && - (file.isDirectory() || - mExtensions.contains(fileExtension(file.getName()).toLowerCase())); - } - - @Override - public boolean isCheckable(@NonNull final File file) - { - // We need to make a small correction to the isCheckable logic due to - // overriding isItemVisible to show files when mode == MODE_DIR. - // AbstractFilePickerFragment always treats files as checkable when - // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. - - return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); - } - - private static String fileExtension(@NonNull String filename) - { - int i = filename.lastIndexOf('.'); - return i < 0 ? "" : filename.substring(i + 1); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt new file mode 100644 index 0000000000..16059d5f4b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.content.FileProvider +import com.nononsenseapps.filepicker.FilePickerFragment +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable +import java.io.File +import java.util.Locale + +class CustomFilePickerFragment : FilePickerFragment() { + private var extensions: HashSet? = null + + fun setExtensions(extensions: HashSet?) { + var b = arguments + if (b == null) + b = Bundle() + b.putSerializable(KEY_EXTENSIONS, extensions) + arguments = b + } + + override fun toUri(file: File): Uri { + return FileProvider.getUriForFile( + requireContext(), + "${requireContext().applicationContext.packageName}.filesprovider", + file + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + extensions = requireArguments().serializable(KEY_EXTENSIONS) as HashSet? + + if (mode == MODE_DIR) { + val ok = requireActivity().findViewById(R.id.nnf_button_ok) + ok.setText(R.string.select_dir) + + val cancel = requireActivity().findViewById(R.id.nnf_button_cancel) + cancel.visibility = View.GONE + } + } + + override fun isItemVisible(file: File): Boolean { + // Some users jump to the conclusion that Dolphin isn't able to detect their + // files if the files don't show up in the file picker when mode == MODE_DIR. + // To avoid this, show files even when the user needs to select a directory. + return (showHiddenItems || !file.isHidden) && + (file.isDirectory || extensions!!.contains(fileExtension(file.name).lowercase(Locale.ENGLISH))) + } + + override fun isCheckable(file: File): Boolean { + // We need to make a small correction to the isCheckable logic due to + // overriding isItemVisible to show files when mode == MODE_DIR. + // AbstractFilePickerFragment always treats files as checkable when + // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. + return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile) + } + + companion object { + const val KEY_EXTENSIONS = "KEY_EXTENSIONS" + + private fun fileExtension(filename: String): String { + val i = filename.lastIndexOf('.') + return if (i < 0) "" else filename.substring(i + 1) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java deleted file mode 100644 index 2a808f8dc5..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ /dev/null @@ -1,354 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.overlay.InputOverlay; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.io.File; - -public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback -{ - private static final String KEY_GAMEPATHS = "gamepaths"; - private static final String KEY_RIIVOLUTION = "riivolution"; - private static final String KEY_SYSTEM_MENU = "systemMenu"; - - private InputOverlay mInputOverlay; - - private String[] mGamePaths; - private boolean mRiivolution; - private boolean mRunWhenSurfaceIsValid; - private boolean mLoadPreviousTemporaryState; - private boolean mLaunchSystemMenu; - - private EmulationActivity activity; - - private FragmentEmulationBinding mBinding; - - public static EmulationFragment newInstance(String[] gamePaths, boolean riivolution, - boolean systemMenu) - { - Bundle args = new Bundle(); - args.putStringArray(KEY_GAMEPATHS, gamePaths); - args.putBoolean(KEY_RIIVOLUTION, riivolution); - args.putBoolean(KEY_SYSTEM_MENU, systemMenu); - - EmulationFragment fragment = new EmulationFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) - { - super.onAttach(context); - - if (context instanceof EmulationActivity) - { - activity = (EmulationActivity) context; - NativeLibrary.setEmulationActivity((EmulationActivity) context); - } - else - { - throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); - } - } - - /** - * Initialize anything that doesn't depend on the layout / views in here. - */ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mGamePaths = getArguments().getStringArray(KEY_GAMEPATHS); - mRiivolution = getArguments().getBoolean(KEY_RIIVOLUTION); - mLaunchSystemMenu = getArguments().getBoolean(KEY_SYSTEM_MENU); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentEmulationBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - // The new Surface created here will get passed to the native code via onSurfaceChanged. - SurfaceView surfaceView = mBinding.surfaceEmulation; - surfaceView.getHolder().addCallback(this); - - mInputOverlay = mBinding.surfaceInputOverlay; - - Button doneButton = mBinding.doneControlConfig; - if (doneButton != null) - { - doneButton.setOnClickListener(v -> stopConfiguringControls()); - } - - if (mInputOverlay != null) - { - view.post(() -> - { - int overlayX = mInputOverlay.getLeft(); - int overlayY = mInputOverlay.getTop(); - mInputOverlay.setSurfacePosition(new Rect( - surfaceView.getLeft() - overlayX, surfaceView.getTop() - overlayY, - surfaceView.getRight() - overlayX, surfaceView.getBottom() - overlayY)); - }); - } - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onResume() - { - super.onResume(); - - if (mInputOverlay != null && NativeLibrary.IsGameMetadataValid()) - mInputOverlay.refreshControls(); - - run(activity.isActivityRecreated()); - } - - @Override - public void onPause() - { - if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Pausing emulation."); - NativeLibrary.PauseEmulation(); - } - - super.onPause(); - } - - @Override - public void onDestroy() - { - if (mInputOverlay != null) - mInputOverlay.onDestroy(); - - super.onDestroy(); - } - - @Override - public void onDetach() - { - NativeLibrary.clearEmulationActivity(); - super.onDetach(); - } - - public void toggleInputOverlayVisibility(Settings settings) - { - BooleanSetting.MAIN_SHOW_INPUT_OVERLAY - .setBoolean(settings, !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.getBoolean()); - - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void initInputPointer() - { - if (mInputOverlay != null) - mInputOverlay.initTouchPointer(); - } - - public void refreshInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void refreshOverlayPointer() - { - if (mInputOverlay != null) - mInputOverlay.refreshOverlayPointer(); - } - - public void resetInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.resetButtonPlacement(); - } - - @Override - public void surfaceCreated(@NonNull SurfaceHolder holder) - { - // We purposely don't do anything here. - // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) - { - Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); - NativeLibrary.SurfaceChanged(holder.getSurface()); - if (mRunWhenSurfaceIsValid) - { - runWithValidSurface(); - } - } - - @Override - public void surfaceDestroyed(@NonNull SurfaceHolder holder) - { - Log.debug("[EmulationFragment] Surface destroyed."); - NativeLibrary.SurfaceDestroyed(); - mRunWhenSurfaceIsValid = true; - } - - public void stopEmulation() - { - Log.debug("[EmulationFragment] Stopping emulation."); - NativeLibrary.StopEmulation(); - } - - public void startConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.VISIBLE); - mInputOverlay.setIsInEditMode(true); - } - } - - public void stopConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.GONE); - mInputOverlay.setIsInEditMode(false); - } - } - - public boolean isConfiguringControls() - { - return mInputOverlay != null && mInputOverlay.isInEditMode(); - } - - private void run(boolean isActivityRecreated) - { - if (isActivityRecreated) - { - if (NativeLibrary.IsRunning()) - { - mLoadPreviousTemporaryState = false; - deleteFile(getTemporaryStateFilePath()); - } - else - { - mLoadPreviousTemporaryState = true; - } - } - else - { - Log.debug("[EmulationFragment] activity resumed or fresh start"); - mLoadPreviousTemporaryState = false; - // activity resumed without being killed or this is the first run - deleteFile(getTemporaryStateFilePath()); - } - - // If the surface is set, run now. Otherwise, wait for it to get set. - if (NativeLibrary.HasSurface()) - { - runWithValidSurface(); - } - else - { - mRunWhenSurfaceIsValid = true; - } - } - - private void runWithValidSurface() - { - mRunWhenSurfaceIsValid = false; - if (!NativeLibrary.IsRunning()) - { - NativeLibrary.SetIsBooting(); - - Thread emulationThread = new Thread(() -> - { - if (mLoadPreviousTemporaryState) - { - Log.debug("[EmulationFragment] Starting emulation thread from previous state."); - NativeLibrary.Run(mGamePaths, mRiivolution, getTemporaryStateFilePath(), true); - } - if (mLaunchSystemMenu) - { - Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu."); - NativeLibrary.RunSystemMenu(); - } - else - { - Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePaths, mRiivolution); - } - EmulationActivity.stopIgnoringLaunchRequests(); - }, "NativeEmulation"); - emulationThread.start(); - } - else - { - if (!EmulationActivity.getHasUserPausedEmulation() && !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Resuming emulation."); - NativeLibrary.UnPauseEmulation(); - } - } - } - - public void saveTemporaryState() - { - NativeLibrary.SaveStateAs(getTemporaryStateFilePath(), true); - } - - private String getTemporaryStateFilePath() - { - return getContext().getFilesDir() + File.separator + "temp.sav"; - } - - private static void deleteFile(String path) - { - try - { - File file = new File(path); - if (!file.delete()) - { - Log.error("[EmulationFragment] Failed to delete " + file.getAbsolutePath()); - } - } - catch (Exception ignored) - { - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt new file mode 100644 index 0000000000..e403e7ef5d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.content.Context +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.overlay.InputOverlay +import org.dolphinemu.dolphinemu.utils.Log +import java.io.File + +class EmulationFragment : Fragment(), SurfaceHolder.Callback { + private var inputOverlay: InputOverlay? = null + + private var gamePaths: Array? = null + private var riivolution = false + private var runWhenSurfaceIsValid = false + private var loadPreviousTemporaryState = false + private var launchSystemMenu = false + + private var emulationActivity: EmulationActivity? = null + + private var _binding: FragmentEmulationBinding? = null + private val binding get() = _binding!! + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is EmulationActivity) { + emulationActivity = context + NativeLibrary.setEmulationActivity(context) + } else { + throw IllegalStateException("EmulationFragment must have EmulationActivity parent") + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireArguments().apply { + gamePaths = getStringArray(KEY_GAMEPATHS) + riivolution = getBoolean(KEY_RIIVOLUTION) + launchSystemMenu = getBoolean(KEY_SYSTEM_MENU) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEmulationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // The new Surface created here will get passed to the native code via onSurfaceChanged. + val surfaceView = binding.surfaceEmulation + surfaceView.holder.addCallback(this) + + inputOverlay = binding.surfaceInputOverlay + + val doneButton = binding.doneControlConfig + doneButton?.setOnClickListener { stopConfiguringControls() } + + if (inputOverlay != null) { + view.post { + val overlayX = inputOverlay!!.left + val overlayY = inputOverlay!!.top + inputOverlay?.setSurfacePosition( + Rect( + surfaceView.left - overlayX, + surfaceView.top - overlayY, + surfaceView.right - overlayX, + surfaceView.bottom - overlayY + ) + ) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onResume() { + super.onResume() + if (NativeLibrary.IsGameMetadataValid()) + inputOverlay?.refreshControls() + + run(emulationActivity!!.isActivityRecreated) + } + + override fun onPause() { + if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Pausing emulation.") + NativeLibrary.PauseEmulation() + } + super.onPause() + } + + override fun onDestroy() { + inputOverlay?.onDestroy() + super.onDestroy() + } + + override fun onDetach() { + NativeLibrary.clearEmulationActivity() + super.onDetach() + } + + fun toggleInputOverlayVisibility(settings: Settings?) { + BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.setBoolean( + settings!!, + !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean + ) + + inputOverlay?.refreshControls() + } + + fun initInputPointer() = inputOverlay?.initTouchPointer() + + fun refreshInputOverlay() = inputOverlay?.refreshControls() + + fun refreshOverlayPointer() = inputOverlay?.refreshOverlayPointer() + + fun resetInputOverlay() = inputOverlay?.resetButtonPlacement() + + override fun surfaceCreated(holder: SurfaceHolder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.debug("[EmulationFragment] Surface changed. Resolution: $width x $height") + NativeLibrary.SurfaceChanged(holder.surface) + if (runWhenSurfaceIsValid) { + runWithValidSurface() + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.debug("[EmulationFragment] Surface destroyed.") + NativeLibrary.SurfaceDestroyed() + runWhenSurfaceIsValid = true + } + + fun stopEmulation() { + Log.debug("[EmulationFragment] Stopping emulation.") + NativeLibrary.StopEmulation() + } + + fun startConfiguringControls() { + binding.doneControlConfig?.visibility = View.VISIBLE + inputOverlay?.editMode = true + } + + fun stopConfiguringControls() { + binding.doneControlConfig?.visibility = View.GONE + inputOverlay?.editMode = false + } + + val isConfiguringControls: Boolean + get() = inputOverlay != null && inputOverlay!!.isInEditMode + + private fun run(isActivityRecreated: Boolean) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + loadPreviousTemporaryState = false + deleteFile(temporaryStateFilePath) + } else { + loadPreviousTemporaryState = true + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start") + loadPreviousTemporaryState = false + // activity resumed without being killed or this is the first run + deleteFile(temporaryStateFilePath) + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (NativeLibrary.HasSurface()) { + runWithValidSurface() + } else { + runWhenSurfaceIsValid = true + } + } + + private fun runWithValidSurface() { + runWhenSurfaceIsValid = false + if (!NativeLibrary.IsRunning()) { + NativeLibrary.SetIsBooting() + val emulationThread = Thread({ + if (loadPreviousTemporaryState) { + Log.debug("[EmulationFragment] Starting emulation thread from previous state.") + NativeLibrary.Run(gamePaths, riivolution, temporaryStateFilePath, true) + } + if (launchSystemMenu) { + Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") + NativeLibrary.RunSystemMenu() + } else { + Log.debug("[EmulationFragment] Starting emulation thread.") + NativeLibrary.Run(gamePaths, riivolution) + } + EmulationActivity.stopIgnoringLaunchRequests() + }, "NativeEmulation") + emulationThread.start() + } else { + if (!EmulationActivity.hasUserPausedEmulation && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Resuming emulation.") + NativeLibrary.UnPauseEmulation() + } + } + } + + fun saveTemporaryState() = NativeLibrary.SaveStateAs(temporaryStateFilePath, true) + + private val temporaryStateFilePath: String + get() = "${requireContext().filesDir}${File.separator}temp.sav" + + companion object { + private const val KEY_GAMEPATHS = "gamepaths" + private const val KEY_RIIVOLUTION = "riivolution" + private const val KEY_SYSTEM_MENU = "systemMenu" + + fun newInstance( + gamePaths: Array?, + riivolution: Boolean, + systemMenu: Boolean + ): EmulationFragment { + val args = Bundle() + args.apply { + putStringArray(KEY_GAMEPATHS, gamePaths) + putBoolean(KEY_RIIVOLUTION, riivolution) + putBoolean(KEY_SYSTEM_MENU, systemMenu) + } + val fragment = EmulationFragment() + fragment.arguments = args + return fragment + } + + private fun deleteFile(path: String) { + try { + val file = File(path) + if (!file.delete()) { + Log.error("[EmulationFragment] Failed to delete ${file.absolutePath}") + } + } catch (ignored: Exception) { + } + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java deleted file mode 100644 index b30ba94b37..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.util.SparseIntArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.elevation.ElevationOverlayProvider; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; -import org.dolphinemu.dolphinemu.utils.InsetsHelper; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; - -public final class MenuFragment extends Fragment implements View.OnClickListener -{ - private static final String KEY_TITLE = "title"; - private static final String KEY_WII = "wii"; - private static SparseIntArray buttonsActionsMap = new SparseIntArray(); - - private int mCutInset = 0; - - static - { - buttonsActionsMap - .append(R.id.menu_pause_emulation, EmulationActivity.MENU_ACTION_PAUSE_EMULATION); - buttonsActionsMap - .append(R.id.menu_unpause_emulation, EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION); - buttonsActionsMap - .append(R.id.menu_take_screenshot, EmulationActivity.MENU_ACTION_TAKE_SCREENSHOT); - buttonsActionsMap.append(R.id.menu_quicksave, EmulationActivity.MENU_ACTION_QUICK_SAVE); - buttonsActionsMap.append(R.id.menu_quickload, EmulationActivity.MENU_ACTION_QUICK_LOAD); - buttonsActionsMap - .append(R.id.menu_emulation_save_root, EmulationActivity.MENU_ACTION_SAVE_ROOT); - buttonsActionsMap - .append(R.id.menu_emulation_load_root, EmulationActivity.MENU_ACTION_LOAD_ROOT); - buttonsActionsMap - .append(R.id.menu_overlay_controls, EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS); - buttonsActionsMap - .append(R.id.menu_refresh_wiimotes, EmulationActivity.MENU_ACTION_REFRESH_WIIMOTES); - buttonsActionsMap.append(R.id.menu_change_disc, EmulationActivity.MENU_ACTION_CHANGE_DISC); - buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT); - buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS); - buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS); - } - - private FragmentIngameMenuBinding mBinding; - - public static MenuFragment newInstance() - { - MenuFragment fragment = new MenuFragment(); - - Bundle arguments = new Bundle(); - if (NativeLibrary.IsGameMetadataValid()) - { - arguments.putString(KEY_TITLE, NativeLibrary.GetCurrentTitleDescription()); - arguments.putBoolean(KEY_WII, NativeLibrary.IsEmulatingWii()); - } - fragment.setArguments(arguments); - - return fragment; - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentIngameMenuBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - if (IntSetting.MAIN_INTERFACE_THEME.getInt() != ThemeHelper.DEFAULT) - { - @ColorInt int color = new ElevationOverlayProvider(view.getContext()).compositeOverlay( - MaterialColors.getColor(view, R.attr.colorSurface), - view.getElevation()); - view.setBackgroundColor(color); - } - - setInsets(); - updatePauseUnpauseVisibility(); - - if (!requireActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) - { - mBinding.menuOverlayControls.setVisibility(View.GONE); - } - - if (!getArguments().getBoolean(KEY_WII, true)) - { - mBinding.menuRefreshWiimotes.setVisibility(View.GONE); - mBinding.menuSkylanders.setVisibility(View.GONE); - } - - if (!BooleanSetting.MAIN_EMULATE_SKYLANDER_PORTAL.getBoolean()) - { - mBinding.menuSkylanders.setVisibility(View.GONE); - } - - LinearLayout options = mBinding.layoutOptions; - for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++) - { - Button button = (Button) options.getChildAt(childIndex); - - button.setOnClickListener(this); - } - - mBinding.menuExit.setOnClickListener(this); - - String title = getArguments().getString(KEY_TITLE, null); - if (title != null) - { - mBinding.textGameTitle.setText(title); - } - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.getRoot(), (v, windowInsets) -> - { - Insets cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()); - mCutInset = cutInsets.left; - - int left = 0; - int right = 0; - if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) - { - left = cutInsets.left; - } - else - { - right = cutInsets.right; - } - - v.post(() -> NativeLibrary.SetObscuredPixelsLeft(v.getWidth())); - - // Don't use padding if the navigation bar isn't in the way - if (InsetsHelper.getBottomPaddingRequired(requireActivity()) > 0) - { - v.setPadding(left, cutInsets.top, right, - cutInsets.bottom + InsetsHelper.getNavigationBarHeight(requireContext())); - } - else - { - v.setPadding(left, cutInsets.top, right, - cutInsets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_large)); - } - return windowInsets; - }); - } - - @Override - public void onResume() - { - super.onResume(); - - boolean savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.getBoolean(); - int savestateVisibility = savestatesEnabled ? View.VISIBLE : View.GONE; - mBinding.menuQuicksave.setVisibility(savestateVisibility); - mBinding.menuQuickload.setVisibility(savestateVisibility); - mBinding.menuEmulationSaveRoot.setVisibility(savestateVisibility); - mBinding.menuEmulationLoadRoot.setVisibility(savestateVisibility); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - - NativeLibrary.SetObscuredPixelsLeft(mCutInset); - mBinding = null; - } - - private void updatePauseUnpauseVisibility() - { - boolean paused = EmulationActivity.getHasUserPausedEmulation(); - - mBinding.menuUnpauseEmulation.setVisibility(paused ? View.VISIBLE : View.GONE); - mBinding.menuPauseEmulation.setVisibility(paused ? View.GONE : View.VISIBLE); - } - - @Override - public void onClick(View button) - { - int action = buttonsActionsMap.get(button.getId()); - EmulationActivity activity = (EmulationActivity) requireActivity(); - - if (action == EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS) - { - // We could use the button parameter as the anchor here, but this often results in a tiny menu - // (because the button often is in the middle of the screen), so let's use mTitleText instead - activity.showOverlayControlsMenu(mBinding.textGameTitle); - } - else if (action >= 0) - { - activity.handleMenuAction(action); - } - - if (action == EmulationActivity.MENU_ACTION_PAUSE_EMULATION || - action == EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION) - { - updatePauseUnpauseVisibility(); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt new file mode 100644 index 0000000000..b668e08da8 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.annotation.ColorInt +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import com.google.android.material.color.MaterialColors +import com.google.android.material.elevation.ElevationOverlayProvider +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.utils.InsetsHelper +import org.dolphinemu.dolphinemu.utils.ThemeHelper + +class MenuFragment : Fragment(), View.OnClickListener { + private var cutInset = 0 + + private var _binding: FragmentIngameMenuBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentIngameMenuBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (IntSetting.MAIN_INTERFACE_THEME.int != ThemeHelper.DEFAULT) { + @ColorInt val color = ElevationOverlayProvider(view.context).compositeOverlay( + MaterialColors.getColor(view, R.attr.colorSurface), + view.elevation + ) + view.setBackgroundColor(color) + } + + setInsets() + updatePauseUnpauseVisibility() + + if (!requireActivity().packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { + binding.menuOverlayControls.visibility = View.GONE + } + + if (!requireArguments().getBoolean(KEY_WII, true)) { + binding.menuRefreshWiimotes.visibility = View.GONE + binding.menuSkylanders.visibility = View.GONE + binding.menuInfinityBase.visibility = View.GONE + } + + if (!BooleanSetting.MAIN_EMULATE_SKYLANDER_PORTAL.boolean) { + binding.menuSkylanders.visibility = View.GONE + } + + if (!BooleanSetting.MAIN_EMULATE_INFINITY_BASE.boolean) { + binding.menuInfinityBase.visibility = View.GONE + } + + val options = binding.layoutOptions + for (childIndex in 0 until options.childCount) { + val button = options.getChildAt(childIndex) as Button + button.setOnClickListener(this) + } + + binding.menuExit.setOnClickListener(this) + + val title = requireArguments().getString(KEY_TITLE, null) + if (title != null) { + binding.textGameTitle.text = title + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v: View, windowInsets: WindowInsetsCompat -> + val cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + cutInset = cutInsets.left + var left = 0 + var right = 0 + if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { + left = cutInsets.left + } else { + right = cutInsets.right + } + v.post { NativeLibrary.SetObscuredPixelsLeft(v.width) } + + // Don't use padding if the navigation bar isn't in the way + if (InsetsHelper.getBottomPaddingRequired(requireActivity()) > 0) { + v.setPadding( + left, cutInsets.top, right, + cutInsets.bottom + InsetsHelper.getNavigationBarHeight(requireContext()) + ) + } else { + v.setPadding( + left, cutInsets.top, right, + cutInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_large) + ) + } + windowInsets + } + + override fun onResume() { + super.onResume() + val savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.boolean + val savestateVisibility = if (savestatesEnabled) View.VISIBLE else View.GONE + binding.menuQuicksave.visibility = savestateVisibility + binding.menuQuickload.visibility = savestateVisibility + binding.menuEmulationSaveRoot.visibility = savestateVisibility + binding.menuEmulationLoadRoot.visibility = savestateVisibility + } + + override fun onDestroyView() { + super.onDestroyView() + NativeLibrary.SetObscuredPixelsLeft(cutInset) + _binding = null + } + + private fun updatePauseUnpauseVisibility() { + val paused = EmulationActivity.hasUserPausedEmulation + binding.menuUnpauseEmulation.visibility = if (paused) View.VISIBLE else View.GONE + binding.menuPauseEmulation.visibility = if (paused) View.GONE else View.VISIBLE + } + + override fun onClick(button: View) { + val action = buttonsActionsMap[button.id] + val activity = requireActivity() as EmulationActivity + + if (action == EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS) { + // We could use the button parameter as the anchor here, but this often results in a tiny menu + // (because the button often is in the middle of the screen), so let's use mTitleText instead + activity.showOverlayControlsMenu(binding.textGameTitle) + } else if (action >= 0) { + activity.handleMenuAction(action) + } + + if (action == EmulationActivity.MENU_ACTION_PAUSE_EMULATION || + action == EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION + ) { + updatePauseUnpauseVisibility() + } + } + + companion object { + private const val KEY_TITLE = "title" + private const val KEY_WII = "wii" + private val buttonsActionsMap = SparseIntArray() + + init { + buttonsActionsMap.append( + R.id.menu_pause_emulation, + EmulationActivity.MENU_ACTION_PAUSE_EMULATION + ) + buttonsActionsMap.append( + R.id.menu_unpause_emulation, + EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION + ) + buttonsActionsMap.append( + R.id.menu_take_screenshot, + EmulationActivity.MENU_ACTION_TAKE_SCREENSHOT + ) + buttonsActionsMap.append(R.id.menu_quicksave, EmulationActivity.MENU_ACTION_QUICK_SAVE) + buttonsActionsMap.append(R.id.menu_quickload, EmulationActivity.MENU_ACTION_QUICK_LOAD) + buttonsActionsMap.append( + R.id.menu_emulation_save_root, + EmulationActivity.MENU_ACTION_SAVE_ROOT + ) + buttonsActionsMap.append( + R.id.menu_emulation_load_root, + EmulationActivity.MENU_ACTION_LOAD_ROOT + ) + buttonsActionsMap.append( + R.id.menu_overlay_controls, + EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS + ) + buttonsActionsMap.append( + R.id.menu_refresh_wiimotes, + EmulationActivity.MENU_ACTION_REFRESH_WIIMOTES + ) + buttonsActionsMap.append( + R.id.menu_change_disc, + EmulationActivity.MENU_ACTION_CHANGE_DISC + ) + buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT) + buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS) + buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS) + buttonsActionsMap.append( + R.id.menu_infinity_base, + EmulationActivity.MENU_ACTION_INFINITY_BASE + ) + } + + fun newInstance(): MenuFragment { + val fragment = MenuFragment() + val arguments = Bundle() + if (NativeLibrary.IsGameMetadataValid()) { + arguments.putString(KEY_TITLE, NativeLibrary.GetCurrentTitleDescription()) + arguments.putBoolean(KEY_WII, NativeLibrary.IsEmulatingWii()) + } + fragment.arguments = arguments + return fragment + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java deleted file mode 100644 index 2afce4d1cf..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.os.Bundle; -import android.text.format.DateUtils; -import android.util.SparseIntArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.GridLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentSaveloadStateBinding; - -public final class SaveLoadStateFragment extends Fragment implements View.OnClickListener -{ - public enum SaveOrLoad - { - SAVE, LOAD - } - - private static final String KEY_SAVEORLOAD = "saveorload"; - - private static int[] saveActionsMap = new int[]{ - EmulationActivity.MENU_ACTION_SAVE_SLOT1, - EmulationActivity.MENU_ACTION_SAVE_SLOT2, - EmulationActivity.MENU_ACTION_SAVE_SLOT3, - EmulationActivity.MENU_ACTION_SAVE_SLOT4, - EmulationActivity.MENU_ACTION_SAVE_SLOT5, - EmulationActivity.MENU_ACTION_SAVE_SLOT6, - }; - - private static int[] loadActionsMap = new int[]{ - EmulationActivity.MENU_ACTION_LOAD_SLOT1, - EmulationActivity.MENU_ACTION_LOAD_SLOT2, - EmulationActivity.MENU_ACTION_LOAD_SLOT3, - EmulationActivity.MENU_ACTION_LOAD_SLOT4, - EmulationActivity.MENU_ACTION_LOAD_SLOT5, - EmulationActivity.MENU_ACTION_LOAD_SLOT6, - }; - - private static SparseIntArray buttonsMap = new SparseIntArray(); - - static - { - buttonsMap.append(R.id.loadsave_state_button_1, 0); - buttonsMap.append(R.id.loadsave_state_button_2, 1); - buttonsMap.append(R.id.loadsave_state_button_3, 2); - buttonsMap.append(R.id.loadsave_state_button_4, 3); - buttonsMap.append(R.id.loadsave_state_button_5, 4); - buttonsMap.append(R.id.loadsave_state_button_6, 5); - } - - private SaveOrLoad mSaveOrLoad; - - private FragmentSaveloadStateBinding mBinding; - - public static SaveLoadStateFragment newInstance(SaveOrLoad saveOrLoad) - { - SaveLoadStateFragment fragment = new SaveLoadStateFragment(); - - Bundle arguments = new Bundle(); - arguments.putSerializable(KEY_SAVEORLOAD, saveOrLoad); - fragment.setArguments(arguments); - - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mSaveOrLoad = (SaveOrLoad) getArguments().getSerializable(KEY_SAVEORLOAD); - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentSaveloadStateBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - GridLayout grid = mBinding.gridStateSlots; - for (int childIndex = 0; childIndex < grid.getChildCount(); childIndex++) - { - Button button = (Button) grid.getChildAt(childIndex); - setButtonText(button, childIndex); - button.setOnClickListener(this); - } - - // So that item clicked to start this Fragment is no longer the focused item. - grid.requestFocus(); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onClick(View view) - { - int buttonIndex = buttonsMap.get(view.getId(), -1); - - int action = (mSaveOrLoad == SaveOrLoad.SAVE ? saveActionsMap : loadActionsMap)[buttonIndex]; - ((EmulationActivity) getActivity()).handleMenuAction(action); - - if (mSaveOrLoad == SaveOrLoad.SAVE) - { - // Update the "last modified" time. - // The savestate most likely hasn't gotten saved to disk yet (it happens asynchronously), - // so we unfortunately can't rely on setButtonText/GetUnixTimeOfStateSlot here. - - Button button = (Button) view; - CharSequence time = DateUtils.getRelativeTimeSpanString(0, 0, DateUtils.MINUTE_IN_MILLIS); - button.setText(getString(R.string.emulation_state_slot, buttonIndex + 1, time)); - } - } - - private void setButtonText(Button button, int index) - { - long creationTime = NativeLibrary.GetUnixTimeOfStateSlot(index); - if (creationTime != 0) - { - CharSequence relativeTime = DateUtils.getRelativeTimeSpanString(creationTime); - button.setText(getString(R.string.emulation_state_slot, index + 1, relativeTime)); - } - else - { - button.setText(getString(R.string.emulation_state_slot_empty, index + 1)); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt new file mode 100644 index 0000000000..8ce2ccde10 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.os.Bundle +import android.text.format.DateUtils +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentSaveloadStateBinding +import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable + +class SaveLoadStateFragment : Fragment(), View.OnClickListener { + enum class SaveOrLoad { SAVE, LOAD } + + private var saveOrLoad: SaveOrLoad? = null + + private var _binding: FragmentSaveloadStateBinding? = null + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + saveOrLoad = requireArguments().serializable(KEY_SAVEORLOAD) as SaveOrLoad? + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSaveloadStateBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val grid = binding.gridStateSlots + for (childIndex in 0 until grid.childCount) { + val button = grid.getChildAt(childIndex) as Button + setButtonText(button, childIndex) + button.setOnClickListener(this) + } + + // So that item clicked to start this Fragment is no longer the focused item. + grid.requestFocus() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onClick(view: View) { + val buttonIndex = buttonsMap[view.id, -1] + + val action = + (if (saveOrLoad == SaveOrLoad.SAVE) saveActionsMap else loadActionsMap)[buttonIndex] + (requireActivity() as EmulationActivity?)?.handleMenuAction(action) + + if (saveOrLoad == SaveOrLoad.SAVE) { + // Update the "last modified" time. + // The savestate most likely hasn't gotten saved to disk yet (it happens asynchronously), + // so we unfortunately can't rely on setButtonText/GetUnixTimeOfStateSlot here. + val button = view as Button + val time = DateUtils.getRelativeTimeSpanString(0, 0, DateUtils.MINUTE_IN_MILLIS) + button.text = getString(R.string.emulation_state_slot, buttonIndex + 1, time) + } + } + + private fun setButtonText(button: Button, index: Int) { + val creationTime = NativeLibrary.GetUnixTimeOfStateSlot(index) + button.text = if (creationTime != 0L) { + val relativeTime = DateUtils.getRelativeTimeSpanString(creationTime) + getString(R.string.emulation_state_slot, index + 1, relativeTime) + } else { + getString(R.string.emulation_state_slot_empty, index + 1) + } + } + + companion object { + private const val KEY_SAVEORLOAD = "saveorload" + + private val saveActionsMap = intArrayOf( + EmulationActivity.MENU_ACTION_SAVE_SLOT1, + EmulationActivity.MENU_ACTION_SAVE_SLOT2, + EmulationActivity.MENU_ACTION_SAVE_SLOT3, + EmulationActivity.MENU_ACTION_SAVE_SLOT4, + EmulationActivity.MENU_ACTION_SAVE_SLOT5, + EmulationActivity.MENU_ACTION_SAVE_SLOT6 + ) + + private val loadActionsMap = intArrayOf( + EmulationActivity.MENU_ACTION_LOAD_SLOT1, + EmulationActivity.MENU_ACTION_LOAD_SLOT2, + EmulationActivity.MENU_ACTION_LOAD_SLOT3, + EmulationActivity.MENU_ACTION_LOAD_SLOT4, + EmulationActivity.MENU_ACTION_LOAD_SLOT5, + EmulationActivity.MENU_ACTION_LOAD_SLOT6 + ) + + private val buttonsMap = SparseIntArray() + + init { + buttonsMap.append(R.id.loadsave_state_button_1, 0) + buttonsMap.append(R.id.loadsave_state_button_2, 1) + buttonsMap.append(R.id.loadsave_state_button_3, 2) + buttonsMap.append(R.id.loadsave_state_button_4, 3) + buttonsMap.append(R.id.loadsave_state_button_5, 4) + buttonsMap.append(R.id.loadsave_state_button_6, 5) + } + + fun newInstance(saveOrLoad: SaveOrLoad): SaveLoadStateFragment { + val fragment = SaveLoadStateFragment() + val arguments = Bundle() + arguments.putSerializable(KEY_SAVEORLOAD, saveOrLoad) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/DriverPackageMetadata.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/DriverPackageMetadata.kt new file mode 100644 index 0000000000..fe142891ad --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/DriverPackageMetadata.kt @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package org.dolphinemu.dolphinemu.model + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.* +import java.io.File + +data class GpuDriverMetadata( + val name : String, + val author : String, + val packageVersion : String, + val vendor : String, + val driverVersion : String, + val minApi : Int, + val description : String, + val libraryName : String, +) { + private constructor(metadataV1 : GpuDriverMetadataV1) : this( + name = metadataV1.name, + author = metadataV1.author, + packageVersion = metadataV1.packageVersion, + vendor = metadataV1.vendor, + driverVersion = metadataV1.driverVersion, + minApi = metadataV1.minApi, + description = metadataV1.description, + libraryName = metadataV1.libraryName, + ) + + val label get() = "${name}-v${packageVersion}" + + companion object { + private const val SCHEMA_VERSION_V1 = 1 + + fun deserialize(metadataFile : File) : GpuDriverMetadata { + val metadataJson = Json.parseToJsonElement(metadataFile.readText()) + + return when (metadataJson.jsonObject["schemaVersion"]?.jsonPrimitive?.intOrNull) { + SCHEMA_VERSION_V1 -> GpuDriverMetadata(Json.decodeFromJsonElement(metadataJson)) + else -> throw SerializationException("Unsupported metadata version") + } + } + } +} + +@Serializable +private data class GpuDriverMetadataV1( + val schemaVersion : Int, + val name : String, + val author : String, + val packageVersion : String, + val vendor : String, + val driverVersion : String, + val minApi : Int, + val description : String, + val libraryName : String, +) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java deleted file mode 100644 index d44acc2786..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java +++ /dev/null @@ -1,1868 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.overlay; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Display; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.Toast; - -import androidx.preference.PreferenceManager; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.features.input.model.InputMappingBooleanSetting; -import org.dolphinemu.dolphinemu.features.input.model.InputOverrider; -import org.dolphinemu.dolphinemu.features.input.model.InputOverrider.ControlId; -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController; -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Draws the interactive input overlay on top of the - * {@link SurfaceView} that is rendering emulation. - */ -public final class InputOverlay extends SurfaceView implements OnTouchListener -{ - public static final int OVERLAY_GAMECUBE = 0; - public static final int OVERLAY_WIIMOTE = 1; - public static final int OVERLAY_WIIMOTE_SIDEWAYS = 2; - public static final int OVERLAY_WIIMOTE_NUNCHUK = 3; - public static final int OVERLAY_WIIMOTE_CLASSIC = 4; - public static final int OVERLAY_NONE = 5; - - private static final int DISABLED_GAMECUBE_CONTROLLER = 0; - private static final int EMULATED_GAMECUBE_CONTROLLER = 6; - private static final int GAMECUBE_ADAPTER = 12; - - private final Set overlayButtons = new HashSet<>(); - private final Set overlayDpads = new HashSet<>(); - private final Set overlayJoysticks = new HashSet<>(); - private InputOverlayPointer overlayPointer = null; - - private Rect mSurfacePosition = null; - - private boolean mIsFirstRun = true; - private boolean[] mGcPadRegistered = new boolean[4]; - private boolean[] mWiimoteRegistered = new boolean[4]; - private boolean mIsInEditMode = false; - private int mControllerType = -1; - private int mControllerIndex = 0; - private InputOverlayDrawableButton mButtonBeingConfigured; - private InputOverlayDrawableDpad mDpadBeingConfigured; - private InputOverlayDrawableJoystick mJoystickBeingConfigured; - - private final SharedPreferences mPreferences; - - // Buttons that have special positions in Wiimote only - private static final ArrayList WIIMOTE_H_BUTTONS = new ArrayList<>(); - - static - { - WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_A); - WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_B); - WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_1); - WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_2); - } - - private static final ArrayList WIIMOTE_O_BUTTONS = new ArrayList<>(); - - static - { - WIIMOTE_O_BUTTONS.add(ButtonType.WIIMOTE_UP); - } - - /** - * Resizes a {@link Bitmap} by a given scale factor - * - * @param context The current {@link Context} - * @param bitmap The {@link Bitmap} to scale. - * @param scale The scale factor for the bitmap. - * @return The scaled {@link Bitmap} - */ - public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) - { - // Determine the button size based on the smaller screen dimension. - // This makes sure the buttons are the same size in both portrait and landscape. - DisplayMetrics dm = context.getResources().getDisplayMetrics(); - int minScreenDimension = Math.min(dm.widthPixels, dm.heightPixels); - - int maxBitmapDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); - float bitmapScale = scale * minScreenDimension / maxBitmapDimension; - - return Bitmap.createScaledBitmap(bitmap, - (int) (bitmap.getWidth() * bitmapScale), - (int) (bitmap.getHeight() * bitmapScale), - true); - } - - /** - * Constructor - * - * @param context The current {@link Context}. - * @param attrs {@link AttributeSet} for parsing XML attributes. - */ - public InputOverlay(Context context, AttributeSet attrs) - { - super(context, attrs); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!mPreferences.getBoolean("OverlayInitV3", false)) - defaultOverlay(); - - // Set the on touch listener. - setOnTouchListener(this); - - // Force draw - setWillNotDraw(false); - - // Request focus for the overlay so it has priority on presses. - requestFocus(); - } - - public void setSurfacePosition(Rect rect) - { - mSurfacePosition = rect; - initTouchPointer(); - } - - public void initTouchPointer() - { - // Check if we have all the data we need yet - boolean aspectRatioAvailable = NativeLibrary.IsRunningAndStarted(); - if (!aspectRatioAvailable || mSurfacePosition == null) - return; - - // Check if there's any point in running the pointer code - if (!NativeLibrary.IsEmulatingWii()) - return; - - int doubleTapButton = IntSetting.MAIN_DOUBLE_TAP_BUTTON.getInt(); - - if (getConfiguredControllerType() != InputOverlay.OVERLAY_WIIMOTE_CLASSIC && - doubleTapButton == ButtonType.CLASSIC_BUTTON_A) - { - doubleTapButton = ButtonType.WIIMOTE_BUTTON_A; - } - - int doubleTapControl = ControlId.WIIMOTE_A_BUTTON; - switch (doubleTapButton) - { - case ButtonType.WIIMOTE_BUTTON_A: - doubleTapControl = ControlId.WIIMOTE_A_BUTTON; - break; - case ButtonType.WIIMOTE_BUTTON_B: - doubleTapControl = ControlId.WIIMOTE_B_BUTTON; - break; - case ButtonType.WIIMOTE_BUTTON_2: - doubleTapControl = ControlId.WIIMOTE_TWO_BUTTON; - break; - } - - overlayPointer = new InputOverlayPointer(mSurfacePosition, doubleTapControl, - IntSetting.MAIN_IR_MODE.getInt(), BooleanSetting.MAIN_IR_ALWAYS_RECENTER.getBoolean(), - mControllerIndex); - } - - @Override - public void draw(Canvas canvas) - { - super.draw(canvas); - - for (InputOverlayDrawableButton button : overlayButtons) - { - button.draw(canvas); - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) - { - dpad.draw(canvas); - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) - { - joystick.draw(canvas); - } - } - - @Override - public boolean onTouch(View v, MotionEvent event) - { - if (isInEditMode()) - { - return onTouchWhileEditing(event); - } - - int action = event.getActionMasked(); - boolean firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && - action != MotionEvent.ACTION_POINTER_UP; - int pointerIndex = firstPointer ? 0 : event.getActionIndex(); - // Tracks if any button/joystick is pressed down - boolean pressed = false; - - for (InputOverlayDrawableButton button : overlayButtons) - { - // Determine the button state to apply based on the MotionEvent action flag. - switch (action) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If a pointer enters the bounds of a button, press that button. - if (button.getBounds() - .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) - { - button.setPressedState(true); - button.setTrackId(event.getPointerId(pointerIndex)); - pressed = true; - InputOverrider.setControlState(mControllerIndex, button.getControl(), 1.0); - - int analogControl = getAnalogControlForTrigger(button.getControl()); - if (analogControl >= 0) - InputOverrider.setControlState(mControllerIndex, analogControl, 1.0); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - // If a pointer ends, release the button it was pressing. - if (button.getTrackId() == event.getPointerId(pointerIndex)) - { - button.setPressedState(false); - InputOverrider.setControlState(mControllerIndex, button.getControl(), 0.0); - - int analogControl = getAnalogControlForTrigger(button.getControl()); - if (analogControl >= 0) - InputOverrider.setControlState(mControllerIndex, analogControl, 0.0); - - button.setTrackId(-1); - } - break; - } - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) - { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If a pointer enters the bounds of a button, press that button. - if (dpad.getBounds() - .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) - { - dpad.setTrackId(event.getPointerId(pointerIndex)); - pressed = true; - } - case MotionEvent.ACTION_MOVE: - if (dpad.getTrackId() == event.getPointerId(pointerIndex)) - { - // Up, Down, Left, Right - boolean[] dpadPressed = {false, false, false, false}; - - if (dpad.getBounds().top + (dpad.getHeight() / 3) > (int) event.getY(pointerIndex)) - dpadPressed[0] = true; - if (dpad.getBounds().bottom - (dpad.getHeight() / 3) < (int) event.getY(pointerIndex)) - dpadPressed[1] = true; - if (dpad.getBounds().left + (dpad.getWidth() / 3) > (int) event.getX(pointerIndex)) - dpadPressed[2] = true; - if (dpad.getBounds().right - (dpad.getWidth() / 3) < (int) event.getX(pointerIndex)) - dpadPressed[3] = true; - - // Release the buttons first, then press - for (int i = 0; i < dpadPressed.length; i++) - { - if (!dpadPressed[i]) - { - InputOverrider.setControlState(mControllerIndex, dpad.getControl(i), 0.0); - } - } - // Press buttons - for (int i = 0; i < dpadPressed.length; i++) - { - if (dpadPressed[i]) - { - InputOverrider.setControlState(mControllerIndex, dpad.getControl(i), 1.0); - } - } - setDpadState(dpad, dpadPressed[0], dpadPressed[1], dpadPressed[2], dpadPressed[3]); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - // If a pointer ends, release the buttons. - if (dpad.getTrackId() == event.getPointerId(pointerIndex)) - { - for (int i = 0; i < 4; i++) - { - dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); - InputOverrider.setControlState(mControllerIndex, dpad.getControl(i), 0.0); - } - dpad.setTrackId(-1); - } - break; - } - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) - { - if (joystick.TrackEvent(event)) - { - if (joystick.getTrackId() != -1) - pressed = true; - } - - InputOverrider.setControlState(mControllerIndex, joystick.getXControl(), joystick.getX()); - InputOverrider.setControlState(mControllerIndex, joystick.getYControl(), -joystick.getY()); - } - - // No button/joystick pressed, safe to move pointer - if (!pressed && overlayPointer != null) - { - overlayPointer.onTouch(event); - InputOverrider.setControlState(mControllerIndex, ControlId.WIIMOTE_IR_X, - overlayPointer.getX()); - InputOverrider.setControlState(mControllerIndex, ControlId.WIIMOTE_IR_Y, - -overlayPointer.getY()); - } - - invalidate(); - - return true; - } - - public boolean onTouchWhileEditing(MotionEvent event) - { - int pointerIndex = event.getActionIndex(); - int fingerPositionX = (int) event.getX(pointerIndex); - int fingerPositionY = (int) event.getY(pointerIndex); - - String orientation = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? - "-Portrait" : ""; - - // Maybe combine Button and Joystick as subclasses of the same parent? - // Or maybe create an interface like IMoveableHUDControl? - - for (InputOverlayDrawableButton button : overlayButtons) - { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If no button is being moved now, remember the currently touched button to move. - if (mButtonBeingConfigured == null && - button.getBounds().contains(fingerPositionX, fingerPositionY)) - { - mButtonBeingConfigured = button; - mButtonBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mButtonBeingConfigured != null) - { - mButtonBeingConfigured.onConfigureTouch(event); - invalidate(); - return true; - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mButtonBeingConfigured == button) - { - // Persist button position by saving new place. - saveControlPosition(mButtonBeingConfigured.getLegacyId(), - mButtonBeingConfigured.getBounds().left, - mButtonBeingConfigured.getBounds().top, orientation); - mButtonBeingConfigured = null; - } - break; - } - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) - { - // Determine the button state to apply based on the MotionEvent action flag. - switch (event.getAction() & MotionEvent.ACTION_MASK) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - // If no button is being moved now, remember the currently touched button to move. - if (mButtonBeingConfigured == null && - dpad.getBounds().contains(fingerPositionX, fingerPositionY)) - { - mDpadBeingConfigured = dpad; - mDpadBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mDpadBeingConfigured != null) - { - mDpadBeingConfigured.onConfigureTouch(event); - invalidate(); - return true; - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mDpadBeingConfigured == dpad) - { - // Persist button position by saving new place. - saveControlPosition(mDpadBeingConfigured.getLegacyId(), - mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, - orientation); - mDpadBeingConfigured = null; - } - break; - } - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) - { - switch (event.getAction()) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - if (mJoystickBeingConfigured == null && - joystick.getBounds().contains(fingerPositionX, fingerPositionY)) - { - mJoystickBeingConfigured = joystick; - mJoystickBeingConfigured.onConfigureTouch(event); - } - break; - case MotionEvent.ACTION_MOVE: - if (mJoystickBeingConfigured != null) - { - mJoystickBeingConfigured.onConfigureTouch(event); - invalidate(); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (mJoystickBeingConfigured != null) - { - saveControlPosition(mJoystickBeingConfigured.getLegacyId(), - mJoystickBeingConfigured.getBounds().left, - mJoystickBeingConfigured.getBounds().top, orientation); - mJoystickBeingConfigured = null; - } - break; - } - } - - return true; - } - - public void onDestroy() - { - unregisterControllers(); - } - - private void unregisterControllers() - { - for (int i = 0; i < mGcPadRegistered.length; i++) - { - if (mGcPadRegistered[i]) - InputOverrider.unregisterGameCube(i); - } - - for (int i = 0; i < mWiimoteRegistered.length; i++) - { - if (mWiimoteRegistered[i]) - InputOverrider.unregisterWii(i); - } - - Arrays.fill(mGcPadRegistered, false); - Arrays.fill(mWiimoteRegistered, false); - } - - private int getAnalogControlForTrigger(int control) - { - switch (control) - { - case ControlId.GCPAD_L_DIGITAL: - return ControlId.GCPAD_L_ANALOG; - case ControlId.GCPAD_R_DIGITAL: - return ControlId.GCPAD_R_ANALOG; - case ControlId.CLASSIC_L_DIGITAL: - return ControlId.CLASSIC_L_ANALOG; - case ControlId.CLASSIC_R_DIGITAL: - return ControlId.CLASSIC_R_ANALOG; - default: - return -1; - } - } - - private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, - boolean right) - { - if (up) - { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); - } - else if (down) - { - if (left) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); - else if (right) - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); - else - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); - } - else if (left) - { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); - } - else if (right) - { - dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); - } - } - - private void addGameCubeOverlayControls(String orientation) - { - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_0.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_a, - R.drawable.gcpad_a_pressed, ButtonType.BUTTON_A, ControlId.GCPAD_A_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_1.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_b, - R.drawable.gcpad_b_pressed, ButtonType.BUTTON_B, ControlId.GCPAD_B_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_2.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_x, - R.drawable.gcpad_x_pressed, ButtonType.BUTTON_X, ControlId.GCPAD_X_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_3.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_y, - R.drawable.gcpad_y_pressed, ButtonType.BUTTON_Y, ControlId.GCPAD_Y_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_4.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_z, - R.drawable.gcpad_z_pressed, ButtonType.BUTTON_Z, ControlId.GCPAD_Z_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_5.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_start, - R.drawable.gcpad_start_pressed, ButtonType.BUTTON_START, ControlId.GCPAD_START_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_6.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_l, - R.drawable.gcpad_l_pressed, ButtonType.TRIGGER_L, ControlId.GCPAD_L_DIGITAL, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_7.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.gcpad_r, - R.drawable.gcpad_r_pressed, ButtonType.TRIGGER_R, ControlId.GCPAD_R_DIGITAL, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_8.getBoolean()) - { - overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.gcwii_dpad, - R.drawable.gcwii_dpad_pressed_one_direction, - R.drawable.gcwii_dpad_pressed_two_directions, - ButtonType.BUTTON_UP, ControlId.GCPAD_DPAD_UP, ControlId.GCPAD_DPAD_DOWN, - ControlId.GCPAD_DPAD_LEFT, ControlId.GCPAD_DPAD_RIGHT, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_9.getBoolean()) - { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.gcwii_joystick_range, - R.drawable.gcwii_joystick, R.drawable.gcwii_joystick_pressed, ButtonType.STICK_MAIN, - ControlId.GCPAD_MAIN_STICK_X, ControlId.GCPAD_MAIN_STICK_Y, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_10.getBoolean()) - { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.gcwii_joystick_range, - R.drawable.gcpad_c, R.drawable.gcpad_c_pressed, ButtonType.STICK_C, - ControlId.GCPAD_C_STICK_X, ControlId.GCPAD_C_STICK_Y, orientation)); - } - } - - private void addWiimoteOverlayControls(String orientation) - { - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_0.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_a, - R.drawable.wiimote_a_pressed, ButtonType.WIIMOTE_BUTTON_A, ControlId.WIIMOTE_A_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_1.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_b, - R.drawable.wiimote_b_pressed, ButtonType.WIIMOTE_BUTTON_B, ControlId.WIIMOTE_B_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_2.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_one, - R.drawable.wiimote_one_pressed, ButtonType.WIIMOTE_BUTTON_1, - ControlId.WIIMOTE_ONE_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_3.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_two, - R.drawable.wiimote_two_pressed, ButtonType.WIIMOTE_BUTTON_2, - ControlId.WIIMOTE_TWO_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_4.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_plus, - R.drawable.wiimote_plus_pressed, ButtonType.WIIMOTE_BUTTON_PLUS, - ControlId.WIIMOTE_PLUS_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_5.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_minus, - R.drawable.wiimote_minus_pressed, ButtonType.WIIMOTE_BUTTON_MINUS, - ControlId.WIIMOTE_MINUS_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_6.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_home, - R.drawable.wiimote_home_pressed, ButtonType.WIIMOTE_BUTTON_HOME, - ControlId.WIIMOTE_HOME_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_7.getBoolean()) - { - overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.gcwii_dpad, - R.drawable.gcwii_dpad_pressed_one_direction, - R.drawable.gcwii_dpad_pressed_two_directions, - ButtonType.WIIMOTE_UP, ControlId.WIIMOTE_DPAD_UP, ControlId.WIIMOTE_DPAD_DOWN, - ControlId.WIIMOTE_DPAD_LEFT, ControlId.WIIMOTE_DPAD_RIGHT, orientation)); - } - } - - private void addNunchukOverlayControls(String orientation) - { - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_8.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.nunchuk_c, - R.drawable.nunchuk_c_pressed, ButtonType.NUNCHUK_BUTTON_C, ControlId.NUNCHUK_C_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_9.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.nunchuk_z, - R.drawable.nunchuk_z_pressed, ButtonType.NUNCHUK_BUTTON_Z, ControlId.NUNCHUK_Z_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_10.getBoolean()) - { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.gcwii_joystick_range, - R.drawable.gcwii_joystick, R.drawable.gcwii_joystick_pressed, - ButtonType.NUNCHUK_STICK, ControlId.NUNCHUK_STICK_X, ControlId.NUNCHUK_STICK_Y, - orientation)); - } - } - - private void addClassicOverlayControls(String orientation) - { - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_0.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_a, - R.drawable.classic_a_pressed, ButtonType.CLASSIC_BUTTON_A, ControlId.CLASSIC_A_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_1.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_b, - R.drawable.classic_b_pressed, ButtonType.CLASSIC_BUTTON_B, ControlId.CLASSIC_B_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_2.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_x, - R.drawable.classic_x_pressed, ButtonType.CLASSIC_BUTTON_X, ControlId.CLASSIC_X_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_3.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_y, - R.drawable.classic_y_pressed, ButtonType.CLASSIC_BUTTON_Y, ControlId.CLASSIC_Y_BUTTON, - orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_4.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_plus, - R.drawable.wiimote_plus_pressed, ButtonType.CLASSIC_BUTTON_PLUS, - ControlId.CLASSIC_PLUS_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_5.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_minus, - R.drawable.wiimote_minus_pressed, ButtonType.CLASSIC_BUTTON_MINUS, - ControlId.CLASSIC_MINUS_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_6.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.wiimote_home, - R.drawable.wiimote_home_pressed, ButtonType.CLASSIC_BUTTON_HOME, - ControlId.CLASSIC_HOME_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_7.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_l, - R.drawable.classic_l_pressed, ButtonType.CLASSIC_TRIGGER_L, - ControlId.CLASSIC_L_DIGITAL, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_8.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_r, - R.drawable.classic_r_pressed, ButtonType.CLASSIC_TRIGGER_R, - ControlId.CLASSIC_R_DIGITAL, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_9.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_zl, - R.drawable.classic_zl_pressed, ButtonType.CLASSIC_BUTTON_ZL, - ControlId.CLASSIC_ZL_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_10.getBoolean()) - { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.classic_zr, - R.drawable.classic_zr_pressed, ButtonType.CLASSIC_BUTTON_ZR, - ControlId.CLASSIC_ZR_BUTTON, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_11.getBoolean()) - { - overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.gcwii_dpad, - R.drawable.gcwii_dpad_pressed_one_direction, - R.drawable.gcwii_dpad_pressed_two_directions, - ButtonType.CLASSIC_DPAD_UP, ControlId.CLASSIC_DPAD_UP, ControlId.CLASSIC_DPAD_DOWN, - ControlId.CLASSIC_DPAD_LEFT, ControlId.CLASSIC_DPAD_RIGHT, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_12.getBoolean()) - { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.gcwii_joystick_range, - R.drawable.gcwii_joystick, R.drawable.gcwii_joystick_pressed, - ButtonType.CLASSIC_STICK_LEFT, ControlId.CLASSIC_LEFT_STICK_X, - ControlId.CLASSIC_LEFT_STICK_Y, orientation)); - } - if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_13.getBoolean()) - { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.gcwii_joystick_range, - R.drawable.gcwii_joystick, R.drawable.gcwii_joystick_pressed, - ButtonType.CLASSIC_STICK_RIGHT, ControlId.CLASSIC_RIGHT_STICK_X, - ControlId.CLASSIC_RIGHT_STICK_Y, orientation)); - } - } - - public void refreshControls() - { - unregisterControllers(); - - // Remove all the overlay buttons from the HashSet. - overlayButtons.removeAll(overlayButtons); - overlayDpads.removeAll(overlayDpads); - overlayJoysticks.removeAll(overlayJoysticks); - - String orientation = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? - "-Portrait" : ""; - - mControllerType = getConfiguredControllerType(); - - IntSetting controllerSetting = NativeLibrary.IsEmulatingWii() ? - IntSetting.MAIN_OVERLAY_WII_CONTROLLER : IntSetting.MAIN_OVERLAY_GC_CONTROLLER; - int controllerIndex = controllerSetting.getInt(); - - if (BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.getBoolean()) - { - // Add all the enabled overlay items back to the HashSet. - switch (mControllerType) - { - case OVERLAY_GAMECUBE: - if (IntSetting.getSettingForSIDevice(controllerIndex).getInt() == - DISABLED_GAMECUBE_CONTROLLER && mIsFirstRun) - { - Toast.makeText(getContext(), R.string.disabled_gc_overlay_notice, Toast.LENGTH_SHORT) - .show(); - } - - mControllerIndex = controllerIndex; - InputOverrider.registerGameCube(mControllerIndex); - mGcPadRegistered[mControllerIndex] = true; - - addGameCubeOverlayControls(orientation); - break; - - case OVERLAY_WIIMOTE: - case OVERLAY_WIIMOTE_SIDEWAYS: - mControllerIndex = controllerIndex - 4; - InputOverrider.registerWii(mControllerIndex); - mWiimoteRegistered[mControllerIndex] = true; - - addWiimoteOverlayControls(orientation); - break; - - case OVERLAY_WIIMOTE_NUNCHUK: - mControllerIndex = controllerIndex - 4; - InputOverrider.registerWii(mControllerIndex); - mWiimoteRegistered[mControllerIndex] = true; - - addWiimoteOverlayControls(orientation); - addNunchukOverlayControls(orientation); - break; - - case OVERLAY_WIIMOTE_CLASSIC: - mControllerIndex = controllerIndex - 4; - InputOverrider.registerWii(mControllerIndex); - mWiimoteRegistered[mControllerIndex] = true; - - addClassicOverlayControls(orientation); - break; - - case OVERLAY_NONE: - break; - } - } - - mIsFirstRun = false; - invalidate(); - } - - public void refreshOverlayPointer() - { - if (overlayPointer != null) - { - overlayPointer.setMode(IntSetting.MAIN_IR_MODE.getInt()); - overlayPointer.setRecenter(BooleanSetting.MAIN_IR_ALWAYS_RECENTER.getBoolean()); - } - } - - public void resetButtonPlacement() - { - boolean isLandscape = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - - final int controller = getConfiguredControllerType(); - if (controller == OVERLAY_GAMECUBE) - { - if (isLandscape) - gcDefaultOverlay(); - else - gcPortraitDefaultOverlay(); - } - else if (controller == OVERLAY_WIIMOTE_CLASSIC) - { - if (isLandscape) - wiiClassicDefaultOverlay(); - else - wiiClassicPortraitDefaultOverlay(); - } - else - { - if (isLandscape) - { - wiiDefaultOverlay(); - wiiOnlyDefaultOverlay(); - } - else - { - wiiPortraitDefaultOverlay(); - wiiOnlyPortraitDefaultOverlay(); - } - } - refreshControls(); - } - - public static int getConfiguredControllerType() - { - IntSetting controllerSetting = NativeLibrary.IsEmulatingWii() ? - IntSetting.MAIN_OVERLAY_WII_CONTROLLER : IntSetting.MAIN_OVERLAY_GC_CONTROLLER; - int controllerIndex = controllerSetting.getInt(); - - if (controllerIndex >= 0 && controllerIndex < 4) - { - // GameCube controller - if (IntSetting.getSettingForSIDevice(controllerIndex).getInt() == 6) - return OVERLAY_GAMECUBE; - } - else if (controllerIndex >= 4 && controllerIndex < 8) - { - // Wii Remote - int wiimoteIndex = controllerIndex - 4; - if (IntSetting.getSettingForWiimoteSource(wiimoteIndex).getInt() == 1) - { - int attachmentIndex = EmulatedController.getSelectedWiimoteAttachment(wiimoteIndex); - switch (attachmentIndex) - { - case 1: - return OVERLAY_WIIMOTE_NUNCHUK; - case 2: - return OVERLAY_WIIMOTE_CLASSIC; - } - - NumericSetting sidewaysSetting = EmulatedController.getSidewaysWiimoteSetting(wiimoteIndex); - boolean sideways = new InputMappingBooleanSetting(sidewaysSetting).getBoolean(); - - return sideways ? OVERLAY_WIIMOTE_SIDEWAYS : OVERLAY_WIIMOTE; - } - } - - return OVERLAY_NONE; - } - - private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) - { - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); - sPrefsEditor.putFloat(getXKey(sharedPrefsId, mControllerType, orientation), x); - sPrefsEditor.putFloat(getYKey(sharedPrefsId, mControllerType, orientation), y); - sPrefsEditor.apply(); - } - - private static String getKey(int sharedPrefsId, int controller, String orientation, String suffix) - { - if (controller == OVERLAY_WIIMOTE_SIDEWAYS && WIIMOTE_H_BUTTONS.contains(sharedPrefsId)) - { - return sharedPrefsId + "_H" + orientation + suffix; - } - else if (controller == OVERLAY_WIIMOTE && WIIMOTE_O_BUTTONS.contains(sharedPrefsId)) - { - return sharedPrefsId + "_O" + orientation + suffix; - } - else - { - return sharedPrefsId + orientation + suffix; - } - } - - private static String getXKey(int sharedPrefsId, int controller, String orientation) - { - return getKey(sharedPrefsId, controller, orientation, "-X"); - } - - private static String getYKey(int sharedPrefsId, int controller, String orientation) - { - return getKey(sharedPrefsId, controller, orientation, "-Y"); - } - - /** - * Initializes an InputOverlayDrawableButton, given by resId, with all of the - * parameters set for it to be properly shown on the InputOverlay. - *

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

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

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

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

- * Technically no modifications should need to be performed on the returned - * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait - * for Android to call the onDraw method. - * - * @param context The current {@link Context}. - * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). - * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). - * @param legacyId Legacy identifier for the button the InputOverlayDrawableButton represents. - * @param control Control identifier for the button the InputOverlayDrawableButton represents. - * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. - */ - private InputOverlayDrawableButton initializeOverlayButton(Context context, - int defaultResId, int pressedResId, int legacyId, int control, String orientation) - { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale; - - switch (legacyId) - { - case ButtonType.BUTTON_A: - case ButtonType.WIIMOTE_BUTTON_B: - case ButtonType.NUNCHUK_BUTTON_Z: - scale = 0.2f; - break; - case ButtonType.BUTTON_X: - case ButtonType.BUTTON_Y: - scale = 0.175f; - break; - case ButtonType.BUTTON_Z: - case ButtonType.TRIGGER_L: - case ButtonType.TRIGGER_R: - scale = 0.225f; - break; - case ButtonType.BUTTON_START: - scale = 0.075f; - break; - case ButtonType.WIIMOTE_BUTTON_1: - case ButtonType.WIIMOTE_BUTTON_2: - if (mControllerType == OVERLAY_WIIMOTE_SIDEWAYS) - scale = 0.14f; - else - scale = 0.0875f; - break; - case ButtonType.WIIMOTE_BUTTON_PLUS: - case ButtonType.WIIMOTE_BUTTON_MINUS: - case ButtonType.WIIMOTE_BUTTON_HOME: - case ButtonType.CLASSIC_BUTTON_PLUS: - case ButtonType.CLASSIC_BUTTON_MINUS: - case ButtonType.CLASSIC_BUTTON_HOME: - scale = 0.0625f; - break; - case ButtonType.CLASSIC_TRIGGER_L: - case ButtonType.CLASSIC_TRIGGER_R: - case ButtonType.CLASSIC_BUTTON_ZL: - case ButtonType.CLASSIC_BUTTON_ZR: - scale = 0.25f; - break; - default: - scale = 0.125f; - break; - } - - scale *= (IntSetting.MAIN_CONTROL_SCALE.getInt() + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableButton. - final Bitmap defaultStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); - final Bitmap pressedStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); - final InputOverlayDrawableButton overlayDrawable = - new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, legacyId, - control); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, mControllerType, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, mControllerType, orientation), 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableButton. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. - overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.getInt() * 255 / 100); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableDpad} - * - * @param context The current {@link Context}. - * @param defaultResId The {@link Bitmap} resource ID of the default sate. - * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction. - * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. - * @param legacyId Legacy identifier for the up button. - * @param upControl Control identifier for the up button. - * @param downControl Control identifier for the down button. - * @param leftControl Control identifier for the left button. - * @param rightControl Control identifier for the right button. - * @return the initialized {@link InputOverlayDrawableDpad} - */ - private InputOverlayDrawableDpad initializeOverlayDpad(Context context, - int defaultResId, - int pressedOneDirectionResId, - int pressedTwoDirectionsResId, - int legacyId, - int upControl, - int downControl, - int leftControl, - int rightControl, - String orientation) - { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale; - - switch (legacyId) - { - case ButtonType.BUTTON_UP: - scale = 0.2375f; - break; - case ButtonType.CLASSIC_DPAD_UP: - scale = 0.275f; - break; - default: - if (mControllerType == OVERLAY_WIIMOTE_SIDEWAYS || mControllerType == OVERLAY_WIIMOTE) - scale = 0.275f; - else - scale = 0.2125f; - break; - } - - scale *= (IntSetting.MAIN_CONTROL_SCALE.getInt() + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableDpad. - final Bitmap defaultStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); - final Bitmap pressedOneDirectionStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), - scale); - final Bitmap pressedTwoDirectionsStateBitmap = - resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), - scale); - final InputOverlayDrawableDpad overlayDrawable = - new InputOverlayDrawableDpad(res, defaultStateBitmap, - pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, - legacyId, upControl, downControl, leftControl, rightControl); - - // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, mControllerType, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, mControllerType, orientation), 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableDpad. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. - overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.getInt() * 255 / 100); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableJoystick} - * - * @param context The current {@link Context} - * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). - * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). - * @param pressedResInner Resource ID for the pressed inner image of the joystick. - * @param legacyId Legacy identifier (ButtonType) for which joystick this is. - * @param xControl Control identifier for the X axis. - * @param yControl Control identifier for the Y axis. - * @return the initialized {@link InputOverlayDrawableJoystick}. - */ - private InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, - int defaultResInner, int pressedResInner, int legacyId, int xControl, int yControl, - String orientation) - { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on user preference - float scale = 0.275f; - scale *= (IntSetting.MAIN_CONTROL_SCALE.getInt() + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableJoystick. - final Bitmap bitmapOuter = - resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); - final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); - final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, mControllerType, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, mControllerType, orientation), 0f); - - // Decide inner scale based on joystick ID - float innerScale; - - if (legacyId == ButtonType.STICK_C) - { - innerScale = 1.833f; - } - else - { - innerScale = 1.375f; - } - - // Now set the bounds for the InputOverlayDrawableJoystick. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. - int outerSize = bitmapOuter.getWidth(); - Rect outerRect = new Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize); - Rect innerRect = new Rect(0, 0, (int) (outerSize / innerScale), (int) (outerSize / innerScale)); - - // Send the drawableId to the joystick so it can be referenced when saving control position. - final InputOverlayDrawableJoystick overlayDrawable = - new InputOverlayDrawableJoystick(res, bitmapOuter, bitmapInnerDefault, - bitmapInnerPressed, outerRect, innerRect, legacyId, xControl, yControl, - mControllerIndex); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.getInt() * 255 / 100); - - return overlayDrawable; - } - - public void setIsInEditMode(boolean isInEditMode) - { - mIsInEditMode = isInEditMode; - } - - public boolean isInEditMode() - { - return mIsInEditMode; - } - - private void defaultOverlay() - { - if (!mPreferences.getBoolean("OverlayInitV2", false)) - { - // It's possible that a user has created their overlay before this was added - // Only change the overlay if the 'A' button is not in the upper corner. - // GameCube - if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) - { - gcDefaultOverlay(); - } - if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) - { - gcPortraitDefaultOverlay(); - } - - // Wii - if (mPreferences.getFloat(ButtonType.WIIMOTE_BUTTON_A + "-X", 0f) == 0f) - { - wiiDefaultOverlay(); - } - if (mPreferences.getFloat(ButtonType.WIIMOTE_BUTTON_A + "-Portrait" + "-X", 0f) == 0f) - { - wiiPortraitDefaultOverlay(); - } - - // Wii Classic - if (mPreferences.getFloat(ButtonType.CLASSIC_BUTTON_A + "-X", 0f) == 0f) - { - wiiClassicDefaultOverlay(); - } - if (mPreferences.getFloat(ButtonType.CLASSIC_BUTTON_A + "-Portrait" + "-X", 0f) == 0f) - { - wiiClassicPortraitDefaultOverlay(); - } - } - - if (!mPreferences.getBoolean("OverlayInitV3", false)) - { - wiiOnlyDefaultOverlay(); - wiiOnlyPortraitDefaultOverlay(); - } - - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - sPrefsEditor.putBoolean("OverlayInitV2", true); - sPrefsEditor.putBoolean("OverlayInitV3", true); - sPrefsEditor.apply(); - } - - private void gcDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for height. - if (maxY > maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", - (((float) res.getInteger(R.integer.BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", - (((float) res.getInteger(R.integer.BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", - (((float) res.getInteger(R.integer.BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", - (((float) res.getInteger(R.integer.BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", - (((float) res.getInteger(R.integer.BUTTON_X_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", - (((float) res.getInteger(R.integer.BUTTON_X_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", - (((float) res.getInteger(R.integer.BUTTON_Y_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", - (((float) res.getInteger(R.integer.BUTTON_Y_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Z + "-X", - (((float) res.getInteger(R.integer.BUTTON_Z_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Z + "-Y", - (((float) res.getInteger(R.integer.BUTTON_Z_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_UP + "-X", - (((float) res.getInteger(R.integer.BUTTON_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_UP + "-Y", - (((float) res.getInteger(R.integer.BUTTON_UP_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", - (((float) res.getInteger(R.integer.TRIGGER_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_L_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", - (((float) res.getInteger(R.integer.TRIGGER_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_R_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", - (((float) res.getInteger(R.integer.BUTTON_START_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", - (((float) res.getInteger(R.integer.BUTTON_START_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", - (((float) res.getInteger(R.integer.STICK_C_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", - (((float) res.getInteger(R.integer.STICK_C_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_MAIN + "-X", - (((float) res.getInteger(R.integer.STICK_MAIN_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_MAIN + "-Y", - (((float) res.getInteger(R.integer.STICK_MAIN_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void gcPortraitDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for height. - if (maxY < maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - String portrait = "-Portrait"; - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_A_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_B_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_X_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Z + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_Z_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Z + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_Z_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_UP + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_UP + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", - (((float) res.getInteger(R.integer.TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", - (((float) res.getInteger(R.integer.TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", - (((float) res.getInteger(R.integer.BUTTON_START_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", - (((float) res.getInteger(R.integer.BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", - (((float) res.getInteger(R.integer.STICK_C_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", - (((float) res.getInteger(R.integer.STICK_C_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_MAIN + portrait + "-X", - (((float) res.getInteger(R.integer.STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_MAIN + portrait + "-Y", - (((float) res.getInteger(R.integer.STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY > maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_1_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_1_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_2_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_2_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_Z + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_Z_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_Z + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_Z_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_C + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_C_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_C + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_C_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_MINUS + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_MINUS + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_PLUS + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_PLUS + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_UP_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_HOME + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_HOME_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_HOME + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_HOME_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_STICK + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_STICK_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_STICK + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_STICK_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiOnlyDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY > maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "_H-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "_H-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "_H-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "_H-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "_H-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_1_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "_H-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_1_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "_H-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_2_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "_H-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_2_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "_O-X", - (((float) res.getInteger(R.integer.WIIMOTE_O_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "_O-Y", - (((float) res.getInteger(R.integer.WIIMOTE_O_UP_Y) / 1000) * maxY)); - - // Horizontal dpad - sPrefsEditor.putFloat(ButtonType.WIIMOTE_RIGHT + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_RIGHT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_RIGHT + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_RIGHT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiPortraitDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY < maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - String portrait = "-Portrait"; - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_1_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_1_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_2_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_2_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_Z + portrait + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_Z_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_Z + portrait + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_Z_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_C + portrait + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_C_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_BUTTON_C + portrait + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_BUTTON_C_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_MINUS + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_MINUS + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_PLUS + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_PLUS + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_UP_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_UP_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_HOME + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_HOME + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_STICK + portrait + "-X", - (((float) res.getInteger(R.integer.NUNCHUK_STICK_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.NUNCHUK_STICK + portrait + "-Y", - (((float) res.getInteger(R.integer.NUNCHUK_STICK_PORTRAIT_Y) / 1000) * maxY)); - // Horizontal dpad - sPrefsEditor.putFloat(ButtonType.WIIMOTE_RIGHT + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_RIGHT_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_RIGHT + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_RIGHT_PORTRAIT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiOnlyPortraitDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY < maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - String portrait = "-Portrait"; - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "_H" + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_A + "_H" + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "_H" + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_B + "_H" + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "_H" + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_1_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_1 + "_H" + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_1_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "_H" + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_2_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_BUTTON_2 + "_H" + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_H_BUTTON_2_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "_O" + portrait + "-X", - (((float) res.getInteger(R.integer.WIIMOTE_O_UP_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.WIIMOTE_UP + "_O" + portrait + "-Y", - (((float) res.getInteger(R.integer.WIIMOTE_O_UP_PORTRAIT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiClassicDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY > maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_A + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_A + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_B + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_B + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_X + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_X_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_X + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_X_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_Y + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_Y_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_Y + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_Y_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_MINUS + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_MINUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_MINUS + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_MINUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_PLUS + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_PLUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_PLUS + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_PLUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_HOME + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_HOME_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_HOME + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_HOME_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZL + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZL_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZL + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZL_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZR + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZR_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZR + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZR_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_DPAD_UP + "-X", - (((float) res.getInteger(R.integer.CLASSIC_DPAD_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_DPAD_UP + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_DPAD_UP_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_LEFT + "-X", - (((float) res.getInteger(R.integer.CLASSIC_STICK_LEFT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_LEFT + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_STICK_LEFT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_RIGHT + "-X", - (((float) res.getInteger(R.integer.CLASSIC_STICK_RIGHT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_RIGHT + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_STICK_RIGHT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_L + "-X", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_L + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_L_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_R + "-X", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_R + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_R_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - private void wiiClassicPortraitDefaultOverlay() - { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY < maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - Resources res = getResources(); - String portrait = "-Portrait"; - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_A + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_A + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_B + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_B + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_X + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_X + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_Y + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_Y + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_MINUS + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_MINUS_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_MINUS + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_MINUS_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_PLUS + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_PLUS_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_PLUS + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_PLUS_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_HOME + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_HOME + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZL + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZL + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZR + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_BUTTON_ZR + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_DPAD_UP + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_DPAD_UP_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_DPAD_UP + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_DPAD_UP_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_LEFT + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_STICK_LEFT_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_LEFT + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_STICK_LEFT_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_RIGHT + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_STICK_RIGHT_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_STICK_RIGHT + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_STICK_RIGHT_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_L + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_L + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_R + portrait + "-X", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.CLASSIC_TRIGGER_R + portrait + "-Y", - (((float) res.getInteger(R.integer.CLASSIC_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt new file mode 100644 index 0000000000..cc974a89df --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt @@ -0,0 +1,2345 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.overlay + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Rect +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.view.MotionEvent +import android.view.SurfaceView +import android.view.View +import android.view.View.OnTouchListener +import android.widget.Toast +import androidx.preference.PreferenceManager +import org.dolphinemu.dolphinemu.DolphinApplication +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.input.model.InputMappingBooleanSetting +import org.dolphinemu.dolphinemu.features.input.model.InputOverrider +import org.dolphinemu.dolphinemu.features.input.model.InputOverrider.ControlId +import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForSIDevice +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForWiimoteSource +import java.util.Arrays + +/** + * Draws the interactive input overlay on top of the + * [SurfaceView] that is rendering emulation. + * + * @param context The current [Context]. + * @param attrs [AttributeSet] for parsing XML attributes. + */ +class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(context, attrs), + OnTouchListener { + private val overlayButtons: MutableSet = HashSet() + private val overlayDpads: MutableSet = HashSet() + private val overlayJoysticks: MutableSet = HashSet() + private var overlayPointer: InputOverlayPointer? = null + + private var surfacePosition: Rect? = null + + private var isFirstRun = true + private val gcPadRegistered = BooleanArray(4) + private val wiimoteRegistered = BooleanArray(4) + var editMode = false + private var controllerType = -1 + private var controllerIndex = 0 + private var buttonBeingConfigured: InputOverlayDrawableButton? = null + private var dpadBeingConfigured: InputOverlayDrawableDpad? = null + private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null + + private val preferences: SharedPreferences + get() = + PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext()) + + init { + if (!preferences.getBoolean("OverlayInitV3", false)) + defaultOverlay() + + // Set the on touch listener. + setOnTouchListener(this) + + // Force draw + setWillNotDraw(false) + + // Request focus for the overlay so it has priority on presses. + requestFocus() + } + + fun setSurfacePosition(rect: Rect?) { + surfacePosition = rect + initTouchPointer() + } + + fun initTouchPointer() { + // Check if we have all the data we need yet + val aspectRatioAvailable = NativeLibrary.IsRunningAndStarted() + if (!aspectRatioAvailable || surfacePosition == null) + return + + // Check if there's any point in running the pointer code + if (!NativeLibrary.IsEmulatingWii()) + return + + var doubleTapButton = IntSetting.MAIN_DOUBLE_TAP_BUTTON.int + + if (configuredControllerType != OVERLAY_WIIMOTE_CLASSIC && + doubleTapButton == ButtonType.CLASSIC_BUTTON_A + ) { + doubleTapButton = ButtonType.WIIMOTE_BUTTON_A + } + + var doubleTapControl = ControlId.WIIMOTE_A_BUTTON + when (doubleTapButton) { + ButtonType.WIIMOTE_BUTTON_A -> doubleTapControl = ControlId.WIIMOTE_A_BUTTON + ButtonType.WIIMOTE_BUTTON_B -> doubleTapControl = ControlId.WIIMOTE_B_BUTTON + ButtonType.WIIMOTE_BUTTON_2 -> doubleTapControl = ControlId.WIIMOTE_TWO_BUTTON + } + + overlayPointer = InputOverlayPointer( + surfacePosition!!, + doubleTapControl, + IntSetting.MAIN_IR_MODE.int, + BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean, + controllerIndex + ) + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + + for (button in overlayButtons) { + button.draw(canvas) + } + + for (dpad in overlayDpads) { + dpad.draw(canvas) + } + + for (joystick in overlayJoysticks) { + joystick.draw(canvas) + } + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (editMode) { + return onTouchWhileEditing(event) + } + + val action = event.actionMasked + val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && + action != MotionEvent.ACTION_POINTER_UP + val pointerIndex = if (firstPointer) 0 else event.actionIndex + // Tracks if any button/joystick is pressed down + var pressed = false + + for (button in overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + when (action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + // If a pointer enters the bounds of a button, press that button. + if (button.bounds.contains( + event.getX(pointerIndex).toInt(), + event.getY(pointerIndex).toInt() + ) + ) { + button.setPressedState(true) + button.trackId = event.getPointerId(pointerIndex) + pressed = true + InputOverrider.setControlState(controllerIndex, button.control, 1.0) + + val analogControl = getAnalogControlForTrigger(button.control) + if (analogControl >= 0) + InputOverrider.setControlState( + controllerIndex, + analogControl, + 1.0 + ) + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + // If a pointer ends, release the button it was pressing. + if (button.trackId == event.getPointerId(pointerIndex)) { + button.setPressedState(false) + InputOverrider.setControlState(controllerIndex, button.control, 0.0) + + val analogControl = getAnalogControlForTrigger(button.control) + if (analogControl >= 0) + InputOverrider.setControlState( + controllerIndex, + analogControl, + 0.0 + ) + + button.trackId = -1 + } + } + } + } + + for (dpad in overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + // If a pointer enters the bounds of a button, press that button. + if (dpad.bounds + .contains( + event.getX(pointerIndex).toInt(), + event.getY(pointerIndex).toInt() + ) + ) { + dpad.trackId = event.getPointerId(pointerIndex) + pressed = true + } + } + } + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_MOVE -> { + if (dpad.trackId == event.getPointerId(pointerIndex)) { + val dpadPressed = booleanArrayOf(false, false, false, false) + + if (dpad.bounds.top + dpad.height / 3 > event.getY(pointerIndex).toInt()) + dpadPressed[0] = true + if (dpad.bounds.bottom - dpad.height / 3 < event.getY(pointerIndex).toInt()) + dpadPressed[1] = true + if (dpad.bounds.left + dpad.width / 3 > event.getX(pointerIndex).toInt()) + dpadPressed[2] = true + if (dpad.bounds.right - dpad.width / 3 < event.getX(pointerIndex).toInt()) + dpadPressed[3] = true + + // Release the buttons first, then press + for (i in dpadPressed.indices) { + if (!dpadPressed[i]) { + InputOverrider.setControlState( + controllerIndex, + dpad.getControl(i), + 0.0 + ) + } else { + InputOverrider.setControlState( + controllerIndex, + dpad.getControl(i), + 1.0 + ) + } + } + setDpadState( + dpad, + dpadPressed[0], + dpadPressed[1], + dpadPressed[2], + dpadPressed[3] + ) + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + // If a pointer ends, release the buttons. + if (dpad.trackId == event.getPointerId(pointerIndex)) { + for (i in 0 until 4) { + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT) + InputOverrider.setControlState( + controllerIndex, + dpad.getControl(i), + 0.0 + ) + } + dpad.trackId = -1 + } + } + } + } + + for (joystick in overlayJoysticks) { + if (joystick.trackEvent(event)) { + if (joystick.trackId != -1) + pressed = true + } + + InputOverrider.setControlState( + controllerIndex, + joystick.xControl, + joystick.x.toDouble() + ) + InputOverrider.setControlState( + controllerIndex, + joystick.yControl, + -joystick.y.toDouble() + ) + } + + // No button/joystick pressed, safe to move pointer + if (!pressed && overlayPointer != null) { + overlayPointer!!.onTouch(event) + InputOverrider.setControlState( + controllerIndex, + ControlId.WIIMOTE_IR_X, + overlayPointer!!.x.toDouble() + ) + InputOverrider.setControlState( + controllerIndex, + ControlId.WIIMOTE_IR_Y, + -overlayPointer!!.y.toDouble() + ) + } + + invalidate() + + return true + } + + fun onTouchWhileEditing(event: MotionEvent): Boolean { + val pointerIndex = event.actionIndex + val fingerPositionX = event.getX(pointerIndex).toInt() + val fingerPositionY = event.getY(pointerIndex).toInt() + + val orientation = + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" + + // Maybe combine Button and Joystick as subclasses of the same parent? + // Or maybe create an interface like IMoveableHUDControl? + + for (button in overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + // If no button is being moved now, remember the currently touched button to move. + if (buttonBeingConfigured == null && + button.bounds.contains(fingerPositionX, fingerPositionY) + ) { + buttonBeingConfigured = button + buttonBeingConfigured?.onConfigureTouch(event) + } + } + + MotionEvent.ACTION_MOVE -> { + if (buttonBeingConfigured != null) { + buttonBeingConfigured?.onConfigureTouch(event) + invalidate() + return true + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (buttonBeingConfigured == button) { + // Persist button position by saving new place. + saveControlPosition( + buttonBeingConfigured!!.legacyId, + buttonBeingConfigured!!.bounds.left, + buttonBeingConfigured!!.bounds.top, orientation + ) + buttonBeingConfigured = null + } + } + } + } + + for (dpad in overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + // If no button is being moved now, remember the currently touched button to move. + if (buttonBeingConfigured == null && + dpad.bounds.contains(fingerPositionX, fingerPositionY) + ) { + dpadBeingConfigured = dpad + dpadBeingConfigured?.onConfigureTouch(event) + } + } + + MotionEvent.ACTION_MOVE -> { + if (dpadBeingConfigured != null) { + dpadBeingConfigured?.onConfigureTouch(event) + invalidate() + return true + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (dpadBeingConfigured == dpad) { + // Persist button position by saving new place. + saveControlPosition( + dpadBeingConfigured!!.legacyId, + dpadBeingConfigured!!.bounds.left, + dpadBeingConfigured!!.bounds.top, + orientation + ) + dpadBeingConfigured = null + } + } + } + } + + for (joystick in overlayJoysticks) { + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + if (joystickBeingConfigured == null && + joystick.bounds.contains(fingerPositionX, fingerPositionY) + ) { + joystickBeingConfigured = joystick + joystickBeingConfigured?.onConfigureTouch(event) + } + } + + MotionEvent.ACTION_MOVE -> { + if (joystickBeingConfigured != null) { + joystickBeingConfigured?.onConfigureTouch(event) + invalidate() + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (joystickBeingConfigured != null) { + saveControlPosition( + joystickBeingConfigured!!.legacyId, + joystickBeingConfigured!!.bounds.left, + joystickBeingConfigured!!.bounds.top, + orientation + ) + joystickBeingConfigured = null + } + } + } + } + return true + } + + fun onDestroy() { + unregisterControllers() + } + + private fun unregisterControllers() { + for (i in gcPadRegistered.indices) { + if (gcPadRegistered[i]) + InputOverrider.unregisterGameCube(i) + } + + for (i in wiimoteRegistered.indices) { + if (wiimoteRegistered[i]) + InputOverrider.unregisterWii(i) + } + + Arrays.fill(gcPadRegistered, false) + Arrays.fill(wiimoteRegistered, false) + } + + private fun getAnalogControlForTrigger(control: Int): Int = when (control) { + ControlId.GCPAD_L_DIGITAL -> ControlId.GCPAD_L_ANALOG + ControlId.GCPAD_R_DIGITAL -> ControlId.GCPAD_R_ANALOG + ControlId.CLASSIC_L_DIGITAL -> ControlId.CLASSIC_L_ANALOG + ControlId.CLASSIC_R_DIGITAL -> ControlId.CLASSIC_R_ANALOG + else -> -1 + } + + private fun setDpadState( + dpad: InputOverlayDrawableDpad, + up: Boolean, + down: Boolean, + left: Boolean, + right: Boolean + ) { + if (up) { + if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT) + } else { + if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT) + } else { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP) + } + } + } else if (down) { + if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT) + } else { + if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT) + } else { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN) + } + } + } else if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT) + } else if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT) + } + } + + private fun addGameCubeOverlayControls(orientation: String) { + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_0.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_a, + R.drawable.gcpad_a_pressed, + ButtonType.BUTTON_A, + ControlId.GCPAD_A_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_1.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_b, + R.drawable.gcpad_b_pressed, + ButtonType.BUTTON_B, + ControlId.GCPAD_B_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_2.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_x, + R.drawable.gcpad_x_pressed, + ButtonType.BUTTON_X, + ControlId.GCPAD_X_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_3.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_y, + R.drawable.gcpad_y_pressed, + ButtonType.BUTTON_Y, + ControlId.GCPAD_Y_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_4.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_z, + R.drawable.gcpad_z_pressed, + ButtonType.BUTTON_Z, + ControlId.GCPAD_Z_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_5.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_start, + R.drawable.gcpad_start_pressed, + ButtonType.BUTTON_START, + ControlId.GCPAD_START_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_6.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_l, + R.drawable.gcpad_l_pressed, + ButtonType.TRIGGER_L, + ControlId.GCPAD_L_DIGITAL, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_7.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_r, + R.drawable.gcpad_r_pressed, + ButtonType.TRIGGER_R, + ControlId.GCPAD_R_DIGITAL, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_8.boolean) { + overlayDpads.add( + initializeOverlayDpad( + context, + R.drawable.gcwii_dpad, + R.drawable.gcwii_dpad_pressed_one_direction, + R.drawable.gcwii_dpad_pressed_two_directions, + ButtonType.BUTTON_UP, + ControlId.GCPAD_DPAD_UP, + ControlId.GCPAD_DPAD_DOWN, + ControlId.GCPAD_DPAD_LEFT, + ControlId.GCPAD_DPAD_RIGHT, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_9.boolean) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.gcwii_joystick_range, + R.drawable.gcwii_joystick, + R.drawable.gcwii_joystick_pressed, + ButtonType.STICK_MAIN, + ControlId.GCPAD_MAIN_STICK_X, + ControlId.GCPAD_MAIN_STICK_Y, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_GC_10.boolean) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.gcwii_joystick_range, + R.drawable.gcpad_c, + R.drawable.gcpad_c_pressed, + ButtonType.STICK_C, + ControlId.GCPAD_C_STICK_X, + ControlId.GCPAD_C_STICK_Y, + orientation + ) + ) + } + } + + private fun addWiimoteOverlayControls(orientation: String) { + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_0.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_a, + R.drawable.wiimote_a_pressed, + ButtonType.WIIMOTE_BUTTON_A, + ControlId.WIIMOTE_A_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_1.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_b, + R.drawable.wiimote_b_pressed, + ButtonType.WIIMOTE_BUTTON_B, + ControlId.WIIMOTE_B_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_2.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_one, + R.drawable.wiimote_one_pressed, + ButtonType.WIIMOTE_BUTTON_1, + ControlId.WIIMOTE_ONE_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_3.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_two, + R.drawable.wiimote_two_pressed, + ButtonType.WIIMOTE_BUTTON_2, + ControlId.WIIMOTE_TWO_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_4.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_plus, + R.drawable.wiimote_plus_pressed, + ButtonType.WIIMOTE_BUTTON_PLUS, + ControlId.WIIMOTE_PLUS_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_5.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_minus, + R.drawable.wiimote_minus_pressed, + ButtonType.WIIMOTE_BUTTON_MINUS, + ControlId.WIIMOTE_MINUS_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_6.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_home, + R.drawable.wiimote_home_pressed, + ButtonType.WIIMOTE_BUTTON_HOME, + ControlId.WIIMOTE_HOME_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_7.boolean) { + overlayDpads.add( + initializeOverlayDpad( + context, + R.drawable.gcwii_dpad, + R.drawable.gcwii_dpad_pressed_one_direction, + R.drawable.gcwii_dpad_pressed_two_directions, + ButtonType.WIIMOTE_UP, + ControlId.WIIMOTE_DPAD_UP, + ControlId.WIIMOTE_DPAD_DOWN, + ControlId.WIIMOTE_DPAD_LEFT, + ControlId.WIIMOTE_DPAD_RIGHT, + orientation + ) + ) + } + } + + private fun addNunchukOverlayControls(orientation: String) { + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_8.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.nunchuk_c, + R.drawable.nunchuk_c_pressed, + ButtonType.NUNCHUK_BUTTON_C, + ControlId.NUNCHUK_C_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_9.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.nunchuk_z, + R.drawable.nunchuk_z_pressed, + ButtonType.NUNCHUK_BUTTON_Z, + ControlId.NUNCHUK_Z_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_10.boolean) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.gcwii_joystick_range, + R.drawable.gcwii_joystick, + R.drawable.gcwii_joystick_pressed, + ButtonType.NUNCHUK_STICK, + ControlId.NUNCHUK_STICK_X, + ControlId.NUNCHUK_STICK_Y, + orientation + ) + ) + } + } + + private fun addClassicOverlayControls(orientation: String) { + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_0.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_a, + R.drawable.classic_a_pressed, + ButtonType.CLASSIC_BUTTON_A, + ControlId.CLASSIC_A_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_1.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_b, + R.drawable.classic_b_pressed, + ButtonType.CLASSIC_BUTTON_B, + ControlId.CLASSIC_B_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_2.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_x, + R.drawable.classic_x_pressed, + ButtonType.CLASSIC_BUTTON_X, + ControlId.CLASSIC_X_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_3.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_y, + R.drawable.classic_y_pressed, + ButtonType.CLASSIC_BUTTON_Y, + ControlId.CLASSIC_Y_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_4.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_plus, + R.drawable.wiimote_plus_pressed, + ButtonType.CLASSIC_BUTTON_PLUS, + ControlId.CLASSIC_PLUS_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_5.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_minus, + R.drawable.wiimote_minus_pressed, + ButtonType.CLASSIC_BUTTON_MINUS, + ControlId.CLASSIC_MINUS_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_6.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.wiimote_home, + R.drawable.wiimote_home_pressed, + ButtonType.CLASSIC_BUTTON_HOME, + ControlId.CLASSIC_HOME_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_7.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_l, + R.drawable.classic_l_pressed, + ButtonType.CLASSIC_TRIGGER_L, + ControlId.CLASSIC_L_DIGITAL, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_8.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_r, + R.drawable.classic_r_pressed, + ButtonType.CLASSIC_TRIGGER_R, + ControlId.CLASSIC_R_DIGITAL, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_9.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_zl, + R.drawable.classic_zl_pressed, + ButtonType.CLASSIC_BUTTON_ZL, + ControlId.CLASSIC_ZL_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_10.boolean) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.classic_zr, + R.drawable.classic_zr_pressed, + ButtonType.CLASSIC_BUTTON_ZR, + ControlId.CLASSIC_ZR_BUTTON, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_11.boolean) { + overlayDpads.add( + initializeOverlayDpad( + context, + R.drawable.gcwii_dpad, + R.drawable.gcwii_dpad_pressed_one_direction, + R.drawable.gcwii_dpad_pressed_two_directions, + ButtonType.CLASSIC_DPAD_UP, + ControlId.CLASSIC_DPAD_UP, + ControlId.CLASSIC_DPAD_DOWN, + ControlId.CLASSIC_DPAD_LEFT, + ControlId.CLASSIC_DPAD_RIGHT, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_12.boolean) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.gcwii_joystick_range, + R.drawable.gcwii_joystick, + R.drawable.gcwii_joystick_pressed, + ButtonType.CLASSIC_STICK_LEFT, + ControlId.CLASSIC_LEFT_STICK_X, + ControlId.CLASSIC_LEFT_STICK_Y, + orientation + ) + ) + } + if (BooleanSetting.MAIN_BUTTON_TOGGLE_CLASSIC_13.boolean) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.gcwii_joystick_range, + R.drawable.gcwii_joystick, + R.drawable.gcwii_joystick_pressed, + ButtonType.CLASSIC_STICK_RIGHT, + ControlId.CLASSIC_RIGHT_STICK_X, + ControlId.CLASSIC_RIGHT_STICK_Y, + orientation + ) + ) + } + } + + fun refreshControls() { + unregisterControllers() + + // Remove all the overlay buttons from the HashSet. + overlayButtons.removeAll(overlayButtons) + overlayDpads.removeAll(overlayDpads) + overlayJoysticks.removeAll(overlayJoysticks) + + val orientation = + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" + + controllerType = configuredControllerType + + val controllerSetting = + if (NativeLibrary.IsEmulatingWii()) IntSetting.MAIN_OVERLAY_WII_CONTROLLER else IntSetting.MAIN_OVERLAY_GC_CONTROLLER + val controllerIndex = controllerSetting.int + + if (BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean) { + // Add all the enabled overlay items back to the HashSet. + when (controllerType) { + OVERLAY_GAMECUBE -> { + if (getSettingForSIDevice(controllerIndex).int == DISABLED_GAMECUBE_CONTROLLER && isFirstRun) { + Toast.makeText( + context, + R.string.disabled_gc_overlay_notice, + Toast.LENGTH_SHORT + ).show() + } + + this.controllerIndex = controllerIndex + InputOverrider.registerGameCube(this.controllerIndex) + gcPadRegistered[this.controllerIndex] = true + + addGameCubeOverlayControls(orientation) + } + + OVERLAY_WIIMOTE, + OVERLAY_WIIMOTE_SIDEWAYS -> { + this.controllerIndex = controllerIndex - 4 + InputOverrider.registerWii(this.controllerIndex) + wiimoteRegistered[this.controllerIndex] = true + + addWiimoteOverlayControls(orientation) + } + + OVERLAY_WIIMOTE_NUNCHUK -> { + this.controllerIndex = controllerIndex - 4 + InputOverrider.registerWii(this.controllerIndex) + wiimoteRegistered[this.controllerIndex] = true + + addWiimoteOverlayControls(orientation) + addNunchukOverlayControls(orientation) + } + + OVERLAY_WIIMOTE_CLASSIC -> { + this.controllerIndex = controllerIndex - 4 + InputOverrider.registerWii(this.controllerIndex) + wiimoteRegistered[this.controllerIndex] = true + + addClassicOverlayControls(orientation) + } + + OVERLAY_NONE -> {} + } + } + + isFirstRun = false + invalidate() + } + + fun refreshOverlayPointer() { + if (overlayPointer != null) { + overlayPointer?.setMode(IntSetting.MAIN_IR_MODE.int) + overlayPointer?.setRecenter(BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean) + } + } + + fun resetButtonPlacement() { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + val controller = configuredControllerType + if (controller == OVERLAY_GAMECUBE) { + if (isLandscape) { + gcDefaultOverlay() + } else { + gcPortraitDefaultOverlay() + } + } else if (controller == OVERLAY_WIIMOTE_CLASSIC) { + if (isLandscape) { + wiiClassicDefaultOverlay() + } else { + wiiClassicPortraitDefaultOverlay() + } + } else { + if (isLandscape) { + wiiDefaultOverlay() + wiiOnlyDefaultOverlay() + } else { + wiiPortraitDefaultOverlay() + wiiOnlyPortraitDefaultOverlay() + } + } + refreshControls() + } + + private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) { + preferences.edit() + .putFloat(getXKey(sharedPrefsId, controllerType, orientation), x.toFloat()) + .putFloat(getYKey(sharedPrefsId, controllerType, orientation), y.toFloat()) + .apply() + } + + /** + * Initializes an InputOverlayDrawableButton, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + * + * This works due to the way the X and Y coordinates are stored within + * the [SharedPreferences]. + * + * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + * + * This has a few benefits over the conventional way of storing the values + * (ie. within the Dolphin ini file). + * + * * No native calls + * * Keeps Android-only values inside the Android environment + * + * Technically no modifications should need to be performed on the returned + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current [Context]. + * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State). + * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State). + * @param legacyId Legacy identifier for the button the InputOverlayDrawableButton represents. + * @param control Control identifier for the button the InputOverlayDrawableButton represents. + * @return An [InputOverlayDrawableButton] with the correct drawing bounds set. + */ + private fun initializeOverlayButton( + context: Context, + defaultResId: Int, pressedResId: Int, legacyId: Int, control: Int, orientation: String + ): InputOverlayDrawableButton { + // Decide scale based on button ID and user preference + var scale = when (legacyId) { + ButtonType.BUTTON_A, + ButtonType.WIIMOTE_BUTTON_B, + ButtonType.NUNCHUK_BUTTON_Z -> 0.2f + + ButtonType.BUTTON_X, + ButtonType.BUTTON_Y -> 0.175f + + ButtonType.BUTTON_Z, + ButtonType.TRIGGER_L, + ButtonType.TRIGGER_R -> 0.225f + + ButtonType.BUTTON_START -> 0.075f + ButtonType.WIIMOTE_BUTTON_1, + ButtonType.WIIMOTE_BUTTON_2 -> if (controllerType == OVERLAY_WIIMOTE_SIDEWAYS) 0.14f else 0.0875f + + ButtonType.WIIMOTE_BUTTON_PLUS, + ButtonType.WIIMOTE_BUTTON_MINUS, + ButtonType.WIIMOTE_BUTTON_HOME, + ButtonType.CLASSIC_BUTTON_PLUS, + ButtonType.CLASSIC_BUTTON_MINUS, + ButtonType.CLASSIC_BUTTON_HOME -> 0.0625f + + ButtonType.CLASSIC_TRIGGER_L, + ButtonType.CLASSIC_TRIGGER_R, + ButtonType.CLASSIC_BUTTON_ZL, + ButtonType.CLASSIC_BUTTON_ZR -> 0.25f + + else -> 0.125f + } + + scale *= (IntSetting.MAIN_CONTROL_SCALE.int + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableButton. + val defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(resources, defaultResId), scale) + val pressedStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(resources, pressedResId), scale) + val overlayDrawable = InputOverlayDrawableButton( + resources, + defaultStateBitmap, + pressedStateBitmap, + legacyId, + control + ) + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableX = + preferences.getFloat(getXKey(legacyId, controllerType, orientation), 0f).toInt() + val drawableY = + preferences.getFloat(getYKey(legacyId, controllerType, orientation), 0f).toInt() + + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY) + overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.int * 255 / 100) + + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableDpad] + * + * @param context The current [Context]. + * @param defaultResId The [Bitmap] resource ID of the default sate. + * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed sate in one direction. + * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed sate in two directions. + * @param legacyId Legacy identifier for the up button. + * @param upControl Control identifier for the up button. + * @param downControl Control identifier for the down button. + * @param leftControl Control identifier for the left button. + * @param rightControl Control identifier for the right button. + * @return the initialized [InputOverlayDrawableDpad] + */ + private fun initializeOverlayDpad( + context: Context, + defaultResId: Int, + pressedOneDirectionResId: Int, + pressedTwoDirectionsResId: Int, + legacyId: Int, + upControl: Int, + downControl: Int, + leftControl: Int, + rightControl: Int, + orientation: String + ): InputOverlayDrawableDpad { + // Decide scale based on button ID and user preference + var scale: Float = when (legacyId) { + ButtonType.BUTTON_UP -> 0.2375f + ButtonType.CLASSIC_DPAD_UP -> 0.275f + else -> if (controllerType == OVERLAY_WIIMOTE_SIDEWAYS || controllerType == OVERLAY_WIIMOTE) 0.275f else 0.2125f + } + + scale *= (IntSetting.MAIN_CONTROL_SCALE.int + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableDpad. + val defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(resources, defaultResId), scale) + val pressedOneDirectionStateBitmap = resizeBitmap( + context, + BitmapFactory.decodeResource(resources, pressedOneDirectionResId), + scale + ) + val pressedTwoDirectionsStateBitmap = resizeBitmap( + context, + BitmapFactory.decodeResource(resources, pressedTwoDirectionsResId), + scale + ) + val overlayDrawable = InputOverlayDrawableDpad( + resources, + defaultStateBitmap, + pressedOneDirectionStateBitmap, + pressedTwoDirectionsStateBitmap, + legacyId, + upControl, + downControl, + leftControl, + rightControl + ) + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableX = + preferences.getFloat(getXKey(legacyId, controllerType, orientation), 0f).toInt() + val drawableY = + preferences.getFloat(getYKey(legacyId, controllerType, orientation), 0f).toInt() + + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY) + overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.int * 255 / 100) + + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableJoystick] + * + * @param context The current [Context] + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). + * @param pressedResInner Resource ID for the pressed inner image of the joystick. + * @param legacyId Legacy identifier (ButtonType) for which joystick this is. + * @param xControl Control identifier for the X axis. + * @param yControl Control identifier for the Y axis. + * @return the initialized [InputOverlayDrawableJoystick]. + */ + private fun initializeOverlayJoystick( + context: Context, + resOuter: Int, + defaultResInner: Int, + pressedResInner: Int, + legacyId: Int, + xControl: Int, + yControl: Int, + orientation: String + ): InputOverlayDrawableJoystick { + // Decide scale based on user preference + var scale = 0.275f + scale *= (IntSetting.MAIN_CONTROL_SCALE.int + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableJoystick. + val bitmapOuter = + resizeBitmap(context, BitmapFactory.decodeResource(resources, resOuter), scale) + val bitmapInnerDefault = BitmapFactory.decodeResource(resources, defaultResInner) + val bitmapInnerPressed = BitmapFactory.decodeResource(resources, pressedResInner) + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableX = + preferences.getFloat(getXKey(legacyId, controllerType, orientation), 0f).toInt() + val drawableY = + preferences.getFloat(getYKey(legacyId, controllerType, orientation), 0f).toInt() + + // Decide inner scale based on joystick ID + val innerScale: Float = if (legacyId == ButtonType.STICK_C) 1.833f else 1.375f + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. + val outerSize = bitmapOuter.width + val outerRect = Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize) + val innerRect = + Rect(0, 0, (outerSize / innerScale).toInt(), (outerSize / innerScale).toInt()) + + // Send the drawableId to the joystick so it can be referenced when saving control position. + val overlayDrawable = InputOverlayDrawableJoystick( + resources, + bitmapOuter, + bitmapInnerDefault, + bitmapInnerPressed, + outerRect, + innerRect, + legacyId, + xControl, + yControl, + controllerIndex + ) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY) + overlayDrawable.setOpacity(IntSetting.MAIN_CONTROL_OPACITY.int * 255 / 100) + return overlayDrawable + } + + override fun isInEditMode(): Boolean { + return editMode + } + + private fun defaultOverlay() { + if (!preferences.getBoolean("OverlayInitV2", false)) { + // It's possible that a user has created their overlay before this was added + // Only change the overlay if the 'A' button is not in the upper corner. + // GameCube + if (preferences.getFloat(ButtonType.BUTTON_A.toString() + "-X", 0f) == 0f) { + gcDefaultOverlay() + } + if (preferences.getFloat( + ButtonType.BUTTON_A.toString() + "-Portrait" + "-X", + 0f + ) == 0f + ) { + gcPortraitDefaultOverlay() + } + + // Wii + if (preferences.getFloat(ButtonType.WIIMOTE_BUTTON_A.toString() + "-X", 0f) == 0f) { + wiiDefaultOverlay() + } + if (preferences.getFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "-Portrait" + "-X", + 0f + ) == 0f + ) { + wiiPortraitDefaultOverlay() + } + + // Wii Classic + if (preferences.getFloat(ButtonType.CLASSIC_BUTTON_A.toString() + "-X", 0f) == 0f) { + wiiClassicDefaultOverlay() + } + if (preferences.getFloat( + ButtonType.CLASSIC_BUTTON_A.toString() + "-Portrait" + "-X", + 0f + ) == 0f + ) { + wiiClassicPortraitDefaultOverlay() + } + } + + if (!preferences.getBoolean("OverlayInitV3", false)) { + wiiOnlyDefaultOverlay() + wiiOnlyPortraitDefaultOverlay() + } + + preferences.edit() + .putBoolean("OverlayInitV2", true) + .putBoolean("OverlayInitV3", true) + .apply() + } + + private fun gcDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.BUTTON_A.toString() + "-X", + resources.getInteger(R.integer.BUTTON_A_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_A.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_A_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-X", + resources.getInteger(R.integer.BUTTON_B_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_B_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-X", + resources.getInteger(R.integer.BUTTON_X_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_X_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-X", + resources.getInteger(R.integer.BUTTON_Y_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_Y_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_Z.toString() + "-X", + resources.getInteger(R.integer.BUTTON_Z_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_Z.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_Z_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_UP.toString() + "-X", + resources.getInteger(R.integer.BUTTON_UP_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_UP.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_UP_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-X", + resources.getInteger(R.integer.TRIGGER_L_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-Y", + resources.getInteger(R.integer.TRIGGER_L_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-X", + resources.getInteger(R.integer.TRIGGER_R_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-Y", + resources.getInteger(R.integer.TRIGGER_R_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_START.toString() + "-X", + resources.getInteger(R.integer.BUTTON_START_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_START.toString() + "-Y", + resources.getInteger(R.integer.BUTTON_START_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_C.toString() + "-X", + resources.getInteger(R.integer.STICK_C_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_C.toString() + "-Y", + resources.getInteger(R.integer.STICK_C_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_MAIN.toString() + "-X", + resources.getInteger(R.integer.STICK_MAIN_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_MAIN.toString() + "-Y", + resources.getInteger(R.integer.STICK_MAIN_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun gcPortraitDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY < maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + val portrait = "-Portrait" + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.BUTTON_A.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_A_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_A.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_A_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_B.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_B_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_B.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_B_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_X.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_X_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_X.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_X_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_Y_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_Y_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_Z.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_Z_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_Z.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_Z_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_UP.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_UP_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_UP.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_UP_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + portrait + "-X", + resources.getInteger(R.integer.TRIGGER_L_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + portrait + "-Y", + resources.getInteger(R.integer.TRIGGER_L_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + portrait + "-X", + resources.getInteger(R.integer.TRIGGER_R_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + portrait + "-Y", + resources.getInteger(R.integer.TRIGGER_R_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_START.toString() + portrait + "-X", + resources.getInteger(R.integer.BUTTON_START_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_START.toString() + portrait + "-Y", + resources.getInteger(R.integer.BUTTON_START_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_C.toString() + portrait + "-X", + resources.getInteger(R.integer.STICK_C_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_C.toString() + portrait + "-Y", + resources.getInteger(R.integer.STICK_C_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_MAIN.toString() + portrait + "-X", + resources.getInteger(R.integer.STICK_MAIN_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_MAIN.toString() + portrait + "-Y", + resources.getInteger(R.integer.STICK_MAIN_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_A_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_A_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_B_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_B_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_1_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_1_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_2_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_2_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_Z.toString() + "-X", + resources.getInteger(R.integer.NUNCHUK_BUTTON_Z_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_Z.toString() + "-Y", + resources.getInteger(R.integer.NUNCHUK_BUTTON_Z_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_C.toString() + "-X", + resources.getInteger(R.integer.NUNCHUK_BUTTON_C_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_C.toString() + "-Y", + resources.getInteger(R.integer.NUNCHUK_BUTTON_C_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_MINUS.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_MINUS.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_PLUS.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_PLUS.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_UP_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_UP_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_HOME.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_HOME_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_HOME.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_HOME_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_STICK.toString() + "-X", + resources.getInteger(R.integer.NUNCHUK_STICK_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_STICK.toString() + "-Y", + resources.getInteger(R.integer.NUNCHUK_STICK_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiOnlyDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "_H-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_A_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "_H-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_A_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "_H-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_B_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "_H-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_B_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "_H-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_1_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "_H-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_1_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "_H-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_2_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "_H-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_2_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "_O-X", + resources.getInteger(R.integer.WIIMOTE_O_UP_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "_O-Y", + resources.getInteger(R.integer.WIIMOTE_O_UP_Y).toFloat() / 1000 * maxY + ) + // Horizontal dpad + .putFloat( + ButtonType.WIIMOTE_RIGHT.toString() + "-X", + resources.getInteger(R.integer.WIIMOTE_RIGHT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_RIGHT.toString() + "-Y", + resources.getInteger(R.integer.WIIMOTE_RIGHT_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiPortraitDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY < maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + val portrait = "-Portrait" + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_A_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_A_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_B_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_B_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_1_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_1_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_2_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_2_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_Z.toString() + portrait + "-X", + resources.getInteger(R.integer.NUNCHUK_BUTTON_Z_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_Z.toString() + portrait + "-Y", + resources.getInteger(R.integer.NUNCHUK_BUTTON_Z_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_C.toString() + portrait + "-X", + resources.getInteger(R.integer.NUNCHUK_BUTTON_C_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_BUTTON_C.toString() + portrait + "-Y", + resources.getInteger(R.integer.NUNCHUK_BUTTON_C_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_MINUS.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_MINUS.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_MINUS_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_PLUS.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_PLUS.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_PLUS_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_UP_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_UP_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_HOME.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_BUTTON_HOME_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_HOME.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_BUTTON_HOME_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.NUNCHUK_STICK.toString() + portrait + "-X", + resources.getInteger(R.integer.NUNCHUK_STICK_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.NUNCHUK_STICK.toString() + portrait + "-Y", + resources.getInteger(R.integer.NUNCHUK_STICK_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + // Horizontal dpad + .putFloat( + ButtonType.WIIMOTE_RIGHT.toString() + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_RIGHT_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_RIGHT.toString() + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_RIGHT_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiOnlyPortraitDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY < maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + val portrait = "-Portrait" + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "_H" + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_A_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_A.toString() + "_H" + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_A_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "_H" + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_B_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_B.toString() + "_H" + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_B_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "_H" + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_1_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_1.toString() + "_H" + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_1_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "_H" + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_2_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_BUTTON_2.toString() + "_H" + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_H_BUTTON_2_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "_O" + portrait + "-X", + resources.getInteger(R.integer.WIIMOTE_O_UP_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.WIIMOTE_UP.toString() + "_O" + portrait + "-Y", + resources.getInteger(R.integer.WIIMOTE_O_UP_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiClassicDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.CLASSIC_BUTTON_A.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_A_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_A.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_A_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_B.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_B_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_B.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_B_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_X.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_X_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_X.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_X_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_Y.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_Y_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_Y.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_Y_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_MINUS.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_MINUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_MINUS.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_PLUS.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_PLUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_PLUS.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_HOME.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_HOME_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_HOME.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_HOME_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZL.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZL_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZL.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZL_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZR.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZR_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZR.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZR_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_DPAD_UP.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_DPAD_UP_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_DPAD_UP.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_DPAD_UP_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_STICK_LEFT.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_STICK_LEFT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_STICK_LEFT.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_STICK_LEFT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_STICK_RIGHT.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_STICK_RIGHT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_STICK_RIGHT.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_STICK_RIGHT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_L.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_TRIGGER_L_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_L.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_TRIGGER_L_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_R.toString() + "-X", + resources.getInteger(R.integer.CLASSIC_TRIGGER_R_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_R.toString() + "-Y", + resources.getInteger(R.integer.CLASSIC_TRIGGER_R_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + private fun wiiClassicPortraitDefaultOverlay() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for maxX. + if (maxY < maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + val portrait = "-Portrait" + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.CLASSIC_BUTTON_A.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_A_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_A.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_A_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_B.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_B_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_B.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_B_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_X.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_X_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_X.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_X_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_Y.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_Y_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_Y.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_Y_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_MINUS.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_MINUS_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_MINUS.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_MINUS_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_PLUS.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_PLUS_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_PLUS.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_PLUS_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_HOME.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_HOME_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_HOME.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_HOME_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZL.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZL_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZL.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZL_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZR.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZR_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_BUTTON_ZR.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_BUTTON_ZR_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_DPAD_UP.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_DPAD_UP_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_DPAD_UP.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_DPAD_UP_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_STICK_LEFT.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_STICK_LEFT_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_STICK_LEFT.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_STICK_LEFT_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_STICK_RIGHT.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_STICK_RIGHT_PORTRAIT_X) + .toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_STICK_RIGHT.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_STICK_RIGHT_PORTRAIT_Y) + .toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_L.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_TRIGGER_L_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_L.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_TRIGGER_L_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_R.toString() + portrait + "-X", + resources.getInteger(R.integer.CLASSIC_TRIGGER_R_PORTRAIT_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.CLASSIC_TRIGGER_R.toString() + portrait + "-Y", + resources.getInteger(R.integer.CLASSIC_TRIGGER_R_PORTRAIT_Y).toFloat() / 1000 * maxY + ) + .apply() + } + + companion object { + const val OVERLAY_GAMECUBE = 0 + const val OVERLAY_WIIMOTE = 1 + const val OVERLAY_WIIMOTE_SIDEWAYS = 2 + const val OVERLAY_WIIMOTE_NUNCHUK = 3 + const val OVERLAY_WIIMOTE_CLASSIC = 4 + const val OVERLAY_NONE = 5 + private const val DISABLED_GAMECUBE_CONTROLLER = 0 + private const val EMULATED_GAMECUBE_CONTROLLER = 6 + private const val GAMECUBE_ADAPTER = 12 + + // Buttons that have special positions in Wiimote only + private val WIIMOTE_H_BUTTONS = ArrayList() + + init { + WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_A) + WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_B) + WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_1) + WIIMOTE_H_BUTTONS.add(ButtonType.WIIMOTE_BUTTON_2) + } + + private val WIIMOTE_O_BUTTONS = ArrayList() + + init { + WIIMOTE_O_BUTTONS.add(ButtonType.WIIMOTE_UP) + } + + /** + * Resizes a [Bitmap] by a given scale factor + * + * @param context The current [Context] + * @param bitmap The [Bitmap] to scale. + * @param scale The scale factor for the bitmap. + * @return The scaled [Bitmap] + */ + fun resizeBitmap(context: Context, bitmap: Bitmap, scale: Float): Bitmap { + // Determine the button size based on the smaller screen dimension. + // This makes sure the buttons are the same size in both portrait and landscape. + val dm = context.resources.displayMetrics + val minScreenDimension = dm.widthPixels.coerceAtMost(dm.heightPixels) + + val maxBitmapDimension = bitmap.width.coerceAtLeast(bitmap.height) + val bitmapScale = scale * minScreenDimension / maxBitmapDimension + + return Bitmap.createScaledBitmap( + bitmap, + (bitmap.width * bitmapScale).toInt(), + (bitmap.height * bitmapScale).toInt(), + true + ) + } + + @JvmStatic + val configuredControllerType: Int + get() { + val controllerSetting = + if (NativeLibrary.IsEmulatingWii()) IntSetting.MAIN_OVERLAY_WII_CONTROLLER else IntSetting.MAIN_OVERLAY_GC_CONTROLLER + val controllerIndex = controllerSetting.int + + if (controllerIndex in 0 until 4) { + // GameCube controller + if (getSettingForSIDevice(controllerIndex).int == 6) + return OVERLAY_GAMECUBE + } else if (controllerIndex in 4 until 8) { + // Wii Remote + val wiimoteIndex = controllerIndex - 4 + if (getSettingForWiimoteSource(wiimoteIndex).int == 1) { + when (EmulatedController.getSelectedWiimoteAttachment(wiimoteIndex)) { + 1 -> return OVERLAY_WIIMOTE_NUNCHUK + 2 -> return OVERLAY_WIIMOTE_CLASSIC + } + + val sidewaysSetting = + EmulatedController.getSidewaysWiimoteSetting(wiimoteIndex) + val sideways: Boolean = + InputMappingBooleanSetting(sidewaysSetting).boolean + + return if (sideways) OVERLAY_WIIMOTE_SIDEWAYS else OVERLAY_WIIMOTE + } + } + return OVERLAY_NONE + } + + private fun getKey( + sharedPrefsId: Int, + controller: Int, + orientation: String, + suffix: String + ): String = + if (controller == OVERLAY_WIIMOTE_SIDEWAYS && WIIMOTE_H_BUTTONS.contains(sharedPrefsId)) { + sharedPrefsId.toString() + "_H" + orientation + suffix + } else if (controller == OVERLAY_WIIMOTE && WIIMOTE_O_BUTTONS.contains(sharedPrefsId)) { + sharedPrefsId.toString() + "_O" + orientation + suffix + } else { + sharedPrefsId.toString() + orientation + suffix + } + } + + private fun getXKey(sharedPrefsId: Int, controller: Int, orientation: String): String = + getKey(sharedPrefsId, controller, orientation, "-X") + + private fun getYKey(sharedPrefsId: Int, controller: Int, orientation: String): String = + getKey(sharedPrefsId, controller, orientation, "-Y") +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java deleted file mode 100644 index d997da94bf..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableButton -{ - // The legacy ID identifying what type of button this Drawable represents. - private int mLegacyId; - private int mControl; - private int mTrackId; - private int mPreviousTouchX, mPreviousTouchY; - private int mControlPositionX, mControlPositionY; - private int mWidth; - private int mHeight; - private BitmapDrawable mDefaultStateBitmap; - private BitmapDrawable mPressedStateBitmap; - private boolean mPressedState = false; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. - * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. - * @param legacyId Legacy identifier (ButtonType) for this type of button. - * @param control Control ID for this type of button. - */ - public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, - Bitmap pressedStateBitmap, int legacyId, int control) - { - mTrackId = -1; - mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); - mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); - mLegacyId = legacyId; - mControl = control; - - mWidth = mDefaultStateBitmap.getIntrinsicWidth(); - mHeight = mDefaultStateBitmap.getIntrinsicHeight(); - } - - /** - * Gets this InputOverlayDrawableButton's legacy button ID. - */ - public int getLegacyId() - { - return mLegacyId; - } - - public int getControl() - { - return mControl; - } - - public void setTrackId(int trackId) - { - mTrackId = trackId; - } - - public int getTrackId() - { - return mTrackId; - } - - public void onConfigureTouch(MotionEvent event) - { - switch (event.getAction()) - { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - case MotionEvent.ACTION_MOVE: - mControlPositionX += (int) event.getX() - mPreviousTouchX; - mControlPositionY += (int) event.getY() - mPreviousTouchY; - setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, - getHeight() + mControlPositionY); - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - } - } - - public void setPosition(int x, int y) - { - mControlPositionX = x; - mControlPositionY = y; - } - - public void draw(Canvas canvas) - { - getCurrentStateBitmapDrawable().draw(canvas); - } - - private BitmapDrawable getCurrentStateBitmapDrawable() - { - return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; - } - - public void setBounds(int left, int top, int right, int bottom) - { - mDefaultStateBitmap.setBounds(left, top, right, bottom); - mPressedStateBitmap.setBounds(left, top, right, bottom); - } - - public void setOpacity(int value) - { - mDefaultStateBitmap.setAlpha(value); - mPressedStateBitmap.setAlpha(value); - } - - public Rect getBounds() - { - return mDefaultStateBitmap.getBounds(); - } - - public int getWidth() - { - return mWidth; - } - - public int getHeight() - { - return mHeight; - } - - public void setPressedState(boolean isPressed) - { - mPressedState = isPressed; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.kt new file mode 100644 index 0000000000..220eb836d5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.kt @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.overlay + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.view.MotionEvent + +/** + * Custom [BitmapDrawable] that is capable + * of storing it's own ID. + * + * @param res [Resources] instance. + * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. + * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. + * @param legacyId Legacy identifier (ButtonType) for this type of button. + * @param control Control ID for this type of button. + */ +class InputOverlayDrawableButton( + res: Resources, + defaultStateBitmap: Bitmap, + pressedStateBitmap: Bitmap, + val legacyId: Int, + val control: Int +) { + var trackId: Int = -1 + private var previousTouchX = 0 + private var previousTouchY = 0 + private var controlPositionX = 0 + private var controlPositionY = 0 + val width: Int + val height: Int + private val defaultStateBitmap: BitmapDrawable + private val pressedStateBitmap: BitmapDrawable + private var pressedState = false + + init { + this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap) + this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap) + width = this.defaultStateBitmap.intrinsicWidth + height = this.defaultStateBitmap.intrinsicHeight + } + + fun onConfigureTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + + MotionEvent.ACTION_MOVE -> { + controlPositionX += event.x.toInt() - previousTouchX + controlPositionY += event.y.toInt() - previousTouchY + setBounds( + controlPositionX, + controlPositionY, + width + controlPositionX, + height + controlPositionY + ) + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + } + } + + fun setPosition(x: Int, y: Int) { + controlPositionX = x + controlPositionY = y + } + + fun draw(canvas: Canvas) { + currentStateBitmapDrawable.draw(canvas) + } + + private val currentStateBitmapDrawable: BitmapDrawable + get() = if (pressedState) pressedStateBitmap else defaultStateBitmap + + fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { + defaultStateBitmap.setBounds(left, top, right, bottom) + pressedStateBitmap.setBounds(left, top, right, bottom) + } + + fun setOpacity(value: Int) { + defaultStateBitmap.alpha = value + pressedStateBitmap.alpha = value + } + + val bounds: Rect + get() = defaultStateBitmap.bounds + + fun setPressedState(isPressed: Boolean) { + pressedState = isPressed + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.java deleted file mode 100644 index 6542609ca0..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2016 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableDpad -{ - // The legacy ID identifying what type of button this Drawable represents. - private int mLegacyId; - private int[] mControls = new int[4]; - private int mTrackId; - private int mPreviousTouchX, mPreviousTouchY; - private int mControlPositionX, mControlPositionY; - private int mWidth; - private int mHeight; - private BitmapDrawable mDefaultStateBitmap; - private BitmapDrawable mPressedOneDirectionStateBitmap; - private BitmapDrawable mPressedTwoDirectionsStateBitmap; - private int mPressState = STATE_DEFAULT; - - public static final int STATE_DEFAULT = 0; - public static final int STATE_PRESSED_UP = 1; - public static final int STATE_PRESSED_DOWN = 2; - public static final int STATE_PRESSED_LEFT = 3; - public static final int STATE_PRESSED_RIGHT = 4; - public static final int STATE_PRESSED_UP_LEFT = 5; - public static final int STATE_PRESSED_UP_RIGHT = 6; - public static final int STATE_PRESSED_DOWN_LEFT = 7; - public static final int STATE_PRESSED_DOWN_RIGHT = 8; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param defaultStateBitmap {@link Bitmap} of the default state. - * @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction. - * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. - * @param legacyId Legacy identifier (ButtonType) for the up button. - * @param upControl Control identifier for the up button. - * @param downControl Control identifier for the down button. - * @param leftControl Control identifier for the left button. - * @param rightControl Control identifier for the right button. - */ - public InputOverlayDrawableDpad(Resources res, Bitmap defaultStateBitmap, - Bitmap pressedOneDirectionStateBitmap, Bitmap pressedTwoDirectionsStateBitmap, - int legacyId, int upControl, int downControl, int leftControl, int rightControl) - { - mTrackId = -1; - mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); - mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); - mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); - - mWidth = mDefaultStateBitmap.getIntrinsicWidth(); - mHeight = mDefaultStateBitmap.getIntrinsicHeight(); - - mLegacyId = legacyId; - mControls[0] = upControl; - mControls[1] = downControl; - mControls[2] = leftControl; - mControls[3] = rightControl; - } - - public void draw(Canvas canvas) - { - int px = mControlPositionX + (getWidth() / 2); - int py = mControlPositionY + (getHeight() / 2); - switch (mPressState) - { - case STATE_DEFAULT: - mDefaultStateBitmap.draw(canvas); - break; - case STATE_PRESSED_UP: - mPressedOneDirectionStateBitmap.draw(canvas); - break; - case STATE_PRESSED_RIGHT: - canvas.save(); - canvas.rotate(90, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN: - canvas.save(); - canvas.rotate(180, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_LEFT: - canvas.save(); - canvas.rotate(270, px, py); - mPressedOneDirectionStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_UP_LEFT: - mPressedTwoDirectionsStateBitmap.draw(canvas); - break; - case STATE_PRESSED_UP_RIGHT: - canvas.save(); - canvas.rotate(90, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN_RIGHT: - canvas.save(); - canvas.rotate(180, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - case STATE_PRESSED_DOWN_LEFT: - canvas.save(); - canvas.rotate(270, px, py); - mPressedTwoDirectionsStateBitmap.draw(canvas); - canvas.restore(); - break; - } - } - - public int getLegacyId() - { - return mLegacyId; - } - - /** - * Gets one of the InputOverlayDrawableDpad's control IDs. - */ - public int getControl(int direction) - { - return mControls[direction]; - } - - public void setTrackId(int trackId) - { - mTrackId = trackId; - } - - public int getTrackId() - { - return mTrackId; - } - - public void onConfigureTouch(MotionEvent event) - { - switch (event.getAction()) - { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - case MotionEvent.ACTION_MOVE: - mControlPositionX += (int) event.getX() - mPreviousTouchX; - mControlPositionY += (int) event.getY() - mPreviousTouchY; - setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, - getHeight() + mControlPositionY); - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - } - } - - public void setPosition(int x, int y) - { - mControlPositionX = x; - mControlPositionY = y; - } - - public void setBounds(int left, int top, int right, int bottom) - { - mDefaultStateBitmap.setBounds(left, top, right, bottom); - mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); - mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); - } - - public void setOpacity(int value) - { - mDefaultStateBitmap.setAlpha(value); - mPressedOneDirectionStateBitmap.setAlpha(value); - mPressedTwoDirectionsStateBitmap.setAlpha(value); - } - - public Rect getBounds() - { - return mDefaultStateBitmap.getBounds(); - } - - public int getWidth() - { - return mWidth; - } - - public int getHeight() - { - return mHeight; - } - - public void setState(int pressState) - { - mPressState = pressState; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.kt new file mode 100644 index 0000000000..2b53046a1e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableDpad.kt @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.overlay + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.view.MotionEvent + +/** + * Custom [BitmapDrawable] that is capable + * of storing it's own ID. + * + * @param res [Resources] instance. + * @param defaultStateBitmap [Bitmap] of the default state. + * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. + * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. + * @param legacyId Legacy identifier (ButtonType) for the up button. + * @param upControl Control identifier for the up button. + * @param downControl Control identifier for the down button. + * @param leftControl Control identifier for the left button. + * @param rightControl Control identifier for the right button. + */ +class InputOverlayDrawableDpad( + res: Resources, + defaultStateBitmap: Bitmap, + pressedOneDirectionStateBitmap: Bitmap, + pressedTwoDirectionsStateBitmap: Bitmap, + val legacyId: Int, + upControl: Int, + downControl: Int, + leftControl: Int, + rightControl: Int +) { + private val controls = IntArray(4) + var trackId: Int = -1 + private var previousTouchX = 0 + private var previousTouchY = 0 + private var controlPositionX = 0 + private var controlPositionY = 0 + val width: Int + val height: Int + private val defaultStateBitmap: BitmapDrawable + private val pressedOneDirectionStateBitmap: BitmapDrawable + private val pressedTwoDirectionsStateBitmap: BitmapDrawable + private var pressState = STATE_DEFAULT + + init { + this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap) + this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap) + this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) + + width = this.defaultStateBitmap.intrinsicWidth + height = this.defaultStateBitmap.intrinsicHeight + + controls[0] = upControl + controls[1] = downControl + controls[2] = leftControl + controls[3] = rightControl + } + + fun draw(canvas: Canvas) { + val px = controlPositionX + width / 2 + val py = controlPositionY + height / 2 + when (pressState) { + STATE_DEFAULT -> defaultStateBitmap.draw(canvas) + STATE_PRESSED_UP -> pressedOneDirectionStateBitmap.draw(canvas) + STATE_PRESSED_RIGHT -> { + canvas.apply { + save() + rotate(90f, px.toFloat(), py.toFloat()) + pressedOneDirectionStateBitmap.draw(canvas) + restore() + } + } + + STATE_PRESSED_DOWN -> { + canvas.apply { + save() + rotate(180f, px.toFloat(), py.toFloat()) + pressedOneDirectionStateBitmap.draw(canvas) + restore() + } + } + + STATE_PRESSED_LEFT -> { + canvas.apply { + save() + rotate(270f, px.toFloat(), py.toFloat()) + pressedOneDirectionStateBitmap.draw(canvas) + restore() + } + } + + STATE_PRESSED_UP_LEFT -> pressedTwoDirectionsStateBitmap.draw(canvas) + STATE_PRESSED_UP_RIGHT -> { + canvas.apply { + save() + rotate(90f, px.toFloat(), py.toFloat()) + pressedTwoDirectionsStateBitmap.draw(canvas) + restore() + } + } + + STATE_PRESSED_DOWN_RIGHT -> { + canvas.apply { + save() + rotate(180f, px.toFloat(), py.toFloat()) + pressedTwoDirectionsStateBitmap.draw(canvas) + restore() + } + } + + STATE_PRESSED_DOWN_LEFT -> { + canvas.apply { + save() + rotate(270f, px.toFloat(), py.toFloat()) + pressedTwoDirectionsStateBitmap.draw(canvas) + restore() + } + } + } + } + + /** + * Gets one of the InputOverlayDrawableDpad's control IDs. + */ + fun getControl(direction: Int): Int { + return controls[direction] + } + + fun onConfigureTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + + MotionEvent.ACTION_MOVE -> { + controlPositionX += event.x.toInt() - previousTouchX + controlPositionY += event.y.toInt() - previousTouchY + setBounds( + controlPositionX, controlPositionY, width + controlPositionX, + height + controlPositionY + ) + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + } + } + + fun setPosition(x: Int, y: Int) { + controlPositionX = x + controlPositionY = y + } + + fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { + defaultStateBitmap.setBounds(left, top, right, bottom) + pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom) + pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom) + } + + fun setOpacity(value: Int) { + defaultStateBitmap.alpha = value + pressedOneDirectionStateBitmap.alpha = value + pressedTwoDirectionsStateBitmap.alpha = value + } + + val bounds: Rect + get() = defaultStateBitmap.bounds + + fun setState(pressState: Int) { + this.pressState = pressState + } + + companion object { + const val STATE_DEFAULT = 0 + const val STATE_PRESSED_UP = 1 + const val STATE_PRESSED_DOWN = 2 + const val STATE_PRESSED_LEFT = 3 + const val STATE_PRESSED_RIGHT = 4 + const val STATE_PRESSED_UP_LEFT = 5 + const val STATE_PRESSED_UP_RIGHT = 6 + const val STATE_PRESSED_DOWN_LEFT = 7 + const val STATE_PRESSED_DOWN_RIGHT = 8 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java deleted file mode 100644 index 6ebe731341..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.view.MotionEvent; - -import org.dolphinemu.dolphinemu.features.input.model.InputOverrider; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; - -/** - * Custom {@link BitmapDrawable} that is capable - * of storing it's own ID. - */ -public final class InputOverlayDrawableJoystick -{ - private float mCurrentX = 0.0f; - private float mCurrentY = 0.0f; - private int trackId = -1; - private final int mJoystickLegacyId; - private final int mJoystickXControl; - private final int mJoystickYControl; - private int mControlPositionX, mControlPositionY; - private int mPreviousTouchX, mPreviousTouchY; - private final int mWidth; - private final int mHeight; - private final int mControllerIndex; - private Rect mVirtBounds; - private Rect mOrigBounds; - private int mOpacity; - private final BitmapDrawable mOuterBitmap; - private final BitmapDrawable mDefaultStateInnerBitmap; - private final BitmapDrawable mPressedStateInnerBitmap; - private final BitmapDrawable mBoundsBoxBitmap; - private boolean mPressedState = false; - - /** - * Constructor - * - * @param res {@link Resources} instance. - * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick. - * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. - * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. - * @param rectOuter {@link Rect} which represents the outer joystick bounds. - * @param rectInner {@link Rect} which represents the inner joystick bounds. - * @param legacyId Legacy identifier (ButtonType) for which joystick this is. - * @param xControl The control which the x value of the joystick will be written to. - * @param yControl The control which the y value of the joystick will be written to. - */ - public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, Bitmap bitmapInnerDefault, - Bitmap bitmapInnerPressed, Rect rectOuter, Rect rectInner, int legacyId, int xControl, - int yControl, int controllerIndex) - { - mJoystickLegacyId = legacyId; - mJoystickXControl = xControl; - mJoystickYControl = yControl; - - mOuterBitmap = new BitmapDrawable(res, bitmapOuter); - mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); - mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); - mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); - mWidth = bitmapOuter.getWidth(); - mHeight = bitmapOuter.getHeight(); - - if (controllerIndex < 0 || controllerIndex >= 4) - throw new IllegalArgumentException("controllerIndex must be 0-3"); - mControllerIndex = controllerIndex; - - setBounds(rectOuter); - mDefaultStateInnerBitmap.setBounds(rectInner); - mPressedStateInnerBitmap.setBounds(rectInner); - mVirtBounds = getBounds(); - mOrigBounds = mOuterBitmap.copyBounds(); - mBoundsBoxBitmap.setAlpha(0); - mBoundsBoxBitmap.setBounds(getVirtBounds()); - SetInnerBounds(); - } - - /** - * Gets this InputOverlayDrawableJoystick's legacy ID. - * - * @return this InputOverlayDrawableJoystick's legacy ID. - */ - public int getLegacyId() - { - return mJoystickLegacyId; - } - - public void draw(Canvas canvas) - { - mOuterBitmap.draw(canvas); - getCurrentStateBitmapDrawable().draw(canvas); - mBoundsBoxBitmap.draw(canvas); - } - - public boolean TrackEvent(MotionEvent event) - { - boolean reCenter = BooleanSetting.MAIN_JOYSTICK_REL_CENTER.getBoolean(); - int action = event.getActionMasked(); - boolean firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && - action != MotionEvent.ACTION_POINTER_UP; - int pointerIndex = firstPointer ? 0 : event.getActionIndex(); - boolean pressed = false; - - switch (action) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) - { - mPressedState = pressed = true; - mOuterBitmap.setAlpha(0); - mBoundsBoxBitmap.setAlpha(mOpacity); - if (reCenter) - { - getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(), - (int) event.getY(pointerIndex) - getVirtBounds().centerY()); - } - mBoundsBoxBitmap.setBounds(getVirtBounds()); - trackId = event.getPointerId(pointerIndex); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (trackId == event.getPointerId(pointerIndex)) - { - pressed = true; - mPressedState = false; - mCurrentX = mCurrentY = 0.0f; - mOuterBitmap.setAlpha(mOpacity); - mBoundsBoxBitmap.setAlpha(0); - setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, - mOrigBounds.bottom)); - setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, - mOrigBounds.bottom)); - SetInnerBounds(); - trackId = -1; - } - break; - } - - if (trackId == -1) - return pressed; - - for (int i = 0; i < event.getPointerCount(); i++) - { - if (trackId == event.getPointerId(i)) - { - float touchX = event.getX(i); - float touchY = event.getY(i); - float maxY = getVirtBounds().bottom; - float maxX = getVirtBounds().right; - touchX -= getVirtBounds().centerX(); - maxX -= getVirtBounds().centerX(); - touchY -= getVirtBounds().centerY(); - maxY -= getVirtBounds().centerY(); - mCurrentX = touchX / maxX; - mCurrentY = touchY / maxY; - - SetInnerBounds(); - } - } - return pressed; - } - - public void onConfigureTouch(MotionEvent event) - { - switch (event.getAction()) - { - case MotionEvent.ACTION_DOWN: - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - case MotionEvent.ACTION_MOVE: - int deltaX = (int) event.getX() - mPreviousTouchX; - int deltaY = (int) event.getY() - mPreviousTouchY; - mControlPositionX += deltaX; - mControlPositionY += deltaY; - setBounds(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() + mControlPositionY)); - setVirtBounds(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() + mControlPositionY)); - SetInnerBounds(); - setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, - mOuterBitmap.getIntrinsicWidth() + mControlPositionX, - mOuterBitmap.getIntrinsicHeight() + mControlPositionY))); - mPreviousTouchX = (int) event.getX(); - mPreviousTouchY = (int) event.getY(); - break; - } - } - - public float getX() - { - return mCurrentX; - } - - public float getY() - { - return mCurrentY; - } - - public int getXControl() - { - return mJoystickXControl; - } - - public int getYControl() - { - return mJoystickYControl; - } - - private void SetInnerBounds() - { - double x = mCurrentX; - double y = mCurrentY; - - double angle = Math.atan2(y, x) + Math.PI + Math.PI; - double radius = Math.hypot(y, x); - double maxRadius = InputOverrider.getGateRadiusAtAngle(mControllerIndex, mJoystickXControl, - angle); - if (radius > maxRadius) - { - y = maxRadius * Math.sin(angle); - x = maxRadius * Math.cos(angle); - mCurrentY = (float) y; - mCurrentX = (float) x; - } - - int pixelX = getVirtBounds().centerX() + (int) (x * (getVirtBounds().width() / 2)); - int pixelY = getVirtBounds().centerY() + (int) (y * (getVirtBounds().height() / 2)); - - int width = mPressedStateInnerBitmap.getBounds().width() / 2; - int height = mPressedStateInnerBitmap.getBounds().height() / 2; - mDefaultStateInnerBitmap.setBounds(pixelX - width, pixelY - height, pixelX + width, - pixelY + height); - mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); - } - - public void setPosition(int x, int y) - { - mControlPositionX = x; - mControlPositionY = y; - } - - private BitmapDrawable getCurrentStateBitmapDrawable() - { - return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; - } - - public void setBounds(Rect bounds) - { - mOuterBitmap.setBounds(bounds); - } - - public void setOpacity(int value) - { - mOpacity = value; - - mDefaultStateInnerBitmap.setAlpha(value); - mPressedStateInnerBitmap.setAlpha(value); - - if (trackId == -1) - { - mOuterBitmap.setAlpha(value); - mBoundsBoxBitmap.setAlpha(0); - } - else - { - mOuterBitmap.setAlpha(0); - mBoundsBoxBitmap.setAlpha(value); - } - } - - public Rect getBounds() - { - return mOuterBitmap.getBounds(); - } - - private void setVirtBounds(Rect bounds) - { - mVirtBounds = bounds; - } - - private void setOrigBounds(Rect bounds) - { - mOrigBounds = bounds; - } - - private Rect getVirtBounds() - { - return mVirtBounds; - } - - public int getWidth() - { - return mWidth; - } - - public int getHeight() - { - return mHeight; - } - - public int getTrackId() - { - return trackId; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt new file mode 100644 index 0000000000..7dff339dcf --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.overlay + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.view.MotionEvent +import org.dolphinemu.dolphinemu.features.input.model.InputOverrider +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.hypot +import kotlin.math.sin + +/** + * Custom [BitmapDrawable] that is capable + * of storing it's own ID. + * + * @param res [Resources] instance. + * @param bitmapOuter [Bitmap] which represents the outer non-movable part of the joystick. + * @param bitmapInnerDefault [Bitmap] which represents the default inner movable part of the joystick. + * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. + * @param rectOuter [Rect] which represents the outer joystick bounds. + * @param rectInner [Rect] which represents the inner joystick bounds. + * @param legacyId Legacy identifier (ButtonType) for which joystick this is. + * @param xControl The control which the x value of the joystick will be written to. + * @param yControl The control which the y value of the joystick will be written to. + */ +class InputOverlayDrawableJoystick( + res: Resources, + bitmapOuter: Bitmap, + bitmapInnerDefault: Bitmap, + bitmapInnerPressed: Bitmap, + rectOuter: Rect, + rectInner: Rect, + val legacyId: Int, + val xControl: Int, + val yControl: Int, + private val controllerIndex: Int +) { + var x = 0.0f + private set + var y = 0.0f + private set + var trackId = -1 + private set + private var controlPositionX = 0 + private var controlPositionY = 0 + private var previousTouchX = 0 + private var previousTouchY = 0 + val width: Int + val height: Int + private var virtBounds: Rect + private var origBounds: Rect + private var opacity = 0 + private val outerBitmap: BitmapDrawable + private val defaultStateInnerBitmap: BitmapDrawable + private val pressedStateInnerBitmap: BitmapDrawable + private val boundsBoxBitmap: BitmapDrawable + private var pressedState = false + + var bounds: Rect + get() = outerBitmap.bounds + set(bounds) { + outerBitmap.bounds = bounds + } + + init { + outerBitmap = BitmapDrawable(res, bitmapOuter) + defaultStateInnerBitmap = BitmapDrawable(res, bitmapInnerDefault) + pressedStateInnerBitmap = BitmapDrawable(res, bitmapInnerPressed) + boundsBoxBitmap = BitmapDrawable(res, bitmapOuter) + width = bitmapOuter.width + height = bitmapOuter.height + + require(!(controllerIndex < 0 || controllerIndex >= 4)) { "controllerIndex must be 0-3" } + + bounds = rectOuter + defaultStateInnerBitmap.bounds = rectInner + pressedStateInnerBitmap.bounds = rectInner + virtBounds = bounds + origBounds = outerBitmap.copyBounds() + boundsBoxBitmap.alpha = 0 + boundsBoxBitmap.bounds = virtBounds + setInnerBounds() + } + + fun draw(canvas: Canvas) { + outerBitmap.draw(canvas) + currentStateBitmapDrawable.draw(canvas) + boundsBoxBitmap.draw(canvas) + } + + fun trackEvent(event: MotionEvent): Boolean { + val reCenter = BooleanSetting.MAIN_JOYSTICK_REL_CENTER.boolean + val action = event.actionMasked + val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && + action != MotionEvent.ACTION_POINTER_UP + val pointerIndex = if (firstPointer) 0 else event.actionIndex + var pressed = false + + when (action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + if (bounds.contains( + event.getX(pointerIndex).toInt(), + event.getY(pointerIndex).toInt() + ) + ) { + pressed = true + pressedState = true + outerBitmap.alpha = 0 + boundsBoxBitmap.alpha = opacity + if (reCenter) { + virtBounds.offset( + event.getX(pointerIndex).toInt() - virtBounds.centerX(), + event.getY(pointerIndex).toInt() - virtBounds.centerY() + ) + } + boundsBoxBitmap.bounds = virtBounds + trackId = event.getPointerId(pointerIndex) + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (trackId == event.getPointerId(pointerIndex)) { + pressed = true + pressedState = false + y = 0f + x = y + outerBitmap.alpha = opacity + boundsBoxBitmap.alpha = 0 + virtBounds = + Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) + bounds = + Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) + setInnerBounds() + trackId = -1 + } + } + } + + if (trackId == -1) + return pressed + + for (i in 0 until event.pointerCount) { + if (trackId == event.getPointerId(i)) { + var touchX = event.getX(i) + var touchY = event.getY(i) + var maxY = virtBounds.bottom.toFloat() + var maxX = virtBounds.right.toFloat() + touchX -= virtBounds.centerX().toFloat() + maxX -= virtBounds.centerX().toFloat() + touchY -= virtBounds.centerY().toFloat() + maxY -= virtBounds.centerY().toFloat() + x = touchX / maxX + y = touchY / maxY + + setInnerBounds() + } + } + return pressed + } + + fun onConfigureTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + + MotionEvent.ACTION_MOVE -> { + val deltaX = event.x.toInt() - previousTouchX + val deltaY = event.y.toInt() - previousTouchY + controlPositionX += deltaX + controlPositionY += deltaY + bounds = Rect( + controlPositionX, + controlPositionY, + outerBitmap.intrinsicWidth + controlPositionX, + outerBitmap.intrinsicHeight + controlPositionY + ) + virtBounds = Rect( + controlPositionX, + controlPositionY, + outerBitmap.intrinsicWidth + controlPositionX, + outerBitmap.intrinsicHeight + controlPositionY + ) + setInnerBounds() + origBounds = Rect( + Rect( + controlPositionX, + controlPositionY, + outerBitmap.intrinsicWidth + controlPositionX, + outerBitmap.intrinsicHeight + controlPositionY + ) + ) + previousTouchX = event.x.toInt() + previousTouchY = event.y.toInt() + } + } + } + + private fun setInnerBounds() { + var x = x.toDouble() + var y = y.toDouble() + + val angle = atan2(y, x) + Math.PI + Math.PI + val radius = hypot(y, x) + val maxRadius = InputOverrider.getGateRadiusAtAngle(controllerIndex, xControl, angle) + if (radius > maxRadius) { + x = maxRadius * cos(angle) + y = maxRadius * sin(angle) + this.x = x.toFloat() + this.y = y.toFloat() + } + + val pixelX = virtBounds.centerX() + (x * (virtBounds.width() / 2)).toInt() + val pixelY = virtBounds.centerY() + (y * (virtBounds.height() / 2)).toInt() + + val width = pressedStateInnerBitmap.bounds.width() / 2 + val height = pressedStateInnerBitmap.bounds.height() / 2 + defaultStateInnerBitmap.setBounds( + pixelX - width, + pixelY - height, + pixelX + width, + pixelY + height + ) + pressedStateInnerBitmap.bounds = defaultStateInnerBitmap.bounds + } + + fun setPosition(x: Int, y: Int) { + controlPositionX = x + controlPositionY = y + } + + private val currentStateBitmapDrawable: BitmapDrawable + get() = if (pressedState) pressedStateInnerBitmap else defaultStateInnerBitmap + + fun setOpacity(value: Int) { + opacity = value + + defaultStateInnerBitmap.alpha = value + pressedStateInnerBitmap.alpha = value + + if (trackId == -1) { + outerBitmap.alpha = value + boundsBoxBitmap.alpha = 0 + } else { + outerBitmap.alpha = 0 + boundsBoxBitmap.alpha = value + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.java deleted file mode 100644 index 09d02a174a..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.java +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.overlay; - -import android.graphics.Rect; -import android.os.Handler; -import android.view.MotionEvent; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.features.input.model.InputOverrider; - -import java.util.ArrayList; - -public class InputOverlayPointer -{ - public static final int MODE_DISABLED = 0; - public static final int MODE_FOLLOW = 1; - public static final int MODE_DRAG = 2; - - private float mCurrentX = 0.0f; - private float mCurrentY = 0.0f; - private float mOldX = 0.0f; - private float mOldY = 0.0f; - - private float mGameCenterX; - private float mGameCenterY; - private float mGameWidthHalfInv; - private float mGameHeightHalfInv; - - private float mTouchStartX; - private float mTouchStartY; - - private int mMode; - private boolean mRecenter; - private int mControllerIndex; - - private boolean doubleTap = false; - private int mDoubleTapControl; - private int trackId = -1; - - public static ArrayList DOUBLE_TAP_OPTIONS = new ArrayList<>(); - - static - { - DOUBLE_TAP_OPTIONS.add(NativeLibrary.ButtonType.WIIMOTE_BUTTON_A); - DOUBLE_TAP_OPTIONS.add(NativeLibrary.ButtonType.WIIMOTE_BUTTON_B); - DOUBLE_TAP_OPTIONS.add(NativeLibrary.ButtonType.WIIMOTE_BUTTON_2); - DOUBLE_TAP_OPTIONS.add(NativeLibrary.ButtonType.CLASSIC_BUTTON_A); - } - - public InputOverlayPointer(Rect surfacePosition, int doubleTapControl, int mode, boolean recenter, - int controllerIndex) - { - mDoubleTapControl = doubleTapControl; - mMode = mode; - mRecenter = recenter; - mControllerIndex = controllerIndex; - - mGameCenterX = (surfacePosition.left + surfacePosition.right) / 2.0f; - mGameCenterY = (surfacePosition.top + surfacePosition.bottom) / 2.0f; - - float gameWidth = surfacePosition.right - surfacePosition.left; - float gameHeight = surfacePosition.bottom - surfacePosition.top; - - // Adjusting for device's black bars. - float surfaceAR = gameWidth / gameHeight; - float gameAR = NativeLibrary.GetGameAspectRatio(); - - if (gameAR <= surfaceAR) - { - // Black bars on left/right - gameWidth = gameHeight * gameAR; - } - else - { - // Black bars on top/bottom - gameHeight = gameWidth / gameAR; - } - - mGameWidthHalfInv = 1.0f / (gameWidth * 0.5f); - mGameHeightHalfInv = 1.0f / (gameHeight * 0.5f); - } - - public void onTouch(MotionEvent event) - { - int action = event.getActionMasked(); - boolean firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && - action != MotionEvent.ACTION_POINTER_UP; - int pointerIndex = firstPointer ? 0 : event.getActionIndex(); - - switch (action) - { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - trackId = event.getPointerId(pointerIndex); - mTouchStartX = event.getX(pointerIndex); - mTouchStartY = event.getY(pointerIndex); - touchPress(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (trackId == event.getPointerId(pointerIndex)) - trackId = -1; - if (mMode == MODE_DRAG) - updateOldAxes(); - if (mRecenter) - reset(); - break; - } - - int eventPointerIndex = event.findPointerIndex(trackId); - if (trackId == -1 || eventPointerIndex == -1) - return; - - if (mMode == MODE_FOLLOW) - { - mCurrentX = (event.getX(eventPointerIndex) - mGameCenterX) * mGameWidthHalfInv; - mCurrentY = (event.getY(eventPointerIndex) - mGameCenterY) * mGameHeightHalfInv; - } - else if (mMode == MODE_DRAG) - { - mCurrentX = mOldX + - (event.getX(eventPointerIndex) - mTouchStartX) * mGameWidthHalfInv; - mCurrentY = mOldY + - (event.getY(eventPointerIndex) - mTouchStartY) * mGameHeightHalfInv; - } - } - - private void touchPress() - { - if (mMode != MODE_DISABLED) - { - if (doubleTap) - { - InputOverrider.setControlState(mControllerIndex, mDoubleTapControl, 1.0); - new Handler().postDelayed(() -> InputOverrider.setControlState(mControllerIndex, - mDoubleTapControl, 0.0), - 50); - } - else - { - doubleTap = true; - new Handler().postDelayed(() -> doubleTap = false, 300); - } - } - } - - private void updateOldAxes() - { - mOldX = mCurrentX; - mOldY = mCurrentY; - } - - private void reset() - { - mCurrentX = mCurrentY = mOldX = mOldY = 0.0f; - } - - public float getX() - { - return mCurrentX; - } - - public float getY() - { - return mCurrentY; - } - - public void setMode(int mode) - { - mMode = mode; - if (mode == MODE_DRAG) - updateOldAxes(); - } - - public void setRecenter(boolean recenter) - { - mRecenter = recenter; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.kt new file mode 100644 index 0000000000..138effca6f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayPointer.kt @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.overlay + +import android.graphics.Rect +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.features.input.model.InputOverrider + +class InputOverlayPointer( + surfacePosition: Rect, + private val doubleTapControl: Int, + private var mode: Int, + private var recenter: Boolean, + private val controllerIndex: Int +) { + var x = 0.0f + var y = 0.0f + private var oldX = 0.0f + private var oldY = 0.0f + + private val gameCenterX: Float + private val gameCenterY: Float + private val gameWidthHalfInv: Float + private val gameHeightHalfInv: Float + + private var touchStartX = 0f + private var touchStartY = 0f + + private var doubleTap = false + private var trackId = -1 + + init { + gameCenterX = (surfacePosition.left + surfacePosition.right) / 2f + gameCenterY = (surfacePosition.top + surfacePosition.bottom) / 2f + + var gameWidth = (surfacePosition.right - surfacePosition.left).toFloat() + var gameHeight = (surfacePosition.bottom - surfacePosition.top).toFloat() + + // Adjusting for device's black bars. + val surfaceAR = gameWidth / gameHeight + val gameAR = NativeLibrary.GetGameAspectRatio() + if (gameAR <= surfaceAR) { + // Black bars on left/right + gameWidth = gameHeight * gameAR + } else { + // Black bars on top/bottom + gameHeight = gameWidth / gameAR + } + + gameWidthHalfInv = 1f / (gameWidth * 0.5f) + gameHeightHalfInv = 1f / (gameHeight * 0.5f) + } + + fun onTouch(event: MotionEvent) { + val action = event.actionMasked + val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && + action != MotionEvent.ACTION_POINTER_UP + val pointerIndex = if (firstPointer) 0 else event.actionIndex + + when (action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + trackId = event.getPointerId(pointerIndex) + touchStartX = event.getX(pointerIndex) + touchStartY = event.getY(pointerIndex) + touchPress() + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (trackId == event.getPointerId(pointerIndex)) + trackId = -1 + if (mode == MODE_DRAG) + updateOldAxes() + if (recenter) + reset() + } + } + + val eventPointerIndex = event.findPointerIndex(trackId) + if (trackId == -1 || eventPointerIndex == -1) + return + + if (mode == MODE_FOLLOW) { + x = (event.getX(eventPointerIndex) - gameCenterX) * gameWidthHalfInv + y = (event.getY(eventPointerIndex) - gameCenterY) * gameHeightHalfInv + } else if (mode == MODE_DRAG) { + x = oldX + (event.getX(eventPointerIndex) - touchStartX) * gameWidthHalfInv + y = oldY + (event.getY(eventPointerIndex) - touchStartY) * gameHeightHalfInv + } + } + + private fun touchPress() { + if (mode != MODE_DISABLED) { + if (doubleTap) { + InputOverrider.setControlState(controllerIndex, doubleTapControl, 1.0) + Handler(Looper.myLooper()!!).postDelayed( + { + InputOverrider.setControlState(controllerIndex, doubleTapControl, 0.0) + }, + 50 + ) + } else { + doubleTap = true + Handler(Looper.myLooper()!!).postDelayed({ doubleTap = false }, 300) + } + } + } + + private fun updateOldAxes() { + oldX = x + oldY = y + } + + private fun reset() { + oldY = 0.0f + oldX = 0.0f + y = 0.0f + x = 0.0f + } + + fun setMode(mode: Int) { + this.mode = mode + if (mode == MODE_DRAG) + updateOldAxes() + } + + fun setRecenter(recenter: Boolean) { + this.recenter = recenter + } + + companion object { + const val MODE_DISABLED = 0 + const val MODE_FOLLOW = 1 + const val MODE_DRAG = 2 + + @JvmField + var DOUBLE_TAP_OPTIONS = arrayListOf( + NativeLibrary.ButtonType.WIIMOTE_BUTTON_A, + NativeLibrary.ButtonType.WIIMOTE_BUTTON_B, + NativeLibrary.ButtonType.WIIMOTE_BUTTON_2, + NativeLibrary.ButtonType.CLASSIC_BUTTON_A + ) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java deleted file mode 100644 index 4b4b763c2b..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -/** - * Work around a bug with the nVidia Shield. - * - * Without this View, the emulation SurfaceView acts like it has the - * highest Z-value, blocking any other View, such as the menu fragments. - */ -public final class NVidiaShieldWorkaroundView extends View -{ - public NVidiaShieldWorkaroundView(Context context, AttributeSet attrs) - { - super(context, attrs); - - // Setting this seems to workaround the bug - setWillNotDraw(false); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt new file mode 100644 index 0000000000..16679e2c5f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View + +/** + * Work around a bug with the nVidia Shield. + * + * + * Without this View, the emulation SurfaceView acts like it has the + * highest Z-value, blocking any other View, such as the menu fragments. + */ +class NVidiaShieldWorkaroundView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + init { + // Setting this seems to workaround the bug + setWillNotDraw(false) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java deleted file mode 100644 index 08e4777303..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.leanback.widget.TitleViewAdapter; - -import org.dolphinemu.dolphinemu.R; - -public class CustomTitleView extends LinearLayout implements TitleViewAdapter.Provider -{ - private final TextView mTitleView; - private final View mBadgeView; - - private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter() - { - @Override - public View getSearchAffordanceView() - { - return null; - } - - @Override - public void setTitle(CharSequence titleText) - { - CustomTitleView.this.setTitle(titleText); - } - - @Override - public void setBadgeDrawable(Drawable drawable) - { - CustomTitleView.this.setBadgeDrawable(drawable); - } - }; - - public CustomTitleView(Context context) - { - this(context, null); - } - - public CustomTitleView(Context context, AttributeSet attrs) - { - this(context, attrs, 0); - } - - public CustomTitleView(Context context, AttributeSet attrs, int defStyle) - { - super(context, attrs, defStyle); - View root = LayoutInflater.from(context).inflate(R.layout.tv_title, this); - mTitleView = root.findViewById(R.id.title); - mBadgeView = root.findViewById(R.id.badge); - } - - public void setTitle(CharSequence title) - { - if (title != null) - { - mTitleView.setText(title); - mTitleView.setVisibility(View.VISIBLE); - mBadgeView.setVisibility(View.VISIBLE); - } - } - - public void setBadgeDrawable(Drawable drawable) - { - if (drawable != null) - { - mTitleView.setVisibility(View.GONE); - mBadgeView.setVisibility(View.VISIBLE); - } - } - - @Override - public TitleViewAdapter getTitleViewAdapter() - { - return mTitleViewAdapter; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt new file mode 100644 index 0000000000..35f69195e9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.leanback.widget.TitleViewAdapter +import org.dolphinemu.dolphinemu.R + +class CustomTitleView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : LinearLayout(context, attrs, defStyle), TitleViewAdapter.Provider { + private val titleView: TextView + private val badgeView: View + private val titleViewAdapter: TitleViewAdapter = object : TitleViewAdapter() { + override fun getSearchAffordanceView(): View? = null + + override fun setTitle(titleText: CharSequence?) = this@CustomTitleView.setTitle(titleText) + + override fun setBadgeDrawable(drawable: Drawable?) = + this@CustomTitleView.setBadgeDrawable(drawable) + } + + init { + val root = LayoutInflater.from(context).inflate(R.layout.tv_title, this) + titleView = root.findViewById(R.id.title) + badgeView = root.findViewById(R.id.badge) + } + + fun setTitle(title: CharSequence?) { + if (title != null) { + titleView.text = title + titleView.visibility = VISIBLE + badgeView.visibility = VISIBLE + } + } + + fun setBadgeDrawable(drawable: Drawable?) { + if (drawable != null) { + titleView.visibility = GONE + badgeView.visibility = VISIBLE + } + } + + override fun getTitleViewAdapter(): TitleViewAdapter = titleViewAdapter +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java deleted file mode 100644 index 488f0909df..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ /dev/null @@ -1,441 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.graphics.Insets; -import androidx.core.splashscreen.SplashScreen; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.tabs.TabLayout; - -import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter; -import org.dolphinemu.dolphinemu.databinding.ActivityMainBinding; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; -import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView; -import org.dolphinemu.dolphinemu.utils.Action1; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.InsetsHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.StartupHandler; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; -import org.dolphinemu.dolphinemu.utils.WiiUtils; - -/** - * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which - * individually display a grid of available games for each Fragment, in a tabbed layout. - */ -public final class MainActivity extends AppCompatActivity - implements MainView, SwipeRefreshLayout.OnRefreshListener, ThemeProvider -{ - private int mThemeId; - - private final MainPresenter mPresenter = new MainPresenter(this, this); - - private ActivityMainBinding mBinding; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - SplashScreen splashScreen = SplashScreen.installSplashScreen(this); - splashScreen.setKeepOnScreenCondition( - () -> !DirectoryInitialization.areDolphinDirectoriesReady()); - - ThemeHelper.setTheme(this); - - super.onCreate(savedInstanceState); - - mBinding = ActivityMainBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - setInsets(); - ThemeHelper.enableStatusBarScrollTint(this, mBinding.appbarMain); - - mBinding.toolbarMain.setTitle(R.string.app_name); - setSupportActionBar(mBinding.toolbarMain); - - // Set up the FAB. - mBinding.buttonAddDirectory.setOnClickListener(view -> mPresenter.onFabClick()); - mBinding.appbarMain.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> - { - if (verticalOffset == 0) - { - mBinding.buttonAddDirectory.extend(); - } - else if (appBarLayout.getTotalScrollRange() == -verticalOffset) - { - mBinding.buttonAddDirectory.shrink(); - } - }); - - mPresenter.onCreate(); - - // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) - if (savedInstanceState == null) - { - StartupHandler.HandleInit(this); - new AfterDirectoryInitializationRunner().runWithLifecycle(this, this::checkTheme); - } - - if (!DirectoryInitialization.isWaitingForWriteAccess(this)) - { - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - } - - @Override - protected void onResume() - { - ThemeHelper.setCorrectTheme(this); - - super.onResume(); - - if (DirectoryInitialization.shouldStart(this)) - { - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - - mPresenter.onResume(); - } - - @Override - protected void onDestroy() - { - super.onDestroy(); - mPresenter.onDestroy(); - } - - @Override - protected void onStart() - { - super.onStart(); - StartupHandler.checkSessionReset(this); - } - - @Override - protected void onStop() - { - super.onStop(); - - if (isChangingConfigurations()) - { - MainPresenter.skipRescanningLibrary(); - } - else if (DirectoryInitialization.areDolphinDirectoriesReady()) - { - // If the currently selected platform tab changed, save it to disk - NativeConfig.save(NativeConfig.LAYER_BASE); - } - - StartupHandler.setSessionTime(this); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_game_grid, menu); - - if (WiiUtils.isSystemMenuInstalled()) - { - int resId = WiiUtils.isSystemMenuvWii() ? - R.string.grid_menu_load_vwii_system_menu_installed : - R.string.grid_menu_load_wii_system_menu_installed; - - menu.findItem(R.id.menu_load_wii_system_menu).setTitle( - getString(resId, WiiUtils.getSystemMenuVersion())); - } - - return true; - } - - /** - * MainView - */ - - @Override - public void setVersionString(String version) - { - mBinding.toolbarMain.setSubtitle(version); - } - - @Override - public void launchSettingsActivity(MenuTag menuTag) - { - SettingsActivity.launch(this, menuTag); - } - - @Override - public void launchFileListActivity() - { - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); - } - else - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); - } - } - - @Override - public void launchOpenFileActivity(int requestCode) - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, requestCode); - } - - /** - * @param requestCode An int describing whether the Activity that is returning did so successfully. - * @param resultCode An int describing what Activity is giving us this callback. - * @param result The information the returning Activity is providing us. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) - { - super.onActivityResult(requestCode, resultCode, result); - - // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) - { - Uri uri = result.getData(); - switch (requestCode) - { - case MainPresenter.REQUEST_DIRECTORY: - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); - } - else - { - mPresenter.onDirectorySelected(result); - } - break; - - case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, - FileBrowserHelper.GAME_LIKE_EXTENSIONS, - () -> EmulationActivity.launch(this, result.getData().toString(), false)); - break; - - case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, - () -> mPresenter.installWAD(result.getData().toString())); - break; - - case MainPresenter.REQUEST_WII_SAVE_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importWiiSave(result.getData().toString())); - break; - - case MainPresenter.REQUEST_NAND_BIN_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importNANDBin(result.getData().toString())); - break; - } - } - else - { - MainPresenter.skipRescanningLibrary(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) - { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) - { - if (grantResults[0] == PackageManager.PERMISSION_DENIED) - { - PermissionsHandler.setWritePermissionDenied(); - } - - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - } - - /** - * Called by the framework whenever any actionbar/toolbar icon is clicked. - * - * @param item The icon that was clicked on. - * @return True if the event was handled, false to bubble it up to the OS. - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - return mPresenter.handleOptionSelection(item.getItemId(), this); - } - - /** - * Called when the user requests a refresh by swiping down. - */ - @Override - public void onRefresh() - { - setRefreshing(true); - GameFileCacheManager.startRescan(); - } - - /** - * Shows or hides the loading indicator. - */ - @Override - public void setRefreshing(boolean refreshing) - { - forEachPlatformGamesView(view -> view.setRefreshing(refreshing)); - } - - /** - * To be called when the game file cache is updated. - */ - @Override - public void showGames() - { - forEachPlatformGamesView(PlatformGamesView::showGames); - } - - @Override - public void reloadGrid() - { - forEachPlatformGamesView(PlatformGamesView::refetchMetadata); - } - - @Override - public void showGridOptions() - { - new GridOptionDialogFragment().show(getSupportFragmentManager(), "gridOptions"); - } - - private void forEachPlatformGamesView(Action1 action) - { - for (Platform platform : Platform.values()) - { - PlatformGamesView fragment = getPlatformGamesView(platform); - if (fragment != null) - { - action.call(fragment); - } - } - } - - @Nullable - private PlatformGamesView getPlatformGamesView(Platform platform) - { - String fragmentTag = - "android:switcher:" + mBinding.pagerPlatforms.getId() + ":" + platform.toInt(); - - return (PlatformGamesView) getSupportFragmentManager().findFragmentByTag(fragmentTag); - } - - // Don't call this before DirectoryInitialization completes. - private void setPlatformTabsAndStartGameFileCacheService() - { - PlatformPagerAdapter platformPagerAdapter = new PlatformPagerAdapter( - getSupportFragmentManager(), this); - mBinding.pagerPlatforms.setAdapter(platformPagerAdapter); - mBinding.pagerPlatforms.setOffscreenPageLimit(platformPagerAdapter.getCount()); - mBinding.tabsPlatforms.setupWithViewPager(mBinding.pagerPlatforms); - mBinding.tabsPlatforms.addOnTabSelectedListener( - new TabLayout.ViewPagerOnTabSelectedListener(mBinding.pagerPlatforms) - { - @Override - public void onTabSelected(@NonNull TabLayout.Tab tab) - { - super.onTabSelected(tab); - IntSetting.MAIN_LAST_PLATFORM_TAB.setInt(NativeConfig.LAYER_BASE, - tab.getPosition()); - } - }); - - for (int i = 0; i < PlatformPagerAdapter.TAB_ICONS.length; i++) - { - mBinding.tabsPlatforms.getTabAt(i).setIcon(PlatformPagerAdapter.TAB_ICONS[i]); - } - - mBinding.pagerPlatforms.setCurrentItem(IntSetting.MAIN_LAST_PLATFORM_TAB.getInt()); - - showGames(); - GameFileCacheManager.startLoad(); - } - - @Override - public void setTheme(int themeId) - { - super.setTheme(themeId); - this.mThemeId = themeId; - } - - @Override - public int getThemeId() - { - return mThemeId; - } - - private void checkTheme() - { - ThemeHelper.setCorrectTheme(this); - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarMain, (v, windowInsets) -> - { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - - InsetsHelper.insetAppBar(insets, mBinding.appbarMain); - - ViewGroup.MarginLayoutParams mlpFab = - (ViewGroup.MarginLayoutParams) mBinding.buttonAddDirectory.getLayoutParams(); - int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large); - mlpFab.leftMargin = insets.left + fabPadding; - mlpFab.bottomMargin = insets.bottom + fabPadding; - mlpFab.rightMargin = insets.right + fabPadding; - mBinding.buttonAddDirectory.setLayoutParams(mlpFab); - - mBinding.pagerPlatforms.setPadding(insets.left, 0, insets.right, 0); - - InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView); - ThemeHelper.setNavigationBarColor(this, - MaterialColors.getColor(mBinding.appbarMain, R.attr.colorSurface)); - - return windowInsets; - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt new file mode 100644 index 0000000000..84d870c921 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.appcompat.app.AppCompatActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.color.MaterialColors +import com.google.android.material.tabs.TabLayout +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter +import org.dolphinemu.dolphinemu.databinding.ActivityMainBinding +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity +import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView +import org.dolphinemu.dolphinemu.utils.Action1 +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.InsetsHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.StartupHandler +import org.dolphinemu.dolphinemu.utils.ThemeHelper +import org.dolphinemu.dolphinemu.utils.WiiUtils + +class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProvider { + override var themeId = 0 + + private val presenter = MainPresenter(this, this) + + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { !DirectoryInitialization.areDolphinDirectoriesReady() } + + ThemeHelper.setTheme(this) + + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + WindowCompat.setDecorFitsSystemWindows(window, false) + setInsets() + ThemeHelper.enableStatusBarScrollTint(this, binding.appbarMain) + + binding.toolbarMain.setTitle(R.string.app_name) + setSupportActionBar(binding.toolbarMain) + + // Set up the FAB. + binding.buttonAddDirectory.setOnClickListener { presenter.onFabClick() } + binding.appbarMain.addOnOffsetChangedListener { appBarLayout: AppBarLayout, verticalOffset: Int -> + if (verticalOffset == 0) { + binding.buttonAddDirectory.extend() + } else if (appBarLayout.totalScrollRange == -verticalOffset) { + binding.buttonAddDirectory.shrink() + } + } + + presenter.onCreate() + + // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) + if (savedInstanceState == null) { + StartupHandler.HandleInit(this) + AfterDirectoryInitializationRunner().runWithLifecycle(this) { + ThemeHelper.setCorrectTheme(this) + } + } + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + } + + override fun onResume() { + ThemeHelper.setCorrectTheme(this) + + super.onResume() + if (DirectoryInitialization.shouldStart(this)) { + DirectoryInitialization.start(this) + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + + presenter.onResume() + } + + override fun onStart() { + super.onStart() + StartupHandler.checkSessionReset(this) + } + + override fun onStop() { + super.onStop() + if (isChangingConfigurations) { + MainPresenter.skipRescanningLibrary() + } else if (DirectoryInitialization.areDolphinDirectoriesReady()) { + // If the currently selected platform tab changed, save it to disk + NativeConfig.save(NativeConfig.LAYER_BASE) + } + + StartupHandler.setSessionTime(this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_game_grid, menu) + if (WiiUtils.isSystemMenuInstalled()) { + val resId = + if (WiiUtils.isSystemMenuvWii()) R.string.grid_menu_load_vwii_system_menu_installed else R.string.grid_menu_load_wii_system_menu_installed + menu.findItem(R.id.menu_load_wii_system_menu).title = + getString(resId, WiiUtils.getSystemMenuVersion()) + } + return true + } + + /** + * MainView + */ + override fun setVersionString(version: String) { + binding.toolbarMain.subtitle = version + } + + override fun launchSettingsActivity(menuTag: MenuTag?) { + SettingsActivity.launch(this, menuTag) + } + + override fun launchFileListActivity() { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS) + } else { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY) + } + } + + override fun launchOpenFileActivity(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + startActivityForResult(intent, requestCode) + } + + /** + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + super.onActivityResult(requestCode, resultCode, result) + + // If the user picked a file, as opposed to just backing out. + if (resultCode == RESULT_OK) { + val uri = result!!.data + when (requestCode) { + MainPresenter.REQUEST_DIRECTORY -> { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + presenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)) + } else { + presenter.onDirectorySelected(result) + } + } + + MainPresenter.REQUEST_GAME_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.GAME_LIKE_EXTENSIONS + ) { EmulationActivity.launch(this, result.data.toString(), false) } + + MainPresenter.REQUEST_WAD_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.WAD_EXTENSION + ) { presenter.installWAD(result.data.toString()) } + + MainPresenter.REQUEST_WII_SAVE_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importWiiSave(result.data.toString()) } + + MainPresenter.REQUEST_NAND_BIN_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importNANDBin(result.data.toString()) } + } + } else { + MainPresenter.skipRescanningLibrary() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + PermissionsHandler.setWritePermissionDenied() + } + + DirectoryInitialization.start(this) + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + } + + /** + * Called by the framework whenever any actionbar/toolbar icon is clicked. + * + * @param item The icon that was clicked on. + * @return True if the event was handled, false to bubble it up to the OS. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return presenter.handleOptionSelection(item.itemId, this) + } + + /** + * Called when the user requests a refresh by swiping down. + */ + override fun onRefresh() { + setRefreshing(true) + GameFileCacheManager.startRescan() + } + + /** + * Shows or hides the loading indicator. + */ + override fun setRefreshing(refreshing: Boolean) = + forEachPlatformGamesView { view: PlatformGamesView -> view.setRefreshing(refreshing) } + + /** + * To be called when the game file cache is updated. + */ + override fun showGames() = + forEachPlatformGamesView { obj: PlatformGamesView -> obj.showGames() } + + override fun reloadGrid() { + forEachPlatformGamesView { obj: PlatformGamesView -> obj.refetchMetadata() } + } + + override fun showGridOptions() = + GridOptionDialogFragment().show(supportFragmentManager, "gridOptions") + + private fun forEachPlatformGamesView(action: Action1) { + for (platform in Platform.values()) { + val fragment = getPlatformGamesView(platform) + if (fragment != null) { + action.call(fragment) + } + } + } + + private fun getPlatformGamesView(platform: Platform): PlatformGamesView? { + val fragmentTag = + "android:switcher:" + binding.pagerPlatforms.id + ":" + platform.toInt() + return supportFragmentManager.findFragmentByTag(fragmentTag) as PlatformGamesView? + } + + // Don't call this before DirectoryInitialization completes. + private fun setPlatformTabsAndStartGameFileCacheService() { + val platformPagerAdapter = PlatformPagerAdapter( + supportFragmentManager, this + ) + binding.pagerPlatforms.adapter = platformPagerAdapter + binding.pagerPlatforms.offscreenPageLimit = platformPagerAdapter.count + binding.tabsPlatforms.setupWithViewPager(binding.pagerPlatforms) + binding.tabsPlatforms.addOnTabSelectedListener( + object : TabLayout.ViewPagerOnTabSelectedListener(binding.pagerPlatforms) { + override fun onTabSelected(tab: TabLayout.Tab) { + super.onTabSelected(tab) + IntSetting.MAIN_LAST_PLATFORM_TAB.setInt( + NativeConfig.LAYER_BASE, + tab.position + ) + } + }) + + for (i in PlatformPagerAdapter.TAB_ICONS.indices) { + binding.tabsPlatforms.getTabAt(i)?.setIcon(PlatformPagerAdapter.TAB_ICONS[i]) + } + + binding.pagerPlatforms.currentItem = IntSetting.MAIN_LAST_PLATFORM_TAB.int + + showGames() + GameFileCacheManager.startLoad() + } + + override fun setTheme(themeId: Int) { + super.setTheme(themeId) + this.themeId = themeId + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.appbarMain) { _: View?, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + InsetsHelper.insetAppBar(insets, binding.appbarMain) + + val mlpFab = binding.buttonAddDirectory.layoutParams as MarginLayoutParams + val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large) + mlpFab.leftMargin = insets.left + fabPadding + mlpFab.bottomMargin = insets.bottom + fabPadding + mlpFab.rightMargin = insets.right + fabPadding + binding.buttonAddDirectory.layoutParams = mlpFab + + binding.pagerPlatforms.setPadding(insets.left, 0, insets.right, 0) + + InsetsHelper.applyNavbarWorkaround(insets.bottom, binding.workaroundView) + ThemeHelper.setNavigationBarColor( + this, + MaterialColors.getColor(binding.appbarMain, R.attr.colorSurface) + ) + + windowInsets + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java deleted file mode 100644 index e136864faa..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java +++ /dev/null @@ -1,349 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.ContentResolver; -import android.content.Intent; -import android.net.Uri; - -import androidx.activity.ComponentActivity; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.dolphinemu.dolphinemu.BuildConfig; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateProgressBarDialogFragment; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemMenuNotInstalledDialogFragment; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateViewModel; -import org.dolphinemu.dolphinemu.fragments.AboutDialogFragment; -import org.dolphinemu.dolphinemu.model.GameFileCache; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.BooleanSupplier; -import org.dolphinemu.dolphinemu.utils.CompletableFuture; -import org.dolphinemu.dolphinemu.utils.ContentHandler; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.ThreadUtil; -import org.dolphinemu.dolphinemu.utils.WiiUtils; - -import java.util.Arrays; -import java.util.concurrent.ExecutionException; - -public final class MainPresenter -{ - public static final int REQUEST_DIRECTORY = 1; - public static final int REQUEST_GAME_FILE = 2; - public static final int REQUEST_SD_FILE = 3; - public static final int REQUEST_WAD_FILE = 4; - public static final int REQUEST_WII_SAVE_FILE = 5; - public static final int REQUEST_NAND_BIN_FILE = 6; - - private static boolean sShouldRescanLibrary = true; - - private final MainView mView; - private final FragmentActivity mActivity; - private String mDirToAdd; - - public MainPresenter(MainView view, FragmentActivity activity) - { - mView = view; - mActivity = activity; - } - - public void onCreate() - { - // Ask the user to grant write permission if relevant and not already granted - if (DirectoryInitialization.isWaitingForWriteAccess(mActivity)) - PermissionsHandler.requestWritePermission(mActivity); - - String versionName = BuildConfig.VERSION_NAME; - mView.setVersionString(versionName); - - GameFileCacheManager.getGameFiles().observe(mActivity, (gameFiles) -> mView.showGames()); - - Observer refreshObserver = (isLoading) -> - { - mView.setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - }; - GameFileCacheManager.isLoading().observe(mActivity, refreshObserver); - GameFileCacheManager.isRescanning().observe(mActivity, refreshObserver); - } - - public void onDestroy() - { - } - - public void onFabClick() - { - new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, - mView::launchFileListActivity); - } - - public boolean handleOptionSelection(int itemId, ComponentActivity activity) - { - switch (itemId) - { - case R.id.menu_settings: - mView.launchSettingsActivity(MenuTag.SETTINGS); - return true; - - case R.id.menu_grid_options: - mView.showGridOptions(); - return true; - - case R.id.menu_refresh: - mView.setRefreshing(true); - GameFileCacheManager.startRescan(); - return true; - - case R.id.button_add_directory: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - mView::launchFileListActivity); - return true; - - case R.id.menu_open_file: - mView.launchOpenFileActivity(REQUEST_GAME_FILE); - return true; - - case R.id.menu_load_wii_system_menu: - launchWiiSystemMenu(); - return true; - - case R.id.menu_online_system_update: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - this::launchOnlineUpdate); - return true; - - case R.id.menu_install_wad: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_WAD_FILE)); - return true; - - case R.id.menu_import_wii_save: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE)); - return true; - - case R.id.menu_import_nand_backup: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE)); - return true; - - case R.id.menu_about: - showAboutDialog(); - } - - return false; - } - - public void onResume() - { - if (mDirToAdd != null) - { - GameFileCache.addGameFolder(mDirToAdd); - mDirToAdd = null; - } - - if (sShouldRescanLibrary) - { - GameFileCacheManager.startRescan(); - } - - sShouldRescanLibrary = true; - } - - /** - * Called when a selection is made using the legacy folder picker. - */ - public void onDirectorySelected(String dir) - { - mDirToAdd = dir; - } - - /** - * Called when a selection is made using the Storage Access Framework folder picker. - */ - public void onDirectorySelected(Intent result) - { - Uri uri = result.getData(); - - boolean recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - String[] childNames = ContentHandler.getChildNames(uri, recursive); - if (Arrays.stream(childNames).noneMatch((name) -> FileBrowserHelper.GAME_EXTENSIONS.contains( - FileBrowserHelper.getExtension(name, false)))) - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(mActivity.getString(R.string.wrong_file_extension_in_directory, - FileBrowserHelper.setToSortedDelimitedString( - FileBrowserHelper.GAME_EXTENSIONS))) - .setPositiveButton(R.string.ok, null) - .show(); - } - - ContentResolver contentResolver = mActivity.getContentResolver(); - Uri canonicalizedUri = contentResolver.canonicalize(uri); - if (canonicalizedUri != null) - uri = canonicalizedUri; - - int takeFlags = result.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION; - mActivity.getContentResolver().takePersistableUriPermission(uri, takeFlags); - - mDirToAdd = uri.toString(); - } - - public void installWAD(String path) - { - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, 0, () -> - { - boolean success = WiiUtils.installWAD(path); - int message = success ? R.string.wad_install_success : R.string.wad_install_failure; - return mActivity.getResources().getString(message); - }); - } - - public void importWiiSave(String path) - { - CompletableFuture canOverwriteFuture = new CompletableFuture<>(); - - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, 0, () -> - { - BooleanSupplier canOverwrite = () -> - { - mActivity.runOnUiThread(() -> - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(R.string.wii_save_exists) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, i) -> canOverwriteFuture.complete(true)) - .setNegativeButton(R.string.no, (dialog, i) -> canOverwriteFuture.complete(false)) - .show(); - }); - - try - { - return canOverwriteFuture.get(); - } - catch (ExecutionException | InterruptedException e) - { - // Shouldn't happen - throw new RuntimeException(e); - } - }; - - int result = WiiUtils.importWiiSave(path, canOverwrite); - - int message; - switch (result) - { - case WiiUtils.RESULT_SUCCESS: - message = R.string.wii_save_import_success; - break; - case WiiUtils.RESULT_CORRUPTED_SOURCE: - message = R.string.wii_save_import_corruped_source; - break; - case WiiUtils.RESULT_TITLE_MISSING: - message = R.string.wii_save_import_title_missing; - break; - case WiiUtils.RESULT_CANCELLED: - return null; - default: - message = R.string.wii_save_import_error; - break; - } - return mActivity.getResources().getString(message); - }); - } - - public void importNANDBin(String path) - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(R.string.nand_import_warning) - .setNegativeButton(R.string.no, (dialog, i) -> dialog.dismiss()) - .setPositiveButton(R.string.yes, (dialog, i) -> - { - dialog.dismiss(); - - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, - R.string.do_not_close_app, () -> - { - // ImportNANDBin unfortunately doesn't provide any result value... - // It does however show a panic alert if something goes wrong. - WiiUtils.importNANDBin(path); - return null; - }); - }) - .show(); - } - - public static void skipRescanningLibrary() - { - sShouldRescanLibrary = false; - } - - private void launchOnlineUpdate() - { - if (WiiUtils.isSystemMenuInstalled()) - { - SystemUpdateViewModel viewModel = - new ViewModelProvider(mActivity).get(SystemUpdateViewModel.class); - viewModel.setRegion(-1); - launchUpdateProgressBarFragment(mActivity); - } - else - { - SystemMenuNotInstalledDialogFragment dialogFragment = - new SystemMenuNotInstalledDialogFragment(); - dialogFragment - .show(mActivity.getSupportFragmentManager(), "SystemMenuNotInstalledDialogFragment"); - } - } - - public static void launchDiscUpdate(String path, FragmentActivity activity) - { - SystemUpdateViewModel viewModel = - new ViewModelProvider(activity).get(SystemUpdateViewModel.class); - viewModel.setDiscPath(path); - launchUpdateProgressBarFragment(activity); - } - - private static void launchUpdateProgressBarFragment(FragmentActivity activity) - { - SystemUpdateProgressBarDialogFragment progressBarFragment = - new SystemUpdateProgressBarDialogFragment(); - progressBarFragment - .show(activity.getSupportFragmentManager(), SystemUpdateProgressBarDialogFragment.TAG); - progressBarFragment.setCancelable(false); - } - - private void launchWiiSystemMenu() - { - new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, () -> - { - if (WiiUtils.isSystemMenuInstalled()) - { - EmulationActivity.launchSystemMenu(mActivity); - } - else - { - SystemMenuNotInstalledDialogFragment dialogFragment = - new SystemMenuNotInstalledDialogFragment(); - dialogFragment - .show(mActivity.getSupportFragmentManager(), - "SystemMenuNotInstalledDialogFragment"); - } - }); - } - - private void showAboutDialog() - { - new AboutDialogFragment().show(mActivity.getSupportFragmentManager(), AboutDialogFragment.TAG); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt new file mode 100644 index 0000000000..64695a0ace --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.DialogInterface +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.dolphinemu.dolphinemu.BuildConfig +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemMenuNotInstalledDialogFragment +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateProgressBarDialogFragment +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateViewModel +import org.dolphinemu.dolphinemu.fragments.AboutDialogFragment +import org.dolphinemu.dolphinemu.model.GameFileCache +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.BooleanSupplier +import org.dolphinemu.dolphinemu.utils.CompletableFuture +import org.dolphinemu.dolphinemu.utils.ContentHandler +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.ThreadUtil +import org.dolphinemu.dolphinemu.utils.WiiUtils +import java.util.Arrays +import java.util.concurrent.ExecutionException + +class MainPresenter(private val mainView: MainView, private val activity: FragmentActivity) { + private var dirToAdd: String? = null + + fun onCreate() { + // Ask the user to grant write permission if relevant and not already granted + if (DirectoryInitialization.isWaitingForWriteAccess(activity)) + PermissionsHandler.requestWritePermission(activity) + + val versionName = BuildConfig.VERSION_NAME + mainView.setVersionString(versionName) + + GameFileCacheManager.getGameFiles().observe(activity) { mainView.showGames() } + val refreshObserver = + Observer { _: Boolean? -> mainView.setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) } + GameFileCacheManager.isLoading().observe(activity, refreshObserver) + GameFileCacheManager.isRescanning().observe(activity, refreshObserver) + } + + fun onFabClick() { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { mainView.launchFileListActivity() } + } + + fun handleOptionSelection(itemId: Int, activity: ComponentActivity): Boolean = + when (itemId) { + R.id.menu_settings -> { + mainView.launchSettingsActivity(MenuTag.SETTINGS) + true + } + + R.id.menu_grid_options -> { + mainView.showGridOptions() + true + } + + R.id.menu_refresh -> { + mainView.setRefreshing(true) + GameFileCacheManager.startRescan() + true + } + + R.id.button_add_directory -> { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { mainView.launchFileListActivity() } + true + } + + R.id.menu_open_file -> { + mainView.launchOpenFileActivity(REQUEST_GAME_FILE) + true + } + + R.id.menu_load_wii_system_menu -> { + launchWiiSystemMenu() + true + } + + R.id.menu_online_system_update -> { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { launchOnlineUpdate() } + true + } + + R.id.menu_install_wad -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_WAD_FILE) } + true + } + + R.id.menu_import_wii_save -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE) } + true + } + + R.id.menu_import_nand_backup -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE) } + true + } + + R.id.menu_about -> { + showAboutDialog() + false + } + + else -> false + } + + fun onResume() { + if (dirToAdd != null) { + GameFileCache.addGameFolder(dirToAdd) + dirToAdd = null + } + + if (shouldRescanLibrary) { + GameFileCacheManager.startRescan() + } + + shouldRescanLibrary = true + } + + /** + * Called when a selection is made using the legacy folder picker. + */ + fun onDirectorySelected(dir: String?) { + dirToAdd = dir + } + + /** + * Called when a selection is made using the Storage Access Framework folder picker. + */ + fun onDirectorySelected(result: Intent) { + var uri = result.data!! + + val recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + val childNames = ContentHandler.getChildNames(uri, recursive) + if (Arrays.stream(childNames).noneMatch { + FileBrowserHelper.GAME_EXTENSIONS + .contains(FileBrowserHelper.getExtension(it, false)) + }) { + MaterialAlertDialogBuilder(activity) + .setMessage( + activity.getString( + R.string.wrong_file_extension_in_directory, + FileBrowserHelper.setToSortedDelimitedString(FileBrowserHelper.GAME_EXTENSIONS) + ) + ) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + val contentResolver = activity.contentResolver + val canonicalizedUri = contentResolver.canonicalize(uri) + if (canonicalizedUri != null) + uri = canonicalizedUri + + val takeFlags = result.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION + activity.contentResolver.takePersistableUriPermission(uri, takeFlags) + + dirToAdd = uri.toString() + } + + fun installWAD(path: String?) { + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + 0, + { + val success = WiiUtils.installWAD(path!!) + val message = + if (success) R.string.wad_install_success else R.string.wad_install_failure + activity.getString(message) + }) + } + + fun importWiiSave(path: String?) { + val canOverwriteFuture = CompletableFuture() + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + 0, + { + val canOverwrite = BooleanSupplier { + activity.runOnUiThread { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.wii_save_exists) + .setCancelable(false) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + canOverwriteFuture.complete(true) + } + .setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> + canOverwriteFuture.complete(false) + } + .show() + } + try { + return@BooleanSupplier canOverwriteFuture.get() + } catch (e: ExecutionException) { + // Shouldn't happen + throw RuntimeException(e) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + val message: Int = when (WiiUtils.importWiiSave(path!!, canOverwrite)) { + WiiUtils.RESULT_SUCCESS -> R.string.wii_save_import_success + WiiUtils.RESULT_CORRUPTED_SOURCE -> R.string.wii_save_import_corruped_source + WiiUtils.RESULT_TITLE_MISSING -> R.string.wii_save_import_title_missing + WiiUtils.RESULT_CANCELLED -> return@runOnThreadAndShowResult null + else -> R.string.wii_save_import_error + } + activity.resources.getString(message) + }) + } + + fun importNANDBin(path: String?) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.nand_import_warning) + .setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .setPositiveButton(R.string.yes) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + R.string.do_not_close_app, + { + // ImportNANDBin unfortunately doesn't provide any result value... + // It does however show a panic alert if something goes wrong. + WiiUtils.importNANDBin(path!!) + null + }) + } + .show() + } + + private fun launchOnlineUpdate() { + if (WiiUtils.isSystemMenuInstalled()) { + val viewModel = ViewModelProvider(activity)[SystemUpdateViewModel::class.java] + viewModel.region = -1 + launchUpdateProgressBarFragment(activity) + } else { + SystemMenuNotInstalledDialogFragment().show( + activity.supportFragmentManager, + SystemMenuNotInstalledDialogFragment.TAG + ) + } + } + + private fun launchWiiSystemMenu() { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { + if (WiiUtils.isSystemMenuInstalled()) { + EmulationActivity.launchSystemMenu(activity) + } else { + SystemMenuNotInstalledDialogFragment().show( + activity.supportFragmentManager, + SystemMenuNotInstalledDialogFragment.TAG + ) + } + } + } + + private fun showAboutDialog() { + AboutDialogFragment().show(activity.supportFragmentManager, AboutDialogFragment.TAG) + } + + companion object { + const val REQUEST_DIRECTORY = 1 + const val REQUEST_GAME_FILE = 2 + const val REQUEST_SD_FILE = 3 + const val REQUEST_WAD_FILE = 4 + const val REQUEST_WII_SAVE_FILE = 5 + const val REQUEST_NAND_BIN_FILE = 6 + const val REQUEST_GPU_DRIVER = 7 + + private var shouldRescanLibrary = true + + @JvmStatic + fun skipRescanningLibrary() { + shouldRescanLibrary = false + } + + @JvmStatic + fun launchDiscUpdate(path: String, activity: FragmentActivity) { + val viewModel = ViewModelProvider(activity)[SystemUpdateViewModel::class.java] + viewModel.discPath = path + launchUpdateProgressBarFragment(activity) + } + + private fun launchUpdateProgressBarFragment(activity: FragmentActivity) { + val progressBarFragment = SystemUpdateProgressBarDialogFragment() + progressBarFragment + .show(activity.supportFragmentManager, SystemUpdateProgressBarDialogFragment.TAG) + progressBarFragment.isCancelable = false + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java deleted file mode 100644 index 604cb5d1a8..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; - -/** - * Abstraction for the screen that shows on application launch. - * Implementations will differ primarily to target touch-screen - * or non-touch screen devices. - */ -public interface MainView -{ - /** - * Pass the view the native library's version string. Displaying - * it is optional. - * - * @param version A string pulled from native code. - */ - void setVersionString(String version); - - void launchSettingsActivity(MenuTag menuTag); - - void launchFileListActivity(); - - void launchOpenFileActivity(int requestCode); - - /** - * Shows or hides the loading indicator. - */ - void setRefreshing(boolean refreshing); - - /** - * To be called when the game file cache is updated. - */ - void showGames(); - - void reloadGrid(); - - void showGridOptions(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt new file mode 100644 index 0000000000..3892ee7599 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag + +/** + * Abstraction for the screen that shows on application launch. + * Implementations will differ primarily to target touch-screen + * or non-touch screen devices. + */ +interface MainView { + /** + * Pass the view the native library's version string. Displaying + * it is optional. + * + * @param version A string pulled from native code. + */ + fun setVersionString(version: String) + + fun launchSettingsActivity(menuTag: MenuTag?) + + fun launchFileListActivity() + + fun launchOpenFileActivity(requestCode: Int) + + /** + * Shows or hides the loading indicator. + */ + fun setRefreshing(refreshing: Boolean) + + /** + * To be called when the game file cache is updated. + */ + fun showGames() + + fun reloadGrid() + + fun showGridOptions() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java deleted file mode 100644 index 1be137f33e..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.dolphinemu.dolphinemu.ui.main; - -public interface ThemeProvider -{ - /** - * Provides theme ID by overriding an activity's 'setTheme' method and returning that result - */ - int getThemeId(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt new file mode 100644 index 0000000000..b49011a987 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +interface ThemeProvider { + /** + * Provides theme ID by overriding an activity's 'setTheme' method and returning that result + */ + val themeId: Int +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java deleted file mode 100644 index 2da1b8591d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ /dev/null @@ -1,418 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.core.splashscreen.SplashScreen; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.leanback.app.BrowseSupportFragment; -import androidx.leanback.widget.ArrayObjectAdapter; -import androidx.leanback.widget.HeaderItem; -import androidx.leanback.widget.ListRow; -import androidx.leanback.widget.ListRowPresenter; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.adapters.GameRowPresenter; -import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter; -import org.dolphinemu.dolphinemu.databinding.ActivityTvMainBinding; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.model.TvSettingsItem; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.StartupHandler; -import org.dolphinemu.dolphinemu.utils.TvUtil; -import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder; - -import java.util.ArrayList; -import java.util.Collection; - -public final class TvMainActivity extends FragmentActivity - implements MainView, SwipeRefreshLayout.OnRefreshListener -{ - private final MainPresenter mPresenter = new MainPresenter(this, this); - - private SwipeRefreshLayout mSwipeRefresh; - - private BrowseSupportFragment mBrowseFragment; - - private final ArrayList mGameRows = new ArrayList<>(); - - private ActivityTvMainBinding mBinding; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - SplashScreen splashScreen = SplashScreen.installSplashScreen(this); - splashScreen.setKeepOnScreenCondition( - () -> !DirectoryInitialization.areDolphinDirectoriesReady()); - - super.onCreate(savedInstanceState); - mBinding = ActivityTvMainBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - setupUI(); - - mPresenter.onCreate(); - - // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) - if (savedInstanceState == null) - { - StartupHandler.HandleInit(this); - } - } - - @Override - protected void onResume() - { - super.onResume(); - - if (DirectoryInitialization.shouldStart(this)) - { - DirectoryInitialization.start(this); - GameFileCacheManager.startLoad(); - } - - mPresenter.onResume(); - } - - @Override - protected void onDestroy() - { - super.onDestroy(); - mPresenter.onDestroy(); - } - - @Override - protected void onStart() - { - super.onStart(); - StartupHandler.checkSessionReset(this); - } - - @Override - protected void onStop() - { - super.onStop(); - - if (isChangingConfigurations()) - { - MainPresenter.skipRescanningLibrary(); - } - - StartupHandler.setSessionTime(this); - } - - void setupUI() - { - mSwipeRefresh = mBinding.swipeRefresh; - - mSwipeRefresh.setOnRefreshListener(this); - - setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - - final FragmentManager fragmentManager = getSupportFragmentManager(); - mBrowseFragment = new BrowseSupportFragment(); - fragmentManager - .beginTransaction() - .add(R.id.content, mBrowseFragment, "BrowseFragment") - .commit(); - - // Set display parameters for the BrowseFragment - mBrowseFragment.setHeadersState(BrowseSupportFragment.HEADERS_ENABLED); - mBrowseFragment.setBrandColor(ContextCompat.getColor(this, R.color.dolphin_blue)); - buildRowsAdapter(); - - mBrowseFragment.setOnItemViewClickedListener( - (itemViewHolder, item, rowViewHolder, row) -> - { - // Special case: user clicked on a settings row item. - if (item instanceof TvSettingsItem) - { - TvSettingsItem settingsItem = (TvSettingsItem) item; - mPresenter.handleOptionSelection(settingsItem.getItemId(), this); - } - else - { - TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder; - - // Start the emulation activity and send the path of the clicked ISO to it. - String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile); - EmulationActivity.launch(TvMainActivity.this, paths, false); - } - }); - } - - /** - * MainView - */ - - @Override - public void setVersionString(String version) - { - mBrowseFragment.setTitle(version); - } - - @Override - public void launchSettingsActivity(MenuTag menuTag) - { - SettingsActivity.launch(this, menuTag); - } - - @Override - public void launchFileListActivity() - { - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); - } - else - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); - } - } - - @Override - public void launchOpenFileActivity(int requestCode) - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, requestCode); - } - - /** - * Shows or hides the loading indicator. - */ - @Override - public void setRefreshing(boolean refreshing) - { - mSwipeRefresh.setRefreshing(refreshing); - } - - @Override - public void showGames() - { - // Kicks off the program services to update all channels - TvUtil.updateAllChannels(getApplicationContext()); - - buildRowsAdapter(); - } - - @Override - public void reloadGrid() - { - for (ArrayObjectAdapter row : mGameRows) - { - row.notifyArrayItemRangeChanged(0, row.size()); - } - } - - @Override - public void showGridOptions() - { - new GridOptionDialogFragment().show(getSupportFragmentManager(), "gridOptions"); - } - - /** - * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity. - * - * @param requestCode An int describing whether the Activity that is returning did so successfully. - * @param resultCode An int describing what Activity is giving us this callback. - * @param result The information the returning Activity is providing us. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) - { - super.onActivityResult(requestCode, resultCode, result); - - // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) - { - Uri uri = result.getData(); - switch (requestCode) - { - case MainPresenter.REQUEST_DIRECTORY: - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); - } - else - { - mPresenter.onDirectorySelected(result); - } - break; - - case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, - FileBrowserHelper.GAME_LIKE_EXTENSIONS, - () -> EmulationActivity.launch(this, result.getData().toString(), false)); - break; - - case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, - () -> mPresenter.installWAD(result.getData().toString())); - break; - - case MainPresenter.REQUEST_WII_SAVE_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importWiiSave(result.getData().toString())); - break; - - case MainPresenter.REQUEST_NAND_BIN_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importNANDBin(result.getData().toString())); - break; - } - } - else - { - MainPresenter.skipRescanningLibrary(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) - { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) - { - if (grantResults[0] == PackageManager.PERMISSION_DENIED) - { - PermissionsHandler.setWritePermissionDenied(); - } - - DirectoryInitialization.start(this); - GameFileCacheManager.startLoad(); - } - } - - /** - * Called when the user requests a refresh by swiping down. - */ - @Override - public void onRefresh() - { - setRefreshing(true); - GameFileCacheManager.startRescan(); - } - - private void buildRowsAdapter() - { - ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - mGameRows.clear(); - - if (!DirectoryInitialization.isWaitingForWriteAccess(this)) - { - GameFileCacheManager.startLoad(); - } - - for (Platform platform : Platform.values()) - { - ListRow row = buildGamesRow(platform, GameFileCacheManager.getGameFilesForPlatform(platform)); - - // Add row to the adapter only if it is not empty. - if (row != null) - { - rowsAdapter.add(row); - } - } - - rowsAdapter.add(buildSettingsRow()); - - mBrowseFragment.setAdapter(rowsAdapter); - } - - private ListRow buildGamesRow(Platform platform, Collection gameFiles) - { - // If there are no games, don't return a Row. - if (gameFiles.size() == 0) - { - return null; - } - - // Create an adapter for this row. - ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter(this)); - row.addAll(0, gameFiles); - - // Keep a reference to the row in case we need to refresh it. - mGameRows.add(row); - - // Create a header for this row. - HeaderItem header = new HeaderItem(platform.toInt(), getString(platform.getHeaderName())); - - // Create the row, passing it the filled adapter and the header, and give it to the master adapter. - return new ListRow(header, row); - } - - private ListRow buildSettingsRow() - { - ArrayObjectAdapter rowItems = new ArrayObjectAdapter(new SettingsRowPresenter()); - - rowItems.add(new TvSettingsItem(R.id.menu_settings, - R.drawable.ic_settings_tv, - R.string.grid_menu_settings)); - - rowItems.add(new TvSettingsItem(R.id.button_add_directory, - R.drawable.ic_add_tv, - R.string.add_directory_title)); - - rowItems.add(new TvSettingsItem(R.id.menu_grid_options, - R.drawable.ic_list_tv, - R.string.grid_menu_grid_options)); - - rowItems.add(new TvSettingsItem(R.id.menu_refresh, - R.drawable.ic_refresh_tv, - R.string.grid_menu_refresh)); - - rowItems.add(new TvSettingsItem(R.id.menu_open_file, - R.drawable.ic_play_tv, - R.string.grid_menu_open_file)); - - rowItems.add(new TvSettingsItem(R.id.menu_install_wad, - R.drawable.ic_folder_tv, - R.string.grid_menu_install_wad)); - - rowItems.add(new TvSettingsItem(R.id.menu_load_wii_system_menu, - R.drawable.ic_folder_tv, - R.string.grid_menu_load_wii_system_menu)); - - rowItems.add(new TvSettingsItem(R.id.menu_import_wii_save, - R.drawable.ic_folder_tv, - R.string.grid_menu_import_wii_save)); - - rowItems.add(new TvSettingsItem(R.id.menu_import_nand_backup, - R.drawable.ic_folder_tv, - R.string.grid_menu_import_nand_backup)); - - rowItems.add(new TvSettingsItem(R.id.menu_online_system_update, - R.drawable.ic_folder_tv, - R.string.grid_menu_online_system_update)); - - rowItems.add(new TvSettingsItem(R.id.menu_about, - R.drawable.ic_info_tv, - R.string.grid_menu_about)); - - // Create a header for this row. - HeaderItem header = new HeaderItem(R.string.settings, getString(R.string.settings)); - - return new ListRow(header, rowItems); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt new file mode 100644 index 0000000000..79ec2f2943 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.fragment.app.FragmentActivity +import androidx.leanback.app.BrowseSupportFragment +import androidx.leanback.widget.ArrayObjectAdapter +import androidx.leanback.widget.HeaderItem +import androidx.leanback.widget.ListRow +import androidx.leanback.widget.ListRowPresenter +import androidx.leanback.widget.OnItemViewClickedListener +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.Row +import androidx.leanback.widget.RowPresenter +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.adapters.GameRowPresenter +import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter +import org.dolphinemu.dolphinemu.databinding.ActivityTvMainBinding +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity +import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.model.TvSettingsItem +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.StartupHandler +import org.dolphinemu.dolphinemu.utils.TvUtil +import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder + +class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { + private val presenter = MainPresenter(this, this) + + private var swipeRefresh: SwipeRefreshLayout? = null + + private var browseFragment: BrowseSupportFragment? = null + + private val gameRows = ArrayList() + + private lateinit var binding: ActivityTvMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { !DirectoryInitialization.areDolphinDirectoriesReady() } + + super.onCreate(savedInstanceState) + binding = ActivityTvMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupUI() + + presenter.onCreate() + + // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) + if (savedInstanceState == null) { + StartupHandler.HandleInit(this) + } + } + + override fun onResume() { + super.onResume() + if (DirectoryInitialization.shouldStart(this)) { + DirectoryInitialization.start(this) + GameFileCacheManager.startLoad() + } + + presenter.onResume() + } + + override fun onStart() { + super.onStart() + StartupHandler.checkSessionReset(this) + } + + override fun onStop() { + super.onStop() + if (isChangingConfigurations) { + MainPresenter.skipRescanningLibrary() + } + StartupHandler.setSessionTime(this) + } + + private fun setupUI() { + swipeRefresh = binding.swipeRefresh + swipeRefresh!!.setOnRefreshListener(this) + setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) + + browseFragment = BrowseSupportFragment() + supportFragmentManager + .beginTransaction() + .add(R.id.content, browseFragment!!, "BrowseFragment") + .commit() + + // Set display parameters for the BrowseFragment + browseFragment?.headersState = BrowseSupportFragment.HEADERS_ENABLED + browseFragment?.brandColor = ContextCompat.getColor(this, R.color.dolphin_blue) + buildRowsAdapter() + + browseFragment?.onItemViewClickedListener = + OnItemViewClickedListener { itemViewHolder: Presenter.ViewHolder, item: Any?, _: RowPresenter.ViewHolder?, _: Row? -> + // Special case: user clicked on a settings row item. + if (item is TvSettingsItem) { + presenter.handleOptionSelection(item.itemId, this) + } else { + val holder = itemViewHolder as TvGameViewHolder + + // Start the emulation activity and send the path of the clicked ISO to it. + val paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile) + EmulationActivity.launch(this@TvMainActivity, paths, false) + } + } + } + + /** + * MainView + */ + override fun setVersionString(version: String) { + browseFragment?.title = version + } + + override fun launchSettingsActivity(menuTag: MenuTag?) { + SettingsActivity.launch(this, menuTag) + } + + override fun launchFileListActivity() { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS) + } else { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY) + } + } + + override fun launchOpenFileActivity(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + startActivityForResult(intent, requestCode) + } + + /** + * Shows or hides the loading indicator. + */ + override fun setRefreshing(refreshing: Boolean) { + swipeRefresh?.isRefreshing = refreshing + } + + override fun showGames() { + // Kicks off the program services to update all channels + TvUtil.updateAllChannels(applicationContext) + + buildRowsAdapter() + } + + override fun reloadGrid() { + for (row in gameRows) { + row.notifyArrayItemRangeChanged(0, row.size()) + } + } + + override fun showGridOptions() { + GridOptionDialogFragment().show(supportFragmentManager, "gridOptions") + } + + /** + * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity. + * + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + super.onActivityResult(requestCode, resultCode, result) + + // If the user picked a file, as opposed to just backing out. + if (resultCode == RESULT_OK) { + val uri = result!!.data + when (requestCode) { + MainPresenter.REQUEST_DIRECTORY -> { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + presenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)) + } else { + presenter.onDirectorySelected(result) + } + } + + MainPresenter.REQUEST_GAME_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.GAME_LIKE_EXTENSIONS + ) { EmulationActivity.launch(this, result.data.toString(), false) } + + MainPresenter.REQUEST_WAD_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.WAD_EXTENSION + ) { presenter.installWAD(result.data.toString()) } + + MainPresenter.REQUEST_WII_SAVE_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importWiiSave(result.data.toString()) } + + MainPresenter.REQUEST_NAND_BIN_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importNANDBin(result.data.toString()) } + } + } else { + MainPresenter.skipRescanningLibrary() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + PermissionsHandler.setWritePermissionDenied() + } + + DirectoryInitialization.start(this) + GameFileCacheManager.startLoad() + } + } + + /** + * Called when the user requests a refresh by swiping down. + */ + override fun onRefresh() { + setRefreshing(true) + GameFileCacheManager.startRescan() + } + + private fun buildRowsAdapter() { + val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) + gameRows.clear() + + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { + GameFileCacheManager.startLoad() + } + + for (platform in Platform.values()) { + val row = + buildGamesRow(platform, GameFileCacheManager.getGameFilesForPlatform(platform)) + + // Add row to the adapter only if it is not empty. + if (row != null) { + rowsAdapter.add(row) + } + } + + rowsAdapter.add(buildSettingsRow()) + + browseFragment!!.adapter = rowsAdapter + } + + private fun buildGamesRow(platform: Platform, gameFiles: Collection): ListRow? { + // If there are no games, don't return a Row. + if (gameFiles.isEmpty()) { + return null + } + + // Create an adapter for this row. + val row = ArrayObjectAdapter(GameRowPresenter(this)) + row.addAll(0, gameFiles) + + // Keep a reference to the row in case we need to refresh it. + gameRows.add(row) + + // Create a header for this row. + val header = HeaderItem(platform.toInt().toLong(), getString(platform.headerName)) + + // Create the row, passing it the filled adapter and the header, and give it to the master adapter. + return ListRow(header, row) + } + + private fun buildSettingsRow(): ListRow { + val rowItems = ArrayObjectAdapter(SettingsRowPresenter()) + rowItems.apply { + add( + TvSettingsItem( + R.id.menu_settings, + R.drawable.ic_settings_tv, + R.string.grid_menu_settings + ) + ) + add( + TvSettingsItem( + R.id.button_add_directory, + R.drawable.ic_add_tv, + R.string.add_directory_title + ) + ) + add( + TvSettingsItem( + R.id.menu_grid_options, + R.drawable.ic_list_tv, + R.string.grid_menu_grid_options + ) + ) + add( + TvSettingsItem( + R.id.menu_refresh, + R.drawable.ic_refresh_tv, + R.string.grid_menu_refresh + ) + ) + add( + TvSettingsItem( + R.id.menu_open_file, + R.drawable.ic_play_tv, + R.string.grid_menu_open_file + ) + ) + add( + TvSettingsItem( + R.id.menu_install_wad, + R.drawable.ic_folder_tv, + R.string.grid_menu_install_wad + ) + ) + add( + TvSettingsItem( + R.id.menu_load_wii_system_menu, + R.drawable.ic_folder_tv, + R.string.grid_menu_load_wii_system_menu + ) + ) + add( + TvSettingsItem( + R.id.menu_import_wii_save, + R.drawable.ic_folder_tv, + R.string.grid_menu_import_wii_save + ) + ) + add( + TvSettingsItem( + R.id.menu_import_nand_backup, + R.drawable.ic_folder_tv, + R.string.grid_menu_import_nand_backup + ) + ) + add( + TvSettingsItem( + R.id.menu_online_system_update, + R.drawable.ic_folder_tv, + R.string.grid_menu_online_system_update + ) + ) + add( + TvSettingsItem( + R.id.menu_about, + R.drawable.ic_info_tv, + R.string.grid_menu_about + ) + ) + } + + // Create a header for this row. + val header = HeaderItem(R.string.settings.toLong(), getString(R.string.settings)) + return ListRow(header, rowItems) + } +} \ No newline at end of file diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java deleted file mode 100644 index f561feb250..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -import org.dolphinemu.dolphinemu.R; - -/** - * Enum to represent platform (eg GameCube, Wii). - */ -public enum Platform -{ - GAMECUBE(0, R.string.platform_gamecube, "GameCube Games"), - WII(1, R.string.platform_wii, "Wii Games"), - WIIWARE(2, R.string.platform_wiiware, "WiiWare Games"); - - private final int value; - private final int headerName; - private final String idString; - - Platform(int value, int headerName, String idString) - { - this.value = value; - this.headerName = headerName; - this.idString = idString; - } - - public static Platform fromInt(int i) - { - return values()[i]; - } - - public static Platform fromNativeInt(int i) - { - // TODO: Proper support for DOL and ELF files - boolean in_range = i >= 0 && i < values().length; - return values()[in_range ? i : WIIWARE.value]; - } - - public static Platform fromPosition(int position) - { - return values()[position]; - } - - public int toInt() - { - return value; - } - - public int getHeaderName() - { - return headerName; - } - - public String getIdString() - { - return idString; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt new file mode 100644 index 0000000000..0bf3e0ea68 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +import org.dolphinemu.dolphinemu.R + +/** + * Enum to represent platform (eg GameCube, Wii). + */ +enum class Platform(private val value: Int, val headerName: Int, val idString: String) { + GAMECUBE(0, R.string.platform_gamecube, "GameCube Games"), + WII(1, R.string.platform_wii, "Wii Games"), + WIIWARE(2, R.string.platform_wiiware, "WiiWare Games"); + + fun toInt(): Int { + return value + } + + companion object { + fun fromInt(i: Int): Platform { + return values()[i] + } + + @JvmStatic + fun fromNativeInt(i: Int): Platform { + // TODO: Proper support for DOL and ELF files + val inRange = i >= 0 && i < values().size + return values()[if (inRange) i else WIIWARE.value] + } + + @JvmStatic + fun fromPosition(position: Int): Platform { + return values()[position] + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java deleted file mode 100644 index 9393a48baf..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -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.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.color.MaterialColors; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.adapters.GameAdapter; -import org.dolphinemu.dolphinemu.databinding.FragmentGridBinding; -import org.dolphinemu.dolphinemu.layout.AutofitGridLayoutManager; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; - -public final class PlatformGamesFragment extends Fragment implements PlatformGamesView -{ - private static final String ARG_PLATFORM = "platform"; - - private SwipeRefreshLayout mSwipeRefresh; - private SwipeRefreshLayout.OnRefreshListener mOnRefreshListener; - - private FragmentGridBinding mBinding; - - public static PlatformGamesFragment newInstance(Platform platform) - { - PlatformGamesFragment fragment = new PlatformGamesFragment(); - - Bundle args = new Bundle(); - args.putSerializable(ARG_PLATFORM, platform); - - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentGridBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) - { - mSwipeRefresh = mBinding.swipeRefresh; - GameAdapter adapter = new GameAdapter(requireActivity()); - adapter.setStateRestorationPolicy( - RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); - mBinding.gridGames.setAdapter(adapter); - mBinding.gridGames.setLayoutManager(new AutofitGridLayoutManager(requireContext(), - getResources().getDimensionPixelSize(R.dimen.card_width))); - - // Set theme color to the refresh animation's background - mSwipeRefresh.setProgressBackgroundColorSchemeColor( - MaterialColors.getColor(mSwipeRefresh, R.attr.colorPrimary)); - mSwipeRefresh.setColorSchemeColors( - MaterialColors.getColor(mSwipeRefresh, R.attr.colorOnPrimary)); - - mSwipeRefresh.setOnRefreshListener(mOnRefreshListener); - - setInsets(); - - setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - - showGames(); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onItemClick(String gameId) - { - // No-op for now - } - - @Override - public void showGames() - { - if (mBinding == null) - return; - - if (mBinding.gridGames.getAdapter() != null) - { - Platform platform = (Platform) getArguments().getSerializable(ARG_PLATFORM); - ((GameAdapter) mBinding.gridGames.getAdapter()).swapDataSet( - GameFileCacheManager.getGameFilesForPlatform(platform)); - } - } - - @Override - public void refetchMetadata() - { - ((GameAdapter) mBinding.gridGames.getAdapter()).refetchMetadata(); - } - - public void setOnRefreshListener(@Nullable SwipeRefreshLayout.OnRefreshListener listener) - { - mOnRefreshListener = listener; - - if (mSwipeRefresh != null) - { - mSwipeRefresh.setOnRefreshListener(listener); - } - } - - public void setRefreshing(boolean refreshing) - { - mBinding.swipeRefresh.setRefreshing(refreshing); - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.gridGames, (v, windowInsets) -> - { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(0, 0, 0, - insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_list) + - getResources().getDimensionPixelSize(R.dimen.spacing_fab)); - return windowInsets; - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt new file mode 100644 index 0000000000..5f1139da8a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.color.MaterialColors +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.adapters.GameAdapter +import org.dolphinemu.dolphinemu.databinding.FragmentGridBinding +import org.dolphinemu.dolphinemu.layout.AutofitGridLayoutManager +import org.dolphinemu.dolphinemu.services.GameFileCacheManager + +class PlatformGamesFragment : Fragment(), PlatformGamesView { + private var swipeRefresh: SwipeRefreshLayout? = null + private var onRefreshListener: OnRefreshListener? = null + + private var _binding: FragmentGridBinding? = null + private val binding: FragmentGridBinding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentGridBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + swipeRefresh = binding.swipeRefresh + val gameAdapter = GameAdapter(requireActivity()) + gameAdapter.stateRestorationPolicy = + RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + + binding.gridGames.apply { + adapter = gameAdapter + layoutManager = AutofitGridLayoutManager( + requireContext(), + resources.getDimensionPixelSize(R.dimen.card_width) + ) + } + + // Set theme color to the refresh animation's background + binding.swipeRefresh.apply { + setProgressBackgroundColorSchemeColor( + MaterialColors.getColor(swipeRefresh!!, R.attr.colorPrimary) + ) + setColorSchemeColors(MaterialColors.getColor(swipeRefresh!!, R.attr.colorOnPrimary)) + setOnRefreshListener(onRefreshListener) + } + + setInsets() + setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) + showGames() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun showGames() { + if (_binding == null) + return + + if (binding.gridGames.adapter != null) { + val platform = requireArguments().getSerializable(ARG_PLATFORM) as Platform + (binding.gridGames.adapter as GameAdapter?)!!.swapDataSet( + GameFileCacheManager.getGameFilesForPlatform(platform) + ) + } + } + + override fun refetchMetadata() { + (binding.gridGames.adapter as GameAdapter).refetchMetadata() + } + + fun setOnRefreshListener(listener: OnRefreshListener?) { + onRefreshListener = listener + swipeRefresh?.setOnRefreshListener(listener) + } + + override fun setRefreshing(refreshing: Boolean) { + binding.swipeRefresh.isRefreshing = refreshing + } + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { v: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding( + 0, + 0, + 0, + insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_list) + + resources.getDimensionPixelSize(R.dimen.spacing_fab) + ) + windowInsets + } + } + + companion object { + private const val ARG_PLATFORM = "platform" + + @JvmStatic + fun newInstance(platform: Platform?): PlatformGamesFragment { + val fragment = PlatformGamesFragment() + val args = Bundle() + args.putSerializable(ARG_PLATFORM, platform) + fragment.arguments = args + return fragment + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java deleted file mode 100644 index 642c531946..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -/** - * Abstraction for a screen representing a single platform's games. - */ -public interface PlatformGamesView -{ - /** - * Pass a click event to the view's Presenter. Typically called from the - * view's list adapter. - * - * @param gameId The ID of the game that was clicked. - */ - void onItemClick(String gameId); - - /** - * Shows or hides the loading indicator. - */ - void setRefreshing(boolean refreshing); - - /** - * To be called when the game file cache is updated. - */ - void showGames(); - - /** - * Re-fetches game metadata from the game file cache. - */ - void refetchMetadata(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt new file mode 100644 index 0000000000..b9d188c2e9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +/** + * Abstraction for a screen representing a single platform's games. + */ +interface PlatformGamesView { + /** + * Pass a click event to the view's Presenter. Typically called from the + * view's list adapter. + * + * @param gameId The ID of the game that was clicked. + */ + fun onItemClick(gameId: String) { /* Empty default impl */ } + + /** + * Shows or hides the loading indicator. + */ + fun setRefreshing(refreshing: Boolean) + + /** + * To be called when the game file cache is updated. + */ + fun showGames() + + /** + * Re-fetches game metadata from the game file cache. + */ + fun refetchMetadata() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.kt index c8aa483d58..5f3d2fc3cb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.kt @@ -20,7 +20,7 @@ object Analytics { @JvmStatic fun checkAnalyticsInit(activity: FragmentActivity) { - AfterDirectoryInitializationRunner().runWithoutLifecycle { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { if (!BooleanSetting.MAIN_ANALYTICS_PERMISSION_ASKED.boolean) { AnalyticsDialog().show(activity.supportFragmentManager, AnalyticsDialog.TAG) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java index f2bb93d476..15d226afd4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java @@ -18,18 +18,17 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.preference.PreferenceManager; -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import org.dolphinemu.dolphinemu.NativeLibrary; +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; + /** * A class that spawns its own thread in order perform initialization. * @@ -46,6 +45,7 @@ public final class DirectoryInitialization private static volatile boolean areDirectoriesAvailable = false; private static String userPath; private static String sysPath; + private static String driverPath; private static boolean isUsingLegacyUserDirectory = false; public enum DirectoryInitializationState @@ -88,8 +88,7 @@ public final class DirectoryInitialization directoryState.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED); } - @Nullable - private static File getLegacyUserDirectoryPath() + @Nullable private static File getLegacyUserDirectoryPath() { File externalPath = Environment.getExternalStorageDirectory(); if (externalPath == null) @@ -98,8 +97,7 @@ public final class DirectoryInitialization return new File(externalPath, "dolphin-emu"); } - @Nullable - public static File getUserDirectoryPath(Context context) + @Nullable public static File getUserDirectoryPath(Context context) { if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) return null; @@ -107,8 +105,8 @@ public final class DirectoryInitialization isUsingLegacyUserDirectory = preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context); - return isUsingLegacyUserDirectory ? - getLegacyUserDirectoryPath() : context.getExternalFilesDir(null); + return isUsingLegacyUserDirectory ? getLegacyUserDirectoryPath() : + context.getExternalFilesDir(null); } private static boolean setDolphinUserDirectory(Context context) @@ -124,7 +122,12 @@ public final class DirectoryInitialization File cacheDir = context.getExternalCacheDir(); if (cacheDir == null) - return false; + { + // In some custom ROMs getExternalCacheDir might return null for some reasons. If that is the case, fallback to getCacheDir which seems to work just fine. + cacheDir = context.getCacheDir(); + if (cacheDir == null) + return false; + } Log.debug("[DirectoryInitialization] Cache Dir: " + cacheDir.getPath()); NativeLibrary.SetCacheDirectory(cacheDir.getPath()); @@ -153,6 +156,19 @@ public final class DirectoryInitialization // Let the native code know where the Sys directory is. sysPath = sysDirectory.getPath(); SetSysDirectory(sysPath); + + File driverDirectory = new File(context.getFilesDir(), "GPUDrivers"); + driverDirectory.mkdirs(); + File driverExtractedDir = new File(driverDirectory, "Extracted"); + driverExtractedDir.mkdirs(); + File driverTmpDir = new File(driverDirectory, "Tmp"); + driverTmpDir.mkdirs(); + File driverFileRedirectDir = new File(driverDirectory, "FileRedirect"); + driverFileRedirectDir.mkdirs(); + + SetGpuDriverDirectories(driverDirectory.getPath(), + context.getApplicationInfo().nativeLibraryDir); + DirectoryInitialization.driverPath = driverExtractedDir.getAbsolutePath(); } private static void deleteDirectoryRecursively(@NonNull final File file) @@ -213,9 +229,19 @@ public final class DirectoryInitialization return sysPath; } + public static String getExtractedDriverDirectory() + { + if (!areDirectoriesAvailable) + { + throw new IllegalStateException( + "DirectoryInitialization must run before accessing the driver directory!"); + } + return driverPath; + } + public static File getGameListCache(Context context) { - return new File(context.getExternalCacheDir(), "gamelist.cache"); + return new File(NativeLibrary.GetCacheDirectory(), "gamelist.cache"); } private static boolean copyAsset(String asset, File output, Context context) @@ -235,16 +261,14 @@ public final class DirectoryInitialization } catch (IOException e) { - Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + - e.getMessage()); + Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + e.getMessage()); } return false; } private static void copyAssetFolder(String assetFolder, File outputFolder, Context context) { - Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + - outputFolder); + Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + outputFolder); try { @@ -267,8 +291,7 @@ public final class DirectoryInitialization } createdFolder = true; } - copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), - context); + copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), context); copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), context); } } @@ -340,8 +363,8 @@ public final class DirectoryInitialization private static boolean preferLegacyUserDirectory(Context context) { return PermissionsHandler.isExternalStorageLegacy() && - !PermissionsHandler.isWritePermissionDenied() && - isExternalFilesDirEmpty(context) && legacyUserDirectoryExists(); + !PermissionsHandler.isWritePermissionDenied() && isExternalFilesDirEmpty(context) && + legacyUserDirectoryExists(); } public static boolean isUsingLegacyUserDirectory() @@ -389,4 +412,6 @@ public final class DirectoryInitialization } private static native void SetSysDirectory(String path); + + private static native void SetGpuDriverDirectories(String path, String libPath); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GpuDriverHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GpuDriverHelper.kt new file mode 100644 index 0000000000..9de5378e57 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GpuDriverHelper.kt @@ -0,0 +1,148 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Partially based on: +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + +// Partially based on: +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.content.Context +import android.os.Build +import kotlinx.serialization.SerializationException +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.model.GpuDriverMetadata +import java.io.File +import java.io.InputStream + +private const val GPU_DRIVER_META_FILE = "meta.json" + +interface GpuDriverHelper { + companion object { + /** + * Returns information about the system GPU driver. + * @return An array containing the driver vendor and the driver version, in this order, or `null` if an error occurred + */ + private external fun getSystemDriverInfo(): Array? + + /** + * Queries the driver for custom driver loading support. + * @return `true` if the device supports loading custom drivers, `false` otherwise + */ + external fun supportsCustomDriverLoading(): Boolean + + /** + * Queries the driver for manual max clock forcing support + */ + external fun supportsForceMaxGpuClocks(): Boolean + + /** + * Calls into the driver to force the GPU to run at the maximum possible clock speed + * @param force Whether to enable or disable the forced clocks + */ + external fun forceMaxGpuClocks(enable: Boolean) + + /** + * Uninstalls the currently installed custom driver + */ + fun uninstallDriver() { + File(DirectoryInitialization.getExtractedDriverDirectory()) + .deleteRecursively() + + File(DirectoryInitialization.getExtractedDriverDirectory()).mkdir() + } + + fun getInstalledDriverMetadata(): GpuDriverMetadata? { + val metadataFile = File( + DirectoryInitialization.getExtractedDriverDirectory(), + GPU_DRIVER_META_FILE + ) + if (!metadataFile.exists()) { + return null; + } + + return try { + GpuDriverMetadata.deserialize(metadataFile) + } catch (e: SerializationException) { + null + } + } + + /** + * Fetches metadata about the system driver. + * @return A [GpuDriverMetadata] object containing data about the system driver + */ + fun getSystemDriverMetadata(context: Context): GpuDriverMetadata? { + val systemDriverInfo = getSystemDriverInfo() + if (systemDriverInfo.isNullOrEmpty()) { + return null; + } + + return GpuDriverMetadata( + name = context.getString(R.string.system_driver), + author = "", + packageVersion = "", + vendor = systemDriverInfo[0], + driverVersion = systemDriverInfo[1], + minApi = 0, + description = context.getString(R.string.system_driver_desc), + libraryName = "" + ) + } + + /** + * Installs the given driver to the emulator's drivers directory. + * @param stream InputStream of a driver package + * @return The exit status of the installation process + */ + fun installDriver(stream: InputStream): GpuDriverInstallResult { + uninstallDriver() + + val driverDir = File(DirectoryInitialization.getExtractedDriverDirectory()) + try { + ZipUtils.unzip(stream, driverDir) + } catch (e: Exception) { + e.printStackTrace() + uninstallDriver() + return GpuDriverInstallResult.InvalidArchive + } + + // Check that the metadata file exists + val metadataFile = File(driverDir, GPU_DRIVER_META_FILE) + if (!metadataFile.isFile) { + uninstallDriver() + return GpuDriverInstallResult.MissingMetadata + } + + // Check that the driver metadata is valid + val driverMetadata = try { + GpuDriverMetadata.deserialize(metadataFile) + } catch (e: SerializationException) { + uninstallDriver() + return GpuDriverInstallResult.InvalidMetadata + } + + // Check that the device satisfies the driver's minimum Android version requirements + if (Build.VERSION.SDK_INT < driverMetadata.minApi) { + uninstallDriver() + return GpuDriverInstallResult.UnsupportedAndroidVersion + } + + return GpuDriverInstallResult.Success + } + } +} + +enum class GpuDriverInstallResult { + Success, + InvalidArchive, + MissingMetadata, + InvalidMetadata, + UnsupportedAndroidVersion, + AlreadyInstalled, + FileNotFound +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ZipUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ZipUtils.kt new file mode 100644 index 0000000000..8ca09fb868 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ZipUtils.kt @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package org.dolphinemu.dolphinemu.utils + +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream + +interface ZipUtils { + companion object { + /** + * Extracts a zip file to the given target directory. + * @exception IOException if unzipping fails for any reason + */ + @Throws(IOException::class) + fun unzip(file : File, targetDirectory : File) { + ZipFile(file).use { zipFile -> + for (zipEntry in zipFile.entries()) { + val destFile = createNewFile(targetDirectory, zipEntry) + // If the zip entry is a file, we need to create its parent directories + val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile + + // Create the destination directory + if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs())) + throw FileNotFoundException("Failed to create destination directory: $destDirectory") + + // If the entry is a directory we don't need to copy anything + if (zipEntry.isDirectory) + continue + + // Copy bytes to destination + try { + zipFile.getInputStream(zipEntry).use { inputStream -> + destFile.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } catch (e : IOException) { + if (destFile.exists()) + destFile.delete() + throw e + } + } + } + } + + /** + * Extracts a zip file from the given stream to the given target directory. + * + * This method is ~5x slower than [unzip], as [ZipInputStream] uses a fixed `512` bytes buffer for inflation, + * instead of `8192` bytes or more used by input streams returned by [ZipFile]. + * This results in ~8x the amount of JNI calls, producing an increased number of array bounds checking, which kills performance. + * Usage of this method is discouraged when possible, [unzip] should be used instead. + * Nevertheless, it's the only option when extracting zips obtained from content URIs, as a [File] object cannot be obtained from them. + * @exception IOException if unzipping fails for any reason + */ + @Throws(IOException::class) + fun unzip(stream : InputStream, targetDirectory : File) { + ZipInputStream(BufferedInputStream(stream)).use { zis -> + do { + // Get the next entry, break if we've reached the end + val zipEntry = zis.nextEntry ?: break + + val destFile = createNewFile(targetDirectory, zipEntry) + // If the zip entry is a file, we need to create its parent directories + val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile + + // Create the destination directory + if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs())) + throw FileNotFoundException("Failed to create destination directory: $destDirectory") + + // If the entry is a directory we don't need to copy anything + if (zipEntry.isDirectory) + continue + + // Copy bytes to destination + try { + BufferedOutputStream(destFile.outputStream()).use { zis.copyTo(it) } + } catch (e : IOException) { + if (destFile.exists()) + destFile.delete() + throw e + } + } while (true) + } + } + + /** + * Safely creates a new destination file where the given zip entry will be extracted to. + * + * @exception IOException if the file was being created outside of the target directory + * **see:** [Zip Slip](https://github.com/snyk/zip-slip-vulnerability) + */ + @Throws(IOException::class) + private fun createNewFile(destinationDir : File, zipEntry : ZipEntry) : File { + val destFile = File(destinationDir, zipEntry.name) + val destDirPath = destinationDir.canonicalPath + val destFilePath = destFile.canonicalPath + + if (!destFilePath.startsWith(destDirPath + File.separator)) + throw IOException("Entry is outside of the target dir: " + zipEntry.name) + + return destFile + } + } +} diff --git a/Source/Android/app/src/main/res/layout/dialog_create_infinity_figure.xml b/Source/Android/app/src/main/res/layout/dialog_create_infinity_figure.xml new file mode 100644 index 0000000000..75e7b0a2e0 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/dialog_create_infinity_figure.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Android/app/src/main/res/layout/dialog_skylanders_manager.xml b/Source/Android/app/src/main/res/layout/dialog_nfc_figures_manager.xml similarity index 78% rename from Source/Android/app/src/main/res/layout/dialog_skylanders_manager.xml rename to Source/Android/app/src/main/res/layout/dialog_nfc_figures_manager.xml index 517399ff3a..c880f7c518 100644 --- a/Source/Android/app/src/main/res/layout/dialog_skylanders_manager.xml +++ b/Source/Android/app/src/main/res/layout/dialog_nfc_figures_manager.xml @@ -4,10 +4,10 @@ android:layout_height="wrap_content"> + android:fadeScrollbars="false" + android:scrollbars="vertical" /> diff --git a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml index 0619f22087..446706b821 100644 --- a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml +++ b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml @@ -106,6 +106,11 @@ android:text="@string/emulate_skylander_portal" style="@style/InGameMenuOption" /> +