Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #10416 from JosJuice/android-import-export
Android: Add import/export options for user data
  • Loading branch information
JMC47 committed Jan 30, 2022
2 parents 86f83de + bf5cd90 commit da05173
Show file tree
Hide file tree
Showing 8 changed files with 466 additions and 53 deletions.
Expand Up @@ -2,23 +2,44 @@

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 android.widget.Button;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.ThreadUtil;

public class UserDataActivity extends AppCompatActivity implements View.OnClickListener
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;

public static void launch(Context context)
{
Intent launcher = new Intent(context, UserDataActivity.class);
Expand All @@ -36,6 +57,8 @@ protected void onCreate(Bundle savedInstanceState)
TextView textPath = findViewById(R.id.text_path);
TextView textAndroid11 = findViewById(R.id.text_android_11);
Button buttonOpenSystemFileManager = findViewById(R.id.button_open_system_file_manager);
Button buttonImportUserData = findViewById(R.id.button_import_user_data);
Button buttonExportUserData = findViewById(R.id.button_export_user_data);

boolean android_10 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
boolean android_11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
Expand All @@ -50,15 +73,63 @@ protected void onCreate(Bundle savedInstanceState)
textAndroid11.setVisibility(android_11 && !legacy ? View.VISIBLE : View.GONE);

buttonOpenSystemFileManager.setVisibility(android_11 ? View.VISIBLE : View.GONE);
buttonOpenSystemFileManager.setOnClickListener(view -> openFileManager());

buttonOpenSystemFileManager.setOnClickListener(this);
buttonImportUserData.setOnClickListener(view -> importUserData());

buttonExportUserData.setOnClickListener(view -> exportUserData());

// show up button
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}

@Override
public void onClick(View v)
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();

AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.DolphinDialogBase);

builder.setMessage(R.string.user_data_import_warning);
builder.setNegativeButton(R.string.no, (dialog, i) -> dialog.dismiss());
builder.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);
}
});
});

builder.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
{
Expand All @@ -84,13 +155,6 @@ public void onClick(View v)
}
}

@Override
public boolean onSupportNavigateUp()
{
onBackPressed();
return true;
}

private Intent getFileManagerIntent(String packageName)
{
// Fragile, but some phones don't expose the system file manager in any better way
Expand All @@ -99,4 +163,178 @@ private Intent getFileManagerIntent(String packageName)
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());

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 (!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()));
}
}
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);
}
}
}
}
}
Expand Up @@ -61,7 +61,12 @@ protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

MainPresenter.skipRescanningLibrary();
// If we came here from the game list, we don't want to rescan when returning to the game list.
// But if we came here after UserDataActivity restarted the app, we do want to rescan.
if (savedInstanceState == null)
{
MainPresenter.skipRescanningLibrary();
}

setContentView(R.layout.activity_settings);

Expand Down

0 comments on commit da05173

Please sign in to comment.