Android: Convert ConvertFragment to Kotlin
This commit is contained in:
parent
c5e00b085e
commit
fb432dd58a
|
@ -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<Runnable> 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<CharSequence> 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Runnable>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue