Merge pull request #11619 from t895/kotlin-convert

Android: Convert Convert Activity to Kotlin
This commit is contained in:
Mai 2023-03-02 15:02:45 -05:00 committed by GitHub
commit 5d00bc088a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 619 additions and 625 deletions

View File

@ -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;
});
}
}

View File

@ -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)
}
}
}

View File

@ -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)
{
}
}
}
}

View File

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