Android: Use DialogFragments to direct UserData actions

This commit is contained in:
Charles Lombardo 2023-01-27 00:46:00 -05:00
parent 8d1cf14565
commit c904e068f0
6 changed files with 291 additions and 34 deletions

View File

@ -4,7 +4,6 @@ package org.dolphinemu.dolphinemu.activities
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -14,10 +13,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityUserDataBinding 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.*
import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint
import org.dolphinemu.dolphinemu.utils.ThemeHelper.setNavigationBarColor import org.dolphinemu.dolphinemu.utils.ThemeHelper.setNavigationBarColor
@ -28,10 +31,9 @@ import java.io.IOException
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.system.exitProcess
class UserDataActivity : AppCompatActivity() { class UserDataActivity : AppCompatActivity() {
private var sMustRestartApp = false private lateinit var taskViewModel: TaskViewModel
private lateinit var mBinding: ActivityUserDataBinding private lateinit var mBinding: ActivityUserDataBinding
@ -79,31 +81,32 @@ class UserDataActivity : AppCompatActivity() {
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
taskViewModel = ViewModelProvider(this)[TaskViewModel::class.java]
if (requestCode == REQUEST_CODE_IMPORT && resultCode == RESULT_OK) { if (requestCode == REQUEST_CODE_IMPORT && resultCode == RESULT_OK) {
val arguments = Bundle()
arguments.putString(
UserDataImportWarningDialog.KEY_URI_RESULT,
data!!.data!!.toString()
)
MaterialAlertDialogBuilder(this) val dialog = UserDataImportWarningDialog()
.setMessage(R.string.user_data_import_warning) dialog.arguments = arguments
.setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.dismiss() } dialog.show(supportFragmentManager, UserDataImportWarningDialog.TAG)
.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()
} else if (requestCode == REQUEST_CODE_EXPORT && resultCode == RESULT_OK) { } else if (requestCode == REQUEST_CODE_EXPORT && resultCode == RESULT_OK) {
ThreadUtil.runOnThreadAndShowResult( taskViewModel.clear()
this, taskViewModel.task = {
R.string.export_in_progress, val resultResource = exportUserData(data!!.data!!)
0 taskViewModel.setResult(resultResource)
) { resources.getString(exportUserData(data!!.data!!)) } }
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) { } catch (e2: ActivityNotFoundException) {
// Activity not found. Perhaps it was removed by the OEM, or by some new Android version // 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. // that didn't exist at the time of writing. Not much we can do other than tell the user.
MaterialAlertDialogBuilder(this) val arguments = Bundle()
.setMessage(R.string.user_data_open_system_file_manager_failed) arguments.putInt(
.setPositiveButton(R.string.ok, null) NotificationDialog.KEY_MESSAGE,
.show() 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) startActivityForResult(intent, REQUEST_CODE_IMPORT)
} }
private fun importUserData(source: Uri): Int { fun importUserData(source: Uri): Int {
try { try {
if (!isDolphinUserDataBackup(source)) if (!isDolphinUserDataBackup(source))
return R.string.user_data_import_invalid_file return R.string.user_data_import_invalid_file
taskViewModel.mustRestartApp = true
contentResolver.openInputStream(source).use { `is` -> contentResolver.openInputStream(source).use { `is` ->
ZipInputStream(`is`).use { zis -> ZipInputStream(`is`).use { zis ->
val userDirectory = File(DirectoryInitialization.getUserDirectory()) val userDirectory = File(DirectoryInitialization.getUserDirectory())
val userDirectoryCanonicalized = userDirectory.canonicalPath + '/' val userDirectoryCanonicalized = userDirectory.canonicalPath + '/'
sMustRestartApp = true
deleteChildrenRecursively(userDirectory) deleteChildrenRecursively(userDirectory)
DirectoryInitialization.getGameListCache(this).delete() DirectoryInitialization.getGameListCache(this).delete()
@ -262,9 +271,13 @@ class UserDataActivity : AppCompatActivity() {
private fun exportUserData(zos: ZipOutputStream, input: File, pathRelativeToRoot: File?) { private fun exportUserData(zos: ZipOutputStream, input: File, pathRelativeToRoot: File?) {
if (input.isDirectory) { if (input.isDirectory) {
val children = input.listFiles() ?: throw IOException("Could not find directory $input") val children = input.listFiles() ?: throw IOException("Could not find directory $input")
// Check if the coroutine was cancelled
if (!taskViewModel.cancelled) {
for (child in children) { for (child in children) {
exportUserData(zos, child, File(pathRelativeToRoot, child.name)) exportUserData(zos, child, File(pathRelativeToRoot, child.name))
} }
}
if (children.isEmpty() && pathRelativeToRoot != null) { if (children.isEmpty() && pathRelativeToRoot != null) {
zos.putNextEntry(ZipEntry(pathRelativeToRoot.path + '/')) zos.putNextEntry(ZipEntry(pathRelativeToRoot.path + '/'))
} }

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