Merge pull request #11515 from t895/user-data-kotlin

Android: Rewrite User Data Activity in Kotlin
This commit is contained in:
Charles Lombardo 2023-02-28 23:47:00 -05:00 committed by GitHub
commit 211be4698f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 614 additions and 440 deletions

View File

@ -1,379 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.activities;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.databinding.ActivityUserDataBinding;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.InsetsHelper;
import org.dolphinemu.dolphinemu.utils.Log;
import org.dolphinemu.dolphinemu.utils.ThemeHelper;
import org.dolphinemu.dolphinemu.utils.ThreadUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class UserDataActivity extends AppCompatActivity
{
private static final int REQUEST_CODE_IMPORT = 0;
private static final int REQUEST_CODE_EXPORT = 1;
private static final int BUFFER_SIZE = 64 * 1024;
private boolean sMustRestartApp = false;
private ActivityUserDataBinding mBinding;
public static void launch(Context context)
{
Intent launcher = new Intent(context, UserDataActivity.class);
context.startActivity(launcher);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
mBinding = ActivityUserDataBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
boolean android_10 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
boolean android_11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
boolean legacy = DirectoryInitialization.isUsingLegacyUserDirectory();
int user_data_new_location = android_10 ?
R.string.user_data_new_location_android_10 : R.string.user_data_new_location;
mBinding.textType.setText(legacy ? R.string.user_data_old_location : user_data_new_location);
mBinding.textPath.setText(DirectoryInitialization.getUserDirectory());
mBinding.textAndroid11.setVisibility(android_11 && !legacy ? View.VISIBLE : View.GONE);
mBinding.buttonOpenSystemFileManager.setVisibility(android_11 ? View.VISIBLE : View.GONE);
mBinding.buttonOpenSystemFileManager.setOnClickListener(view -> openFileManager());
mBinding.buttonImportUserData.setOnClickListener(view -> importUserData());
mBinding.buttonExportUserData.setOnClickListener(view -> exportUserData());
setSupportActionBar(mBinding.toolbarUserData);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setInsets();
ThemeHelper.enableScrollTint(this, mBinding.toolbarUserData, mBinding.appbarUserData);
}
@Override
public boolean onSupportNavigateUp()
{
onBackPressed();
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_IMPORT && resultCode == Activity.RESULT_OK)
{
Uri uri = data.getData();
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.user_data_import_warning)
.setNegativeButton(R.string.no, (dialog, i) -> dialog.dismiss())
.setPositiveButton(R.string.yes, (dialog, i) ->
{
dialog.dismiss();
ThreadUtil.runOnThreadAndShowResult(this, R.string.import_in_progress,
R.string.do_not_close_app,
() -> getResources().getString(importUserData(uri)),
(dialogInterface) ->
{
if (sMustRestartApp)
{
System.exit(0);
}
});
})
.show();
}
else if (requestCode == REQUEST_CODE_EXPORT && resultCode == Activity.RESULT_OK)
{
Uri uri = data.getData();
ThreadUtil.runOnThreadAndShowResult(this, R.string.export_in_progress, 0,
() -> getResources().getString(exportUserData(uri)));
}
}
private void openFileManager()
{
try
{
// First, try the package name used on "normal" phones
startActivity(getFileManagerIntent("com.google.android.documentsui"));
}
catch (ActivityNotFoundException e)
{
try
{
// Next, try the AOSP package name
startActivity(getFileManagerIntent("com.android.documentsui"));
}
catch (ActivityNotFoundException e2)
{
// Activity not found. Perhaps it was removed by the OEM, or by some new Android version
// that didn't exist at the time of writing. Not much we can do other than tell the user
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.user_data_open_system_file_manager_failed)
.setPositiveButton(R.string.ok, null)
.show();
}
}
}
private Intent getFileManagerIntent(String packageName)
{
// Fragile, but some phones don't expose the system file manager in any better way
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
private void importUserData()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("application/zip");
startActivityForResult(intent, REQUEST_CODE_IMPORT);
}
private int importUserData(Uri source)
{
try
{
if (!isDolphinUserDataBackup(source))
{
return R.string.user_data_import_invalid_file;
}
try (InputStream is = getContentResolver().openInputStream(source))
{
try (ZipInputStream zis = new ZipInputStream(is))
{
File userDirectory = new File(DirectoryInitialization.getUserDirectory());
String userDirectoryCanonicalized = userDirectory.getCanonicalPath() + '/';
sMustRestartApp = true;
deleteChildrenRecursively(userDirectory);
DirectoryInitialization.getGameListCache(this).delete();
ZipEntry ze;
byte[] buffer = new byte[BUFFER_SIZE];
while ((ze = zis.getNextEntry()) != null)
{
File destFile = new File(userDirectory, ze.getName());
File destDirectory = ze.isDirectory() ? destFile : destFile.getParentFile();
if (!destFile.getCanonicalPath().startsWith(userDirectoryCanonicalized))
{
Log.error("Zip file attempted path traversal! " + ze.getName());
return R.string.user_data_import_failure;
}
if (!destDirectory.isDirectory() && !destDirectory.mkdirs())
{
throw new IOException("Failed to create directory " + destDirectory);
}
if (!ze.isDirectory())
{
try (FileOutputStream fos = new FileOutputStream(destFile))
{
int count;
while ((count = zis.read(buffer)) != -1)
{
fos.write(buffer, 0, count);
}
}
long time = ze.getTime();
if (time > 0)
{
destFile.setLastModified(time);
}
}
}
}
}
}
catch (IOException | NullPointerException e)
{
e.printStackTrace();
return R.string.user_data_import_failure;
}
return R.string.user_data_import_success;
}
private boolean isDolphinUserDataBackup(Uri uri) throws IOException
{
try (InputStream is = getContentResolver().openInputStream(uri))
{
try (ZipInputStream zis = new ZipInputStream(is))
{
ZipEntry ze;
while ((ze = zis.getNextEntry()) != null)
{
String name = ze.getName();
if (name.equals("Config/Dolphin.ini"))
{
return true;
}
}
}
}
return false;
}
private void deleteChildrenRecursively(File directory) throws IOException
{
File[] children = directory.listFiles();
if (children == null)
{
throw new IOException("Could not find directory " + directory);
}
for (File child : children)
{
deleteRecursively(child);
}
}
private void deleteRecursively(File file) throws IOException
{
if (file.isDirectory())
{
deleteChildrenRecursively(file);
}
if (!file.delete())
{
throw new IOException("Failed to delete " + file);
}
}
private void exportUserData()
{
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_TITLE, "dolphin-emu.zip");
startActivityForResult(intent, REQUEST_CODE_EXPORT);
}
private int exportUserData(Uri destination)
{
try (OutputStream os = getContentResolver().openOutputStream(destination))
{
try (ZipOutputStream zos = new ZipOutputStream(os))
{
exportUserData(zos, new File(DirectoryInitialization.getUserDirectory()), null);
}
}
catch (IOException e)
{
e.printStackTrace();
return R.string.user_data_export_failure;
}
return R.string.user_data_export_success;
}
private void exportUserData(ZipOutputStream zos, File input, @Nullable File pathRelativeToRoot)
throws IOException
{
if (input.isDirectory())
{
File[] children = input.listFiles();
if (children == null)
{
throw new IOException("Could not find directory " + input);
}
for (File child : children)
{
exportUserData(zos, child, new File(pathRelativeToRoot, child.getName()));
}
if (children.length == 0 && pathRelativeToRoot != null)
{
zos.putNextEntry(new ZipEntry(pathRelativeToRoot.getPath() + '/'));
}
}
else
{
try (FileInputStream fis = new FileInputStream(input))
{
byte[] buffer = new byte[BUFFER_SIZE];
ZipEntry entry = new ZipEntry(pathRelativeToRoot.getPath());
entry.setTime(input.lastModified());
zos.putNextEntry(entry);
int count;
while ((count = fis.read(buffer, 0, buffer.length)) != -1)
{
zos.write(buffer, 0, count);
}
}
}
}
private void setInsets()
{
ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarUserData, (v, windowInsets) ->
{
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
InsetsHelper.insetAppBar(insets, mBinding.appbarUserData);
mBinding.scrollViewUserData.setPadding(insets.left, 0, insets.right, insets.bottom);
InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView);
ThemeHelper.setNavigationBarColor(this,
MaterialColors.getColor(mBinding.appbarUserData, R.attr.colorSurface));
return windowInsets;
});
}
}

