mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-29 04:08:55 +00:00
Merge branch 'master' of https://github.com/dolphin-emu/dolphin into dolphin-emu-master
This commit is contained in:
commit
6280ce45a7
415 changed files with 14795 additions and 9405 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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 "$<CONFIG:Debug>")
|
||||
elseif(ARGV2 STREQUAL "NO_DEBINFO_ONLY")
|
||||
set(genexp_config_test "$<NOT:$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>")
|
||||
elseif(ARGV2 STREQUAL "RELEASE_ONLY")
|
||||
set(genexp_config_test "$<NOT:$<CONFIG:Debug>>")
|
||||
elseif(ARGV2)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
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()
|
||||
|
|
|
@ -14,3 +14,4 @@
|
|||
|
||||
[Video_Hacks]
|
||||
EFBToTextureEnable = False
|
||||
DeferEFBCopies = False
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
[Video_Settings]
|
||||
SuggestedAspectRatio = 2
|
||||
SafeTextureCacheColorSamples = 0
|
||||
|
|
|
@ -49,6 +49,10 @@
|
|||
{
|
||||
"type": "efb",
|
||||
"texture_filename": "efb1_n000000_256x256_1"
|
||||
},
|
||||
{
|
||||
"type": "efb",
|
||||
"texture_filename": "efb1_n000000_512x512_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
86
Data/Sys/Shaders/default_pre_post_process.glsl
Normal file
86
Data/Sys/Shaders/default_pre_post_process.glsl
Normal file
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
|
|
2
Externals/Qt
vendored
2
Externals/Qt
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 376baafde6cce2f8892c34c17ed397afa6c46d08
|
||||
Subproject commit 495517af2b922c10c24f543e0fd6ea3ddf774e50
|
1
Externals/libadrenotools
vendored
Submodule
1
Externals/libadrenotools
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f4ce3c9618e7ecfcdd238b17dad9a0b888f5de90
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
40
Source/Android/app/proguard-rules.pro
vendored
40
Source/Android/app/proguard-rules.pro
vendored
|
@ -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
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
android:supportsRtl="true"
|
||||
android:isGame="true"
|
||||
android:banner="@drawable/banner_tv"
|
||||
android:hasFragileUserData="true">
|
||||
android:hasFragileUserData="true"
|
||||
android:extractNativeLibs="true">
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="2.1"/>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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<String> mExtensions;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null)
|
||||
{
|
||||
mExtensions = (HashSet<String>) intent.getSerializableExtra(EXTRA_EXTENSIONS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractFilePickerFragment<File> 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;
|
||||
}
|
||||
}
|
|
@ -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<String>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent != null) {
|
||||
extensions = intent.getSerializableExtra(EXTRA_EXTENSIONS) as HashSet<String>?
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFragment(
|
||||
startPath: String?,
|
||||
mode: Int,
|
||||
allowMultiple: Boolean,
|
||||
allowCreateDir: Boolean,
|
||||
allowExistingFile: Boolean,
|
||||
singleClick: Boolean
|
||||
): AbstractFilePickerFragment<File> {
|
||||
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"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.infinitybase
|
||||
|
||||
object InfinityConfig {
|
||||
var LIST_FIGURES: Map<Long, String> = getFigureMap()
|
||||
var REVERSE_LIST_FIGURES: Map<String, Long> = getInverseFigureMap()
|
||||
|
||||
private external fun getFigureMap(): Map<Long, String>
|
||||
private external fun getInverseFigureMap(): Map<String, Long>
|
||||
|
||||
@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?
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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<FigureSlot>,
|
||||
private val activity: EmulationActivity
|
||||
) : RecyclerView.Adapter<FigureSlotAdapter.ViewHolder>(),
|
||||
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<String> = 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())
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<BooleanSetting> =
|
||||
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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<String> =
|
||||
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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<SettingsItem>) {
|
||||
|
@ -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<SettingsItem>) {
|
||||
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<SkylanderSlot>,
|
||||
private val activity: EmulationActivity
|
||||
) : RecyclerView.Adapter<SkylanderSlotAdapter.ViewHolder>(), 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 {
|
||||
|
|
|
@ -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<String> mExtensions;
|
||||
|
||||
public void setExtensions(HashSet<String> 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<String>) 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);
|
||||
}
|
||||
}
|
|
@ -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<String>? = null
|
||||
|
||||
fun setExtensions(extensions: HashSet<String>?) {
|
||||
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<String>?
|
||||
|
||||
if (mode == MODE_DIR) {
|
||||
val ok = requireActivity().findViewById<TextView>(R.id.nnf_button_ok)
|
||||
ok.setText(R.string.select_dir)
|
||||
|
||||
val cancel = requireActivity().findViewById<TextView>(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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String>? = 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<String>?,
|
||||
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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GpuDriverMetadataV1>(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,
|
||||
)
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Integer> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<PlatformGamesView> 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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
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<PlatformGamesView>) {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<Boolean> 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<Boolean> 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);
|
||||
}
|
||||
}
|
|
@ -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> { _: 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<Boolean>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<ArrayObjectAdapter> 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<GameFile> 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);
|
||||
}
|
||||
}
|
|
@ -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<ArrayObjectAdapter>()
|
||||
|
||||
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<String>,
|
||||
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<GameFile?>): 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
@ -123,8 +121,13 @@ public final class DirectoryInitialization
|
|||
NativeLibrary.SetUserDirectory(userPath);
|
||||
|
||||
File cacheDir = context.getExternalCacheDir();
|
||||
if (cacheDir == null)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -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<String>?
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/root"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/spacing_medlarge">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_infinity_dropdown"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/spacing_medlarge"
|
||||
android:paddingHorizontal="@dimen/spacing_medlarge"
|
||||
android:hint="@string/infinity_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/infinity_dropdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:spinnerMode="dialog"
|
||||
android:imeOptions="actionDone" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layout_infinity_dropdown"
|
||||
android:gravity="center_vertical"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_infinity_num"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/infinity_number"
|
||||
android:paddingHorizontal="@dimen/spacing_medlarge">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/infinity_num"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,10 +4,10 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/skylanders_manager"
|
||||
android:id="@+id/figure_manager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"/>
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
|
@ -106,6 +106,11 @@
|
|||
android:text="@string/emulate_skylander_portal"
|
||||
style="@style/InGameMenuOption" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/menu_infinity_base"
|
||||
android:text="@string/emulate_infinity_base"
|
||||
style="@style/InGameMenuOption" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
@ -8,43 +8,43 @@
|
|||
android:padding="@dimen/spacing_medlarge">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_create_skylander"
|
||||
android:id="@+id/button_create_figure"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="@dimen/spacing_small"
|
||||
android:layout_toStartOf="@id/button_load_skylander"
|
||||
android:contentDescription="@string/create_skylander"
|
||||
android:tooltipText="@string/create_skylander"
|
||||
android:layout_toStartOf="@id/button_load_figure"
|
||||
android:contentDescription="@string/create_figure"
|
||||
android:tooltipText="@string/create_figure"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_load_skylander"
|
||||
android:id="@+id/button_load_figure"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="@dimen/spacing_small"
|
||||
android:layout_toStartOf="@+id/button_clear_skylander"
|
||||
android:contentDescription="@string/load_skylander"
|
||||
android:tooltipText="@string/load_skylander"
|
||||
android:layout_toStartOf="@+id/button_clear_figure"
|
||||
android:contentDescription="@string/load_figure"
|
||||
android:tooltipText="@string/load_figure"
|
||||
app:icon="@drawable/ic_load" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_clear_skylander"
|
||||
android:id="@+id/button_clear_figure"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:contentDescription="@string/remove_skylander"
|
||||
android:tooltipText="@string/remove_skylander"
|
||||
android:contentDescription="@string/remove_figure"
|
||||
android:tooltipText="@string/remove_figure"
|
||||
app:icon="@drawable/ic_clear" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_skylander_name"
|
||||
android:id="@+id/text_figure_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
|
@ -52,7 +52,7 @@
|
|||
android:layout_marginBottom="@dimen/spacing_medlarge"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_toStartOf="@+id/button_create_skylander"
|
||||
android:layout_toStartOf="@+id/button_create_figure"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="16sp"
|
|
@ -841,14 +841,28 @@ It can efficiently compress both junk data and encrypted Wii data.
|
|||
<string name="about_github"><a href="https://github.com/dolphin-emu/dolphin">GitHub</a></string>
|
||||
<string name="about_support"><a href="https://forums.dolphin-emu.org/">Support</a></string>
|
||||
<string name="about_copyright_warning">\u00A9 2003–2015+ Dolphin Team. \u201cGameCube\u201d and \u201cWii\u201d are trademarks of Nintendo. Dolphin is not affiliated with Nintendo in any way.</string>
|
||||
<string name="system_driver">System driver</string>
|
||||
<string name="system_driver_desc">The GPU driver that is part of the OS.</string>
|
||||
|
||||
<!-- Custom GPU drivers -->
|
||||
<string name="gpu_driver_dialog_title">Select the GPU driver for Dolphin</string>
|
||||
<string name="gpu_driver_dialog_system">Default</string>
|
||||
<string name="gpu_driver_dialog_install">Install driver</string>
|
||||
<string name="gpu_driver_dialog_uninstall_done">Successfully switched to the system driver</string>
|
||||
<string name="gpu_driver_submenu">GPU Driver</string>
|
||||
<string name="gpu_driver_install_inprogress">Installing the GPU driver…</string>
|
||||
<string name="gpu_driver_install_success">GPU driver installed successfully</string>
|
||||
<string name="gpu_driver_install_invalid_archive">Failed to unzip the provided driver package</string>
|
||||
<string name="gpu_driver_install_missing_metadata">The supplied driver package is invalid due to missing metadata.</string>
|
||||
<string name="gpu_driver_install_invalid_metadata">The supplied driver package contains invalid metadata, it may be corrupted.</string>
|
||||
<string name="gpu_driver_install_unsupported_android_version">Your device doesn\'t meet the minimum Android version requirements for the supplied driver.</string>
|
||||
<string name="gpu_driver_install_already_installed">The supplied driver package is already installed.</string>
|
||||
<string name="gpu_driver_install_file_not_found">The selected file could not be found or accessed.</string>
|
||||
|
||||
<!-- Emulated USB Devices -->
|
||||
<string name="emulated_usb_devices">Emulated USB Devices</string>
|
||||
<string name="emulate_skylander_portal">Skylanders Portal</string>
|
||||
<string name="skylanders_manager">Skylanders Manager</string>
|
||||
<string name="load_skylander">Load</string>
|
||||
<string name="remove_skylander">Remove</string>
|
||||
<string name="create_skylander">Create</string>
|
||||
<string name="create_skylander_title">Create Skylander</string>
|
||||
<string name="skylander_label">Skylander</string>
|
||||
<string name="skylander_slot">Slot %1$d</string>
|
||||
|
@ -856,4 +870,23 @@ It can efficiently compress both junk data and encrypted Wii data.
|
|||
<string name="skylander_variant">Variant</string>
|
||||
<string name="invalid_skylander">Invalid Skylander Selection</string>
|
||||
|
||||
<string name="emulate_infinity_base">Infinity Base</string>
|
||||
<string name="infinity_manager">Infinity Manager</string>
|
||||
<string name="load_figure">Load</string>
|
||||
<string name="remove_figure">Remove</string>
|
||||
<string name="create_figure">Create</string>
|
||||
<string name="create_figure_title">Create Figure</string>
|
||||
<string name="infinity_label">Infinity Figure</string>
|
||||
<string name="infinity_number">Figure Number</string>
|
||||
<string name="invalid_infinity_figure">Invalid Figure Selection</string>
|
||||
<string name="infinity_hexagon_label">Power Disc/Play Set</string>
|
||||
<string name="infinity_p1_label">Player One</string>
|
||||
<string name="infinity_p2_label">Player Two</string>
|
||||
<string name="infinity_p1a1_label">P1 Ability One</string>
|
||||
<string name="infinity_p2a1_label">P2 Ability One</string>
|
||||
<string name="infinity_p1a2_label">P1 Ability Two</string>
|
||||
<string name="infinity_p2a2_label">P2 Ability Two</string>
|
||||
<string name="incompatible_figure_selected">Incompatible Figure Selected</string>
|
||||
<string name="select_compatible_figure">Please select a compatible figure file</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
android.enableJetifier=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
|
|
|
@ -32,6 +32,10 @@ static jclass s_linked_hash_map_class;
|
|||
static jmethodID s_linked_hash_map_init;
|
||||
static jmethodID s_linked_hash_map_put;
|
||||
|
||||
static jclass s_hash_map_class;
|
||||
static jmethodID s_hash_map_init;
|
||||
static jmethodID s_hash_map_put;
|
||||
|
||||
static jclass s_ini_file_class;
|
||||
static jfieldID s_ini_file_pointer;
|
||||
static jclass s_ini_file_section_class;
|
||||
|
@ -221,6 +225,21 @@ jmethodID GetLinkedHashMapPut()
|
|||
return s_linked_hash_map_put;
|
||||
}
|
||||
|
||||
jclass GetHashMapClass()
|
||||
{
|
||||
return s_hash_map_class;
|
||||
}
|
||||
|
||||
jmethodID GetHashMapInit()
|
||||
{
|
||||
return s_hash_map_init;
|
||||
}
|
||||
|
||||
jmethodID GetHashMapPut()
|
||||
{
|
||||
return s_hash_map_put;
|
||||
}
|
||||
|
||||
jclass GetIniFileClass()
|
||||
{
|
||||
return s_ini_file_class;
|
||||
|
@ -575,12 +594,19 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
ini_file_section_class, "<init>", "(Lorg/dolphinemu/dolphinemu/utils/IniFile;J)V");
|
||||
env->DeleteLocalRef(ini_file_section_class);
|
||||
|
||||
const jclass map_class = env->FindClass("java/util/LinkedHashMap");
|
||||
s_linked_hash_map_class = reinterpret_cast<jclass>(env->NewGlobalRef(map_class));
|
||||
const jclass linked_hash_map_class = env->FindClass("java/util/LinkedHashMap");
|
||||
s_linked_hash_map_class = reinterpret_cast<jclass>(env->NewGlobalRef(linked_hash_map_class));
|
||||
s_linked_hash_map_init = env->GetMethodID(s_linked_hash_map_class, "<init>", "(I)V");
|
||||
s_linked_hash_map_put = env->GetMethodID(
|
||||
s_linked_hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
env->DeleteLocalRef(map_class);
|
||||
env->DeleteLocalRef(linked_hash_map_class);
|
||||
|
||||
const jclass hash_map_class = env->FindClass("java/util/HashMap");
|
||||
s_hash_map_class = reinterpret_cast<jclass>(env->NewGlobalRef(hash_map_class));
|
||||
s_hash_map_init = env->GetMethodID(s_hash_map_class, "<init>", "(I)V");
|
||||
s_hash_map_put = env->GetMethodID(s_hash_map_class, "put",
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
env->DeleteLocalRef(hash_map_class);
|
||||
|
||||
const jclass compress_cb_class =
|
||||
env->FindClass("org/dolphinemu/dolphinemu/utils/CompressCallback");
|
||||
|
@ -741,6 +767,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
|
|||
env->DeleteGlobalRef(s_game_file_cache_class);
|
||||
env->DeleteGlobalRef(s_analytics_class);
|
||||
env->DeleteGlobalRef(s_linked_hash_map_class);
|
||||
env->DeleteGlobalRef(s_hash_map_class);
|
||||
env->DeleteGlobalRef(s_ini_file_class);
|
||||
env->DeleteGlobalRef(s_ini_file_section_class);
|
||||
env->DeleteGlobalRef(s_compress_cb_class);
|
||||
|
|
|
@ -32,6 +32,10 @@ jclass GetLinkedHashMapClass();
|
|||
jmethodID GetLinkedHashMapInit();
|
||||
jmethodID GetLinkedHashMapPut();
|
||||
|
||||
jclass GetHashMapClass();
|
||||
jmethodID GetHashMapInit();
|
||||
jmethodID GetHashMapPut();
|
||||
|
||||
jclass GetIniFileClass();
|
||||
jfieldID GetIniFilePointer();
|
||||
jclass GetIniFileSectionClass();
|
||||
|
|
|
@ -10,6 +10,9 @@ add_library(main SHARED
|
|||
GameList/GameFile.cpp
|
||||
GameList/GameFile.h
|
||||
GameList/GameFileCache.cpp
|
||||
Host.cpp
|
||||
Host.h
|
||||
InfinityConfig.cpp
|
||||
Input/Control.cpp
|
||||
Input/Control.h
|
||||
Input/ControlGroup.cpp
|
||||
|
@ -29,6 +32,7 @@ add_library(main SHARED
|
|||
RiivolutionPatches.cpp
|
||||
SkylanderConfig.cpp
|
||||
WiiUtils.cpp
|
||||
GpuDriver.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(main
|
||||
|
@ -49,6 +53,12 @@ PRIVATE
|
|||
"-Wl,--no-whole-archive"
|
||||
)
|
||||
|
||||
target_include_directories(main
|
||||
PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/Externals/libadrenotools/include
|
||||
${CMAKE_SOURCE_DIR}/Externals/VulkanMemoryAllocator/include
|
||||
)
|
||||
|
||||
file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/)
|
||||
file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/)
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/Data/Sys DESTINATION ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "Core/ConfigLoaders/GameConfigLoader.h"
|
||||
#include "Core/ConfigLoaders/IsSettingSaveable.h"
|
||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||
#include "jni/Host.h"
|
||||
|
||||
constexpr jint LAYER_BASE_OR_CURRENT = 0;
|
||||
constexpr jint LAYER_BASE = 1;
|
||||
|
@ -122,6 +123,7 @@ Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_loadGameInis
|
|||
jstring jGameId,
|
||||
jint jRevision)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
const std::string game_id = GetJString(env, jGameId);
|
||||
const u16 revision = static_cast<u16>(jRevision);
|
||||
Config::AddLayer(ConfigLoaders::GenerateGlobalGameConfigLoader(game_id, revision));
|
||||
|
@ -131,6 +133,7 @@ Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_loadGameInis
|
|||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_unloadGameInis(JNIEnv*, jclass)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
Config::RemoveLayer(Config::LayerType::GlobalGame);
|
||||
Config::RemoveLayer(Config::LayerType::LocalGame);
|
||||
}
|
||||
|
@ -138,6 +141,7 @@ Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_unloadGameIn
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_save(
|
||||
JNIEnv*, jclass, jint layer)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return GetLayer(layer, {})->Save();
|
||||
}
|
||||
|
||||
|
@ -145,6 +149,7 @@ JNIEXPORT void JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_deleteAllKeys(JNIEnv*, jclass,
|
||||
jint layer)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return GetLayer(layer, {})->DeleteAllKeys();
|
||||
}
|
||||
|
||||
|
@ -161,6 +166,7 @@ JNIEXPORT jboolean JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_deleteKey(
|
||||
JNIEnv* env, jclass, jint layer, jstring file, jstring section, jstring key)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
const Config::Location location = GetLocation(env, file, section, key);
|
||||
const bool had_value = GetLayer(layer, location)->DeleteKey(location);
|
||||
if (had_value)
|
||||
|
@ -214,6 +220,7 @@ JNIEXPORT void JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_setString(
|
||||
JNIEnv* env, jclass, jint layer, jstring file, jstring section, jstring key, jstring value)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return Set(layer, GetLocation(env, file, section, key), GetJString(env, value));
|
||||
}
|
||||
|
||||
|
@ -221,18 +228,21 @@ JNIEXPORT void JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_setBoolean(
|
||||
JNIEnv* env, jclass, jint layer, jstring file, jstring section, jstring key, jboolean value)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return Set(layer, GetLocation(env, file, section, key), static_cast<bool>(value));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_setInt(
|
||||
JNIEnv* env, jclass, jint layer, jstring file, jstring section, jstring key, jint value)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return Set(layer, GetLocation(env, file, section, key), value);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_setFloat(
|
||||
JNIEnv* env, jclass, jint layer, jstring file, jstring section, jstring key, jfloat value)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
return Set(layer, GetLocation(env, file, section, key), value);
|
||||
}
|
||||
}
|
||||
|
|
147
Source/Android/jni/GpuDriver.cpp
Normal file
147
Source/Android/jni/GpuDriver.cpp
Normal file
|
@ -0,0 +1,147 @@
|
|||
// Copyright 2023 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on: Skyline Emulator Project
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include "Common/IniFile.h"
|
||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||
#include "jni/AndroidCommon/IDCache.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <jni.h>
|
||||
#include <unistd.h>
|
||||
#include "adrenotools/driver.h"
|
||||
|
||||
#include "VideoBackends/Vulkan/VulkanContext.h"
|
||||
#include "VideoBackends/Vulkan/VulkanLoader.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
#if defined(_M_ARM_64)
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_getSystemDriverInfo(JNIEnv* env,
|
||||
jobject)
|
||||
{
|
||||
if (!Vulkan::LoadVulkanLibrary(true))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
u32 vk_api_version = 0;
|
||||
VkInstance instance = Vulkan::VulkanContext::CreateVulkanInstance(WindowSystemType::Headless,
|
||||
false, false, &vk_api_version);
|
||||
if (!instance)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!Vulkan::LoadVulkanInstanceFunctions(instance))
|
||||
{
|
||||
vkDestroyInstance(instance, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Vulkan::VulkanContext::GPUList gpu_list = Vulkan::VulkanContext::EnumerateGPUs(instance);
|
||||
|
||||
if (gpu_list.empty())
|
||||
{
|
||||
vkDestroyInstance(instance, nullptr);
|
||||
Vulkan::UnloadVulkanLibrary();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VkPhysicalDeviceProperties properties;
|
||||
vkGetPhysicalDeviceProperties(gpu_list.front(), &properties);
|
||||
|
||||
std::string driverId;
|
||||
if (vkGetPhysicalDeviceProperties2 && vk_api_version >= VK_VERSION_1_1)
|
||||
{
|
||||
VkPhysicalDeviceDriverProperties driverProperties;
|
||||
driverProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;
|
||||
driverProperties.pNext = nullptr;
|
||||
VkPhysicalDeviceProperties2 properties2;
|
||||
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
properties2.pNext = &driverProperties;
|
||||
vkGetPhysicalDeviceProperties2(gpu_list.front(), &properties2);
|
||||
driverId = fmt::format("{}", driverProperties.driverID);
|
||||
}
|
||||
else
|
||||
{
|
||||
driverId = "Unknown";
|
||||
}
|
||||
|
||||
std::string driverVersion =
|
||||
fmt::format("{}.{}.{}", VK_API_VERSION_MAJOR(properties.driverVersion),
|
||||
VK_API_VERSION_MINOR(properties.driverVersion),
|
||||
VK_API_VERSION_PATCH(properties.driverVersion));
|
||||
|
||||
vkDestroyInstance(instance, nullptr);
|
||||
Vulkan::UnloadVulkanLibrary();
|
||||
|
||||
auto array = env->NewObjectArray(2, env->FindClass("java/lang/String"), nullptr);
|
||||
env->SetObjectArrayElement(array, 0, ToJString(env, driverId));
|
||||
env->SetObjectArrayElement(array, 1, ToJString(env, driverVersion));
|
||||
return array;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_supportsCustomDriverLoading(
|
||||
JNIEnv* env, jobject instance)
|
||||
{
|
||||
// If the KGSL device exists custom drivers can be loaded using adrenotools
|
||||
return Vulkan::SupportsCustomDriver();
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_supportsForceMaxGpuClocks(
|
||||
JNIEnv* env, jobject instance)
|
||||
{
|
||||
// If the KGSL device exists adrenotools can be used to set GPU turbo mode
|
||||
return Vulkan::SupportsCustomDriver();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_forceMaxGpuClocks(
|
||||
JNIEnv* env, jobject instance, jboolean enable)
|
||||
{
|
||||
adrenotools_set_turbo(enable);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_getSystemDriverInfo(
|
||||
JNIEnv* env, jobject instance)
|
||||
{
|
||||
auto array = env->NewObjectArray(0, env->FindClass("java/lang/String"), nullptr);
|
||||
return array;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_supportsCustomDriverLoading(
|
||||
JNIEnv* env, jobject instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_supportsForceMaxGpuClocks(
|
||||
JNIEnv* env, jobject instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_GpuDriverHelper_00024Companion_forceMaxGpuClocks(
|
||||
JNIEnv* env, jobject instance, jboolean enable)
|
||||
{
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
8
Source/Android/jni/Host.cpp
Normal file
8
Source/Android/jni/Host.cpp
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2023 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "jni/Host.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
std::mutex HostThreadLock::s_host_identity_mutex;
|
44
Source/Android/jni/Host.h
Normal file
44
Source/Android/jni/Host.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2023 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "Core/Core.h"
|
||||
|
||||
// The Core only supports using a single Host thread.
|
||||
// If multiple threads want to call host functions then they need to queue
|
||||
// sequentially for access.
|
||||
struct HostThreadLock
|
||||
{
|
||||
public:
|
||||
explicit HostThreadLock() : m_lock(s_host_identity_mutex) { Core::DeclareAsHostThread(); }
|
||||
|
||||
~HostThreadLock()
|
||||
{
|
||||
if (m_lock.owns_lock())
|
||||
Core::UndeclareAsHostThread();
|
||||
}
|
||||
|
||||
HostThreadLock(const HostThreadLock& other) = delete;
|
||||
HostThreadLock(HostThreadLock&& other) = delete;
|
||||
HostThreadLock& operator=(const HostThreadLock& other) = delete;
|
||||
HostThreadLock& operator=(HostThreadLock&& other) = delete;
|
||||
|
||||
void Lock()
|
||||
{
|
||||
m_lock.lock();
|
||||
Core::DeclareAsHostThread();
|
||||
}
|
||||
|
||||
void Unlock()
|
||||
{
|
||||
m_lock.unlock();
|
||||
Core::UndeclareAsHostThread();
|
||||
}
|
||||
|
||||
private:
|
||||
static std::mutex s_host_identity_mutex;
|
||||
std::unique_lock<std::mutex> m_lock;
|
||||
};
|
121
Source/Android/jni/InfinityConfig.cpp
Normal file
121
Source/Android/jni/InfinityConfig.cpp
Normal file
|
@ -0,0 +1,121 @@
|
|||
// Copyright 2023 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "AndroidCommon/AndroidCommon.h"
|
||||
#include "AndroidCommon/IDCache.h"
|
||||
#include "Core/IOS/USB/Emulated/Infinity.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_infinitybase_InfinityConfig_getFigureMap(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
|
||||
jobject hash_map_obj = env->NewObject(IDCache::GetHashMapClass(), IDCache::GetHashMapInit(),
|
||||
system.GetInfinityBase().GetFigureList().size());
|
||||
|
||||
jclass long_class = env->FindClass("java/lang/Long");
|
||||
jmethodID long_init = env->GetMethodID(long_class, "<init>", "(J)V");
|
||||
|
||||
for (const auto& it : system.GetInfinityBase().GetFigureList())
|
||||
{
|
||||
const std::string& name = it.first;
|
||||
jobject figure_number = env->NewObject(long_class, long_init, (jlong)it.second);
|
||||
env->CallObjectMethod(hash_map_obj, IDCache::GetHashMapPut(), figure_number,
|
||||
ToJString(env, name));
|
||||
env->DeleteLocalRef(figure_number);
|
||||
}
|
||||
|
||||
return hash_map_obj;
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_infinitybase_InfinityConfig_getInverseFigureMap(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
|
||||
jobject hash_map_obj = env->NewObject(IDCache::GetHashMapClass(), IDCache::GetHashMapInit(),
|
||||
system.GetInfinityBase().GetFigureList().size());
|
||||
|
||||
jclass long_class = env->FindClass("java/lang/Long");
|
||||
jmethodID long_init = env->GetMethodID(long_class, "<init>", "(J)V");
|
||||
|
||||
for (const auto& it : system.GetInfinityBase().GetFigureList())
|
||||
{
|
||||
const std::string& name = it.first;
|
||||
jobject figure_number = env->NewObject(long_class, long_init, (jlong)it.second);
|
||||
env->CallObjectMethod(hash_map_obj, IDCache::GetHashMapPut(), ToJString(env, name),
|
||||
figure_number);
|
||||
env->DeleteLocalRef(figure_number);
|
||||
}
|
||||
|
||||
return hash_map_obj;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_infinitybase_InfinityConfig_removeFigure(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jint position)
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetInfinityBase().RemoveFigure(position);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_infinitybase_InfinityConfig_loadFigure(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jint position,
|
||||
jstring file_name)
|
||||
{
|
||||
File::IOFile inf_file(GetJString(env, file_name), "r+b");
|
||||
if (!inf_file)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
std::array<u8, 0x14 * 0x10> file_data{};
|
||||
if (!inf_file.ReadBytes(file_data.data(), file_data.size()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetInfinityBase().RemoveFigure(position);
|
||||
return ToJString(env,
|
||||
system.GetInfinityBase().LoadFigure(file_data, std::move(inf_file), position));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_infinitybase_InfinityConfig_createFigure(
|
||||
JNIEnv* env, jclass clazz, jlong figure_number, jstring fileName, jint position)
|
||||
{
|
||||
u32 fig_num = static_cast<u32>(figure_number);
|
||||
|
||||
std::string file_name = GetJString(env, fileName);
|
||||
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetInfinityBase().CreateFigure(file_name, fig_num);
|
||||
system.GetInfinityBase().RemoveFigure(position);
|
||||
|
||||
File::IOFile inf_file(file_name, "r+b");
|
||||
if (!inf_file)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
std::array<u8, 0x14 * 0x10> file_data{};
|
||||
if (!inf_file.ReadBytes(file_data.data(), file_data.size()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return ToJString(env,
|
||||
system.GetInfinityBase().LoadFigure(file_data, std::move(inf_file), position));
|
||||
}
|
||||
}
|
|
@ -63,6 +63,7 @@
|
|||
|
||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||
#include "jni/AndroidCommon/IDCache.h"
|
||||
#include "jni/Host.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
@ -70,10 +71,6 @@ constexpr char DOLPHIN_TAG[] = "DolphinEmuNative";
|
|||
|
||||
ANativeWindow* s_surf;
|
||||
|
||||
// The Core only supports using a single Host thread.
|
||||
// If multiple threads want to call host functions then they need to queue
|
||||
// sequentially for access.
|
||||
std::mutex s_host_identity_lock;
|
||||
Common::Event s_update_main_frame_event;
|
||||
|
||||
// This exists to prevent surfaces from being destroyed during the boot process,
|
||||
|
@ -248,19 +245,19 @@ extern "C" {
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_UnPauseEmulation(JNIEnv*,
|
||||
jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
Core::SetState(Core::State::Running);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_PauseEmulation(JNIEnv*, jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
Core::SetState(Core::State::Paused);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_StopEmulation(JNIEnv*, jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
Core::Stop();
|
||||
|
||||
// Kick the waiting event
|
||||
|
@ -303,7 +300,7 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGitRev
|
|||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveScreenShot(JNIEnv*, jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
Core::SaveScreenShot();
|
||||
}
|
||||
|
||||
|
@ -317,7 +314,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveState(JN
|
|||
jint slot,
|
||||
jboolean wait)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
State::Save(slot, wait);
|
||||
}
|
||||
|
||||
|
@ -325,21 +322,21 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveStateAs(
|
|||
jstring path,
|
||||
jboolean wait)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
State::SaveAs(GetJString(env, path), wait);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_LoadState(JNIEnv*, jclass,
|
||||
jint slot)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
State::Load(slot);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_LoadStateAs(JNIEnv* env, jclass,
|
||||
jstring path)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
State::LoadAs(GetJString(env, path));
|
||||
}
|
||||
|
||||
|
@ -356,10 +353,19 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_utils_DirectoryInitializat
|
|||
File::SetSysDirectory(path);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_utils_DirectoryInitialization_SetGpuDriverDirectories(
|
||||
JNIEnv* env, jclass, jstring jPath, jstring jLibPath)
|
||||
{
|
||||
const std::string path = GetJString(env, jPath);
|
||||
const std::string lib_path = GetJString(env, jLibPath);
|
||||
File::SetGpuDriverDirectories(path, lib_path);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetUserDirectory(
|
||||
JNIEnv* env, jclass, jstring jDirectory)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
UICommon::SetUserDirectory(GetJString(env, jDirectory));
|
||||
}
|
||||
|
||||
|
@ -372,10 +378,16 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetUserDi
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetCacheDirectory(
|
||||
JNIEnv* env, jclass, jstring jDirectory)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
File::SetUserPath(D_CACHE_IDX, GetJString(env, jDirectory));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCacheDirectory(JNIEnv* env, jclass)
|
||||
{
|
||||
return ToJString(env, File::GetUserPath(D_CACHE_IDX));
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_DefaultCPUCore(JNIEnv*, jclass)
|
||||
{
|
||||
return static_cast<jint>(PowerPC::DefaultCPUCore());
|
||||
|
@ -395,7 +407,7 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetMaxLogLev
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling(JNIEnv*, jclass,
|
||||
jboolean enable)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
Core::SetState(Core::State::Paused);
|
||||
auto& jit_interface = Core::System::GetInstance().GetJitInterface();
|
||||
jit_interface.ClearCache();
|
||||
|
@ -407,7 +419,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_WriteProfileResults(JNIEnv*,
|
||||
jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
std::string filename = File::GetUserPath(D_DUMP_IDX) + "Debug/profiler.txt";
|
||||
File::CreateFullPath(filename);
|
||||
auto& jit_interface = Core::System::GetInstance().GetJitInterface();
|
||||
|
@ -436,14 +448,14 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceDestr
|
|||
// If emulation continues running without a valid surface, we will probably crash,
|
||||
// so pause emulation until we get a valid surface again. EmulationFragment handles resuming.
|
||||
|
||||
std::unique_lock host_identity_guard(s_host_identity_lock);
|
||||
HostThreadLock host_identity_guard;
|
||||
|
||||
while (s_is_booting.IsSet())
|
||||
{
|
||||
// Need to wait for boot to finish before we can pause
|
||||
host_identity_guard.unlock();
|
||||
host_identity_guard.Unlock();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
host_identity_guard.lock();
|
||||
host_identity_guard.Lock();
|
||||
}
|
||||
|
||||
if (Core::GetState() == Core::State::Running)
|
||||
|
@ -477,18 +489,20 @@ JNIEXPORT jfloat JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGameAsp
|
|||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RefreshWiimotes(JNIEnv*, jclass)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(s_host_identity_lock);
|
||||
HostThreadLock guard;
|
||||
WiimoteReal::Refresh();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReloadConfig(JNIEnv*, jclass)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
SConfig::GetInstance().LoadSettings();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_NativeLibrary_UpdateGCAdapterScanThread(JNIEnv*, jclass)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
if (GCAdapter::UseAdapter())
|
||||
{
|
||||
GCAdapter::StartScanThread();
|
||||
|
@ -501,6 +515,9 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_UpdateGCAdapterScanThread(JNIEnv*,
|
|||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Initialize(JNIEnv*, jclass)
|
||||
{
|
||||
// InitControllers ends up calling config code, and some config callbacks use RunAsCPUThread
|
||||
HostThreadLock guard;
|
||||
|
||||
UICommon::CreateDirectories();
|
||||
Common::RegisterMsgAlertHandler(&MsgAlert);
|
||||
Common::AndroidSetReportHandler(&ReportSend);
|
||||
|
@ -514,12 +531,18 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Initialize(J
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReportStartToAnalytics(JNIEnv*,
|
||||
jclass)
|
||||
{
|
||||
// Identity generation ends up calling config code, and some config callbacks use RunAsCPUThread
|
||||
HostThreadLock guard;
|
||||
|
||||
DolphinAnalytics::Instance().ReportDolphinStart(GetAnalyticValue("DEVICE_TYPE"));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GenerateNewStatisticsId(JNIEnv*,
|
||||
jclass)
|
||||
{
|
||||
// Identity generation ends up calling config code, and some config callbacks use RunAsCPUThread
|
||||
HostThreadLock guard;
|
||||
|
||||
DolphinAnalytics::Instance().GenerateNewIdentity();
|
||||
}
|
||||
|
||||
|
@ -535,7 +558,7 @@ static float GetRenderSurfaceScale(JNIEnv* env)
|
|||
|
||||
static void Run(JNIEnv* env, std::unique_ptr<BootParameters>&& boot, bool riivolution)
|
||||
{
|
||||
std::unique_lock<std::mutex> host_identity_guard(s_host_identity_lock);
|
||||
HostThreadLock host_identity_guard;
|
||||
|
||||
if (riivolution && std::holds_alternative<BootParameters::Disc>(boot->parameters))
|
||||
{
|
||||
|
@ -566,15 +589,15 @@ static void Run(JNIEnv* env, std::unique_ptr<BootParameters>&& boot, bool riivol
|
|||
|
||||
while (Core::IsRunning())
|
||||
{
|
||||
host_identity_guard.unlock();
|
||||
host_identity_guard.Unlock();
|
||||
s_update_main_frame_event.Wait();
|
||||
host_identity_guard.lock();
|
||||
host_identity_guard.Lock();
|
||||
Core::HostDispatchJobs();
|
||||
}
|
||||
|
||||
s_game_metadata_is_valid = false;
|
||||
Core::Shutdown();
|
||||
host_identity_guard.unlock();
|
||||
host_identity_guard.Unlock();
|
||||
|
||||
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetFinishEmulationActivity());
|
||||
|
@ -615,6 +638,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunSystemMen
|
|||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ChangeDisc(JNIEnv* env, jclass,
|
||||
jstring jFile)
|
||||
{
|
||||
HostThreadLock guard;
|
||||
const std::string path = GetJString(env, jFile);
|
||||
__android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Change Disc: %s", path.c_str());
|
||||
Core::RunAsCPUThread([&path] { Core::System::GetInstance().GetDVDInterface().ChangeDisc(path); });
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <array>
|
||||
|
||||
#include "AndroidCommon/AndroidCommon.h"
|
||||
#include "AndroidCommon/IDCache.h"
|
||||
#include "Core/IOS/USB/Emulated/Skylander.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
|
@ -15,12 +16,8 @@ JNIEXPORT jobject JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getSkylanderMap(JNIEnv* env,
|
||||
jclass clazz)
|
||||
{
|
||||
jclass hash_map_class = env->FindClass("java/util/HashMap");
|
||||
jmethodID hash_map_init = env->GetMethodID(hash_map_class, "<init>", "(I)V");
|
||||
jobject hash_map_obj = env->NewObject(hash_map_class, hash_map_init,
|
||||
jobject hash_map_obj = env->NewObject(IDCache::GetHashMapClass(), IDCache::GetHashMapInit(),
|
||||
static_cast<u16>(IOS::HLE::USB::list_skylanders.size()));
|
||||
jmethodID hash_map_put = env->GetMethodID(
|
||||
hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
|
||||
jclass skylander_class =
|
||||
env->FindClass("org/dolphinemu/dolphinemu/features/skylanders/model/SkylanderPair");
|
||||
|
@ -29,10 +26,11 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getSkylanderM
|
|||
|
||||
for (const auto& it : IOS::HLE::USB::list_skylanders)
|
||||
{
|
||||
const std::string& name = it.second;
|
||||
const std::string& name = it.second.name;
|
||||
jobject skylander_obj =
|
||||
env->NewObject(skylander_class, skylander_init, it.first.first, it.first.second);
|
||||
env->CallObjectMethod(hash_map_obj, hash_map_put, skylander_obj, ToJString(env, name));
|
||||
env->CallObjectMethod(hash_map_obj, IDCache::GetHashMapPut(), skylander_obj,
|
||||
ToJString(env, name));
|
||||
env->DeleteLocalRef(skylander_obj);
|
||||
}
|
||||
|
||||
|
@ -43,12 +41,8 @@ JNIEXPORT jobject JNICALL
|
|||
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getInverseSkylanderMap(
|
||||
JNIEnv* env, jclass clazz)
|
||||
{
|
||||
jclass hash_map_class = env->FindClass("java/util/HashMap");
|
||||
jmethodID hash_map_init = env->GetMethodID(hash_map_class, "<init>", "(I)V");
|
||||
jobject hash_map_obj = env->NewObject(hash_map_class, hash_map_init,
|
||||
jobject hash_map_obj = env->NewObject(IDCache::GetHashMapClass(), IDCache::GetHashMapInit(),
|
||||
static_cast<u16>(IOS::HLE::USB::list_skylanders.size()));
|
||||
jmethodID hash_map_put = env->GetMethodID(
|
||||
hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
|
||||
jclass skylander_class =
|
||||
env->FindClass("org/dolphinemu/dolphinemu/features/skylanders/model/SkylanderPair");
|
||||
|
@ -57,10 +51,11 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getInverseSky
|
|||
|
||||
for (const auto& it : IOS::HLE::USB::list_skylanders)
|
||||
{
|
||||
const std::string& name = it.second;
|
||||
const std::string& name = it.second.name;
|
||||
jobject skylander_obj =
|
||||
env->NewObject(skylander_class, skylander_init, it.first.first, it.first.second);
|
||||
env->CallObjectMethod(hash_map_obj, hash_map_put, ToJString(env, name), skylander_obj);
|
||||
env->CallObjectMethod(hash_map_obj, IDCache::GetHashMapPut(), ToJString(env, name),
|
||||
skylander_obj);
|
||||
env->DeleteLocalRef(skylander_obj);
|
||||
}
|
||||
|
||||
|
@ -109,7 +104,7 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_loadSkylander
|
|||
|
||||
if (it != IOS::HLE::USB::list_skylanders.end())
|
||||
{
|
||||
name = it->second;
|
||||
name = it->second.name;
|
||||
}
|
||||
|
||||
return env->NewObject(pair_class, pair_init,
|
||||
|
@ -156,7 +151,7 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_createSkyland
|
|||
|
||||
if (it != IOS::HLE::USB::list_skylanders.end())
|
||||
{
|
||||
name = it->second;
|
||||
name = it->second.name;
|
||||
}
|
||||
|
||||
return env->NewObject(pair_class, pair_init,
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IOFile.h"
|
||||
|
@ -159,9 +161,9 @@ void WaveFileWriter::AddStereoSamplesBE(const short* sample_data, u32 count,
|
|||
{
|
||||
Stop();
|
||||
file_index++;
|
||||
std::ostringstream filename;
|
||||
filename << File::GetUserPath(D_DUMPAUDIO_IDX) << basename << file_index << ".wav";
|
||||
Start(filename.str(), sample_rate_divisor);
|
||||
const std::string filename =
|
||||
fmt::format("{}{}{}.wav", File::GetUserPath(D_DUMPAUDIO_IDX), basename, file_index);
|
||||
Start(filename, sample_rate_divisor);
|
||||
current_sample_rate_divisor = sample_rate_divisor;
|
||||
}
|
||||
|
||||
|
|
|
@ -644,7 +644,7 @@ private:
|
|||
void EncodeAddressInst(u32 op, ARM64Reg Rd, s32 imm);
|
||||
void EncodeLoadStoreUnscaled(u32 size, u32 op, ARM64Reg Rt, ARM64Reg Rn, s32 imm);
|
||||
|
||||
FixupBranch WriteFixupBranch();
|
||||
[[nodiscard]] FixupBranch WriteFixupBranch();
|
||||
|
||||
template <typename T>
|
||||
void MOVI2RImpl(ARM64Reg Rd, T imm);
|
||||
|
@ -680,13 +680,13 @@ public:
|
|||
|
||||
// FixupBranch branching
|
||||
void SetJumpTarget(FixupBranch const& branch);
|
||||
FixupBranch CBZ(ARM64Reg Rt);
|
||||
FixupBranch CBNZ(ARM64Reg Rt);
|
||||
FixupBranch B(CCFlags cond);
|
||||
FixupBranch TBZ(ARM64Reg Rt, u8 bit);
|
||||
FixupBranch TBNZ(ARM64Reg Rt, u8 bit);
|
||||
FixupBranch B();
|
||||
FixupBranch BL();
|
||||
[[nodiscard]] FixupBranch CBZ(ARM64Reg Rt);
|
||||
[[nodiscard]] FixupBranch CBNZ(ARM64Reg Rt);
|
||||
[[nodiscard]] FixupBranch B(CCFlags cond);
|
||||
[[nodiscard]] FixupBranch TBZ(ARM64Reg Rt, u8 bit);
|
||||
[[nodiscard]] FixupBranch TBNZ(ARM64Reg Rt, u8 bit);
|
||||
[[nodiscard]] FixupBranch B();
|
||||
[[nodiscard]] FixupBranch BL();
|
||||
|
||||
// Compare and Branch
|
||||
void CBZ(ARM64Reg Rt, const void* ptr);
|
||||
|
|
|
@ -21,15 +21,15 @@ namespace Common
|
|||
class BlockingLoop
|
||||
{
|
||||
public:
|
||||
enum StopMode
|
||||
enum class StopMode
|
||||
{
|
||||
kNonBlock,
|
||||
kBlock,
|
||||
kBlockAndGiveUp,
|
||||
NonBlock,
|
||||
Block,
|
||||
BlockAndGiveUp,
|
||||
};
|
||||
|
||||
BlockingLoop() { m_stopped.Set(); }
|
||||
~BlockingLoop() { Stop(kBlockAndGiveUp); }
|
||||
~BlockingLoop() { Stop(StopMode::BlockAndGiveUp); }
|
||||
// Triggers to rerun the payload of the Run() function at least once again.
|
||||
// This function will never block and is designed to finish as fast as possible.
|
||||
void Wakeup()
|
||||
|
@ -200,7 +200,7 @@ public:
|
|||
// Quits the main loop.
|
||||
// By default, it will wait until the main loop quits.
|
||||
// Be careful to not use the blocking way within the payload of the Run() method.
|
||||
void Stop(StopMode mode = kBlock)
|
||||
void Stop(StopMode mode = StopMode::Block)
|
||||
{
|
||||
if (m_stopped.IsSet())
|
||||
return;
|
||||
|
@ -212,12 +212,12 @@ public:
|
|||
|
||||
switch (mode)
|
||||
{
|
||||
case kNonBlock:
|
||||
case StopMode::NonBlock:
|
||||
break;
|
||||
case kBlock:
|
||||
case StopMode::Block:
|
||||
Wait();
|
||||
break;
|
||||
case kBlockAndGiveUp:
|
||||
case StopMode::BlockAndGiveUp:
|
||||
WaitYield(std::chrono::milliseconds(100), [&] {
|
||||
// If timed out, assume no one will come along to call Run, so force a break
|
||||
m_stopped.Set();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue