Merge branch 'master' of https://github.com/dolphin-emu/dolphin into dolphin-emu-master

This commit is contained in:
Nayla Hanegan 2023-06-23 12:58:43 -04:00
commit 6280ce45a7
No known key found for this signature in database
GPG key ID: 3075216CED0DB01D
415 changed files with 14795 additions and 9405 deletions

3
.gitmodules vendored
View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -14,3 +14,4 @@
[Video_Hacks]
EFBToTextureEnable = False
DeferEFBCopies = False

View file

@ -2,3 +2,4 @@
[Video_Settings]
SuggestedAspectRatio = 2
SafeTextureCacheColorSamples = 0

View file

@ -49,6 +49,10 @@
{
"type": "efb",
"texture_filename": "efb1_n000000_256x256_1"
},
{
"type": "efb",
"texture_filename": "efb1_n000000_512x512_1"
}
]
}

View 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);
}

View file

@ -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

@ -1 +1 @@
Subproject commit 376baafde6cce2f8892c34c17ed397afa6c46d08
Subproject commit 495517af2b922c10c24f543e0fd6ea3ddf774e50

1
Externals/libadrenotools vendored Submodule

@ -0,0 +1 @@
Subproject commit f4ce3c9618e7ecfcdd238b17dad9a0b888f5de90

View file

@ -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'
}

View file

@ -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

View file

@ -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"/>

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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"
}
}

View file

@ -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;
}
}

View file

@ -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"
}
}

View file

@ -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;
}
}

View file

@ -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
)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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?
}

View file

@ -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")
}
}

View file

@ -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)

View file

@ -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())
}
}

View file

@ -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 {

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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!!)
}
}
}

View file

@ -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"

View file

@ -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"

View file

@ -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()
}

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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) {
}
}
}
}

View file

@ -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();
}
}
}

View file

@ -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
}
}
}

View file

@ -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));
}
}
}

View file

@ -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
}
}
}

View file

@ -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,
)

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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
)
}
}

View file

@ -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);
}
}

View file

@ -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)
}
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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;
});
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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
}
}
}

View file

@ -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();
}

View file

@ -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()
}

View file

@ -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();
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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)
}
}

View file

@ -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;
}
}

View file

@ -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]
}
}
}

View file

@ -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;
});
}
}

View file

@ -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
}
}
}

View file

@ -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();
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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);
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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 20032015+ 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>

View file

@ -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

View file

@ -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);

View file

@ -32,6 +32,10 @@ jclass GetLinkedHashMapClass();
jmethodID GetLinkedHashMapInit();
jmethodID GetLinkedHashMapPut();
jclass GetHashMapClass();
jmethodID GetHashMapInit();
jmethodID GetHashMapPut();
jclass GetIniFileClass();
jfieldID GetIniFilePointer();
jclass GetIniFileSectionClass();

View file

@ -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/)

View file

@ -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);
}
}

View 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
}

View 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
View 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;
};

View 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));
}
}

View file

@ -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); });

View file

@ -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,

View file

@ -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;
}

View file

@ -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);

View file

@ -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