View File

@ -0,0 +1,327 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.activities
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
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 androidx.lifecycle.ViewModelProvider
import com.google.android.material.color.MaterialColors
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityUserDataBinding
import org.dolphinemu.dolphinemu.dialogs.NotificationDialog
import org.dolphinemu.dolphinemu.dialogs.TaskDialog
import org.dolphinemu.dolphinemu.dialogs.UserDataImportWarningDialog
import org.dolphinemu.dolphinemu.model.TaskViewModel
import org.dolphinemu.dolphinemu.utils.*
import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint
import org.dolphinemu.dolphinemu.utils.ThemeHelper.setNavigationBarColor
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class UserDataActivity : AppCompatActivity() {
private lateinit var taskViewModel: TaskViewModel
private lateinit var mBinding: ActivityUserDataBinding
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
mBinding = ActivityUserDataBinding.inflate(layoutInflater)
setContentView(mBinding.root)
WindowCompat.setDecorFitsSystemWindows(window, false)
val android10 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val android11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
val legacy = DirectoryInitialization.isUsingLegacyUserDirectory()
val userDataNewLocation =
if (android10) R.string.user_data_new_location_android_10 else R.string.user_data_new_location
mBinding.textType.setText(if (legacy) R.string.user_data_old_location else userDataNewLocation)
mBinding.textPath.text = DirectoryInitialization.getUserDirectory()
mBinding.textAndroid11.visibility = if (android11 && !legacy) View.VISIBLE else View.GONE
mBinding.buttonOpenSystemFileManager.visibility = if (android11) View.VISIBLE else View.GONE
mBinding.buttonOpenSystemFileManager.setOnClickListener { openFileManager() }
mBinding.buttonImportUserData.setOnClickListener { importUserData() }
mBinding.buttonExportUserData.setOnClickListener { exportUserData() }
setSupportActionBar(mBinding.toolbarUserData)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
setInsets()
enableScrollTint(this, mBinding.toolbarUserData, mBinding.appbarUserData)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
taskViewModel = ViewModelProvider(this)[TaskViewModel::class.java]
if (requestCode == REQUEST_CODE_IMPORT && resultCode == RESULT_OK) {
val arguments = Bundle()
arguments.putString(
UserDataImportWarningDialog.KEY_URI_RESULT,
data!!.data!!.toString()
)
val dialog = UserDataImportWarningDialog()
dialog.arguments = arguments
dialog.show(supportFragmentManager, UserDataImportWarningDialog.TAG)
} else if (requestCode == REQUEST_CODE_EXPORT && resultCode == RESULT_OK) {
taskViewModel.clear()
taskViewModel.task = {
val resultResource = exportUserData(data!!.data!!)
taskViewModel.setResult(resultResource)
}
val arguments = Bundle()
arguments.putInt(TaskDialog.KEY_TITLE, R.string.export_in_progress)
arguments.putInt(TaskDialog.KEY_MESSAGE, 0)
arguments.putBoolean(TaskDialog.KEY_CANCELLABLE, true)
val dialog = TaskDialog()
dialog.arguments = arguments
dialog.show(supportFragmentManager, TaskDialog.TAG)
}
}
private fun openFileManager() {
try {
// First, try the package name used on "normal" phones
startActivity(getFileManagerIntent("com.google.android.documentsui"))
} catch (e: ActivityNotFoundException) {
try {
// Next, try the AOSP package name
startActivity(getFileManagerIntent("com.android.documentsui"))
} catch (e2: ActivityNotFoundException) {
// Activity not found. Perhaps it was removed by the OEM, or by some new Android version
// that didn't exist at the time of writing. Not much we can do other than tell the user.
val arguments = Bundle()
arguments.putInt(
NotificationDialog.KEY_MESSAGE,
R.string.user_data_open_system_file_manager_failed
)
val dialog = NotificationDialog()
dialog.arguments = arguments
dialog.show(supportFragmentManager, NotificationDialog.TAG)
}
}
}
private fun getFileManagerIntent(packageName: String): Intent {
// Fragile, but some phones don't expose the system file manager in any better way
val intent = Intent(Intent.ACTION_MAIN)
intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
private fun importUserData() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "application/zip"
startActivityForResult(intent, REQUEST_CODE_IMPORT)
}
fun importUserData(source: Uri): Int {
try {
if (!isDolphinUserDataBackup(source))
return R.string.user_data_import_invalid_file
taskViewModel.mustRestartApp = true
contentResolver.openInputStream(source).use { `is` ->
ZipInputStream(`is`).use { zis ->
val userDirectory = File(DirectoryInitialization.getUserDirectory())
val userDirectoryCanonicalized = userDirectory.canonicalPath + '/'
deleteChildrenRecursively(userDirectory)
DirectoryInitialization.getGameListCache(this).delete()
var ze: ZipEntry? = zis.nextEntry
val buffer = ByteArray(BUFFER_SIZE)
while (ze != null) {
val destFile = File(userDirectory, ze.name)
val destDirectory = if (ze.isDirectory) destFile else destFile.parentFile
if (!destFile.canonicalPath.startsWith(userDirectoryCanonicalized)) {
Log.error("Zip file attempted path traversal! " + ze.name)
return R.string.user_data_import_failure
}
if (!destDirectory.isDirectory && !destDirectory.mkdirs()) {
throw IOException("Failed to create directory $destDirectory")
}
if (!ze.isDirectory) {
FileOutputStream(destFile).use { fos ->
var count: Int
while (zis.read(buffer).also { count = it } != -1) {
fos.write(buffer, 0, count)
}
}
val time = ze.time
if (time > 0) {
destFile.setLastModified(time)
}
}
ze = zis.nextEntry
}
}
}
} catch (e: IOException) {
e.printStackTrace()
return R.string.user_data_import_failure
} catch (e: NullPointerException) {
e.printStackTrace()
return R.string.user_data_import_failure
}
return R.string.user_data_import_success
}
@Throws(IOException::class)
private fun isDolphinUserDataBackup(uri: Uri): Boolean {
contentResolver.openInputStream(uri).use { `is` ->
ZipInputStream(`is`).use { zis ->
var ze: ZipEntry
while (zis.nextEntry.also { ze = it } != null) {
val name = ze.name
if (name == "Config/Dolphin.ini") {
return true
}
}
}
}
return false
}
@Throws(IOException::class)
private fun deleteChildrenRecursively(directory: File) {
val children =
directory.listFiles() ?: throw IOException("Could not find directory $directory")
for (child in children) {
deleteRecursively(child)
}
}
@Throws(IOException::class)
private fun deleteRecursively(file: File) {
if (file.isDirectory) {
deleteChildrenRecursively(file)
}
if (!file.delete()) {
throw IOException("Failed to delete $file")
}
}
private fun exportUserData() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.type = "application/zip"
intent.putExtra(Intent.EXTRA_TITLE, "dolphin-emu.zip")
startActivityForResult(intent, REQUEST_CODE_EXPORT)
}
private fun exportUserData(destination: Uri): Int {
try {
contentResolver.openOutputStream(destination).use { os ->
ZipOutputStream(os).use { zos ->
exportUserData(
zos,
File(DirectoryInitialization.getUserDirectory()),
null
)
}
}
} catch (e: IOException) {
e.printStackTrace()
return R.string.user_data_export_failure
}
return R.string.user_data_export_success
}
@Throws(IOException::class)
private fun exportUserData(zos: ZipOutputStream, input: File, pathRelativeToRoot: File?) {
if (input.isDirectory) {
val children = input.listFiles() ?: throw IOException("Could not find directory $input")
// Check if the coroutine was cancelled
if (!taskViewModel.cancelled) {
for (child in children) {
exportUserData(zos, child, File(pathRelativeToRoot, child.name))
}
}
if (children.isEmpty() && pathRelativeToRoot != null) {
zos.putNextEntry(ZipEntry(pathRelativeToRoot.path + '/'))
}
} else {
FileInputStream(input).use { fis ->
val buffer = ByteArray(BUFFER_SIZE)
val entry = ZipEntry(pathRelativeToRoot!!.path)
entry.time = input.lastModified()
zos.putNextEntry(entry)
var count: Int
while (fis.read(buffer, 0, buffer.size).also { count = it } != -1) {
zos.write(buffer, 0, count)
}
}
}
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarUserData) { _: View?, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
InsetsHelper.insetAppBar(insets, mBinding.appbarUserData)
mBinding.scrollViewUserData.setPadding(insets.left, 0, insets.right, insets.bottom)
InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView)
setNavigationBarColor(
this,
MaterialColors.getColor(mBinding.appbarUserData, R.attr.colorSurface)
)
windowInsets
}
}
companion object {
private const val REQUEST_CODE_IMPORT = 0
private const val REQUEST_CODE_EXPORT = 1
private const val BUFFER_SIZE = 64 * 1024
@JvmStatic
fun launch(context: Context) {
val launcher = Intent(context, UserDataActivity::class.java)
context.startActivity(launcher)
}
}
}

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class NotificationDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(requireArguments().getInt(KEY_MESSAGE))
.setPositiveButton(android.R.string.ok, null)
return dialog.create()
}
companion object {
const val TAG = "NotificationDialog"
const val KEY_MESSAGE = "message"
}
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.model.TaskViewModel
class TaskCompleteDialog : DialogFragment() {
private lateinit var viewModel: TaskViewModel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
viewModel = ViewModelProvider(requireActivity())[TaskViewModel::class.java]
val dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(requireArguments().getInt(KEY_MESSAGE))
.setPositiveButton(android.R.string.ok, null)
return dialog.create()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
if (viewModel.onResultDismiss != null)
viewModel.onResultDismiss!!.invoke()
viewModel.clear()
}
companion object {
const val TAG = "TaskCompleteDialog"
const val KEY_MESSAGE = "message"
}
}

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.model.TaskViewModel
class TaskDialog : DialogFragment() {
private lateinit var viewModel: TaskViewModel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
viewModel = ViewModelProvider(requireActivity())[TaskViewModel::class.java]
val dialogBuilder = MaterialAlertDialogBuilder(requireContext())
.setTitle(requireArguments().getInt(KEY_TITLE))
.setView(R.layout.dialog_indeterminate_progress)
if (requireArguments().getBoolean(KEY_CANCELLABLE)) {
dialogBuilder.setCancelable(true)
.setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int ->
viewModel.cancelled = true
dialog.dismiss()
}
}
val dialog = dialogBuilder.create()
dialog.setCanceledOnTouchOutside(false)
val progressMessage = requireArguments().getInt(KEY_MESSAGE)
if (progressMessage != 0) dialog.setMessage(resources.getString(progressMessage))
viewModel.isComplete.observe(this) { complete: Boolean ->
if (complete && viewModel.result.value != null) {
dialog.dismiss()
val notificationArguments = Bundle()
notificationArguments.putInt(
TaskCompleteDialog.KEY_MESSAGE,
viewModel.result.value!!
)
val taskCompleteDialog = TaskCompleteDialog()
taskCompleteDialog.arguments = notificationArguments
taskCompleteDialog.show(
requireActivity().supportFragmentManager,
TaskCompleteDialog.TAG
)
}
}
viewModel.runTask()
return dialog
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
viewModel.cancelled = true
}
companion object {
const val TAG = "TaskDialog"
const val KEY_TITLE = "title"
const val KEY_MESSAGE = "message"
const val KEY_CANCELLABLE = "cancellable"
}
}

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.activities.UserDataActivity
import org.dolphinemu.dolphinemu.model.TaskViewModel
import kotlin.system.exitProcess
class UserDataImportWarningDialog : DialogFragment() {
private lateinit var taskViewModel: TaskViewModel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
taskViewModel = ViewModelProvider(requireActivity())[TaskViewModel::class.java]
val dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.user_data_import_warning)
.setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.setPositiveButton(R.string.yes) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
val taskArguments = Bundle()
taskArguments.putInt(TaskDialog.KEY_TITLE, R.string.import_in_progress)
taskArguments.putInt(TaskDialog.KEY_MESSAGE, R.string.do_not_close_app)
taskArguments.putBoolean(TaskDialog.KEY_CANCELLABLE, false)
taskViewModel.task = {
taskViewModel.setResult(
(requireActivity() as UserDataActivity).importUserData(
requireArguments().getString(KEY_URI_RESULT)!!.toUri()
)
)
}
taskViewModel.onResultDismiss = {
if (taskViewModel.mustRestartApp) {
exitProcess(0)
}
}
val taskDialog = TaskDialog()
taskDialog.arguments = taskArguments
taskDialog.show(requireActivity().supportFragmentManager, TaskDialog.TAG)
}
return dialog.create()
}
companion object {
const val TAG = "UserDataImportWarningDialog"
const val KEY_URI_RESULT = "uri"
}
}

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class TaskViewModel : ViewModel() {
var cancelled = false
var mustRestartApp = false
private val _result = MutableLiveData<Int>()
val result: LiveData<Int> get() = _result
private val _isComplete = MutableLiveData<Boolean>()
val isComplete: LiveData<Boolean> get() = _isComplete
private val _isRunning = MutableLiveData<Boolean>()
val isRunning: LiveData<Boolean> get() = _isRunning
lateinit var task: () -> Unit
var onResultDismiss: (() -> Unit)? = null
init {
clear()
}
fun clear() {
_result.value = 0
_isComplete.value = false
cancelled = false
mustRestartApp = false
onResultDismiss = null
_isRunning.value = false
}
fun runTask() {
if (isRunning.value == true) return
_isRunning.value = true
viewModelScope.launch {
withContext(Dispatchers.IO) {
task.invoke()
_isRunning.postValue(false)
_isComplete.postValue(true)
}
}
}
fun setResult(result: Int) {
_result.postValue(result)
}
}

