Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Backup/Restore and Apk Backup support #54

Merged
merged 13 commits into from
Aug 3, 2020
Merged
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".AppManager"
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ protected void onCreate(Bundle savedInstanceState) {
case R.id.action_select_all:
mAdapter.selectAll();
return true;
case R.id.action_backup_apk:
handleBatchOp(BatchOpsManager.OP_BACKUP_APK, R.string.failed_to_backup_some_apk_files);
return true;
case R.id.action_block_trackers:
handleBatchOp(BatchOpsManager.OP_BLOCK_TRACKERS, R.string.alert_failed_to_disable_trackers);
return true;
Expand Down Expand Up @@ -260,7 +263,6 @@ protected void onCreate(Bundle savedInstanceState) {
case R.id.action_uninstall:
handleBatchOp(BatchOpsManager.OP_UNINSTALL, R.string.alert_failed_to_uninstall);
return true;
case R.id.action_backup_apk:
case R.id.action_backup_data:
Toast.makeText(this, "This operation is not supported yet.", Toast.LENGTH_LONG).show();
mAdapter.clearSelection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,47 @@
import android.content.Context;
import android.text.TextUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import io.github.muntashirakon.AppManager.appops.AppOpsManager;
import io.github.muntashirakon.AppManager.fragments.BackupDialogFragment;
import io.github.muntashirakon.AppManager.storage.backup.BackupStorageManager;
import io.github.muntashirakon.AppManager.storage.compontents.ComponentsBlocker;
import io.github.muntashirakon.AppManager.runner.Runner;
import io.github.muntashirakon.AppManager.storage.compontents.ExternalComponentsImporter;
import io.github.muntashirakon.AppManager.utils.RunnerUtils;

// TODO: Will be converted to service one day
public class BatchOpsManager {
@IntDef(value = {
OP_BACKUP_APK,
OP_BACKUP_DATA,
OP_BACKUP,
OP_BLOCK_TRACKERS,
OP_CLEAR_DATA,
OP_DELETE_BACKUP,
OP_DISABLE,
OP_DISABLE_BACKGROUND,
OP_EXPORT_RULES,
OP_KILL,
OP_RESTORE_BACKUP,
OP_UNINSTALL
})
public @interface OpType {}
public static final int OP_BACKUP_APK = 0;
public static final int OP_BACKUP_DATA = 1;
public static final int OP_BACKUP = 1;
public static final int OP_BLOCK_TRACKERS = 2;
public static final int OP_CLEAR_DATA = 3;
public static final int OP_DISABLE = 4;
public static final int OP_DISABLE_BACKGROUND = 5;
public static final int OP_EXPORT_RULES = 6;
public static final int OP_KILL = 7;
public static final int OP_UNINSTALL = 8;
public static final int OP_DELETE_BACKUP = 4;
public static final int OP_DISABLE = 5;
public static final int OP_DISABLE_BACKGROUND = 6;
public static final int OP_EXPORT_RULES = 7;
public static final int OP_KILL = 8;
public static final int OP_RESTORE_BACKUP = 9;
public static final int OP_UNINSTALL = 10;

private Runner runner;
private Context context;
Expand All @@ -45,22 +53,28 @@ public BatchOpsManager(Context context) {
}

private List<String> packageNames;
private int flags = 0; // Currently only for backup/restore
private Result lastResult;

public void setFlags(int flags) {
this.flags = flags;
}

@NonNull
public Result performOp(@OpType int op, List<String> packageNames) {
this.runner.clear();
this.packageNames = packageNames;
switch (op) {
case OP_BACKUP_APK: // TODO
case OP_BACKUP_DATA: // TODO
break;
case OP_BACKUP_APK: return opBackupApk();
case OP_BACKUP: return opBackupRestore(BackupDialogFragment.MODE_BACKUP);
case OP_BLOCK_TRACKERS: return opBlockTrackers();
case OP_CLEAR_DATA: return opClearData();
case OP_DELETE_BACKUP: return opBackupRestore(BackupDialogFragment.MODE_DELETE);
case OP_DISABLE: return opDisable();
case OP_DISABLE_BACKGROUND: return opDisableBackground();
case OP_EXPORT_RULES: break; // Done in the main activity
case OP_KILL: return opKill();
case OP_RESTORE_BACKUP: return opBackupRestore(BackupDialogFragment.MODE_RESTORE);
case OP_UNINSTALL: return opUninstall();
}
lastResult = new Result() {
Expand All @@ -69,9 +83,10 @@ public boolean isSuccessful() {
return false;
}

@NonNull
@Override
public List<String> failedPackages() {
return null;
return new ArrayList<>();
}
};
return lastResult;
Expand All @@ -81,6 +96,58 @@ public Result getLastResult() {
return lastResult;
}

private Result opBackupApk() {
List<String> failedPackages = new ArrayList<>();
for (String packageName: packageNames) {
if (!BackupStorageManager.backupApk(packageName))
failedPackages.add(packageName);
}
return lastResult = new Result() {
@Override
public boolean isSuccessful() {
return failedPackages.size() == 0;
}

@NonNull
@Override
public List<String> failedPackages() {
return failedPackages;
}
};
}

private Result opBackupRestore(@BackupDialogFragment.ActionMode int mode) {
List<String> failedPackages = new ArrayList<>();
for (String packageName: packageNames) {
try (BackupStorageManager backupStorageManager = BackupStorageManager.getInstance(packageName)) {
backupStorageManager.setFlags(flags);
switch (mode) {
case BackupDialogFragment.MODE_BACKUP:
if (!backupStorageManager.backup()) failedPackages.add(packageName);
break;
case BackupDialogFragment.MODE_DELETE:
if (!backupStorageManager.delete_backup()) failedPackages.add(packageName);
break;
case BackupDialogFragment.MODE_RESTORE:
if (!backupStorageManager.restore()) failedPackages.add(packageName);
break;
}
}
}
return lastResult = new Result() {
@Override
public boolean isSuccessful() {
return failedPackages.size() == 0;
}

@NonNull
@Override
public List<String> failedPackages() {
return failedPackages;
}
};
}

private Result opBlockTrackers() {
final List<String> failedPkgList = ExternalComponentsImporter.applyFromTrackingComponents(context, packageNames);
return lastResult = new Result() {
Expand All @@ -89,6 +156,7 @@ public boolean isSuccessful() {
return failedPkgList.size() == 0;
}

@NonNull
@Override
public List<String> failedPackages() {
return failedPkgList;
Expand Down Expand Up @@ -170,6 +238,7 @@ public boolean isSuccessful() {
return TextUtils.isEmpty(result.getOutput());
}

@NonNull
@Override
public List<String> failedPackages() {
return result.getOutputAsList();
Expand All @@ -180,6 +249,6 @@ public List<String> failedPackages() {

public interface Result {
boolean isSuccessful();
List<String> failedPackages();
@NonNull List<String> failedPackages();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ private void setHorizontalView() {
R.string.uninstall_system_app_message : R.string.uninstall_app_message)
.setPositiveButton(R.string.uninstall, (dialog, which) -> new Thread(() -> {
// Try without root first then with root
if (RunnerUtils.uninstallPackage(mPackageName).isSuccessful()) {
if (RunnerUtils.uninstallPackageWithData(mPackageName).isSuccessful()) {
runOnUiThread(() -> {
Toast.makeText(mActivity, getString(R.string.uninstalled_successfully, mPackageLabel), Toast.LENGTH_LONG).show();
mActivity.finish();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package io.github.muntashirakon.AppManager.fragments;

import android.app.Dialog;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.Toast;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.batchops.BatchOpsManager;
import io.github.muntashirakon.AppManager.storage.backup.BackupStorageManager;

import static io.github.muntashirakon.AppManager.usage.Utils.getPackageLabel;
import static io.github.muntashirakon.AppManager.utils.Utils.requestExternalStoragePermissions;

public class BackupDialogFragment extends DialogFragment {
public static final String ARG_PACKAGES = "ARG_PACKAGES";

@IntDef(value = {
MODE_BACKUP,
MODE_RESTORE,
MODE_DELETE
})
public @interface ActionMode {}
public static final int MODE_BACKUP = 864;
public static final int MODE_RESTORE = 169;
public static final int MODE_DELETE = 642;

private @BackupStorageManager.BackupFlags int flags = BackupStorageManager.BACKUP_APK
| BackupStorageManager.BACKUP_DATA | BackupStorageManager.BACKUP_EXCLUDE_CACHE
| BackupStorageManager.BACKUP_RULES;
private @ActionMode int mode = MODE_BACKUP;
private List<String> packageNames;
FragmentActivity activity;

@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
activity = requireActivity();
Bundle args = requireArguments();
packageNames = args.getStringArrayList(ARG_PACKAGES);
if (packageNames == null) return super.onCreateDialog(savedInstanceState);
boolean[] checkedItems = new boolean[5];
Arrays.fill(checkedItems, true);
// Set external data to false
checkedItems[2] = false;
return new MaterialAlertDialogBuilder(activity, R.style.AppTheme_AlertDialog)
.setTitle(packageNames.size() == 1 ? getPackageLabel(activity
.getPackageManager(), packageNames.get(0)) : getString(R.string.backup_options))
.setMultiChoiceItems(R.array.backup_flags, checkedItems, (dialog, which, isChecked) -> {
if (isChecked) flags |= (1 << which);
else flags &= ~(1 << which);
})
.setPositiveButton(R.string.backup, (dialog, which) -> {
mode = MODE_BACKUP;
if (requestExternalStoragePermissions(activity)) {
handleBackup();
}
})
.setNegativeButton(R.string.restore, (dialog, which) -> {
mode = MODE_RESTORE;
if (requestExternalStoragePermissions(activity)) {
handleRestore();
}
})
.setNeutralButton(R.string.delete_backup, (dialog, which) -> {
mode = MODE_DELETE;
if (requestExternalStoragePermissions(activity)) {
handleDelete();
}
})
.create();
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length == 2 && grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
switch (mode) {
case MODE_BACKUP: handleBackup(); break;
case MODE_DELETE: handleDelete(); break;
case MODE_RESTORE: handleRestore(); break;
}
}
}

public void handleBackup() {
BatchOpsManager batchOpsManager = new BatchOpsManager(activity);
batchOpsManager.setFlags(flags);
new Thread(() -> {
if (!batchOpsManager.performOp(BatchOpsManager.OP_BACKUP, new ArrayList<>(packageNames)).isSuccessful()) {
final List<String> failedPackages = batchOpsManager.getLastResult().failedPackages();
activity.runOnUiThread(() -> new MaterialAlertDialogBuilder(activity, R.style.AppTheme_AlertDialog)
.setTitle(getResources().getQuantityString(R.plurals.alert_failed_to_backup, failedPackages.size(), failedPackages.size()))
.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, failedPackages), null)
.setNegativeButton(android.R.string.ok, null)
.show());
} else {
activity.runOnUiThread(() -> Toast.makeText(activity, R.string.the_operation_was_successful, Toast.LENGTH_LONG).show());
}
}).start();
}

public void handleRestore() {
BatchOpsManager batchOpsManager = new BatchOpsManager(activity);
batchOpsManager.setFlags(flags);
new Thread(() -> {
if (!batchOpsManager.performOp(BatchOpsManager.OP_RESTORE_BACKUP, new ArrayList<>(packageNames)).isSuccessful()) {
final List<String> failedPackages = batchOpsManager.getLastResult().failedPackages();
activity.runOnUiThread(() -> new MaterialAlertDialogBuilder(activity, R.style.AppTheme_AlertDialog)
.setTitle(getResources().getQuantityString(R.plurals.alert_failed_to_restore, failedPackages.size(), failedPackages.size()))
.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, failedPackages), null)
.setNegativeButton(android.R.string.ok, null)
.show());
} else {
activity.runOnUiThread(() -> Toast.makeText(activity, R.string.the_operation_was_successful, Toast.LENGTH_LONG).show());
}
}).start();
}

public void handleDelete() {
BatchOpsManager batchOpsManager = new BatchOpsManager(activity);
batchOpsManager.setFlags(flags);
new Thread(() -> {
if (!batchOpsManager.performOp(BatchOpsManager.OP_DELETE_BACKUP, new ArrayList<>(packageNames)).isSuccessful()) {
final List<String> failedPackages = batchOpsManager.getLastResult().failedPackages();
activity.runOnUiThread(() -> new MaterialAlertDialogBuilder(activity, R.style.AppTheme_AlertDialog)
.setTitle(getResources().getQuantityString(R.plurals.alert_failed_to_delete_backup, failedPackages.size(), failedPackages.size()))
.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, failedPackages), null)
.setNegativeButton(android.R.string.ok, null)
.show());
} else {
activity.runOnUiThread(() -> Toast.makeText(activity, R.string.the_operation_was_successful, Toast.LENGTH_LONG).show());
}
}).start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.muntashirakon.AppManager.misc;

public final class RequestCodes {
public static final int REQUEST_CODE_EXTERNAL_STORAGE_PERMISSIONS = 147;
}
Loading