Merge pull request #9452 from JosJuice/android-wii-saves

Android: Add "Import Wii Save"
This commit is contained in:
Léo Lam 2021-01-27 23:00:47 +01:00 committed by GitHub
commit 96e3360f05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 324 additions and 60 deletions

View File

@ -444,8 +444,6 @@ public final class NativeLibrary
public static native void ReloadLoggerConfig(); public static native void ReloadLoggerConfig();
public static native boolean InstallWAD(String file);
public static native boolean ConvertDiscImage(String inPath, String outPath, int platform, public static native boolean ConvertDiscImage(String inPath, String outPath, int platform,
int format, int blockSize, int compression, int compressionLevel, boolean scrub, int format, int blockSize, int compression, int compressionLevel, boolean scrub,
CompressCallback callback); CompressCallback callback);

View File

@ -175,21 +175,12 @@ public final class MainActivity extends AppCompatActivity implements MainView
} }
@Override @Override
public void launchOpenFileActivity() public void launchOpenFileActivity(int requestCode)
{ {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); intent.setType("*/*");
startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); startActivityForResult(intent, requestCode);
}
@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);
} }
/** /**
@ -229,6 +220,11 @@ public final class MainActivity extends AppCompatActivity implements MainView
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION,
() -> mPresenter.installWAD(result.getData().toString())); () -> mPresenter.installWAD(result.getData().toString()));
break; break;
case MainPresenter.REQUEST_WII_SAVE_FILE:
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION,
() -> mPresenter.importWiiSave(result.getData().toString()));
break;
} }
} }
else else

View File

@ -7,24 +7,26 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.Uri; import android.net.Uri;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.BuildConfig;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.model.GameFileCache; import org.dolphinemu.dolphinemu.model.GameFileCache;
import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; 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.ContentHandler;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.WiiUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
public final class MainPresenter 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_GAME_FILE = 2;
public static final int REQUEST_SD_FILE = 3; public static final int REQUEST_SD_FILE = 3;
public static final int REQUEST_WAD_FILE = 4; public static final int REQUEST_WAD_FILE = 4;
public static final int REQUEST_WII_SAVE_FILE = 5;
private final MainView mView; private final MainView mView;
private final Context mContext; private final Context mContext;
@ -92,11 +95,17 @@ public final class MainPresenter
return true; return true;
case R.id.menu_open_file: case R.id.menu_open_file:
mView.launchOpenFileActivity(); mView.launchOpenFileActivity(REQUEST_GAME_FILE);
return true; return true;
case R.id.menu_install_wad: 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; return true;
} }
@ -150,32 +159,98 @@ public final class MainPresenter
mDirToAdd = uri.toString(); 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; final Activity mainPresenterActivity = (Activity) mContext;
AlertDialog dialog = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase).create(); CompletableFuture<Boolean> canOverwriteFuture = new CompletableFuture<>();
dialog.setTitle("Installing WAD");
dialog.setMessage("Installing...");
dialog.setCancelable(false);
dialog.show();
Thread installWADThread = new Thread(() -> runOnThreadAndShowResult(R.string.import_in_progress, () ->
{ {
if (NativeLibrary.InstallWAD(file)) BooleanSupplier canOverwrite = () ->
{ {
mainPresenterActivity.runOnUiThread( mainPresenterActivity.runOnUiThread(() ->
() -> Toast.makeText(mContext, R.string.wad_install_success, Toast.LENGTH_SHORT) {
.show()); AlertDialog.Builder builder =
} new AlertDialog.Builder(mContext, R.style.DolphinDialogBase);
else 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( case WiiUtils.RESULT_SUCCESS:
() -> Toast.makeText(mContext, R.string.wad_install_failure, Toast.LENGTH_SHORT) message = R.string.wii_save_import_success;
.show()); 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); return mContext.getResources().getString(message);
}, "InstallWAD"); });
installWADThread.start(); }
private void runOnThreadAndShowResult(int progressMessage, Supplier<String> 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();
} }
} }

View File

@ -21,9 +21,7 @@ public interface MainView
void launchFileListActivity(); void launchFileListActivity();
void launchOpenFileActivity(); void launchOpenFileActivity(int requestCode);
void launchInstallWAD();
/** /**
* To be called when the game file cache is updated. * To be called when the game file cache is updated.

View File

@ -180,21 +180,12 @@ public final class TvMainActivity extends FragmentActivity implements MainView
} }
@Override @Override
public void launchOpenFileActivity() public void launchOpenFileActivity(int requestCode)
{ {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); intent.setType("*/*");
startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); startActivityForResult(intent, requestCode);
}
@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);
} }
@Override @Override
@ -253,6 +244,11 @@ public final class TvMainActivity extends FragmentActivity implements MainView
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION,
() -> mPresenter.installWAD(result.getData().toString())); () -> mPresenter.installWAD(result.getData().toString()));
break; break;
case MainPresenter.REQUEST_WII_SAVE_FILE:
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION,
() -> mPresenter.importWiiSave(result.getData().toString()));
break;
} }
} }
else else
@ -353,6 +349,10 @@ public final class TvMainActivity extends FragmentActivity implements MainView
R.drawable.ic_folder, R.drawable.ic_folder,
R.string.grid_menu_install_wad)); 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. // Create a header for this row.
HeaderItem header = HeaderItem header =
new HeaderItem(R.string.preferences_settings, getString(R.string.preferences_settings)); new HeaderItem(R.string.preferences_settings, getString(R.string.preferences_settings));

View File

@ -0,0 +1,6 @@
package org.dolphinemu.dolphinemu.utils;
public interface BooleanSupplier
{
boolean get();
}

View File

@ -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<T> implements Future<T>
{
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();
}
}
}

View File

