diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java index 425551ff77..9a779e7f6b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -444,8 +444,6 @@ public final class NativeLibrary public static native void ReloadLoggerConfig(); - public static native boolean InstallWAD(String file); - public static native boolean ConvertDiscImage(String inPath, String outPath, int platform, int format, int blockSize, int compression, int compressionLevel, boolean scrub, CompressCallback callback); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index 7b4f886d87..2beb2112f2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -175,21 +175,12 @@ public final class MainActivity extends AppCompatActivity implements MainView } @Override - public void launchOpenFileActivity() + public void launchOpenFileActivity(int requestCode) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); - } - - @Override - public void launchInstallWAD() - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, MainPresenter.REQUEST_WAD_FILE); + startActivityForResult(intent, requestCode); } /** @@ -229,6 +220,11 @@ public final class MainActivity extends AppCompatActivity implements MainView 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; } } else diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java index 1ded576794..ebd43513ab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java @@ -7,24 +7,26 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; -import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.dolphinemu.dolphinemu.BuildConfig; -import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; import org.dolphinemu.dolphinemu.model.GameFileCache; import org.dolphinemu.dolphinemu.services.GameFileCacheService; 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.FileBrowserHelper; +import org.dolphinemu.dolphinemu.utils.WiiUtils; import java.util.Arrays; -import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; public final class MainPresenter { @@ -32,6 +34,7 @@ public final class MainPresenter 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; private final MainView mView; private final Context mContext; @@ -92,11 +95,17 @@ public final class MainPresenter return true; case R.id.menu_open_file: - mView.launchOpenFileActivity(); + mView.launchOpenFileActivity(REQUEST_GAME_FILE); return true; case R.id.menu_install_wad: - new AfterDirectoryInitializationRunner().run(context, true, mView::launchInstallWAD); + new AfterDirectoryInitializationRunner().run(context, true, + () -> mView.launchOpenFileActivity(REQUEST_WAD_FILE)); + return true; + + case R.id.menu_import_wii_save: + new AfterDirectoryInitializationRunner().run(context, true, + () -> mView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE)); return true; } @@ -150,32 +159,98 @@ public final class MainPresenter mDirToAdd = uri.toString(); } - public void installWAD(String file) + public void installWAD(String path) + { + runOnThreadAndShowResult(R.string.import_in_progress, () -> + { + boolean success = WiiUtils.installWAD(path); + int message = success ? R.string.wad_install_success : R.string.wad_install_failure; + return mContext.getResources().getString(message); + }); + } + + public void importWiiSave(String path) { final Activity mainPresenterActivity = (Activity) mContext; - AlertDialog dialog = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase).create(); - dialog.setTitle("Installing WAD"); - dialog.setMessage("Installing..."); - dialog.setCancelable(false); - dialog.show(); + CompletableFuture canOverwriteFuture = new CompletableFuture<>(); - Thread installWADThread = new Thread(() -> + runOnThreadAndShowResult(R.string.import_in_progress, () -> { - if (NativeLibrary.InstallWAD(file)) + BooleanSupplier canOverwrite = () -> { - mainPresenterActivity.runOnUiThread( - () -> Toast.makeText(mContext, R.string.wad_install_success, Toast.LENGTH_SHORT) - .show()); - } - else + mainPresenterActivity.runOnUiThread(() -> + { + AlertDialog.Builder builder = + new AlertDialog.Builder(mContext, R.style.DolphinDialogBase); + builder.setMessage(R.string.wii_save_exists); + builder.setCancelable(false); + builder.setPositiveButton(R.string.yes, (dialog, i) -> canOverwriteFuture.complete(true)); + builder.setNegativeButton(R.string.no, (dialog, i) -> canOverwriteFuture.complete(false)); + builder.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) { - mainPresenterActivity.runOnUiThread( - () -> Toast.makeText(mContext, R.string.wad_install_failure, Toast.LENGTH_SHORT) - .show()); + 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; } - mainPresenterActivity.runOnUiThread(dialog::dismiss); - }, "InstallWAD"); - installWADThread.start(); + return mContext.getResources().getString(message); + }); + } + + private void runOnThreadAndShowResult(int progressMessage, Supplier f) + { + final Activity mainPresenterActivity = (Activity) mContext; + + AlertDialog progressDialog = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase) + .create(); + progressDialog.setTitle(progressMessage); + progressDialog.setCancelable(false); + progressDialog.show(); + + new Thread(() -> + { + String result = f.get(); + mainPresenterActivity.runOnUiThread(() -> + { + progressDialog.dismiss(); + + if (result != null) + { + AlertDialog.Builder builder = + new AlertDialog.Builder(mContext, R.style.DolphinDialogBase); + builder.setMessage(result); + builder.setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss()); + builder.show(); + } + }); + }, mContext.getResources().getString(progressMessage)).start(); } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java index fc4ed31513..b2e6b74495 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java @@ -21,9 +21,7 @@ public interface MainView void launchFileListActivity(); - void launchOpenFileActivity(); - - void launchInstallWAD(); + void launchOpenFileActivity(int requestCode); /** * To be called when the game file cache is updated. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 8e96083ce1..d34c3d8518 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -180,21 +180,12 @@ public final class TvMainActivity extends FragmentActivity implements MainView } @Override - public void launchOpenFileActivity() + public void launchOpenFileActivity(int requestCode) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); - } - - @Override - public void launchInstallWAD() - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, MainPresenter.REQUEST_WAD_FILE); + startActivityForResult(intent, requestCode); } @Override @@ -253,6 +244,11 @@ public final class TvMainActivity extends FragmentActivity implements MainView 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; } } else @@ -353,6 +349,10 @@ public final class TvMainActivity extends FragmentActivity implements MainView R.drawable.ic_folder, R.string.grid_menu_install_wad)); + rowItems.add(new TvSettingsItem(R.id.menu_import_wii_save, + R.drawable.ic_folder, + R.string.grid_menu_import_wii_save)); + // Create a header for this row. HeaderItem header = new HeaderItem(R.string.preferences_settings, getString(R.string.preferences_settings)); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/BooleanSupplier.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/BooleanSupplier.java new file mode 100644 index 0000000000..d1c490fe2a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/BooleanSupplier.java @@ -0,0 +1,6 @@ +package org.dolphinemu.dolphinemu.utils; + +public interface BooleanSupplier +{ + boolean get(); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompletableFuture.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompletableFuture.java new file mode 100644 index 0000000000..bdd1b02faf --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompletableFuture.java @@ -0,0 +1,95 @@ +package org.dolphinemu.dolphinemu.utils; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Simplified re-implementation of a subset of {@link java.util.concurrent.CompletableFuture}. + * Replace this class with that class once we have full Java 8 support (once we require API 24). + */ +public class CompletableFuture implements Future +{ + private final Lock lock = new ReentrantLock(); + private final Condition done = lock.newCondition(); + + private boolean isDone = false; + private T result = null; + + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCancelled() + { + return false; + } + + @Override + public boolean isDone() + { + return isDone; + } + + @Override + public T get() throws ExecutionException, InterruptedException + { + lock.lock(); + try + { + while (!isDone) + done.await(); + + return result; + } + finally + { + lock.unlock(); + } + } + + @Override + public T get(long timeout, TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException + { + lock.lock(); + try + { + while (!isDone) + { + if (!done.await(timeout, unit)) + throw new TimeoutException(); + } + + return result; + } + finally + { + lock.unlock(); + } + } + + public boolean complete(T value) + { + lock.lock(); + try + { + boolean wasDone = isDone; + result = value; + isDone = true; + done.signalAll(); + return !wasDone; + } + finally + { + lock.unlock(); + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index f68bc46857..94b10806df 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -37,6 +37,9 @@ public final class FileBrowserHelper GAME_LIKE_EXTENSIONS.add("dff"); } + public static final HashSet BIN_EXTENSION = new HashSet<>(Collections.singletonList( + "bin")); + public static final HashSet RAW_EXTENSION = new HashSet<>(Collections.singletonList( "raw")); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/WiiUtils.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/WiiUtils.java new file mode 100644 index 0000000000..9a6440c9e5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/WiiUtils.java @@ -0,0 +1,14 @@ +package org.dolphinemu.dolphinemu.utils; + +public final class WiiUtils +{ + public static final int RESULT_SUCCESS = 0; + public static final int RESULT_ERROR = 1; + public static final int RESULT_CANCELLED = 2; + public static final int RESULT_CORRUPTED_SOURCE = 3; + public static final int RESULT_TITLE_MISSING = 4; + + public static native boolean installWAD(String file); + + public static native int importWiiSave(String file, BooleanSupplier canOverwrite); +} diff --git a/Source/Android/app/src/main/res/menu/menu_game_grid.xml b/Source/Android/app/src/main/res/menu/menu_game_grid.xml index f4a489e824..6b0dde4aa2 100644 --- a/Source/Android/app/src/main/res/menu/menu_game_grid.xml +++ b/Source/Android/app/src/main/res/menu/menu_game_grid.xml @@ -25,4 +25,9 @@ android:title="@string/grid_menu_install_wad" app:showAsAction="never"/> + + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 0dc8701e90..6a53b7ce3c 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -329,8 +329,15 @@ Refresh Library Open File Install WAD + Import Wii Save + Importing... Successfully installed this title to the NAND. Failed to install this title to the NAND. + Save data for this title already exists in the NAND. Consider backing up the current data before overwriting.\nOverwrite now? + Successfully imported save file. + Failed to import save file. Your NAND may be corrupt, or something is preventing access to files within it. + Failed to import save file. The given file appears to be corrupted or is not a valid Wii save. + Failed to import save file. Please launch the game once, then try again. Details diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 4c6c4b2ce7..b5ca604c6e 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -56,6 +56,9 @@ static jmethodID s_network_helper_get_network_ip_address; static jmethodID s_network_helper_get_network_prefix_length; static jmethodID s_network_helper_get_network_gateway; +static jclass s_boolean_supplier_class; +static jmethodID s_boolean_supplier_get; + namespace IDCache { JNIEnv* GetEnvForThread() @@ -261,6 +264,11 @@ jmethodID GetNetworkHelperGetNetworkGateway() return s_network_helper_get_network_gateway; } +jmethodID GetBooleanSupplierGet() +{ + return s_boolean_supplier_get; +} + } // namespace IDCache #ifdef __cplusplus @@ -361,6 +369,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) s_network_helper_get_network_gateway = env->GetStaticMethodID(s_network_helper_class, "GetNetworkGateway", "()I"); + const jclass boolean_supplier_class = + env->FindClass("org/dolphinemu/dolphinemu/utils/BooleanSupplier"); + s_boolean_supplier_class = reinterpret_cast(env->NewGlobalRef(boolean_supplier_class)); + s_boolean_supplier_get = env->GetMethodID(s_boolean_supplier_class, "get", "()Z"); + return JNI_VERSION; } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index b633267f55..b0e7a3d814 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -56,4 +56,6 @@ jmethodID GetNetworkHelperGetNetworkIpAddress(); jmethodID GetNetworkHelperGetNetworkPrefixLength(); jmethodID GetNetworkHelperGetNetworkGateway(); +jmethodID GetBooleanSupplierGet(); + } // namespace IDCache diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index 7f31a144aa..405d621958 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(main SHARED IniFile.cpp MainAndroid.cpp NativeConfig.cpp + WiiUtils.cpp ) target_link_libraries(main diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 71c4fac565..ad91ce17c5 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -45,7 +45,6 @@ #include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/Profiler.h" #include "Core/State.h" -#include "Core/WiiUtils.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" @@ -596,14 +595,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReloadLogger Common::Log::LogManager::Init(); } -JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_InstallWAD(JNIEnv* env, - jclass, - jstring jFile) -{ - const std::string path = GetJString(env, jFile); - return static_cast(WiiUtils::InstallWAD(path)); -} - JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ConvertDiscImage( JNIEnv* env, jclass, jstring jInPath, jstring jOutPath, jint jPlatform, jint jFormat, jint jBlockSize, jint jCompression, jint jCompressionLevel, jboolean jScrub, jobject jCallback) diff --git a/Source/Android/jni/WiiUtils.cpp b/Source/Android/jni/WiiUtils.cpp new file mode 100644 index 0000000000..9b0601c1df --- /dev/null +++ b/Source/Android/jni/WiiUtils.cpp @@ -0,0 +1,59 @@ +// Copyright 2021 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include + +#include + +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" + +#include "Core/HW/WiiSave.h" +#include "Core/WiiUtils.h" + +// The hardcoded values here must match WiiUtils.java +static jint ConvertCopyResult(WiiSave::CopyResult result) +{ + switch (result) + { + case WiiSave::CopyResult::Success: + return 0; + case WiiSave::CopyResult::Error: + return 1; + case WiiSave::CopyResult::Cancelled: + return 2; + case WiiSave::CopyResult::CorruptedSource: + return 3; + case WiiSave::CopyResult::TitleMissing: + return 4; + default: + ASSERT(false); + return 1; + } + + static_assert(static_cast(WiiSave::CopyResult::NumberOfEntries) == 5); +} + +extern "C" { + +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_utils_WiiUtils_installWAD(JNIEnv* env, + jclass, + jstring jFile) +{ + const std::string path = GetJString(env, jFile); + return static_cast(WiiUtils::InstallWAD(path)); +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_utils_WiiUtils_importWiiSave( + JNIEnv* env, jclass, jstring jFile, jobject jCanOverwrite) +{ + const std::string path = GetJString(env, jFile); + const auto can_overwrite = [&] { + const jmethodID get = IDCache::GetBooleanSupplierGet(); + return static_cast(env->CallBooleanMethod(jCanOverwrite, get)); + }; + + return ConvertCopyResult(WiiSave::Import(path, can_overwrite)); +} +} diff --git a/Source/Core/Core/HW/WiiSave.h b/Source/Core/Core/HW/WiiSave.h index f7d414586a..423f72b098 100644 --- a/Source/Core/Core/HW/WiiSave.h +++ b/Source/Core/Core/HW/WiiSave.h @@ -39,6 +39,7 @@ enum class CopyResult Cancelled, CorruptedSource, TitleMissing, + NumberOfEntries }; CopyResult Copy(Storage* source, Storage* destination);