mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 06:09:50 -06:00
Merge pull request #11515 from t895/user-data-kotlin
Android: Rewrite User Data Activity in Kotlin
This commit is contained in:
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user