diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml
index 7298c10137..56d6eadfbf 100644
--- a/Source/Android/app/src/main/AndroidManifest.xml
+++ b/Source/Android/app/src/main/AndroidManifest.xml
@@ -96,6 +96,10 @@
+
+
impl
return true;
}
- GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile.getPath(),
- gameId, holder.gameFile.getRevision(), holder.gameFile.getPlatform());
-
+ GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile);
((FragmentActivity) view.getContext()).getSupportFragmentManager().beginTransaction()
.add(fragment, GamePropertiesDialog.TAG).commit();
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java
index 6790f331b2..aa600173f4 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java
@@ -100,9 +100,7 @@ public final class GameRowPresenter extends Presenter
return true;
}
- GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile.getPath(),
- gameId, holder.gameFile.getRevision(), holder.gameFile.getPlatform());
-
+ GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile);
((FragmentActivity) view.getContext()).getSupportFragmentManager().beginTransaction()
.add(fragment, GamePropertiesDialog.TAG).commit();
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
index 15521e6043..6480b24c2c 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
@@ -9,11 +9,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.activities.ConvertActivity;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
+import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
+import org.dolphinemu.dolphinemu.utils.AlertDialogItemsBuilder;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.Log;
@@ -22,21 +25,22 @@ import java.io.File;
public class GamePropertiesDialog extends DialogFragment
{
public static final String TAG = "GamePropertiesDialog";
- public static final String ARG_PATH = "path";
- public static final String ARG_GAMEID = "game_id";
+ private static final String ARG_PATH = "path";
+ private static final String ARG_GAMEID = "game_id";
public static final String ARG_REVISION = "revision";
- public static final String ARG_PLATFORM = "platform";
+ private static final String ARG_PLATFORM = "platform";
+ private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion";
- public static GamePropertiesDialog newInstance(String path, String gameId, int revision,
- int platform)
+ public static GamePropertiesDialog newInstance(GameFile gameFile)
{
GamePropertiesDialog fragment = new GamePropertiesDialog();
Bundle arguments = new Bundle();
- arguments.putString(ARG_PATH, path);
- arguments.putString(ARG_GAMEID, gameId);
- arguments.putInt(ARG_REVISION, revision);
- arguments.putInt(ARG_PLATFORM, platform);
+ arguments.putString(ARG_PATH, gameFile.getPath());
+ arguments.putString(ARG_GAMEID, gameFile.getGameId());
+ arguments.putInt(ARG_REVISION, gameFile.getRevision());
+ arguments.putInt(ARG_PLATFORM, gameFile.getPlatform());
+ arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion());
fragment.setArguments(arguments);
return fragment;
@@ -46,59 +50,60 @@ public class GamePropertiesDialog extends DialogFragment
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
- AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
- R.style.DolphinDialogBase);
-
String path = requireArguments().getString(ARG_PATH);
String gameId = requireArguments().getString(ARG_GAMEID);
int revision = requireArguments().getInt(ARG_REVISION);
int platform = requireArguments().getInt(ARG_PLATFORM);
+ boolean shouldAllowConversion = requireArguments().getBoolean(ARG_SHOULD_ALLOW_CONVERSION);
+ AlertDialogItemsBuilder itemsBuilder = new AlertDialogItemsBuilder(requireContext());
+
+ itemsBuilder.add(R.string.properties_details, (dialog, i) ->
+ GameDetailsDialog.newInstance(path).show(requireActivity()
+ .getSupportFragmentManager(), "game_details"));
+
+ if (shouldAllowConversion)
+ {
+ itemsBuilder.add(R.string.properties_convert, (dialog, i) ->
+ ConvertActivity.launch(getContext(), path));
+ }
+
+ itemsBuilder.add(R.string.properties_set_default_iso, (dialog, i) ->
+ {
+ try (Settings settings = new Settings())
+ {
+ settings.loadSettings(null);
+ StringSetting.MAIN_DEFAULT_ISO.setString(settings, path);
+ settings.saveSettings(null, getContext());
+ }
+ });
+
+ itemsBuilder.add(R.string.properties_core_settings, (dialog, i) ->
+ SettingsActivity.launch(getContext(), MenuTag.CONFIG, gameId, revision));
+
+ itemsBuilder.add(R.string.properties_gfx_settings, (dialog, i) ->
+ SettingsActivity.launch(getContext(), MenuTag.GRAPHICS, gameId, revision));
+
+ itemsBuilder.add(R.string.properties_gc_controller, (dialog, i) ->
+ SettingsActivity.launch(getContext(), MenuTag.GCPAD_TYPE, gameId, revision));
+
+ if (platform != Platform.GAMECUBE.toInt())
+ {
+ itemsBuilder.add(R.string.properties_wii_controller, (dialog, i) ->
+ SettingsActivity.launch(getActivity(), MenuTag.WIIMOTE, gameId, revision));
+ }
+
+ itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) ->
+ clearGameSettings(gameId));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
+ R.style.DolphinDialogBase);
+ itemsBuilder.applyToBuilder(builder);
builder.setTitle(requireContext()
- .getString(R.string.preferences_game_properties) + ": " + gameId)
- .setItems(platform == Platform.GAMECUBE.toInt() ?
- R.array.gameSettingsMenusGC :
- R.array.gameSettingsMenusWii, (dialog, which) ->
- {
- switch (which)
- {
- case 0:
- GameDetailsDialog.newInstance(path).show((requireActivity())
- .getSupportFragmentManager(), "game_details");
- break;
- case 1:
- try (Settings settings = new Settings())
- {
- settings.loadSettings(null);
- StringSetting.MAIN_DEFAULT_ISO.setString(settings, path);
- settings.saveSettings(null, getContext());
- }
- break;
- case 2:
- SettingsActivity.launch(getContext(), MenuTag.CONFIG, gameId, revision);
- break;
- case 3:
- SettingsActivity.launch(getContext(), MenuTag.GRAPHICS, gameId, revision);
- break;
- case 4:
- SettingsActivity.launch(getContext(), MenuTag.GCPAD_TYPE, gameId, revision);
- break;
- case 5:
- // Clear option for GC, Wii controls for else
- if (platform == Platform.GAMECUBE.toInt())
- clearGameSettings(gameId);
- else
- SettingsActivity.launch(getActivity(), MenuTag.WIIMOTE, gameId, revision);
- break;
- case 6:
- clearGameSettings(gameId);
- break;
- }
- });
+ .getString(R.string.preferences_game_properties) + ": " + gameId);
return builder.create();
}
-
private void clearGameSettings(String gameId)
{
String gameSettingsPath =
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.java
new file mode 100644
index 0000000000..ac1237ba59
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.java
@@ -0,0 +1,487 @@
+package org.dolphinemu.dolphinemu.fragments;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.Spinner;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.model.GameFile;
+import org.dolphinemu.dolphinemu.services.GameFileCacheService;
+import org.dolphinemu.dolphinemu.ui.platform.Platform;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
+public class ConvertFragment extends Fragment implements View.OnClickListener
+{
+ private static class SpinnerValue implements AdapterView.OnItemSelectedListener
+ {
+ private int mValuesId = -1;
+ private int mCurrentPosition = -1;
+ private ArrayList mCallbacks = new ArrayList<>();
+
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int position, long id)
+ {
+ if (mCurrentPosition != position)
+ setPosition(position);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView)
+ {
+ mCurrentPosition = -1;
+ }
+
+ int getPosition()
+ {
+ return mCurrentPosition;
+ }
+
+ void setPosition(int position)
+ {
+ mCurrentPosition = position;
+ for (Runnable callback : mCallbacks)
+ callback.run();
+ }
+
+ void populate(int valuesId)
+ {
+ mValuesId = valuesId;
+ }
+
+ boolean hasValues()
+ {
+ return mValuesId != -1;
+ }
+
+ int getValue(Context context)
+ {
+ return context.getResources().getIntArray(mValuesId)[mCurrentPosition];
+ }
+
+ int getValueOr(Context context, int defaultValue)
+ {
+ return hasValues() ? getValue(context) : defaultValue;
+ }
+
+ void addCallback(Runnable callback)
+ {
+ mCallbacks.add(callback);
+ }
+ }
+
+ private static final String ARG_GAME_PATH = "game_path";
+
+ private static final String KEY_FORMAT = "convert_format";
+ private static final String KEY_BLOCK_SIZE = "convert_block_size";
+ private static final String KEY_COMPRESSION = "convert_compression";
+ private static final String KEY_COMPRESSION_LEVEL = "convert_compression_level";
+ private static final String KEY_REMOVE_JUNK_DATA = "remove_junk_data";
+
+ private static final int REQUEST_CODE_SAVE_FILE = 0;
+
+ private static final int BLOB_TYPE_PLAIN = 0;
+ private static final int BLOB_TYPE_GCZ = 3;
+ private static final int BLOB_TYPE_WIA = 7;
+ private static final int BLOB_TYPE_RVZ = 8;
+
+ private static final int COMPRESSION_NONE = 0;
+ private static final int COMPRESSION_PURGE = 1;
+ private static final int COMPRESSION_BZIP2 = 2;
+ private static final int COMPRESSION_LZMA = 3;
+ private static final int COMPRESSION_LZMA2 = 4;
+ private static final int COMPRESSION_ZSTD = 5;
+
+ private SpinnerValue mFormat = new SpinnerValue();
+ private SpinnerValue mBlockSize = new SpinnerValue();
+ private SpinnerValue mCompression = new SpinnerValue();
+ private SpinnerValue mCompressionLevel = new SpinnerValue();
+
+ private GameFile gameFile;
+
+ private volatile boolean mCanceled;
+ private volatile Thread mThread = null;
+
+ public static ConvertFragment newInstance(String gamePath)
+ {
+ Bundle args = new Bundle();
+ args.putString(ARG_GAME_PATH, gamePath);
+
+ ConvertFragment fragment = new ConvertFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ gameFile = GameFileCacheService.addOrGet(requireArguments().getString(ARG_GAME_PATH));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.fragment_convert, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState)
+ {
+ populateFormats();
+ populateBlockSize();
+ populateCompression();
+ populateCompressionLevel();
+ populateRemoveJunkData();
+
+ mFormat.addCallback(this::populateBlockSize);
+ mFormat.addCallback(this::populateCompression);
+ mCompression.addCallback(this::populateCompressionLevel);
+ mFormat.addCallback(this::populateRemoveJunkData);
+
+ view.findViewById(R.id.button_convert).setOnClickListener(this);
+
+ if (savedInstanceState != null)
+ {
+ setSpinnerSelection(R.id.spinner_format, mFormat, savedInstanceState.getInt(KEY_FORMAT));
+ setSpinnerSelection(R.id.spinner_block_size, mBlockSize,
+ savedInstanceState.getInt(KEY_BLOCK_SIZE));
+ setSpinnerSelection(R.id.spinner_compression, mCompression,
+ savedInstanceState.getInt(KEY_COMPRESSION));
+ setSpinnerSelection(R.id.spinner_compression_level, mCompressionLevel,
+ savedInstanceState.getInt(KEY_COMPRESSION_LEVEL));
+
+ CheckBox removeJunkData = requireView().findViewById(R.id.checkbox_remove_junk_data);
+ removeJunkData.setChecked(savedInstanceState.getBoolean(KEY_REMOVE_JUNK_DATA));
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState)
+ {
+ outState.putInt(KEY_FORMAT, mFormat.getPosition());
+ outState.putInt(KEY_BLOCK_SIZE, mBlockSize.getPosition());
+ outState.putInt(KEY_COMPRESSION, mCompression.getPosition());
+ outState.putInt(KEY_COMPRESSION_LEVEL, mCompressionLevel.getPosition());
+
+ CheckBox removeJunkData = requireView().findViewById(R.id.checkbox_remove_junk_data);
+ outState.putBoolean(KEY_REMOVE_JUNK_DATA, removeJunkData.isChecked());
+ }
+
+ private void setSpinnerSelection(int id, SpinnerValue valueWrapper, int i)
+ {
+ ((Spinner) requireView().findViewById(id)).setSelection(i);
+ valueWrapper.setPosition(i);
+ }
+
+ @Override
+ public void onStop()
+ {
+ super.onStop();
+
+ mCanceled = true;
+ joinThread();
+ }
+
+ private Spinner populateSpinner(int spinnerId, int entriesId, int valuesId,
+ SpinnerValue valueWrapper)
+ {
+ Spinner spinner = requireView().findViewById(spinnerId);
+
+ ArrayAdapter adapter = ArrayAdapter.createFromResource(requireContext(),
+ entriesId, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+
+ spinner.setEnabled(spinner.getCount() > 1);
+
+ valueWrapper.populate(valuesId);
+ valueWrapper.setPosition(spinner.getSelectedItemPosition());
+ spinner.setOnItemSelectedListener(valueWrapper);
+
+ return spinner;
+ }
+
+ private Spinner clearSpinner(int spinnerId, SpinnerValue valueWrapper)
+ {
+ Spinner spinner = requireView().findViewById(spinnerId);
+
+ spinner.setAdapter(null);
+
+ spinner.setEnabled(false);
+
+ valueWrapper.populate(-1);
+ valueWrapper.setPosition(-1);
+ spinner.setOnItemSelectedListener(valueWrapper);
+
+ return spinner;
+ }
+
+ private void populateFormats()
+ {
+ Spinner spinner = populateSpinner(R.id.spinner_format, R.array.convertFormatEntries,
+ R.array.convertFormatValues, mFormat);
+
+ if (gameFile.getBlobType() == BLOB_TYPE_PLAIN)
+ spinner.setSelection(spinner.getCount() - 1);
+ }
+
+ private void populateBlockSize()
+ {
+ switch (mFormat.getValue(requireContext()))
+ {
+ case BLOB_TYPE_GCZ:
+ // In the equivalent DolphinQt code, we have some logic for avoiding block sizes that can
+ // trigger bugs in Dolphin versions older than 5.0-11893, but it was too annoying to port.
+ // TODO: Port it?
+ populateSpinner(R.id.spinner_block_size, R.array.convertBlockSizeGczEntries,
+ R.array.convertBlockSizeGczValues, mBlockSize);
+ break;
+ case BLOB_TYPE_WIA:
+ populateSpinner(R.id.spinner_block_size, R.array.convertBlockSizeWiaEntries,
+ R.array.convertBlockSizeWiaValues, mBlockSize);
+ break;
+ case BLOB_TYPE_RVZ:
+ populateSpinner(R.id.spinner_block_size, R.array.convertBlockSizeRvzEntries,
+ R.array.convertBlockSizeRvzValues, mBlockSize).setSelection(2);
+ break;
+ default:
+ clearSpinner(R.id.spinner_block_size, mBlockSize);
+ }
+ }
+
+ private void populateCompression()
+ {
+ switch (mFormat.getValue(requireContext()))
+ {
+ case BLOB_TYPE_GCZ:
+ populateSpinner(R.id.spinner_compression, R.array.convertCompressionGczEntries,
+ R.array.convertCompressionGczValues, mCompression);
+ break;
+ case BLOB_TYPE_WIA:
+ populateSpinner(R.id.spinner_compression, R.array.convertCompressionWiaEntries,
+ R.array.convertCompressionWiaValues, mCompression);
+ break;
+ case BLOB_TYPE_RVZ:
+ populateSpinner(R.id.spinner_compression, R.array.convertCompressionRvzEntries,
+ R.array.convertCompressionRvzValues, mCompression).setSelection(4);
+ break;
+ default:
+ clearSpinner(R.id.spinner_compression, mCompression);
+ }
+ }
+
+ private void populateCompressionLevel()
+ {
+ switch (mCompression.getValueOr(requireContext(), COMPRESSION_NONE))
+ {
+ case COMPRESSION_BZIP2:
+ case COMPRESSION_LZMA:
+ case COMPRESSION_LZMA2:
+ populateSpinner(R.id.spinner_compression_level, R.array.convertCompressionLevelEntries,
+ R.array.convertCompressionLevelValues, mCompressionLevel).setSelection(4);
+ break;
+ case COMPRESSION_ZSTD:
+ // TODO: Query DiscIO for the supported compression levels, like we do in DolphinQt?
+ populateSpinner(R.id.spinner_compression_level, R.array.convertCompressionLevelZstdEntries,
+ R.array.convertCompressionLevelZstdValues, mCompressionLevel).setSelection(4);
+ break;
+ default:
+ clearSpinner(R.id.spinner_compression_level, mCompressionLevel);
+ }
+ }
+
+ private void populateRemoveJunkData()
+ {
+ boolean scrubbingAllowed = mFormat.getValue(requireContext()) != BLOB_TYPE_RVZ &&
+ !gameFile.isDatelDisc();
+
+ CheckBox removeJunkData = requireView().findViewById(R.id.checkbox_remove_junk_data);
+ removeJunkData.setEnabled(scrubbingAllowed);
+ if (!scrubbingAllowed)
+ removeJunkData.setChecked(false);
+ }
+
+ private boolean getRemoveJunkData()
+ {
+ CheckBox checkBoxScrub = requireView().findViewById(R.id.checkbox_remove_junk_data);
+ return checkBoxScrub.isChecked();
+ }
+
+ @Override
+ public void onClick(View view)
+ {
+ Context context = requireContext();
+
+ boolean scrub = getRemoveJunkData();
+ int format = mFormat.getValue(context);
+
+ boolean iso_warning = scrub && format == BLOB_TYPE_PLAIN;
+ boolean gcz_warning = !scrub && format == BLOB_TYPE_GCZ && !gameFile.isDatelDisc() &&
+ gameFile.getPlatform() == Platform.WII.toInt();
+
+ if (iso_warning || gcz_warning)
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.DolphinDialogBase);
+ builder.setMessage(iso_warning ? R.string.convert_warning_iso : R.string.convert_warning_gcz)
+ .setPositiveButton(R.string.yes, (dialog, i) ->
+ {
+ dialog.dismiss();
+ showSavePrompt();
+ })
+ .setNegativeButton(R.string.no, (dialog, i) -> dialog.dismiss());
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ else
+ {
+ showSavePrompt();
+ }
+ }
+
+ private void showSavePrompt()
+ {
+ String originalPath = gameFile.getPath();
+
+ StringBuilder path = new StringBuilder(new File(originalPath).getName());
+ int dotIndex = path.lastIndexOf(".");
+ if (dotIndex != -1)
+ path.setLength(dotIndex);
+ switch (mFormat.getValue(requireContext()))
+ {
+ case BLOB_TYPE_PLAIN:
+ path.append(".iso");
+ break;
+ case BLOB_TYPE_GCZ:
+ path.append(".gcz");
+ break;
+ case BLOB_TYPE_WIA:
+ path.append(".wia");
+ break;
+ case BLOB_TYPE_RVZ:
+ path.append(".rvz");
+ break;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("application/octet-stream");
+ intent.putExtra(Intent.EXTRA_TITLE, path.toString());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, originalPath);
+ startActivityForResult(intent, REQUEST_CODE_SAVE_FILE);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == Activity.RESULT_OK)
+ {
+ convert(data.getData().toString());
+ }
+ }
+
+ private void convert(String outPath)
+ {
+ final int PROGRESS_RESOLUTION = 1000;
+
+ Context context = requireContext();
+
+ joinThread();
+
+ mCanceled = false;
+
+ // For some reason, setting R.style.DolphinDialogBase as the theme here gives us white text
+ // on a white background when the device is set to dark mode, so let's not set a theme.
+ ProgressDialog progressDialog = new ProgressDialog(context);
+
+ progressDialog.setTitle(R.string.convert_converting);
+
+ progressDialog.setIndeterminate(false);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.setMax(PROGRESS_RESOLUTION);
+
+ progressDialog.setCancelable(true);
+ progressDialog.setOnCancelListener((dialog) -> mCanceled = true);
+
+ progressDialog.show();
+
+ mThread = new Thread(() ->
+ {
+ boolean success = NativeLibrary.ConvertDiscImage(gameFile.getPath(), outPath,
+ gameFile.getPlatform(), mFormat.getValue(context), mBlockSize.getValueOr(context, 0),
+ mCompression.getValueOr(context, 0), mCompressionLevel.getValueOr(context, 0),
+ getRemoveJunkData(), (text, completion) ->
+ {
+ requireActivity().runOnUiThread(() ->
+ {
+ progressDialog.setMessage(text);
+ progressDialog.setProgress((int) (completion * PROGRESS_RESOLUTION));
+ });
+
+ return !mCanceled;
+ });
+
+ if (!mCanceled)
+ {
+ requireActivity().runOnUiThread(() ->
+ {
+ progressDialog.dismiss();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.DolphinDialogBase);
+ if (success)
+ {
+ builder.setMessage(R.string.convert_success_message)
+ .setCancelable(false)
+ .setPositiveButton(R.string.ok, (dialog, i) ->
+ {
+ dialog.dismiss();
+ requireActivity().finish();
+ });
+ }
+ else
+ {
+ builder.setMessage(R.string.convert_failure_message)
+ .setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss());
+ }
+ AlertDialog alert = builder.create();
+ alert.show();
+ });
+ }
+ });
+
+ mThread.start();
+ }
+
+ private void joinThread()
+ {
+ if (mThread != null)
+ {
+ try
+ {
+ mThread.join();
+ }
+ catch (InterruptedException ignored)
+ {
+ }
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java
index dd1dd2898e..9ac331c2b2 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java
@@ -38,6 +38,8 @@ public class GameFile
public native int getRevision();
+ public native int getBlobType();
+
public native String getBlobTypeString();
public native long getBlockSize();
@@ -46,8 +48,12 @@ public class GameFile
public native boolean shouldShowFileFormatDetails();
+ public native boolean shouldAllowConversion();
+
public native long getFileSize();
+ public native boolean isDatelDisc();
+
public native int[] getBanner();
public native int getBannerWidth();
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AlertDialogItemsBuilder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AlertDialogItemsBuilder.java
new file mode 100644
index 0000000000..0c4f358069
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AlertDialogItemsBuilder.java
@@ -0,0 +1,40 @@
+package org.dolphinemu.dolphinemu.utils;
+
+import androidx.appcompat.app.AlertDialog;
+
+import android.content.Context;
+import android.content.DialogInterface.OnClickListener;
+
+import java.util.ArrayList;
+
+public class AlertDialogItemsBuilder
+{
+ private Context mContext;
+
+ private ArrayList mLabels = new ArrayList<>();
+ private ArrayList mListeners = new ArrayList<>();
+
+ public AlertDialogItemsBuilder(Context context)
+ {
+ mContext = context;
+ }
+
+ public void add(int stringId, OnClickListener listener)
+ {
+ mLabels.add(mContext.getResources().getString(stringId));
+ mListeners.add(listener);
+ }
+
+ public void add(CharSequence label, OnClickListener listener)
+ {
+ mLabels.add(label);
+ mListeners.add(listener);
+ }
+
+ public void applyToBuilder(AlertDialog.Builder builder)
+ {
+ CharSequence[] labels = new CharSequence[mLabels.size()];
+ labels = mLabels.toArray(labels);
+ builder.setItems(labels, (dialog, i) -> mListeners.get(i).onClick(dialog, i));
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompressCallback.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompressCallback.java
new file mode 100644
index 0000000000..83aa8d7919
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CompressCallback.java
@@ -0,0 +1,6 @@
+package org.dolphinemu.dolphinemu.utils;
+
+public interface CompressCallback
+{
+ boolean run(String text, float completion);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
new file mode 100644
index 0000000000..83b9c585c9
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
@@ -0,0 +1,39 @@
+package org.dolphinemu.dolphinemu.utils;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+
+import org.dolphinemu.dolphinemu.DolphinApplication;
+
+import java.io.FileNotFoundException;
+
+public class ContentHandler
+{
+ public static int openFd(String uri, String mode)
+ {
+ try
+ {
+ return DolphinApplication.getAppContext().getContentResolver()
+ .openFileDescriptor(Uri.parse(uri), mode).detachFd();
+ }
+ catch (FileNotFoundException | NullPointerException e)
+ {
+ return -1;
+ }
+ }
+
+ public static boolean delete(String uri)
+ {
+ try
+ {
+ ContentResolver resolver = DolphinApplication.getAppContext().getContentResolver();
+ return DocumentsContract.deleteDocument(resolver, Uri.parse(uri));
+ }
+ catch (FileNotFoundException e)
+ {
+ // Return true because we care about the file not being there, not the actual delete.
+ return true;
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/res/layout-w680dp-land/activity_convert.xml b/Source/Android/app/src/main/res/layout-w680dp-land/activity_convert.xml
new file mode 100644
index 0000000000..bd37c3deb6
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout-w680dp-land/activity_convert.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Android/app/src/main/res/layout/activity_convert.xml b/Source/Android/app/src/main/res/layout/activity_convert.xml
new file mode 100644
index 0000000000..b63559e248
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/activity_convert.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Android/app/src/main/res/layout/fragment_convert.xml b/Source/Android/app/src/main/res/layout/fragment_convert.xml
new file mode 100644
index 0000000000..938c3239a5
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/fragment_convert.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml
index 275f6257a6..2c7655b66b 100644
--- a/Source/Android/app/src/main/res/values/arrays.xml
+++ b/Source/Android/app/src/main/res/values/arrays.xml
@@ -433,24 +433,6 @@
- Classic A
-
- - Details
- - Set as Default ISO
- - Core Settings
- - GFX Settings
- - GameCube Controller Settings
- - Clear Game Settings
-
-
- - Details
- - Set as Default ISO
- - Core Settings
- - GFX Settings
- - GameCube Controller Settings
- - Wii Controller Settings
- - Clear Game Settings
-
-
- Landscape
- Portrait
@@ -467,4 +449,147 @@
- Use Device Sensors (Without Pointer Emulation)
- Don\'t Use Device Sensors
+
+
+ - ISO
+ - GCZ
+ - WIA
+ - RVZ
+
+
+ - 0
+ - 3
+ - 7
+ - 8
+
+
+
+ - 32 KiB
+
+
+ - 32768
+
+
+
+ - 2 MiB
+
+
+ - 2097152
+
+
+
+ - 32 KiB
+ - 64 KiB
+ - 128 KiB
+ - 256 KiB
+ - 512 KiB
+ - 1 MiB
+ - 2 MiB
+
+
+ - 32768
+ - 65536
+ - 131072
+ - 262144
+ - 524288
+ - 1048576
+ - 2097152
+
+
+
+ - Deflate
+
+
+ - 0
+
+
+
+ - No Compression
+ - Purge
+ - bzip2 (slow)
+ - LZMA (slow)
+ - LZMA2 (slow)
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
+
+
+ - No Compression
+ - bzip2 (slow)
+ - LZMA (slow)
+ - LZMA2 (slow)
+ - Zstandard (recommended)
+
+
+ - 0
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ - 10
+ - 11
+ - 12
+ - 13
+ - 14
+ - 15
+ - 16
+ - 17
+ - 18
+ - 19
+ - 20
+ - 21
+ - 22
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index a600725e6e..ce43f7e576 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -324,7 +324,15 @@
Successfully installed this title to the NAND.
Failed to install this title to the NAND.
-
+
+ Details
+ Convert File
+ Set as Default ISO
+ Core Settings
+ GFX Settings
+ GameCube Controller Settings
+ Wii Controller Settings
+ Clear Game Settings
Save and Exit
Settings
Game Properties
@@ -341,6 +349,30 @@
Block Size
No Compression
+
+ Format
+ Block Size
+ Compression
+ Compression Level
+ Remove Junk Data (Irreversible)
+ Convert
+ Converting
+ Removing junk data does not save any space when converting to ISO (unless you package the ISO file in a compressed file format such as ZIP afterwards). Do you want to continue anyway?
+ Converting Wii disc images to GCZ without removing junk data does not save any noticeable amount of space compared to converting to ISO. Do you want to continue anyway?
+ The disc image was successfully converted.
+ Dolphin failed to complete the requested action.
+
+ISO: A simple and robust format which is supported by many programs. It takes up more space
+than any other format.
+\n\nGCZ: A basic compressed format which is compatible with most versions of Dolphin and some
+other programs. It can\'t efficiently compress junk data (unless removed) or encrypted Wii data.
+\n\nWIA: An advanced compressed format which is compatible with Dolphin 5.0-12188 and later,
+and a few other programs. It can efficiently compress encrypted Wii data, but not junk data
+(unless removed).
+\n\nRVZ: An advanced compressed format which is compatible with Dolphin 5.0-12188 and later.
+It can efficiently compress both junk data and encrypted Wii data.
+
+
Pause Emulation
Unpause Emulation
diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp
index 1f9973d451..73405b6d8e 100644
--- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp
+++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp
@@ -11,6 +11,7 @@
#include
#include "Common/StringUtil.h"
+#include "jni/AndroidCommon/IDCache.h"
std::string GetJString(JNIEnv* env, jstring jstr)
{
@@ -40,3 +41,27 @@ std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array)
return result;
}
+
+int OpenAndroidContent(const std::string& uri, const std::string& mode)
+{
+ JNIEnv* env = IDCache::GetEnvForThread();
+ const jint fd = env->CallStaticIntMethod(IDCache::GetContentHandlerClass(),
+ IDCache::GetContentHandlerOpenFd(), ToJString(env, uri),
+ ToJString(env, mode));
+
+ // We can get an IllegalArgumentException when passing an invalid mode
+ if (env->ExceptionCheck())
+ {
+ env->ExceptionDescribe();
+ abort();
+ }
+
+ return fd;
+}
+
+bool DeleteAndroidContent(const std::string& uri)
+{
+ JNIEnv* env = IDCache::GetEnvForThread();
+ return env->CallStaticBooleanMethod(IDCache::GetContentHandlerClass(),
+ IDCache::GetContentHandlerDelete(), ToJString(env, uri));
+}
diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h
index e60678a3e8..ca8245182d 100644
--- a/Source/Android/jni/AndroidCommon/AndroidCommon.h
+++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h
@@ -11,3 +11,6 @@
std::string GetJString(JNIEnv* env, jstring jstr);
jstring ToJString(JNIEnv* env, const std::string& str);
std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array);
+
+int OpenAndroidContent(const std::string& uri, const std::string& mode);
+bool DeleteAndroidContent(const std::string& uri);
diff --git a/Source/Android/jni/AndroidCommon/CMakeLists.txt b/Source/Android/jni/AndroidCommon/CMakeLists.txt
new file mode 100644
index 0000000000..e8bcc22863
--- /dev/null
+++ b/Source/Android/jni/AndroidCommon/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_library(androidcommon STATIC
+ AndroidCommon.cpp
+ AndroidCommon.h
+ IDCache.cpp
+ IDCache.h
+)
+
+target_link_libraries(androidcommon
+PRIVATE
+ android
+ log
+ "-Wl,--no-warn-mismatch"
+ "-Wl,--whole-archive"
+ "-Wl,--no-whole-archive"
+)
diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp
index 8383f8ed68..b4052c42bf 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.cpp
+++ b/Source/Android/jni/AndroidCommon/IDCache.cpp
@@ -36,6 +36,13 @@ static jclass s_ini_file_section_class;
static jfieldID s_ini_file_section_pointer;
static jmethodID s_ini_file_section_constructor;
+static jclass s_compress_cb_class;
+static jmethodID s_compress_cb_run;
+
+static jclass s_content_handler_class;
+static jmethodID s_content_handler_open_fd;
+static jmethodID s_content_handler_delete;
+
namespace IDCache
{
JNIEnv* GetEnvForThread()
@@ -161,6 +168,31 @@ jmethodID GetIniFileSectionConstructor()
return s_ini_file_section_constructor;
}
+jclass GetCompressCallbackClass()
+{
+ return s_compress_cb_class;
+}
+
+jmethodID GetCompressCallbackRun()
+{
+ return s_compress_cb_run;
+}
+
+jclass GetContentHandlerClass()
+{
+ return s_content_handler_class;
+}
+
+jmethodID GetContentHandlerOpenFd()
+{
+ return s_content_handler_open_fd;
+}
+
+jmethodID GetContentHandlerDelete()
+{
+ return s_content_handler_delete;
+}
+
} // namespace IDCache
#ifdef __cplusplus
@@ -223,6 +255,19 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_linked_hash_map_put = env->GetMethodID(
s_linked_hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+ const jclass compress_cb_class =
+ env->FindClass("org/dolphinemu/dolphinemu/utils/CompressCallback");
+ s_compress_cb_class = reinterpret_cast(env->NewGlobalRef(compress_cb_class));
+ s_compress_cb_run = env->GetMethodID(s_compress_cb_class, "run", "(Ljava/lang/String;F)Z");
+
+ const jclass content_handler_class =
+ env->FindClass("org/dolphinemu/dolphinemu/utils/ContentHandler");
+ s_content_handler_class = reinterpret_cast(env->NewGlobalRef(content_handler_class));
+ s_content_handler_open_fd = env->GetStaticMethodID(s_content_handler_class, "openFd",
+ "(Ljava/lang/String;Ljava/lang/String;)I");
+ s_content_handler_delete =
+ env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z");
+
return JNI_VERSION;
}
@@ -239,6 +284,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_linked_hash_map_class);
env->DeleteGlobalRef(s_ini_file_class);
env->DeleteGlobalRef(s_ini_file_section_class);
+ env->DeleteGlobalRef(s_compress_cb_class);
+ env->DeleteGlobalRef(s_content_handler_class);
}
#ifdef __cplusplus
diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h
index 77f49658b8..8fd43ea41d 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.h
+++ b/Source/Android/jni/AndroidCommon/IDCache.h
@@ -38,4 +38,11 @@ jclass GetIniFileSectionClass();
jfieldID GetIniFileSectionPointer();
jmethodID GetIniFileSectionConstructor();
+jclass GetCompressCallbackClass();
+jmethodID GetCompressCallbackRun();
+
+jclass GetContentHandlerClass();
+jmethodID GetContentHandlerOpenFd();
+jmethodID GetContentHandlerDelete();
+
} // namespace IDCache
diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt
index e11e8dde76..2b8329f99a 100644
--- a/Source/Android/jni/CMakeLists.txt
+++ b/Source/Android/jni/CMakeLists.txt
@@ -1,7 +1,6 @@
add_library(main SHARED
- AndroidCommon/AndroidCommon.cpp
- AndroidCommon/IDCache.cpp
GameList/GameFile.cpp
+ GameList/GameFile.h
GameList/GameFileCache.cpp
IniFile.cpp
MainAndroid.cpp
@@ -10,6 +9,7 @@ add_library(main SHARED
target_link_libraries(main
PRIVATE
+ androidcommon
core
uicommon
)
@@ -32,3 +32,5 @@ file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/R
file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/Themes/)
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} main)
+
+add_subdirectory(AndroidCommon)
diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp
index 741cf39b4f..21e8aa24aa 100644
--- a/Source/Android/jni/GameList/GameFile.cpp
+++ b/Source/Android/jni/GameList/GameFile.cpp
@@ -63,6 +63,8 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDiscNumb
jobject obj);
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision(JNIEnv* env,
jobject obj);
+JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobType(JNIEnv* env,
+ jobject obj);
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobTypeString(JNIEnv* env, jobject obj);
JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlockSize(JNIEnv* env,
@@ -71,8 +73,12 @@ JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getCompressionMethod(JNIEnv* env, jobject obj);
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldShowFileFormatDetails(JNIEnv* env, jobject obj);
+JNIEXPORT jboolean JNICALL
+Java_org_dolphinemu_dolphinemu_model_GameFile_shouldAllowConversion(JNIEnv* env, jobject obj);
JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSize(JNIEnv* env,
jobject obj);
+JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_isDatelDisc(JNIEnv* env,
+ jobject obj);
JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env,
jobject obj);
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerWidth(JNIEnv* env,
@@ -154,6 +160,12 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision
return GetRef(env, obj)->GetRevision();
}
+JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobType(JNIEnv* env,
+ jobject obj)
+{
+ return static_cast(GetRef(env, obj)->GetBlobType());
+}
+
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobTypeString(JNIEnv* env, jobject obj)
{
@@ -175,7 +187,13 @@ Java_org_dolphinemu_dolphinemu_model_GameFile_getCompressionMethod(JNIEnv* env,
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldShowFileFormatDetails(JNIEnv* env, jobject obj)
{
- return GetRef(env, obj)->ShouldShowFileFormatDetails();
+ return static_cast(GetRef(env, obj)->ShouldShowFileFormatDetails());
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_dolphinemu_dolphinemu_model_GameFile_shouldAllowConversion(JNIEnv* env, jobject obj)
+{
+ return static_cast(GetRef(env, obj)->ShouldAllowConversion());
}
JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSize(JNIEnv* env,
@@ -184,6 +202,12 @@ JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSiz
return GetRef(env, obj)->GetFileSize();
}
+JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_isDatelDisc(JNIEnv* env,
+ jobject obj)
+{
+ return static_cast(GetRef(env, obj)->IsDatelDisc());
+}
+
JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env,
jobject obj)
{
diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp
index 25df43dc20..c845b9847e 100644
--- a/Source/Android/jni/MainAndroid.cpp
+++ b/Source/Android/jni/MainAndroid.cpp
@@ -18,6 +18,7 @@
#include
#include "Common/AndroidAnalytics.h"
+#include "Common/Assert.h"
#include "Common/CPUDetect.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
@@ -26,6 +27,7 @@
#include "Common/IniFile.h"
#include "Common/Logging/LogManager.h"
#include "Common/MsgHandler.h"
+#include "Common/ScopeGuard.h"
#include "Common/Version.h"
#include "Common/WindowSystemInfo.h"
@@ -45,7 +47,9 @@
#include "Core/State.h"
#include "Core/WiiUtils.h"
+#include "DiscIO/Blob.h"
#include "DiscIO/Enums.h"
+#include "DiscIO/ScrubbedBlob.h"
#include "DiscIO/Volume.h"
#include "InputCommon/ControllerInterface/Android/Android.h"
@@ -663,6 +667,65 @@ JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_InstallW
return static_cast(WiiUtils::InstallWAD(path));
}
+JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ConvertDiscImage(
+ JNIEnv* env, jobject obj, jstring jInPath, jstring jOutPath, jint jPlatform, jint jFormat,
+ jint jBlockSize, jint jCompression, jint jCompressionLevel, jboolean jScrub, jobject jCallback)
+{
+ const std::string in_path = GetJString(env, jInPath);
+ const std::string out_path = GetJString(env, jOutPath);
+ const DiscIO::Platform platform = static_cast(jPlatform);
+ const DiscIO::BlobType format = static_cast(jFormat);
+ const DiscIO::WIARVZCompressionType compression =
+ static_cast(jCompression);
+ const bool scrub = static_cast(jScrub);
+
+ std::unique_ptr blob_reader;
+ if (scrub)
+ blob_reader = DiscIO::ScrubbedBlob::Create(in_path);
+ else
+ blob_reader = DiscIO::CreateBlobReader(in_path);
+
+ if (!blob_reader)
+ return static_cast(false);
+
+ jobject jCallbackGlobal = env->NewGlobalRef(jCallback);
+ Common::ScopeGuard scope_guard([jCallbackGlobal, env] { env->DeleteGlobalRef(jCallbackGlobal); });
+
+ const auto callback = [&jCallbackGlobal](const std::string& text, float completion) {
+ JNIEnv* env = IDCache::GetEnvForThread();
+ return static_cast(env->CallBooleanMethod(
+ jCallbackGlobal, IDCache::GetCompressCallbackRun(), ToJString(env, text), completion));
+ };
+
+ bool success = false;
+
+ switch (format)
+ {
+ case DiscIO::BlobType::PLAIN:
+ success = DiscIO::ConvertToPlain(blob_reader.get(), in_path, out_path, callback);
+ break;
+
+ case DiscIO::BlobType::GCZ:
+ success =
+ DiscIO::ConvertToGCZ(blob_reader.get(), in_path, out_path,
+ platform == DiscIO::Platform::WiiDisc ? 1 : 0, jBlockSize, callback);
+ break;
+
+ case DiscIO::BlobType::WIA:
+ case DiscIO::BlobType::RVZ:
+ success = DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), in_path, out_path,
+ format == DiscIO::BlobType::RVZ, compression,
+ jCompressionLevel, jBlockSize, callback);
+ break;
+
+ default:
+ ASSERT(false);
+ break;
+ }
+
+ return static_cast(success);
+}
+
JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_FormatSize(JNIEnv* env,
jobject obj,
jlong bytes,
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 6b65fc3a7d..015b03f882 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -64,8 +64,8 @@ if (MSVC)
add_definitions(/I${PCH_DIRECTORY})
add_definitions(/Yu${PCH_PATH})
- # Don't include timestamps in binaries
- add_link_options(/Brepro)
+ # Don't include timestamps in binaries
+ add_link_options(/Brepro)
endif()
# These aren't actually needed for C11/C++11
diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt
index a805635af6..9592043954 100644
--- a/Source/Core/Common/CMakeLists.txt
+++ b/Source/Core/Common/CMakeLists.txt
@@ -164,6 +164,11 @@ elseif(WIN32)
if (_M_X86_64)
target_link_libraries(common PRIVATE opengl32.lib)
endif()
+elseif (ANDROID)
+ target_link_libraries(common
+ PRIVATE
+ androidcommon
+ )
endif()
if(ANDROID)
diff --git a/Source/Core/Common/File.cpp b/Source/Core/Common/File.cpp
index 9dfcdbd7d8..a724cbb723 100644
--- a/Source/Core/Common/File.cpp
+++ b/Source/Core/Common/File.cpp
@@ -15,6 +15,13 @@
#include
#endif
+#ifdef ANDROID
+#include
+
+#include "Common/StringUtil.h"
+#include "jni/AndroidCommon/AndroidCommon.h"
+#endif
+
#include "Common/CommonTypes.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
@@ -62,7 +69,21 @@ bool IOFile::Open(const std::string& filename, const char openmode[])
#ifdef _WIN32
m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0;
#else
- m_file = std::fopen(filename.c_str(), openmode);
+#ifdef ANDROID
+ if (StringBeginsWith(filename, "content://"))
+ {
+ // The Java method which OpenAndroidContent passes the mode to does not support the b specifier.
+ // Since we're on POSIX, it's fine to just remove the b.
+ std::string mode_without_b(openmode);
+ mode_without_b.erase(std::remove(mode_without_b.begin(), mode_without_b.end(), 'b'),
+ mode_without_b.end());
+ m_file = fdopen(OpenAndroidContent(filename, mode_without_b), mode_without_b.c_str());
+ }
+ else
+#endif
+ {
+ m_file = std::fopen(filename.c_str(), openmode);
+ }
m_good = m_file != nullptr;
#endif
diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp
index b3efa6b868..3c524ed9d8 100644
--- a/Source/Core/Common/FileUtil.cpp
+++ b/Source/Core/Common/FileUtil.cpp
@@ -46,6 +46,11 @@
#include
#endif
+#ifdef ANDROID
+#include "Common/StringUtil.h"
+#include "jni/AndroidCommon/AndroidCommon.h"
+#endif
+
#ifndef S_ISDIR
#define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
#endif
@@ -132,10 +137,19 @@ bool Delete(const std::string& filename)
{
INFO_LOG(COMMON, "Delete: file %s", filename.c_str());
+#ifdef ANDROID
+ if (StringBeginsWith(filename, "content://"))
+ {
+ const bool success = DeleteAndroidContent(filename);
+ if (!success)
+ WARN_LOG(COMMON, "Delete failed on %s", filename.c_str());
+ return success;
+ }
+#endif
+
const FileInfo file_info(filename);
- // Return true because we care about the file no
- // being there, not the actual delete.
+ // Return true because we care about the file not being there, not the actual delete.
if (!file_info.Exists())
{
WARN_LOG(COMMON, "Delete: %s does not exist", filename.c_str());
diff --git a/Source/Core/DiscIO/Blob.h b/Source/Core/DiscIO/Blob.h
index 473aa9bfce..2166d8c677 100644
--- a/Source/Core/DiscIO/Blob.h
+++ b/Source/Core/DiscIO/Blob.h
@@ -15,6 +15,7 @@
// automatically do the right thing.
#include
+#include
#include
#include
#include
@@ -175,17 +176,16 @@ private:
// Factory function - examines the path to choose the right type of BlobReader, and returns one.
std::unique_ptr CreateBlobReader(const std::string& filename);
-typedef bool (*CompressCB)(const std::string& text, float percent, void* arg);
+using CompressCB = std::function;
bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
- const std::string& outfile_path, u32 sub_type, int sector_size = 16384,
- CompressCB callback = nullptr, void* arg = nullptr);
+ const std::string& outfile_path, u32 sub_type, int sector_size,
+ CompressCB callback);
bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
- const std::string& outfile_path, CompressCB callback = nullptr,
- void* arg = nullptr);
+ const std::string& outfile_path, CompressCB callback);
bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, bool rvz,
WIARVZCompressionType compression_type, int compression_level,
- int chunk_size, CompressCB callback = nullptr, void* arg = nullptr);
+ int chunk_size, CompressCB callback);
} // namespace DiscIO
diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp
index fdfd65baf8..4486b54827 100644
--- a/Source/Core/DiscIO/CompressedBlob.cpp
+++ b/Source/Core/DiscIO/CompressedBlob.cpp
@@ -238,7 +238,7 @@ static ConversionResult Compress(CompressThreadState* state,
static ConversionResultCode Output(OutputParameters parameters, File::IOFile* outfile,
u64* position, std::vector* offsets, int progress_monitor,
- u32 num_blocks, CompressCB callback, void* arg)
+ u32 num_blocks, CompressCB callback)
{
u64 offset = *position;
if (!parameters.compressed)
@@ -261,7 +261,7 @@ static ConversionResultCode Output(OutputParameters parameters, File::IOFile* ou
const float completion = static_cast(parameters.block_number) / num_blocks;
- if (!callback(text, completion, arg))
+ if (!callback(text, completion))
return ConversionResultCode::Canceled;
}
@@ -270,7 +270,7 @@ static ConversionResultCode Output(OutputParameters parameters, File::IOFile* ou
bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, u32 sub_type, int block_size,
- CompressCB callback, void* arg)
+ CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());
@@ -284,7 +284,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
return false;
}
- callback(Common::GetStringT("Files opened, ready to compress."), 0, arg);
+ callback(Common::GetStringT("Files opened, ready to compress."), 0);
CompressedBlobHeader header;
header.magic_cookie = GCZ_MAGIC;
@@ -317,7 +317,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
const auto output = [&](OutputParameters parameters) {
return Output(std::move(parameters), &outfile, &position, &offsets, progress_monitor,
- header.num_blocks, callback, arg);
+ header.num_blocks, callback);
};
MultithreadedCompressor compressor(
@@ -364,7 +364,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
outfile.WriteArray(offsets.data(), header.num_blocks);
outfile.WriteArray(hashes.data(), header.num_blocks);
- callback(Common::GetStringT("Done compressing disc image."), 1.0f, arg);
+ callback(Common::GetStringT("Done compressing disc image."), 1.0f);
}
if (result == ConversionResultCode::ReadFailed)
diff --git a/Source/Core/DiscIO/FileBlob.cpp b/Source/Core/DiscIO/FileBlob.cpp
index ca0aa2cfd0..980b158dc9 100644
--- a/Source/Core/DiscIO/FileBlob.cpp
+++ b/Source/Core/DiscIO/FileBlob.cpp
@@ -42,7 +42,7 @@ bool PlainFileReader::Read(u64 offset, u64 nbytes, u8* out_ptr)
}
bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
- const std::string& outfile_path, CompressCB callback, void* arg)
+ const std::string& outfile_path, CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());
@@ -78,7 +78,7 @@ bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
if (i % progress_monitor == 0)
{
const bool was_cancelled =
- !callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers, arg);
+ !callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers);
if (was_cancelled)
{
success = false;
diff --git a/Source/Core/DiscIO/WIABlob.cpp b/Source/Core/DiscIO/WIABlob.cpp
index 51a9c65694..4d4b104242 100644
--- a/Source/Core/DiscIO/WIABlob.cpp
+++ b/Source/Core/DiscIO/WIABlob.cpp
@@ -1686,9 +1686,9 @@ ConversionResultCode WIARVZFileReader::Output(std::vector
-ConversionResultCode
-WIARVZFileReader::RunCallback(size_t groups_written, u64 bytes_read, u64 bytes_written,
- u32 total_groups, u64 iso_size, CompressCB callback, void* arg)
+ConversionResultCode WIARVZFileReader::RunCallback(size_t groups_written, u64 bytes_read,
+ u64 bytes_written, u32 total_groups,
+ u64 iso_size, CompressCB callback)
{
int ratio = 0;
if (bytes_read != 0)
@@ -1700,8 +1700,8 @@ WIARVZFileReader::RunCallback(size_t groups_written, u64 bytes_read, u64 by
const float completion = static_cast(bytes_read) / iso_size;
- return callback(text, completion, arg) ? ConversionResultCode::Success :
- ConversionResultCode::Canceled;
+ return callback(text, completion) ? ConversionResultCode::Success :
+ ConversionResultCode::Canceled;
}
template
@@ -1729,8 +1729,7 @@ template
ConversionResultCode
WIARVZFileReader::Convert(BlobReader* infile, const VolumeDisc* infile_volume,
File::IOFile* outfile, WIARVZCompressionType compression_type,
- int compression_level, int chunk_size, CompressCB callback,
- void* arg)
+ int compression_level, int chunk_size, CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());
ASSERT(chunk_size > 0);
@@ -1832,7 +1831,7 @@ WIARVZFileReader::Convert(BlobReader* infile, const VolumeDisc* infile_volu
return result;
return RunCallback(parameters.group_index + parameters.entries.size(), parameters.bytes_read,
- bytes_written, total_groups, iso_size, callback, arg);
+ bytes_written, total_groups, iso_size, callback);
};
MultithreadedCompressor mt_compressor(
@@ -2030,7 +2029,7 @@ WIARVZFileReader::Convert(BlobReader* infile, const VolumeDisc* infile_volu
bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, bool rvz,
WIARVZCompressionType compression_type, int compression_level,
- int chunk_size, CompressCB callback, void* arg)
+ int chunk_size, CompressCB callback)
{
File::IOFile outfile(outfile_path, "wb");
if (!outfile)
@@ -2047,7 +2046,7 @@ bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const auto convert = rvz ? RVZFileReader::Convert : WIAFileReader::Convert;
const ConversionResultCode result =
convert(infile, infile_volume.get(), &outfile, compression_type, compression_level,
- chunk_size, callback, arg);
+ chunk_size, callback);
if (result == ConversionResultCode::ReadFailed)
PanicAlertT("Failed to read from the input file \"%s\".", infile_path.c_str());
diff --git a/Source/Core/DiscIO/WIABlob.h b/Source/Core/DiscIO/WIABlob.h
index c76af169f2..5b08dd227c 100644
--- a/Source/Core/DiscIO/WIABlob.h
+++ b/Source/Core/DiscIO/WIABlob.h
@@ -64,8 +64,7 @@ public:
static ConversionResultCode Convert(BlobReader* infile, const VolumeDisc* infile_volume,
File::IOFile* outfile, WIARVZCompressionType compression_type,
- int compression_level, int chunk_size, CompressCB callback,
- void* arg);
+ int compression_level, int chunk_size, CompressCB callback);
private:
using SHA1 = std::array;
@@ -351,8 +350,7 @@ private:
std::mutex* reusable_groups_mutex, GroupEntry* group_entry,
u64* bytes_written);
static ConversionResultCode RunCallback(size_t groups_written, u64 bytes_read, u64 bytes_written,
- u32 total_groups, u64 iso_size, CompressCB callback,
- void* arg);
+ u32 total_groups, u64 iso_size, CompressCB callback);
bool m_valid;
WIARVZCompressionType m_compression_type;
diff --git a/Source/Core/DolphinQt/ConvertDialog.cpp b/Source/Core/DolphinQt/ConvertDialog.cpp
index 5a41ccea36..a14e728c40 100644
--- a/Source/Core/DolphinQt/ConvertDialog.cpp
+++ b/Source/Core/DolphinQt/ConvertDialog.cpp
@@ -32,17 +32,6 @@
#include "UICommon/GameFile.h"
#include "UICommon/UICommon.h"
-static bool CompressCB(const std::string& text, float percent, void* ptr)
-{
- if (ptr == nullptr)
- return false;
-
- auto* progress_dialog = static_cast(ptr);
-
- progress_dialog->SetValue(percent * 100);
- return !progress_dialog->WasCanceled();
-}
-
ConvertDialog::ConvertDialog(QList> files,
QWidget* parent)
: QDialog(parent), m_files(std::move(files))
@@ -463,15 +452,19 @@ void ConvertDialog::Convert()
}
else
{
+ const auto callback = [&progress_dialog](const std::string& text, float percent) {
+ progress_dialog.SetValue(percent * 100);
+ return !progress_dialog.WasCanceled();
+ };
+
std::future success;
switch (format)
{
case DiscIO::BlobType::PLAIN:
success = std::async(std::launch::async, [&] {
- const bool good =
- DiscIO::ConvertToPlain(blob_reader.get(), original_path, dst_path.toStdString(),
- &CompressCB, &progress_dialog);
+ const bool good = DiscIO::ConvertToPlain(blob_reader.get(), original_path,
+ dst_path.toStdString(), callback);
progress_dialog.Reset();
return good;
});
@@ -479,10 +472,9 @@ void ConvertDialog::Convert()
case DiscIO::BlobType::GCZ:
success = std::async(std::launch::async, [&] {
- const bool good =
- DiscIO::ConvertToGCZ(blob_reader.get(), original_path, dst_path.toStdString(),
- file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0,
- block_size, &CompressCB, &progress_dialog);
+ const bool good = DiscIO::ConvertToGCZ(
+ blob_reader.get(), original_path, dst_path.toStdString(),
+ file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0, block_size, callback);
progress_dialog.Reset();
return good;
});
@@ -491,10 +483,10 @@ void ConvertDialog::Convert()
case DiscIO::BlobType::WIA:
case DiscIO::BlobType::RVZ:
success = std::async(std::launch::async, [&] {
- const bool good = DiscIO::ConvertToWIAOrRVZ(
- blob_reader.get(), original_path, dst_path.toStdString(),
- format == DiscIO::BlobType::RVZ, compression, compression_level, block_size,
- &CompressCB, &progress_dialog);
+ const bool good =
+ DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), original_path, dst_path.toStdString(),
+ format == DiscIO::BlobType::RVZ, compression,
+ compression_level, block_size, callback);
progress_dialog.Reset();
return good;
});
diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp
index 956bc704dc..11cbadb2d3 100644
--- a/Source/Core/DolphinQt/GameList/GameList.cpp
+++ b/Source/Core/DolphinQt/GameList/GameList.cpp
@@ -267,9 +267,8 @@ void GameList::ShowContextMenu(const QPoint&)
{
const auto selected_games = GetSelectedGames();
- if (std::all_of(selected_games.begin(), selected_games.end(), [](const auto& game) {
- return DiscIO::IsDisc(game->GetPlatform()) && game->IsVolumeSizeAccurate();
- }))
+ if (std::all_of(selected_games.begin(), selected_games.end(),
+ [](const auto& game) { return game->ShouldAllowConversion(); }))
{
menu->addAction(tr("Convert Selected Files..."), this, &GameList::ConvertFile);
menu->addSeparator();
@@ -301,7 +300,7 @@ void GameList::ShowContextMenu(const QPoint&)
{
menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO);
- if (game->IsVolumeSizeAccurate())
+ if (game->ShouldAllowConversion())
menu->addAction(tr("Convert File..."), this, &GameList::ConvertFile);
QAction* change_disc = menu->addAction(tr("Change &Disc"), this, &GameList::ChangeDisc);
diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp
index 30641d58e5..ff0fa84798 100644
--- a/Source/Core/UICommon/GameFile.cpp
+++ b/Source/Core/UICommon/GameFile.cpp
@@ -672,6 +672,11 @@ std::string GameFile::GetFileFormatName() const
}
}
+bool GameFile::ShouldAllowConversion() const
+{
+ return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate;
+}
+
const GameBanner& GameFile::GetBannerImage() const
{
return m_custom_banner.empty() ? m_volume_banner : m_custom_banner;
diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h
index 9366b33f2e..3aa693507f 100644
--- a/Source/Core/UICommon/GameFile.h
+++ b/Source/Core/UICommon/GameFile.h
@@ -101,6 +101,7 @@ public:
const std::string& GetCompressionMethod() const { return m_compression_method; }
bool ShouldShowFileFormatDetails() const;
std::string GetFileFormatName() const;
+ bool ShouldAllowConversion() const;
const std::string& GetApploaderDate() const { return m_apploader_date; }
u64 GetFileSize() const { return m_file_size; }
u64 GetVolumeSize() const { return m_volume_size; }