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