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 deleted file mode 100644 index 9525b5cf39..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.activities; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; - -import com.google.android.material.color.MaterialColors; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.ActivityConvertBinding; -import org.dolphinemu.dolphinemu.fragments.ConvertFragment; -import org.dolphinemu.dolphinemu.utils.InsetsHelper; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; - -public class ConvertActivity extends AppCompatActivity -{ - private static final String ARG_GAME_PATH = "game_path"; - - private ActivityConvertBinding mBinding; - - 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) - { - ThemeHelper.setTheme(this); - - super.onCreate(savedInstanceState); - - mBinding = ActivityConvertBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - - 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(); - } - - mBinding.toolbarConvertLayout.setTitle(getString(R.string.convert_convert)); - setSupportActionBar(mBinding.toolbarConvert); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setInsets(); - ThemeHelper.enableScrollTint(this, mBinding.toolbarConvert, mBinding.appbarConvert); - } - - @Override - public boolean onSupportNavigateUp() - { - onBackPressed(); - return true; - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarConvert, (v, windowInsets) -> - { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - - InsetsHelper.insetAppBar(insets, mBinding.appbarConvert); - - mBinding.scrollViewConvert.setPadding(insets.left, 0, insets.right, insets.bottom); - - InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView); - ThemeHelper.setNavigationBarColor(this, - MaterialColors.getColor(mBinding.appbarConvert, R.attr.colorSurface)); - - return windowInsets; - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.kt new file mode 100644 index 0000000000..2fcc939983 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/ConvertActivity.kt @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.google.android.material.color.MaterialColors +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.databinding.ActivityConvertBinding +import org.dolphinemu.dolphinemu.fragments.ConvertFragment +import org.dolphinemu.dolphinemu.fragments.ConvertFragment.Companion.newInstance +import org.dolphinemu.dolphinemu.utils.InsetsHelper +import org.dolphinemu.dolphinemu.utils.ThemeHelper + +class ConvertActivity : AppCompatActivity() { + private lateinit var binding: ActivityConvertBinding + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.setTheme(this) + + super.onCreate(savedInstanceState) + + binding = ActivityConvertBinding.inflate(layoutInflater) + setContentView(binding.root) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + val path = intent.getStringExtra(ARG_GAME_PATH) + + var fragment = supportFragmentManager + .findFragmentById(R.id.fragment_convert) as ConvertFragment? + if (fragment == null) { + fragment = newInstance(path!!) + supportFragmentManager.beginTransaction().add(R.id.fragment_convert, fragment).commit() + } + + binding.toolbarConvertLayout.title = getString(R.string.convert_convert) + setSupportActionBar(binding.toolbarConvert) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + setInsets() + ThemeHelper.enableScrollTint(this, binding.toolbarConvert, binding.appbarConvert) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.appbarConvert) { _: View?, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + InsetsHelper.insetAppBar(insets, binding.appbarConvert) + + binding.scrollViewConvert.setPadding(insets.left, 0, insets.right, insets.bottom) + + InsetsHelper.applyNavbarWorkaround(insets.bottom, binding.workaroundView) + ThemeHelper.setNavigationBarColor( + this, + MaterialColors.getColor(binding.appbarConvert, R.attr.colorSurface) + ) + + windowInsets + } + } + + companion object { + private const val ARG_GAME_PATH = "game_path" + + @JvmStatic + fun launch(context: Context, gamePath: String) { + val launcher = Intent(context, ConvertActivity::class.java) + launcher.putExtra(ARG_GAME_PATH, gamePath) + context.startActivity(launcher) + } + } +} 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 deleted file mode 100644 index f18f276663..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.java +++ /dev/null @@ -1,535 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.app.Activity; -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 androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.progressindicator.LinearProgressIndicator; -import com.google.android.material.textfield.MaterialAutoCompleteTextView; -import com.google.android.material.textfield.TextInputLayout; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.DialogProgressBinding; -import org.dolphinemu.dolphinemu.databinding.FragmentConvertBinding; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.platform.Platform; - -import java.io.File; -import java.util.ArrayList; - -public class ConvertFragment extends Fragment implements View.OnClickListener -{ - private static class DropdownValue implements AdapterView.OnItemClickListener - { - private int mValuesId = -1; - private int mCurrentPosition = 0; - private ArrayList mCallbacks = new ArrayList<>(); - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - if (mCurrentPosition != position) - setPosition(position); - } - - 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_ISO = 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 DropdownValue mFormat = new DropdownValue(); - private DropdownValue mBlockSize = new DropdownValue(); - private DropdownValue mCompression = new DropdownValue(); - private DropdownValue mCompressionLevel = new DropdownValue(); - - private GameFile gameFile; - - private volatile boolean mCanceled; - private volatile Thread mThread = null; - - private FragmentConvertBinding mBinding; - - 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 = GameFileCacheManager.addOrGet(requireArguments().getString(ARG_GAME_PATH)); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentConvertBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) - { - // TODO: Remove workaround for text filtering issue in material components when fixed - // https://github.com/material-components/material-components-android/issues/1464 - mBinding.dropdownFormat.setSaveEnabled(false); - mBinding.dropdownBlockSize.setSaveEnabled(false); - mBinding.dropdownCompression.setSaveEnabled(false); - mBinding.dropdownCompressionLevel.setSaveEnabled(false); - - populateFormats(); - populateBlockSize(); - populateCompression(); - populateCompressionLevel(); - populateRemoveJunkData(); - - mFormat.addCallback(this::populateBlockSize); - mFormat.addCallback(this::populateCompression); - mFormat.addCallback(this::populateCompressionLevel); - mCompression.addCallback(this::populateCompressionLevel); - mFormat.addCallback(this::populateRemoveJunkData); - - mBinding.buttonConvert.setOnClickListener(this); - - if (savedInstanceState != null) - { - setDropdownSelection(mBinding.dropdownFormat, mFormat, savedInstanceState.getInt(KEY_FORMAT)); - setDropdownSelection(mBinding.dropdownBlockSize, mBlockSize, - savedInstanceState.getInt(KEY_BLOCK_SIZE)); - setDropdownSelection(mBinding.dropdownCompression, mCompression, - savedInstanceState.getInt(KEY_COMPRESSION)); - setDropdownSelection(mBinding.dropdownCompressionLevel, mCompressionLevel, - savedInstanceState.getInt(KEY_COMPRESSION_LEVEL)); - - mBinding.switchRemoveJunkData.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()); - - outState.putBoolean(KEY_REMOVE_JUNK_DATA, mBinding.switchRemoveJunkData.isChecked()); - } - - private void setDropdownSelection(MaterialAutoCompleteTextView dropdown, - DropdownValue valueWrapper, int selection) - { - if (dropdown.getAdapter() != null) - { - dropdown.setText(dropdown.getAdapter().getItem(selection).toString(), false); - } - valueWrapper.setPosition(selection); - } - - @Override - public void onStop() - { - super.onStop(); - - mCanceled = true; - joinThread(); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - private void populateDropdown(TextInputLayout layout, MaterialAutoCompleteTextView dropdown, - int entriesId, int valuesId, DropdownValue valueWrapper) - { - ArrayAdapter adapter = ArrayAdapter.createFromResource(requireContext(), - entriesId, R.layout.support_simple_spinner_dropdown_item); - dropdown.setAdapter(adapter); - - valueWrapper.populate(valuesId); - dropdown.setOnItemClickListener(valueWrapper); - - layout.setEnabled(adapter.getCount() > 1); - } - - private void clearDropdown(TextInputLayout layout, MaterialAutoCompleteTextView dropdown, - DropdownValue valueWrapper) - { - dropdown.setAdapter(null); - layout.setEnabled(false); - - valueWrapper.populate(-1); - valueWrapper.setPosition(0); - dropdown.setText(null, false); - dropdown.setOnItemClickListener(valueWrapper); - } - - private void populateFormats() - { - populateDropdown(mBinding.format, mBinding.dropdownFormat, R.array.convertFormatEntries, - R.array.convertFormatValues, mFormat); - if (gameFile.getBlobType() == BLOB_TYPE_ISO) - { - setDropdownSelection(mBinding.dropdownFormat, mFormat, - mBinding.dropdownFormat.getAdapter().getCount() - 1); - } - mBinding.dropdownFormat.setText( - mBinding.dropdownFormat.getAdapter().getItem(mFormat.getPosition()).toString(), - false); - } - - 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? - populateDropdown(mBinding.blockSize, mBinding.dropdownBlockSize, - R.array.convertBlockSizeGczEntries, - R.array.convertBlockSizeGczValues, mBlockSize); - mBlockSize.setPosition(0); - mBinding.dropdownBlockSize.setText( - mBinding.dropdownBlockSize.getAdapter().getItem(0).toString(), false); - break; - case BLOB_TYPE_WIA: - populateDropdown(mBinding.blockSize, mBinding.dropdownBlockSize, - R.array.convertBlockSizeWiaEntries, - R.array.convertBlockSizeWiaValues, mBlockSize); - mBlockSize.setPosition(0); - mBinding.dropdownBlockSize.setText( - mBinding.dropdownBlockSize.getAdapter().getItem(0).toString(), false); - break; - case BLOB_TYPE_RVZ: - populateDropdown(mBinding.blockSize, mBinding.dropdownBlockSize, - R.array.convertBlockSizeRvzEntries, - R.array.convertBlockSizeRvzValues, mBlockSize); - mBlockSize.setPosition(2); - mBinding.dropdownBlockSize.setText( - mBinding.dropdownBlockSize.getAdapter().getItem(2).toString(), false); - break; - default: - clearDropdown(mBinding.blockSize, mBinding.dropdownBlockSize, mBlockSize); - } - } - - private void populateCompression() - { - switch (mFormat.getValue(requireContext())) - { - case BLOB_TYPE_GCZ: - populateDropdown(mBinding.compression, mBinding.dropdownCompression, - R.array.convertCompressionGczEntries, R.array.convertCompressionGczValues, - mCompression); - mCompression.setPosition(0); - mBinding.dropdownCompression.setText( - mBinding.dropdownCompression.getAdapter().getItem(0).toString(), false); - break; - case BLOB_TYPE_WIA: - populateDropdown(mBinding.compression, mBinding.dropdownCompression, - R.array.convertCompressionWiaEntries, R.array.convertCompressionWiaValues, - mCompression); - mCompression.setPosition(0); - mBinding.dropdownCompression.setText( - mBinding.dropdownCompression.getAdapter().getItem(0).toString(), false); - break; - case BLOB_TYPE_RVZ: - populateDropdown(mBinding.compression, mBinding.dropdownCompression, - R.array.convertCompressionRvzEntries, R.array.convertCompressionRvzValues, - mCompression); - mCompression.setPosition(4); - mBinding.dropdownCompression.setText( - mBinding.dropdownCompression.getAdapter().getItem(4).toString(), false); - break; - default: - clearDropdown(mBinding.compression, mBinding.dropdownCompression, mCompression); - } - } - - private void populateCompressionLevel() - { - switch (mCompression.getValueOr(requireContext(), COMPRESSION_NONE)) - { - case COMPRESSION_BZIP2: - case COMPRESSION_LZMA: - case COMPRESSION_LZMA2: - populateDropdown(mBinding.compressionLevel, mBinding.dropdownCompressionLevel, - R.array.convertCompressionLevelEntries, R.array.convertCompressionLevelValues, - mCompressionLevel); - mCompressionLevel.setPosition(4); - mBinding.dropdownCompressionLevel.setText( - mBinding.dropdownCompressionLevel.getAdapter().getItem(4).toString(), false); - break; - case COMPRESSION_ZSTD: - // TODO: Query DiscIO for the supported compression levels, like we do in DolphinQt? - populateDropdown(mBinding.compressionLevel, mBinding.dropdownCompressionLevel, - R.array.convertCompressionLevelZstdEntries, - R.array.convertCompressionLevelZstdValues, mCompressionLevel); - mCompressionLevel.setPosition(4); - mBinding.dropdownCompressionLevel.setText( - mBinding.dropdownCompressionLevel.getAdapter().getItem(4).toString(), false); - break; - default: - clearDropdown(mBinding.compressionLevel, mBinding.dropdownCompressionLevel, - mCompressionLevel); - } - } - - private void populateRemoveJunkData() - { - boolean scrubbingAllowed = mFormat.getValue(requireContext()) != BLOB_TYPE_RVZ && - !gameFile.isDatelDisc(); - - mBinding.switchRemoveJunkData.setEnabled(scrubbingAllowed); - if (!scrubbingAllowed) - mBinding.switchRemoveJunkData.setChecked(false); - } - - @Override - public void onClick(View view) - { - boolean scrub = mBinding.switchRemoveJunkData.isChecked(); - int format = mFormat.getValue(requireContext()); - - Runnable action = this::showSavePrompt; - - if (gameFile.isNKit()) - { - action = addAreYouSureDialog(action, R.string.convert_warning_nkit); - } - - if (!scrub && format == BLOB_TYPE_GCZ && !gameFile.isDatelDisc() && - gameFile.getPlatform() == Platform.WII.toInt()) - { - action = addAreYouSureDialog(action, R.string.convert_warning_gcz); - } - - if (scrub && format == BLOB_TYPE_ISO) - { - action = addAreYouSureDialog(action, R.string.convert_warning_iso); - } - - action.run(); - } - - private Runnable addAreYouSureDialog(Runnable action, @StringRes int warning_text) - { - return () -> - { - Context context = requireContext(); - new MaterialAlertDialogBuilder(context) - .setMessage(warning_text) - .setPositiveButton(R.string.yes, (dialog, i) -> action.run()) - .setNegativeButton(R.string.no, null) - .show(); - }; - } - - private void showSavePrompt() - { - String originalPath = gameFile.getPath(); - - StringBuilder filename = new StringBuilder(new File(originalPath).getName()); - int dotIndex = filename.lastIndexOf("."); - if (dotIndex != -1) - filename.setLength(dotIndex); - switch (mFormat.getValue(requireContext())) - { - case BLOB_TYPE_ISO: - filename.append(".iso"); - break; - case BLOB_TYPE_GCZ: - filename.append(".gcz"); - break; - case BLOB_TYPE_WIA: - filename.append(".wia"); - break; - case BLOB_TYPE_RVZ: - filename.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, filename.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; - - DialogProgressBinding dialogProgressBinding = - DialogProgressBinding.inflate(getLayoutInflater(), null, false); - dialogProgressBinding.updateProgress.setMax(PROGRESS_RESOLUTION); - - AlertDialog progressDialog = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.convert_converting) - .setOnCancelListener((dialog) -> mCanceled = true) - .setNegativeButton(getString(R.string.cancel), (dialog, i) -> dialog.dismiss()) - .setView(dialogProgressBinding.getRoot()) - .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), - mBinding.switchRemoveJunkData.isChecked(), (text, completion) -> - { - requireActivity().runOnUiThread(() -> - { - progressDialog.setMessage(text); - dialogProgressBinding.updateProgress.setProgress( - (int) (completion * PROGRESS_RESOLUTION)); - }); - return !mCanceled; - }); - - if (!mCanceled) - { - requireActivity().runOnUiThread(() -> - { - progressDialog.dismiss(); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - 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()); - } - builder.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/fragments/ConvertFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.kt new file mode 100644 index 0000000000..01a5d97743 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/ConvertFragment.kt @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +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.AdapterView.OnItemClickListener +import android.widget.ArrayAdapter +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.MaterialAutoCompleteTextView +import com.google.android.material.textfield.TextInputLayout +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.databinding.DialogProgressBinding +import org.dolphinemu.dolphinemu.databinding.FragmentConvertBinding +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.platform.Platform +import java.io.File + +class ConvertFragment : Fragment(), View.OnClickListener { + private class DropdownValue : OnItemClickListener { + private var valuesId = -1 + var position = 0 + set(position) { + field = position + for (callback in callbacks) callback.run() + } + private val callbacks = ArrayList() + + override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) { + if (this.position != position) this.position = position + } + + fun populate(valuesId: Int) { + this.valuesId = valuesId + } + + fun hasValues(): Boolean { + return valuesId != -1 + } + + fun getValue(context: Context): Int { + return context.resources.getIntArray(valuesId)[position] + } + + fun getValueOr(context: Context, defaultValue: Int): Int { + return if (hasValues()) getValue(context) else defaultValue + } + + fun addCallback(callback: Runnable) { + callbacks.add(callback) + } + } + + private val format = DropdownValue() + private val blockSize = DropdownValue() + private val compression = DropdownValue() + private val compressionLevel = DropdownValue() + private lateinit var gameFile: GameFile + + @Volatile + private var canceled = false + + @Volatile + private var thread: Thread? = null + + private var _binding: FragmentConvertBinding? = null + val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + gameFile = GameFileCacheManager.addOrGet(requireArguments().getString(ARG_GAME_PATH)) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentConvertBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // TODO: Remove workaround for text filtering issue in material components when fixed + // https://github.com/material-components/material-components-android/issues/1464 + binding.dropdownFormat.isSaveEnabled = false + binding.dropdownBlockSize.isSaveEnabled = false + binding.dropdownCompression.isSaveEnabled = false + binding.dropdownCompressionLevel.isSaveEnabled = false + + populateFormats() + populateBlockSize() + populateCompression() + populateCompressionLevel() + populateRemoveJunkData() + + format.addCallback { populateBlockSize() } + format.addCallback { populateCompression() } + format.addCallback { populateCompressionLevel() } + compression.addCallback { populateCompressionLevel() } + format.addCallback { populateRemoveJunkData() } + + binding.buttonConvert.setOnClickListener(this) + + if (savedInstanceState != null) { + setDropdownSelection( + binding.dropdownFormat, format, savedInstanceState.getInt( + KEY_FORMAT + ) + ) + setDropdownSelection( + binding.dropdownBlockSize, blockSize, + savedInstanceState.getInt(KEY_BLOCK_SIZE) + ) + setDropdownSelection( + binding.dropdownCompression, compression, + savedInstanceState.getInt(KEY_COMPRESSION) + ) + setDropdownSelection( + binding.dropdownCompressionLevel, compressionLevel, + savedInstanceState.getInt(KEY_COMPRESSION_LEVEL) + ) + binding.switchRemoveJunkData.isChecked = savedInstanceState.getBoolean( + KEY_REMOVE_JUNK_DATA + ) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(KEY_FORMAT, format.position) + outState.putInt(KEY_BLOCK_SIZE, blockSize.position) + outState.putInt(KEY_COMPRESSION, compression.position) + outState.putInt(KEY_COMPRESSION_LEVEL, compressionLevel.position) + outState.putBoolean(KEY_REMOVE_JUNK_DATA, binding.switchRemoveJunkData.isChecked) + } + + private fun setDropdownSelection( + dropdown: MaterialAutoCompleteTextView, + valueWrapper: DropdownValue, + selection: Int + ) { + if (dropdown.adapter != null) { + dropdown.setText(dropdown.adapter.getItem(selection).toString(), false) + } + valueWrapper.position = selection + } + + override fun onStop() { + super.onStop() + canceled = true + joinThread() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun populateDropdown( + layout: TextInputLayout, + dropdown: MaterialAutoCompleteTextView, + entriesId: Int, + valuesId: Int, + valueWrapper: DropdownValue + ) { + val adapter = ArrayAdapter.createFromResource( + requireContext(), + entriesId, + R.layout.support_simple_spinner_dropdown_item + ) + dropdown.setAdapter(adapter) + + valueWrapper.populate(valuesId) + dropdown.onItemClickListener = valueWrapper + + layout.isEnabled = adapter.count > 1 + } + + private fun clearDropdown( + layout: TextInputLayout, + dropdown: MaterialAutoCompleteTextView, + valueWrapper: DropdownValue + ) { + dropdown.setAdapter(null) + layout.isEnabled = false + + valueWrapper.populate(-1) + valueWrapper.position = 0 + dropdown.setText(null, false) + dropdown.onItemClickListener = valueWrapper + } + + private fun populateFormats() { + populateDropdown( + binding.format, + binding.dropdownFormat, + R.array.convertFormatEntries, + R.array.convertFormatValues, + format + ) + if (gameFile.blobType == BLOB_TYPE_ISO) { + setDropdownSelection( + binding.dropdownFormat, + format, + binding.dropdownFormat.adapter.count - 1 + ) + } + binding.dropdownFormat.setText( + binding.dropdownFormat.adapter.getItem(format.position).toString(), false + ) + } + + private fun populateBlockSize() { + when (format.getValue(requireContext())) { + 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? + populateDropdown( + binding.blockSize, + binding.dropdownBlockSize, + R.array.convertBlockSizeGczEntries, + R.array.convertBlockSizeGczValues, + blockSize + ) + blockSize.position = 0 + binding.dropdownBlockSize.setText( + binding.dropdownBlockSize.adapter.getItem(0).toString(), false + ) + } + BLOB_TYPE_WIA -> { + populateDropdown( + binding.blockSize, + binding.dropdownBlockSize, + R.array.convertBlockSizeWiaEntries, + R.array.convertBlockSizeWiaValues, + blockSize + ) + blockSize.position = 0 + binding.dropdownBlockSize.setText( + binding.dropdownBlockSize.adapter.getItem(0).toString(), false + ) + } + BLOB_TYPE_RVZ -> { + populateDropdown( + binding.blockSize, + binding.dropdownBlockSize, + R.array.convertBlockSizeRvzEntries, + R.array.convertBlockSizeRvzValues, + blockSize + ) + blockSize.position = 2 + binding.dropdownBlockSize.setText( + binding.dropdownBlockSize.adapter.getItem(2).toString(), false + ) + } + else -> clearDropdown(binding.blockSize, binding.dropdownBlockSize, blockSize) + } + } + + private fun populateCompression() { + when (format.getValue(requireContext())) { + BLOB_TYPE_GCZ -> { + populateDropdown( + binding.compression, + binding.dropdownCompression, + R.array.convertCompressionGczEntries, + R.array.convertCompressionGczValues, + compression + ) + compression.position = 0 + binding.dropdownCompression.setText( + binding.dropdownCompression.adapter.getItem(0).toString(), false + ) + } + BLOB_TYPE_WIA -> { + populateDropdown( + binding.compression, + binding.dropdownCompression, + R.array.convertCompressionWiaEntries, + R.array.convertCompressionWiaValues, + compression + ) + compression.position = 0 + binding.dropdownCompression.setText( + binding.dropdownCompression.adapter.getItem(0).toString(), false + ) + } + BLOB_TYPE_RVZ -> { + populateDropdown( + binding.compression, + binding.dropdownCompression, + R.array.convertCompressionRvzEntries, + R.array.convertCompressionRvzValues, + compression + ) + compression.position = 4 + binding.dropdownCompression.setText( + binding.dropdownCompression.adapter.getItem(4).toString(), false + ) + } + else -> clearDropdown( + binding.compression, + binding.dropdownCompression, + compression + ) + } + } + + private fun populateCompressionLevel() { + when (compression.getValueOr(requireContext(), COMPRESSION_NONE)) { + COMPRESSION_BZIP2, COMPRESSION_LZMA, COMPRESSION_LZMA2 -> { + populateDropdown( + binding.compressionLevel, + binding.dropdownCompressionLevel, + R.array.convertCompressionLevelEntries, + R.array.convertCompressionLevelValues, + compressionLevel + ) + compressionLevel.position = 4 + binding.dropdownCompressionLevel.setText( + binding.dropdownCompressionLevel.adapter.getItem( + 4 + ).toString(), false + ) + } + COMPRESSION_ZSTD -> { + // TODO: Query DiscIO for the supported compression levels, like we do in DolphinQt? + populateDropdown( + binding.compressionLevel, + binding.dropdownCompressionLevel, + R.array.convertCompressionLevelZstdEntries, + R.array.convertCompressionLevelZstdValues, + compressionLevel + ) + compressionLevel.position = 4 + binding.dropdownCompressionLevel.setText( + binding.dropdownCompressionLevel.adapter.getItem( + 4 + ).toString(), false + ) + } + else -> clearDropdown( + binding.compressionLevel, + binding.dropdownCompressionLevel, + compressionLevel + ) + } + } + + private fun populateRemoveJunkData() { + val scrubbingAllowed = format.getValue(requireContext()) != BLOB_TYPE_RVZ && + !gameFile.isDatelDisc + + binding.switchRemoveJunkData.isEnabled = scrubbingAllowed + if (!scrubbingAllowed) binding.switchRemoveJunkData.isChecked = false + } + + override fun onClick(view: View) { + val scrub = binding.switchRemoveJunkData.isChecked + val format = format.getValue(requireContext()) + + var action = Runnable { showSavePrompt() } + + if (gameFile.isNKit) { + action = addAreYouSureDialog(action, R.string.convert_warning_nkit) + } + + if (!scrub && format == BLOB_TYPE_GCZ && !gameFile.isDatelDisc && gameFile.platform == Platform.WII.toInt()) { + action = addAreYouSureDialog(action, R.string.convert_warning_gcz) + } + + if (scrub && format == BLOB_TYPE_ISO) { + action = addAreYouSureDialog(action, R.string.convert_warning_iso) + } + + action.run() + } + + private fun addAreYouSureDialog(action: Runnable, @StringRes warning_text: Int): Runnable { + return Runnable { + val context = requireContext() + MaterialAlertDialogBuilder(context) + .setMessage(warning_text) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> action.run() } + .setNegativeButton(R.string.no, null) + .show() + } + } + + private fun showSavePrompt() { + val originalPath = gameFile.path + + val filename = StringBuilder(File(originalPath).name) + val dotIndex = filename.lastIndexOf(".") + if (dotIndex != -1) filename.setLength(dotIndex) + when (format.getValue(requireContext())) { + BLOB_TYPE_ISO -> filename.append(".iso") + BLOB_TYPE_GCZ -> filename.append(".gcz") + BLOB_TYPE_WIA -> filename.append(".wia") + BLOB_TYPE_RVZ -> filename.append(".rvz") + } + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "application/octet-stream" + intent.putExtra(Intent.EXTRA_TITLE, filename.toString()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) intent.putExtra( + DocumentsContract.EXTRA_INITIAL_URI, + originalPath + ) + startActivityForResult(intent, REQUEST_CODE_SAVE_FILE) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == Activity.RESULT_OK) { + convert(data!!.data.toString()) + } + } + + private fun convert(outPath: String) { + val context = requireContext() + + joinThread() + + canceled = false + + val dialogProgressBinding = DialogProgressBinding.inflate(layoutInflater, null, false) + dialogProgressBinding.updateProgress.max = PROGRESS_RESOLUTION + + val progressDialog = MaterialAlertDialogBuilder(context) + .setTitle(R.string.convert_converting) + .setOnCancelListener { canceled = true } + .setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .setView(dialogProgressBinding.root) + .show() + + thread = Thread { + val success = NativeLibrary.ConvertDiscImage( + gameFile.path, + outPath, + gameFile.platform, + format.getValue(context), + blockSize.getValueOr(context, 0), + compression.getValueOr(context, 0), + compressionLevel.getValueOr(context, 0), + binding.switchRemoveJunkData.isChecked + ) { text: String?, completion: Float -> + requireActivity().runOnUiThread { + progressDialog.setMessage(text) + dialogProgressBinding.updateProgress.progress = + (completion * PROGRESS_RESOLUTION).toInt() + } + !canceled + } + + if (!canceled) { + requireActivity().runOnUiThread { + progressDialog.dismiss() + + val builder = MaterialAlertDialogBuilder(context) + if (success) { + builder.setMessage(R.string.convert_success_message) + .setCancelable(false) + .setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + requireActivity().finish() + } + } else { + builder.setMessage(R.string.convert_failure_message) + .setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + } + builder.show() + } + } + } + thread!!.start() + } + + private fun joinThread() { + if (thread != null) { + try { + thread!!.join() + } catch (ignored: InterruptedException) { + } + } + } + + companion object { + private const val ARG_GAME_PATH = "game_path" + + private const val KEY_FORMAT = "convert_format" + private const val KEY_BLOCK_SIZE = "convert_block_size" + private const val KEY_COMPRESSION = "convert_compression" + private const val KEY_COMPRESSION_LEVEL = "convert_compression_level" + private const val KEY_REMOVE_JUNK_DATA = "remove_junk_data" + + private const val REQUEST_CODE_SAVE_FILE = 0 + + private const val BLOB_TYPE_ISO = 0 + private const val BLOB_TYPE_GCZ = 3 + private const val BLOB_TYPE_WIA = 7 + private const val BLOB_TYPE_RVZ = 8 + + private const val COMPRESSION_NONE = 0 + private const val COMPRESSION_PURGE = 1 + private const val COMPRESSION_BZIP2 = 2 + private const val COMPRESSION_LZMA = 3 + private const val COMPRESSION_LZMA2 = 4 + private const val COMPRESSION_ZSTD = 5 + + private const val PROGRESS_RESOLUTION = 1000 + + @JvmStatic + fun newInstance(gamePath: String): ConvertFragment { + val args = Bundle() + args.putString(ARG_GAME_PATH, gamePath) + val fragment = ConvertFragment() + fragment.arguments = args + return fragment + } + } +}