View File

@ -1,61 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.utils;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.dolphinemu.dolphinemu.R;
import java.util.function.Supplier;
public class ThreadUtil
{
public static void runOnThreadAndShowResult(Activity activity, int progressTitle,
int progressMessage, @NonNull Supplier<String> f)
{
runOnThreadAndShowResult(activity, progressTitle, progressMessage, f, null);
}
public static void runOnThreadAndShowResult(Activity activity, int progressTitle,
int progressMessage, @NonNull Supplier<String> f,
@Nullable DialogInterface.OnDismissListener onResultDismiss)
{
Resources resources = activity.getResources();
AlertDialog progressDialog = new MaterialAlertDialogBuilder(activity)
.setTitle(progressTitle)
.setView(R.layout.dialog_indeterminate_progress)
.setCancelable(false)
.create();
if (progressMessage != 0)
progressDialog.setMessage(resources.getString(progressMessage));
progressDialog.show();
new Thread(() ->
{
String result = f.get();
activity.runOnUiThread(() ->
{
progressDialog.dismiss();
if (result != null)
{
new MaterialAlertDialogBuilder(activity)
.setMessage(result)
.setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss())
.setOnDismissListener(onResultDismiss)
.show();
}
});
}, resources.getString(progressTitle)).start();
}
}

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.utils
import android.app.Activity
import android.content.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R
import java.util.function.Supplier
object ThreadUtil {
@JvmStatic
@JvmOverloads
fun runOnThreadAndShowResult(
activity: Activity,
progressTitle: Int,
progressMessage: Int,
f: Supplier<String?>,
onResultDismiss: DialogInterface.OnDismissListener? = null
) {
val resources = activity.resources
val progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(progressTitle)
.setView(R.layout.dialog_indeterminate_progress)
.setCancelable(false)
.create()
if (progressMessage != 0) progressDialog.setMessage(resources.getString(progressMessage))
progressDialog.show()
Thread({
val result = f.get()
activity.runOnUiThread {
progressDialog.dismiss()
if (result != null) {
MaterialAlertDialogBuilder(activity)
.setMessage(result)
.setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.setOnDismissListener(onResultDismiss)
.show()
}
}
}, resources.getString(progressTitle)).start()
}
}