From c904e068f0d52c3cdb8a659bb98a2c87da238d39 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Fri, 27 Jan 2023 00:46:00 -0500 Subject: [PATCH] Android: Use DialogFragments to direct UserData actions --- .../dolphinemu/activities/UserDataActivity.kt | 81 +++++++++++-------- .../dolphinemu/dialogs/NotificationDialog.kt | 22 +++++ .../dolphinemu/dialogs/TaskCompleteDialog.kt | 37 +++++++++ .../dolphinemu/dialogs/TaskDialog.kt | 70 ++++++++++++++++ .../dialogs/UserDataImportWarningDialog.kt | 59 ++++++++++++++ .../dolphinemu/model/TaskViewModel.kt | 56 +++++++++++++ 6 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/NotificationDialog.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskCompleteDialog.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskDialog.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/UserDataImportWarningDialog.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TaskViewModel.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt index f110efe2eb..5046e7c7c0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt @@ -4,7 +4,6 @@ package org.dolphinemu.dolphinemu.activities import android.content.ActivityNotFoundException import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Build @@ -14,10 +13,14 @@ 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 com.google.android.material.dialog.MaterialAlertDialogBuilder 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 @@ -28,10 +31,9 @@ import java.io.IOException import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import kotlin.system.exitProcess class UserDataActivity : AppCompatActivity() { - private var sMustRestartApp = false + private lateinit var taskViewModel: TaskViewModel private lateinit var mBinding: ActivityUserDataBinding @@ -79,31 +81,32 @@ class UserDataActivity : AppCompatActivity() { 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() + ) - MaterialAlertDialogBuilder(this) - .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() - - ThreadUtil.runOnThreadAndShowResult( - this, - R.string.import_in_progress, - R.string.do_not_close_app, - { resources.getString(importUserData(data!!.data!!)) }) { - if (sMustRestartApp) { - exitProcess(0) - } - } - } - .show() + val dialog = UserDataImportWarningDialog() + dialog.arguments = arguments + dialog.show(supportFragmentManager, UserDataImportWarningDialog.TAG) } else if (requestCode == REQUEST_CODE_EXPORT && resultCode == RESULT_OK) { - ThreadUtil.runOnThreadAndShowResult( - this, - R.string.export_in_progress, - 0 - ) { resources.getString(exportUserData(data!!.data!!)) } + 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) } } @@ -118,10 +121,15 @@ class UserDataActivity : AppCompatActivity() { } 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. - MaterialAlertDialogBuilder(this) - .setMessage(R.string.user_data_open_system_file_manager_failed) - .setPositiveButton(R.string.ok, null) - .show() + 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) } } } @@ -140,17 +148,18 @@ class UserDataActivity : AppCompatActivity() { startActivityForResult(intent, REQUEST_CODE_IMPORT) } - private fun importUserData(source: Uri): Int { + 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 + '/' - sMustRestartApp = true deleteChildrenRecursively(userDirectory) DirectoryInitialization.getGameListCache(this).delete() @@ -262,8 +271,12 @@ class UserDataActivity : AppCompatActivity() { private fun exportUserData(zos: ZipOutputStream, input: File, pathRelativeToRoot: File?) { if (input.isDirectory) { val children = input.listFiles() ?: throw IOException("Could not find directory $input") - for (child in children) { - exportUserData(zos, child, File(pathRelativeToRoot, child.name)) + + // 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 + '/')) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/NotificationDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/NotificationDialog.kt new file mode 100644 index 0000000000..a8026a1a78 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/NotificationDialog.kt @@ -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" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskCompleteDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskCompleteDialog.kt new file mode 100644 index 0000000000..734921bf0f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskCompleteDialog.kt @@ -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" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskDialog.kt new file mode 100644 index 0000000000..f28ff70884 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/TaskDialog.kt @@ -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" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/UserDataImportWarningDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/UserDataImportWarningDialog.kt new file mode 100644 index 0000000000..4ae469bb17 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/UserDataImportWarningDialog.kt @@ -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" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TaskViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TaskViewModel.kt new file mode 100644 index 0000000000..084ee0d512 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TaskViewModel.kt @@ -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() + val result: LiveData get() = _result + + private val _isComplete = MutableLiveData() + val isComplete: LiveData get() = _isComplete + + private val _isRunning = MutableLiveData() + val isRunning: LiveData 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) + } +}