diff options
| author | JMC47 <JMC4789@gmail.com> | 2020-09-23 12:27:43 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-09-23 12:27:43 -0400 |
| commit | 54e570a95f2cb54fdc7be29d04870746c69a8b9e (patch) | |
| tree | 1db68bbdaa591d6cd8025493478fa5d0dadf7980 /Source/Android | |
| parent | f33767f19cca6ee95af9727f0050f27e2571f770 (diff) | |
| parent | 5ba083215852a7f1afa2ac73316c2c0c500a6e7d (diff) | |
Merge pull request #8902 from JosJuice/android-convert
Android: Add disc image conversion
Diffstat (limited to 'Source/Android')
24 files changed, 1273 insertions, 81 deletions
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 @@ </intent-filter> </activity> + <activity + android:name=".activities.ConvertActivity" + android:theme="@style/DolphinBase" /> + <service android:name=".utils.DirectoryInitialization"/> <service android:name=".services.GameFileCacheService"/> <service 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 a03376bb8a..757aa74dfc 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 @@ -12,6 +12,7 @@ import android.view.Surface; import androidx.appcompat.app.AlertDialog; import org.dolphinemu.dolphinemu.activities.EmulationActivity; +import org.dolphinemu.dolphinemu.utils.CompressCallback; import org.dolphinemu.dolphinemu.utils.Log; import org.dolphinemu.dolphinemu.utils.Rumble; @@ -429,6 +430,10 @@ public final class NativeLibrary 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); + public static native String FormatSize(long bytes, int decimals); public static native void SetObscuredPixelsLeft(int width); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.java new file mode 100644 index 0000000000..40cf22397b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.java @@ -0,0 +1,40 @@ +package org.dolphinemu.dolphinemu.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.fragments.ConvertFragment; + +import androidx.appcompat.app.AppCompatActivity; + +public class ConvertActivity extends AppCompatActivity +{ + private static final String ARG_GAME_PATH = "game_path"; + + public static void launch(Context context, String gamePath) + { + Intent launcher = new Intent(context, ConvertActivity.class); + launcher.putExtra(ARG_GAME_PATH, gamePath); + context.startActivity(launcher); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_convert); + + String path = getIntent().getStringExtra(ARG_GAME_PATH); + + ConvertFragment fragment = (ConvertFragment) getSupportFragmentManager() + .findFragmentById(R.id.fragment_convert); + if (fragment == null) + { + fragment = ConvertFragment.newInstance(path); + getSupportFragmentManager().beginTransaction().add(R.id.fragment_convert, fragment).commit(); + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java index ff2db6b6a5..5fd27ad5ab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java @@ -156,9 +156,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> 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<Runnable> 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<CharSequence> 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<CharSequence> mLabels = new ArrayList<>(); + private ArrayList<OnClickListener> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp"> + + <TextView + android:id="@+id/label_format_info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/convert_format_info" + app:layout_constraintWidth_max="400dp" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/divider" /> + + <View + android:id="@+id/divider" + android:layout_width="1dp" + android:layout_height="0dp" + android:background="#1F000000" + android:layout_marginStart="24dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/label_format_info" + app:layout_constraintEnd_toStartOf="@id/fragment_convert" /> + + <FrameLayout + android:id="@+id/fragment_convert" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + app:layout_constraintWidth_max="400dp" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/divider" + app:layout_constraintEnd_toEndOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</ScrollView> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp"> + + <FrameLayout + android:id="@+id/fragment_convert" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintWidth_max="400dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/divider" /> + + <View + android:id="@+id/divider" + android:layout_width="0dp" + android:layout_height="1dp" + android:background="#1F000000" + android:layout_marginTop="24dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/fragment_convert" + app:layout_constraintBottom_toTopOf="@id/label_format_info" /> + + <TextView + android:id="@+id/label_format_info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="@string/convert_format_info" + app:layout_constraintWidth_max="400dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/divider" + app:layout_constraintBottom_toBottomOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</ScrollView> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <TextView + android:id="@+id/label_format" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:text="@string/convert_format" + app:layout_constraintTop_toTopOf="@id/spinner_format" + app:layout_constraintBottom_toBottomOf="@id/spinner_format" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/label_block_size" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:text="@string/convert_block_size" + app:layout_constraintTop_toTopOf="@id/spinner_block_size" + app:layout_constraintBottom_toBottomOf="@id/spinner_block_size" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/label_compression" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:text="@string/convert_compression" + app:layout_constraintTop_toTopOf="@id/spinner_compression" + app:layout_constraintBottom_toBottomOf="@id/spinner_compression" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/label_compression_level" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:text="@string/convert_compression_level" + app:layout_constraintTop_toTopOf="@id/spinner_compression_level" + app:layout_constraintBottom_toBottomOf="@id/spinner_compression_level" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/label_remove_junk_data" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:text="@string/convert_remove_junk_data" + app:layout_constraintTop_toTopOf="@id/checkbox_remove_junk_data" + app:layout_constraintBottom_toBottomOf="@id/checkbox_remove_junk_data" + app:layout_constraintStart_toStartOf="parent" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/label_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="end" + app:constraint_referenced_ids="label_format,label_block_size,label_compression,label_compression_level" /> + + <Spinner + android:id="@+id/spinner_format" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + app:layout_constraintStart_toEndOf="@id/label_barrier" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Spinner + android:id="@+id/spinner_block_size" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:gravity="end" + app:layout_constraintStart_toEndOf="@id/label_barrier" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/spinner_format" /> + + <Spinner + android:id="@+id/spinner_compression" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:gravity="end" + app:layout_constraintStart_toEndOf="@id/label_barrier" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/spinner_block_size" /> + + <Spinner + android:id="@+id/spinner_compression_level" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:gravity="end" + app:layout_constraintStart_toEndOf="@id/label_barrier" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/spinner_compression" /> + + <CheckBox + android:id="@+id/checkbox_remove_junk_data" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="8dp" + android:layout_marginTop="16dp" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toEndOf="@id/label_remove_junk_data" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/spinner_compression_level" /> + + <Button + android:id="@+id/button_convert" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/convert_convert" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/checkbox_remove_junk_data" /> + +</androidx.constraintlayout.widget.ConstraintLayout> 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 @@ <item>Classic A</item> </string-array> - <string-array name="gameSettingsMenusGC"> - <item>Details</item> - <item>Set as Default ISO</item> - <item>Core Settings</item> - <item>GFX Settings</item> - <item>GameCube Controller Settings</item> - <item>Clear Game Settings</item> - </string-array> - <string-array name="gameSettingsMenusWii"> - <item>Details</item> - <item>Set as Default ISO</item> - <item>Core Settings</item> - <item>GFX Settings</item> - <item>GameCube Controller Settings</item> - <item>Wii Controller Settings</item> - <item>Clear Game Settings</item> - </string-array> - <string-array name="orientationEntries"> <item>Landscape</item> <item>Portrait</item> @@ -467,4 +449,147 @@ <item>Use Device Sensors (Without Pointer Emulation)</item> <item>Don\'t Use Device Sensors</item> </string-array> + + <string-array name="convertFormatEntries" translatable="false"> + <item>ISO</item> + <item>GCZ</item> + <item>WIA</item> + <item>RVZ</item> + </string-array> + <integer-array name="convertFormatValues"> + <item>0</item> + <item>3</item> + <item>7</item> + <item>8</item> + </integer-array> + + <string-array name="convertBlockSizeGczEntries"> + <item>32 KiB</item> + </string-array> + <integer-array name="convertBlockSizeGczValues"> + <item>32768</item> + </integer-array> + + <string-array name="convertBlockSizeWiaEntries"> + <item>2 MiB</item> + </string-array> + <integer-array name="convertBlockSizeWiaValues"> + <item>2097152</item> + </integer-array> + + <string-array name="convertBlockSizeRvzEntries"> + <item>32 KiB</item> + <item>64 KiB</item> + <item>128 KiB</item> + <item>256 KiB</item> + <item>512 KiB</item> + <item>1 MiB</item> + <item>2 MiB</item> + </string-array> + <integer-array name="convertBlockSizeRvzValues"> + <item>32768</item> + <item>65536</item> + <item>131072</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + <item>2097152</item> + </integer-array> + + <string-array name="convertCompressionGczEntries" translatable="false"> + <item>Deflate</item> + </string-array> + <integer-array name="convertCompressionGczValues"> + <item>0</item> + </integer-array> + + <string-array name="convertCompressionWiaEntries"> + <item>No Compression</item> + <item>Purge</item> + <item>bzip2 (slow)</item> + <item>LZMA (slow)</item> + <item>LZMA2 (slow)</item> + </string-array> + <integer-array name="convertCompressionWiaValues"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + </integer-array> + + <string-array name="convertCompressionRvzEntries"> + <item>No Compression</item> + <item>bzip2 (slow)</item> + <item>LZMA (slow)</item> + <item>LZMA2 (slow)</item> + <item>Zstandard (recommended)</item> + </string-array> + <integer-array name="convertCompressionRvzValues"> + <item>0</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + </integer-array> + + <string-array name="convertCompressionLevelEntries"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + </string-array> + <integer-array name="convertCompressionLevelValues"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + </integer-array> + + <string-array name="convertCompressionLevelZstdEntries"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + <item>10</item> + <item>11</item> + <item>12</item> + <item>13</item> + <item>14</item> + <item>15</item> + <item>16</item> + <item>17</item> + <item>18</item> + <item>19</item> + <item>20</item> + <item>21</item> + <item>22</item> + </string-array> + <integer-array name="convertCompressionLevelZstdValues"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + </integer-array> </resources> 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 @@ <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> - <!-- Preferences Screen --> + <!-- Game Properties Screen --> + <string name="properties_details">Details</string> + <string name="properties_convert">Convert File</string> + <string name="properties_set_default_iso">Set as Default ISO</string> + <string name="properties_core_settings">Core Settings</string> + <string name="properties_gfx_settings">GFX Settings</string> + <string name="properties_gc_controller">GameCube Controller Settings</string> + <string name="properties_wii_controller">Wii Controller Settings</string> + <string name="properties_clear_game_settings">Clear Game Settings</string> <string name="preferences_save_exit">Save and Exit</string> <string name="preferences_settings">Settings</string> <string name="preferences_game_properties">Game Properties</string> @@ -341,6 +349,30 @@ <string name="game_details_block_size">Block Size</string> <string name="game_details_no_compression">No Compression</string> + <!-- Convert Screen --> + <string name="convert_format">Format</string> + <string name="convert_block_size">Block Size</string> + <string name="convert_compression">Compression</string> + <string name="convert_compression_level">Compression Level</string> + <string name="convert_remove_junk_data">Remove Junk Data (Irreversible)</string> + <string name="convert_convert">Convert</string> + <string name="convert_converting">Converting</string> + <string name="convert_warning_iso">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?</string> + <string name="convert_warning_gcz">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?</string> + <string name="convert_success_message">The disc image was successfully converted.</string> + <string name="convert_failure_message">Dolphin failed to complete the requested action.</string> + <string name="convert_format_info"> +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. + </string> + <!-- Emulation Menu --> <string name="pause_emulation">Pause Emulation</string> <string name="unpause_emulation">Unpause Emulation</string> 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 <jni.h> #include "Common/StringUtil.h" +#include "jni/AndroidCommon/IDCache.h" std::string GetJString(JNIEnv* env, jstring jstr) { @@ -40,3 +41,27 @@ std::vector<std::string> 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<std::string> 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<jclass>(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<jclass>(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<jint>(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<jboolean>(GetRef(env, obj)->ShouldShowFileFormatDetails()); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_model_GameFile_shouldAllowConversion(JNIEnv* env, jobject obj) +{ + return static_cast<jboolean>(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<jboolean>(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 <utility> #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<jboolean>(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<DiscIO::Platform>(jPlatform); + const DiscIO::BlobType format = static_cast<DiscIO::BlobType>(jFormat); + const DiscIO::WIARVZCompressionType compression = + static_cast<DiscIO::WIARVZCompressionType>(jCompression); + const bool scrub = static_cast<bool>(jScrub); + + std::unique_ptr<DiscIO::BlobReader> blob_reader; + if (scrub) + blob_reader = DiscIO::ScrubbedBlob::Create(in_path); + else + blob_reader = DiscIO::CreateBlobReader(in_path); + + if (!blob_reader) + return static_cast<jboolean>(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<bool>(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<jboolean>(success); +} + JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_FormatSize(JNIEnv* env, jobject obj, jlong bytes, |