@ -37,6 +37,9 @@ public final class FileBrowserHelper
GAME_LIKE_EXTENSIONS.add("dff"); GAME_LIKE_EXTENSIONS.add("dff");
} }
public static final HashSet<String> BIN_EXTENSION = new HashSet<>(Collections.singletonList(
"bin"));
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList( public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw")); "raw"));

View File

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

View File

@ -25,4 +25,9 @@
android:title="@string/grid_menu_install_wad" android:title="@string/grid_menu_install_wad"
app:showAsAction="never"/> app:showAsAction="never"/>
<item
android:id="@+id/menu_import_wii_save"
android:title="@string/grid_menu_import_wii_save"
app:showAsAction="never"/>
</menu> </menu>

View File

@ -329,8 +329,15 @@
<string name="grid_menu_refresh">Refresh Library</string> <string name="grid_menu_refresh">Refresh Library</string>
<string name="grid_menu_open_file">Open File</string> <string name="grid_menu_open_file">Open File</string>
<string name="grid_menu_install_wad">Install WAD</string> <string name="grid_menu_install_wad">Install WAD</string>
<string name="grid_menu_import_wii_save">Import Wii Save</string>
<string name="import_in_progress">Importing...</string>
<string name="wad_install_success">Successfully installed this title to the NAND.</string> <string name="wad_install_success">Successfully installed this title to the NAND.</string>
<string name="wad_install_failure">Failed to install this title to the NAND.</string> <string name="wad_install_failure">Failed to install this title to the NAND.</string>
<string name="wii_save_exists">Save data for this title already exists in the NAND. Consider backing up the current data before overwriting.\nOverwrite now?</string>
<string name="wii_save_import_success">Successfully imported save file.</string>
<string name="wii_save_import_error">Failed to import save file. Your NAND may be corrupt, or something is preventing access to files within it.</string>
<string name="wii_save_import_corruped_source">Failed to import save file. The given file appears to be corrupted or is not a valid Wii save.</string>
<string name="wii_save_import_title_missing">Failed to import save file. Please launch the game once, then try again.</string>
<!-- Game Properties Screen --> <!-- Game Properties Screen -->
<string name="properties_details">Details</string> <string name="properties_details">Details</string>

View File

@ -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_prefix_length;
static jmethodID s_network_helper_get_network_gateway; static jmethodID s_network_helper_get_network_gateway;
static jclass s_boolean_supplier_class;
static jmethodID s_boolean_supplier_get;
namespace IDCache namespace IDCache
{ {
JNIEnv* GetEnvForThread() JNIEnv* GetEnvForThread()
@ -261,6 +264,11 @@ jmethodID GetNetworkHelperGetNetworkGateway()
return s_network_helper_get_network_gateway; return s_network_helper_get_network_gateway;
} }
jmethodID GetBooleanSupplierGet()
{
return s_boolean_supplier_get;
}
} // namespace IDCache } // namespace IDCache
#ifdef __cplusplus #ifdef __cplusplus
@ -361,6 +369,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_network_helper_get_network_gateway = s_network_helper_get_network_gateway =
env->GetStaticMethodID(s_network_helper_class, "GetNetworkGateway", "()I"); 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<jclass>(env->NewGlobalRef(boolean_supplier_class));
s_boolean_supplier_get = env->GetMethodID(s_boolean_supplier_class, "get", "()Z");
return JNI_VERSION; return JNI_VERSION;
} }

View File

@ -56,4 +56,6 @@ jmethodID GetNetworkHelperGetNetworkIpAddress();
jmethodID GetNetworkHelperGetNetworkPrefixLength(); jmethodID GetNetworkHelperGetNetworkPrefixLength();
jmethodID GetNetworkHelperGetNetworkGateway(); jmethodID GetNetworkHelperGetNetworkGateway();
jmethodID GetBooleanSupplierGet();
} // namespace IDCache } // namespace IDCache

View File

@ -5,6 +5,7 @@ add_library(main SHARED
IniFile.cpp IniFile.cpp
MainAndroid.cpp MainAndroid.cpp
NativeConfig.cpp NativeConfig.cpp
WiiUtils.cpp
) )
target_link_libraries(main target_link_libraries(main

View File

@ -45,7 +45,6 @@
#include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/PowerPC.h"
#include "Core/PowerPC/Profiler.h" #include "Core/PowerPC/Profiler.h"
#include "Core/State.h" #include "Core/State.h"
#include "Core/WiiUtils.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
@ -596,14 +595,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReloadLogger
Common::Log::LogManager::Init(); 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<jboolean>(WiiUtils::InstallWAD(path));
}
JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ConvertDiscImage( JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ConvertDiscImage(
JNIEnv* env, jclass, jstring jInPath, jstring jOutPath, jint jPlatform, jint jFormat, JNIEnv* env, jclass, jstring jInPath, jstring jOutPath, jint jPlatform, jint jFormat,
jint jBlockSize, jint jCompression, jint jCompressionLevel, jboolean jScrub, jobject jCallback) jint jBlockSize, jint jCompression, jint jCompressionLevel, jboolean jScrub, jobject jCallback)

View File

@ -0,0 +1,59 @@
// Copyright 2021 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <string>
#include <jni.h>
#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<int>(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<jboolean>(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<bool>(env->CallBooleanMethod(jCanOverwrite, get));
};
return ConvertCopyResult(WiiSave::Import(path, can_overwrite));
}
}

View File

@ -39,6 +39,7 @@ enum class CopyResult
Cancelled, Cancelled,
CorruptedSource, CorruptedSource,
TitleMissing, TitleMissing,
NumberOfEntries
}; };
CopyResult Copy(Storage* source, Storage* destination); CopyResult Copy(Storage* source, Storage* destination);