Merge pull request #27 from grote/background-backups
Background backups
This commit is contained in:
commit
a18806cf99
29 changed files with 391 additions and 507 deletions
|
@ -14,10 +14,15 @@ AOSP.
|
|||
|
||||
## What makes this different?
|
||||
This application is compiled with the operating system and does not require a rooted device for use. It uses the same
|
||||
internal APIs as `adb backup` and only requires one permission: `android.permission.BACKUP`.
|
||||
internal APIs as `adb backup` and only requires the permission `android.permission.BACKUP` for this.
|
||||
|
||||
## Contributing
|
||||
Bug reports and pull requests are welcome on GitHub at https://github.com/stevesoltys/backup.
|
||||
|
||||
## Permissions
|
||||
|
||||
* `android.permission.BACKUP` to be allowed to back up apps
|
||||
* `android.permission.RECEIVE_BOOT_COMPLETED` to schedule automatic backups after boot
|
||||
|
||||
## License
|
||||
This application is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
||||
|
|
|
@ -9,7 +9,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 26
|
||||
targetSdkVersion 28
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -50,6 +50,7 @@ gradle.projectsEvaluated {
|
|||
}
|
||||
}
|
||||
|
||||
// http://www.31mins.com/android-studio-build-system-application/
|
||||
preBuild.doLast {
|
||||
def imlFile = file(project.name + ".iml")
|
||||
|
||||
|
|
|
@ -16,19 +16,20 @@ LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/sysconfig
|
|||
LOCAL_SRC_FILES := $(LOCAL_MODULE)
|
||||
include $(BUILD_PREBUILT)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
|
||||
commons-io:../../libs/commons-io-2.6.jar
|
||||
include $(BUILD_MULTI_PREBUILT)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_PACKAGE_NAME := Backup
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_REQUIRED_MODULES := permissions_com.stevesoltys.backup.xml whitelist_com.stevesoltys.backup.xml
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
LOCAL_PRIVATE_PLATFORM_APIS := true
|
||||
LOCAL_CERTIFICATE := platform
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := commons-io
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, java)
|
||||
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
|
||||
commons-io:../../libs/commons-io-2.6.jar
|
||||
include $(BUILD_MULTI_PREBUILT)
|
|
@ -7,13 +7,15 @@
|
|||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="26"
|
||||
android:targetSdkVersion="26"
|
||||
android:targetSdkVersion="28"
|
||||
tools:ignore="GradleOverrides,OldTargetApi" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.BACKUP"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:name=".Backup"
|
||||
android:supportsRtl="true"
|
||||
|
@ -48,5 +50,10 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.backup.BackupJobService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
package com.stevesoltys.backup;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class Backup extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
public static final int JOB_ID_BACKGROUND_BACKUP = 1;
|
||||
|
||||
startForegroundService(new Intent(this, ConfigurableBackupTransportService.class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.stevesoltys.backup.R;
|
|||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static com.stevesoltys.backup.settings.SettingsManager.areBackupsScheduled;
|
||||
|
||||
public class MainActivity extends Activity implements View.OnClickListener {
|
||||
|
||||
|
@ -21,6 +22,7 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
|||
public static final int LOAD_DOCUMENT_REQUEST_CODE = 3;
|
||||
|
||||
private MainActivityController controller;
|
||||
private Button automaticBackupsButton;
|
||||
private Button changeLocationButton;
|
||||
|
||||
@Override
|
||||
|
@ -33,6 +35,10 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
|||
findViewById(R.id.create_backup_button).setOnClickListener(this);
|
||||
findViewById(R.id.restore_backup_button).setOnClickListener(this);
|
||||
|
||||
automaticBackupsButton = findViewById(R.id.automatic_backups_button);
|
||||
automaticBackupsButton.setOnClickListener(this);
|
||||
if (areBackupsScheduled(this)) automaticBackupsButton.setVisibility(GONE);
|
||||
|
||||
changeLocationButton = findViewById(R.id.change_backup_location_button);
|
||||
changeLocationButton.setOnClickListener(this);
|
||||
}
|
||||
|
@ -61,6 +67,12 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
|||
controller.showLoadDocumentActivity(this);
|
||||
break;
|
||||
|
||||
case R.id.automatic_backups_button:
|
||||
if (controller.onAutomaticBackupsButtonClicked(this)) {
|
||||
automaticBackupsButton.setVisibility(GONE);
|
||||
}
|
||||
break;
|
||||
|
||||
case R.id.change_backup_location_button:
|
||||
controller.onChangeBackupLocationButtonClicked(this);
|
||||
break;
|
||||
|
|
|
@ -1,59 +1,52 @@
|
|||
package com.stevesoltys.backup.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.job.JobInfo;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stevesoltys.backup.activity.backup.CreateBackupActivity;
|
||||
import com.stevesoltys.backup.activity.restore.RestoreBackupActivity;
|
||||
import com.stevesoltys.backup.service.backup.BackupJobService;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import static android.app.job.JobInfo.NETWORK_TYPE_UNMETERED;
|
||||
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
|
||||
import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE;
|
||||
import static android.content.Intent.CATEGORY_OPENABLE;
|
||||
import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
|
||||
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
import static android.provider.DocumentsContract.buildDocumentUriUsingTree;
|
||||
import static android.provider.DocumentsContract.createDocument;
|
||||
import static android.provider.DocumentsContract.getTreeDocumentId;
|
||||
import static com.stevesoltys.backup.Backup.JOB_ID_BACKGROUND_BACKUP;
|
||||
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE;
|
||||
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE;
|
||||
import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri;
|
||||
import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword;
|
||||
import static com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri;
|
||||
import static com.stevesoltys.backup.settings.SettingsManager.setBackupsScheduled;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
* @author Torsten Grote
|
||||
*/
|
||||
class MainActivityController {
|
||||
public class MainActivityController {
|
||||
|
||||
private static final String TAG = MainActivityController.class.getName();
|
||||
|
||||
private static final String DOCUMENT_MIME_TYPE = "application/octet-stream";
|
||||
private static final String DOCUMENT_SUFFIX = "yyyy-MM-dd_HH_mm_ss";
|
||||
public static final String DOCUMENT_MIME_TYPE = "application/octet-stream";
|
||||
|
||||
void onBackupButtonClicked(Activity parent) {
|
||||
Uri folderUri = getBackupFolderUri(parent);
|
||||
if (folderUri == null) {
|
||||
showChooseFolderActivity(parent, true);
|
||||
} else {
|
||||
try {
|
||||
Uri fileUri = createBackupFile(parent.getContentResolver(), folderUri);
|
||||
showCreateBackupActivity(parent, fileUri);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Error creating backup file: ", e);
|
||||
showChooseFolderActivity(parent, true);
|
||||
}
|
||||
// ensure that backup service is started
|
||||
parent.startService(new Intent(parent, ConfigurableBackupTransportService.class));
|
||||
showCreateBackupActivity(parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +83,34 @@ class MainActivityController {
|
|||
}
|
||||
}
|
||||
|
||||
boolean onAutomaticBackupsButtonClicked(Activity parent) {
|
||||
if (getBackupFolderUri(parent) == null || getBackupPassword(parent) == null) {
|
||||
Toast.makeText(parent, "Please make at least one manual backup first.", Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
// schedule backups
|
||||
final ComponentName serviceName = new ComponentName(parent, BackupJobService.class);
|
||||
JobInfo job = new JobInfo.Builder(JOB_ID_BACKGROUND_BACKUP, serviceName)
|
||||
.setRequiredNetworkType(NETWORK_TYPE_UNMETERED)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiresStorageNotLow(true) // TODO warn the user instead
|
||||
.setPeriodic(DAYS.toMillis(1))
|
||||
.setRequiresCharging(true)
|
||||
.setPersisted(true)
|
||||
.build();
|
||||
JobScheduler scheduler = requireNonNull(parent.getSystemService(JobScheduler.class));
|
||||
scheduler.schedule(job);
|
||||
|
||||
// remember that backups were scheduled
|
||||
setBackupsScheduled(parent);
|
||||
|
||||
// show Toast informing the user
|
||||
Toast.makeText(parent, "Backups will run automatically now", Toast.LENGTH_SHORT).show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onChangeBackupLocationButtonClicked(Activity parent) {
|
||||
showChooseFolderActivity(parent, false);
|
||||
}
|
||||
|
@ -101,34 +122,22 @@ class MainActivityController {
|
|||
}
|
||||
|
||||
Uri folderUri = result.getData();
|
||||
ContentResolver contentResolver = parent.getContentResolver();
|
||||
|
||||
// persist permission to access backup folder across reboots
|
||||
int takeFlags = result.getFlags() &
|
||||
(FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
contentResolver.takePersistableUriPermission(folderUri, takeFlags);
|
||||
parent.getContentResolver().takePersistableUriPermission(folderUri, takeFlags);
|
||||
|
||||
// store backup folder location in settings
|
||||
setBackupFolderUri(parent, folderUri);
|
||||
|
||||
if (!continueToBackup) return;
|
||||
|
||||
try {
|
||||
// create a new backup file in folder
|
||||
Uri fileUri = createBackupFile(contentResolver, folderUri);
|
||||
|
||||
showCreateBackupActivity(parent, fileUri);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error creating backup file: ", e);
|
||||
// TODO show better error message once more infrastructure is in place
|
||||
Toast.makeText(parent, "Error creating backup file", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
showCreateBackupActivity(parent);
|
||||
}
|
||||
|
||||
private void showCreateBackupActivity(Activity parent, Uri fileUri) {
|
||||
private void showCreateBackupActivity(Activity parent) {
|
||||
Intent intent = new Intent(parent, CreateBackupActivity.class);
|
||||
intent.setData(fileUri);
|
||||
parent.startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -143,23 +152,4 @@ class MainActivityController {
|
|||
parent.startActivity(intent);
|
||||
}
|
||||
|
||||
private Uri createBackupFile(ContentResolver contentResolver, Uri folderUri) throws IOException {
|
||||
Uri documentUri = buildDocumentUriUsingTree(folderUri, getTreeDocumentId(folderUri));
|
||||
try {
|
||||
Uri fileUri = createDocument(contentResolver, documentUri, DOCUMENT_MIME_TYPE, getBackupFileName());
|
||||
if (fileUri == null) throw new IOException();
|
||||
return fileUri;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
// happens when folder was deleted and thus Uri permission don't exist anymore
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getBackupFileName() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat(DOCUMENT_SUFFIX, Locale.US);
|
||||
String date = dateFormat.format(new Date());
|
||||
return "backup-" + date;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.backup.activity.backup;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
|
@ -16,14 +15,12 @@ public class CreateBackupActivity extends PackageListActivity implements View.On
|
|||
|
||||
private CreateBackupActivityController controller;
|
||||
|
||||
private Uri contentUri;
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int viewId = view.getId();
|
||||
|
||||
if (viewId == R.id.create_confirm_button) {
|
||||
controller.onCreateBackupButtonClicked(selectedPackageList, contentUri, this);
|
||||
controller.onCreateBackupButtonClicked(selectedPackageList, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +33,6 @@ public class CreateBackupActivity extends PackageListActivity implements View.On
|
|||
|
||||
packageListView = findViewById(R.id.create_package_list);
|
||||
selectedPackageList = new HashSet<>();
|
||||
contentUri = getIntent().getData();
|
||||
|
||||
controller = new CreateBackupActivityController();
|
||||
AsyncTask.execute(() -> controller.populatePackageList(packageListView, CreateBackupActivity.this));
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.stevesoltys.backup.activity.backup;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
|
@ -75,16 +74,16 @@ class CreateBackupActivityController {
|
|||
});
|
||||
}
|
||||
|
||||
void onCreateBackupButtonClicked(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
|
||||
String password = SettingsManager.getBackupPassword(parent);
|
||||
if (password == null) {
|
||||
showEnterPasswordAlert(selectedPackages, contentUri, parent);
|
||||
showEnterPasswordAlert(selectedPackages, parent);
|
||||
} else {
|
||||
backupService.backupPackageData(selectedPackages, contentUri, parent, password);
|
||||
backupService.backupPackageData(selectedPackages, parent);
|
||||
}
|
||||
}
|
||||
|
||||
private void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) {
|
||||
final EditText passwordTextView = new EditText(parent);
|
||||
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
|
@ -97,9 +96,9 @@ class CreateBackupActivityController {
|
|||
if (passwordTextView.getText().length() == 0) {
|
||||
Toast.makeText(parent, "Please enter a password", Toast.LENGTH_SHORT).show();
|
||||
dialog.cancel();
|
||||
showEnterPasswordAlert(selectedPackages, contentUri, parent);
|
||||
showEnterPasswordAlert(selectedPackages, parent);
|
||||
} else {
|
||||
showConfirmPasswordAlert(selectedPackages, contentUri, parent,
|
||||
showConfirmPasswordAlert(selectedPackages, parent,
|
||||
passwordTextView.getText().toString());
|
||||
}
|
||||
})
|
||||
|
@ -108,7 +107,7 @@ class CreateBackupActivityController {
|
|||
.show();
|
||||
}
|
||||
|
||||
private void showConfirmPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent,
|
||||
private void showConfirmPasswordAlert(Set<String> selectedPackages, Activity parent,
|
||||
String originalPassword) {
|
||||
final EditText passwordTextView = new EditText(parent);
|
||||
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
@ -122,7 +121,7 @@ class CreateBackupActivityController {
|
|||
|
||||
if (originalPassword.equals(password)) {
|
||||
SettingsManager.setBackupPassword(parent, password);
|
||||
backupService.backupPackageData(selectedPackages, contentUri, parent, password);
|
||||
backupService.backupPackageData(selectedPackages, parent);
|
||||
|
||||
} else {
|
||||
new AlertDialog.Builder(parent)
|
||||
|
|
|
@ -7,12 +7,15 @@ import android.os.ParcelFileDescriptor;
|
|||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.*;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.stevesoltys.backup.R;
|
||||
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||
import com.stevesoltys.backup.service.restore.RestoreService;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -24,6 +27,10 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_FULL_BACKUP_DIRECTORY;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
|
@ -76,7 +83,7 @@ class RestoreBackupActivityController {
|
|||
while ((zipEntry = inputStream.getNextEntry()) != null) {
|
||||
String zipEntryPath = zipEntry.getName();
|
||||
|
||||
if (zipEntryPath.startsWith(ContentProviderBackupConfigurationBuilder.DEFAULT_FULL_BACKUP_DIRECTORY)) {
|
||||
if (zipEntryPath.startsWith(DEFAULT_FULL_BACKUP_DIRECTORY)) {
|
||||
String fileName = new File(zipEntryPath).getName();
|
||||
results.add(fileName);
|
||||
}
|
||||
|
|
|
@ -3,17 +3,11 @@ package com.stevesoltys.backup.service;
|
|||
import android.app.backup.IBackupManager;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
|
||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
||||
import com.stevesoltys.backup.session.backup.BackupSessionObserver;
|
||||
import com.stevesoltys.backup.session.restore.RestoreSession;
|
||||
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.RestoreComponent;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -30,19 +24,6 @@ public class TransportService {
|
|||
backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup"));
|
||||
}
|
||||
|
||||
public boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
|
||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
||||
|
||||
if (backupTransport.isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
|
||||
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
|
||||
backupTransport.initialize(backupComponent, restoreComponent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public BackupSession backup(BackupSessionObserver observer, Set<String> packages) throws RemoteException {
|
||||
|
||||
if (!BACKUP_TRANSPORT.equals(backupManager.getCurrentTransport())) {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package com.stevesoltys.backup.service.backup;
|
||||
|
||||
import android.app.backup.BackupManager;
|
||||
import android.app.backup.IBackupManager;
|
||||
import android.app.job.JobParameters;
|
||||
import android.app.job.JobService;
|
||||
import android.content.Intent;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.collect.Sets;
|
||||
import com.stevesoltys.backup.service.PackageService;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Set;
|
||||
|
||||
import static android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP;
|
||||
import static android.os.ServiceManager.getService;
|
||||
import static com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport;
|
||||
|
||||
public class BackupJobService extends JobService {
|
||||
|
||||
private final static String TAG = BackupJobService.class.getName();
|
||||
|
||||
private static final Set<String> IGNORED_PACKAGES = Sets.newArraySet(
|
||||
"com.android.providers.downloads.ui", "com.android.providers.downloads", "com.android.providers.media",
|
||||
"com.android.providers.calendar", "com.android.providers.contacts", "com.stevesoltys.backup"
|
||||
);
|
||||
|
||||
private final IBackupManager backupManager;
|
||||
private final PackageService packageService = new PackageService();
|
||||
|
||||
public BackupJobService() {
|
||||
backupManager = IBackupManager.Stub.asInterface(getService("backup"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.i(TAG, "Triggering full backup");
|
||||
startService(new Intent(this, ConfigurableBackupTransportService.class));
|
||||
try {
|
||||
LinkedList<String> packages = new LinkedList<>(packageService.getEligiblePackages());
|
||||
packages.removeAll(IGNORED_PACKAGES);
|
||||
// TODO use an observer to know when backups fail
|
||||
String[] packageArray = packages.toArray(new String[packages.size()]);
|
||||
ConfigurableBackupTransport backupTransport = getBackupTransport(getApplication());
|
||||
backupTransport.prepareBackup(packageArray.length);
|
||||
int result = backupManager.requestBackup(packageArray, null, null, FLAG_NON_INCREMENTAL_BACKUP);
|
||||
if (result == BackupManager.SUCCESS) {
|
||||
Log.i(TAG, "Backup succeeded ");
|
||||
} else {
|
||||
Log.e(TAG, "Backup failed: " + result);
|
||||
}
|
||||
|
||||
// TODO show notification on backup error
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error during backup: ", e);
|
||||
} finally {
|
||||
jobFinished(params, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
try {
|
||||
backupManager.cancelBackups();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error cancelling backup: ", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,14 +6,11 @@ import android.widget.PopupWindow;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stevesoltys.backup.R;
|
||||
import com.stevesoltys.backup.session.backup.BackupResult;
|
||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
||||
import com.stevesoltys.backup.session.backup.BackupSessionObserver;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -24,12 +21,9 @@ class BackupObserver implements BackupSessionObserver {
|
|||
|
||||
private final PopupWindow popupWindow;
|
||||
|
||||
private final URI contentUri;
|
||||
|
||||
BackupObserver(Activity context, PopupWindow popupWindow, URI contentUri) {
|
||||
BackupObserver(Activity context, PopupWindow popupWindow) {
|
||||
this.context = context;
|
||||
this.popupWindow = popupWindow;
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,14 +59,6 @@ class BackupObserver implements BackupSessionObserver {
|
|||
|
||||
@Override
|
||||
public void backupSessionCompleted(BackupSession backupSession, BackupResult backupResult) {
|
||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
||||
|
||||
if (!backupTransport.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
backupTransport.reset();
|
||||
|
||||
context.runOnUiThread(() -> {
|
||||
if (backupResult == BackupResult.SUCCESS) {
|
||||
Toast.makeText(context, R.string.backup_success, Toast.LENGTH_LONG).show();
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
package com.stevesoltys.backup.service.backup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stevesoltys.backup.R;
|
||||
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||
import com.stevesoltys.backup.activity.backup.BackupPopupWindowListener;
|
||||
import com.stevesoltys.backup.service.TransportService;
|
||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
|
@ -27,27 +26,14 @@ public class BackupService {
|
|||
|
||||
private final TransportService transportService = new TransportService();
|
||||
|
||||
public void backupPackageData(Set<String> selectedPackages, Uri contentUri, Activity parent,
|
||||
String selectedPassword) {
|
||||
public void backupPackageData(Set<String> selectedPackages, Activity parent) {
|
||||
try {
|
||||
selectedPackages.add("@pm@");
|
||||
|
||||
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
|
||||
.setContext(parent)
|
||||
.setOutputUri(contentUri)
|
||||
.setPackages(selectedPackages)
|
||||
.setPassword(selectedPassword)
|
||||
.build();
|
||||
|
||||
boolean success = transportService.initializeBackupTransport(backupConfiguration);
|
||||
|
||||
if (!success) {
|
||||
Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
PopupWindow popupWindow = PopupWindowUtil.showLoadingPopupWindow(parent);
|
||||
BackupObserver backupObserver = new BackupObserver(parent, popupWindow, new URI(contentUri.getPath()));
|
||||
BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
|
||||
ConfigurableBackupTransport backupTransport = getBackupTransport(parent.getApplication());
|
||||
backupTransport.prepareBackup(selectedPackages.size());
|
||||
BackupSession backupSession = transportService.backup(backupObserver, selectedPackages);
|
||||
|
||||
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
|
||||
|
|
|
@ -5,11 +5,10 @@ import android.widget.PopupWindow;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stevesoltys.backup.R;
|
||||
import com.stevesoltys.backup.session.restore.RestoreResult;
|
||||
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -56,14 +55,6 @@ class RestoreObserver implements RestoreSessionObserver {
|
|||
|
||||
@Override
|
||||
public void restoreSessionCompleted(RestoreResult restoreResult) {
|
||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
||||
|
||||
if (!backupTransport.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
backupTransport.reset();
|
||||
|
||||
context.runOnUiThread(() -> {
|
||||
if (restoreResult == RestoreResult.SUCCESS) {
|
||||
Toast.makeText(context, R.string.restore_success, Toast.LENGTH_LONG).show();
|
||||
|
|
|
@ -6,17 +6,18 @@ import android.os.RemoteException;
|
|||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stevesoltys.backup.R;
|
||||
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||
import com.stevesoltys.backup.activity.restore.RestorePopupWindowListener;
|
||||
import com.stevesoltys.backup.service.TransportService;
|
||||
import com.stevesoltys.backup.session.restore.RestoreSession;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
|
@ -27,21 +28,9 @@ public class RestoreService {
|
|||
private final TransportService transportService = new TransportService();
|
||||
|
||||
public void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent, String password) {
|
||||
ConfigurableBackupTransport backupTransport = getBackupTransport(parent.getApplication());
|
||||
backupTransport.prepareRestore(password, contentUri);
|
||||
try {
|
||||
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
|
||||
setContext(parent)
|
||||
.setOutputUri(contentUri)
|
||||
.setPackages(selectedPackages)
|
||||
.setPassword(password)
|
||||
.build();
|
||||
|
||||
boolean success = transportService.initializeBackupTransport(backupConfiguration);
|
||||
|
||||
if (!success) {
|
||||
Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
PopupWindow popupWindow = PopupWindowUtil.showLoadingPopupWindow(parent);
|
||||
RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size());
|
||||
RestoreSession restoreSession = transportService.restore(restoreObserver, selectedPackages);
|
||||
|
|
|
@ -10,6 +10,7 @@ public class SettingsManager {
|
|||
|
||||
private static final String PREF_KEY_BACKUP_URI = "backupUri";
|
||||
private static final String PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword";
|
||||
private static final String PREF_KEY_BACKUPS_SCHEDULED = "backupsScheduled";
|
||||
|
||||
public static void setBackupFolderUri(Context context, Uri uri) {
|
||||
getDefaultSharedPreferences(context)
|
||||
|
@ -41,4 +42,16 @@ public class SettingsManager {
|
|||
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null);
|
||||
}
|
||||
|
||||
public static void setBackupsScheduled(Context context) {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putBoolean(PREF_KEY_BACKUPS_SCHEDULED, true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Boolean areBackupsScheduled(Context context) {
|
||||
return getDefaultSharedPreferences(context).getBoolean(PREF_KEY_BACKUPS_SCHEDULED, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,14 +3,16 @@ package com.stevesoltys.backup.transport;
|
|||
import android.app.backup.BackupTransport;
|
||||
import android.app.backup.RestoreDescription;
|
||||
import android.app.backup.RestoreSet;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.RestoreComponent;
|
||||
import com.stevesoltys.backup.transport.component.stub.StubBackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.stub.StubRestoreComponent;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -20,31 +22,23 @@ public class ConfigurableBackupTransport extends BackupTransport {
|
|||
private static final String TRANSPORT_DIRECTORY_NAME =
|
||||
"com.stevesoltys.backup.transport.ConfigurableBackupTransport";
|
||||
|
||||
private BackupComponent backupComponent;
|
||||
private static final String TAG = TRANSPORT_DIRECTORY_NAME;
|
||||
|
||||
private RestoreComponent restoreComponent;
|
||||
private final BackupComponent backupComponent;
|
||||
|
||||
ConfigurableBackupTransport() {
|
||||
backupComponent = new StubBackupComponent();
|
||||
restoreComponent = new StubRestoreComponent();
|
||||
private final RestoreComponent restoreComponent;
|
||||
|
||||
ConfigurableBackupTransport(Context context) {
|
||||
backupComponent = new ContentProviderBackupComponent(context);
|
||||
restoreComponent = new ContentProviderRestoreComponent(context);
|
||||
}
|
||||
|
||||
public void initialize(BackupComponent backupComponent, RestoreComponent restoreComponent) {
|
||||
Preconditions.checkNotNull(backupComponent);
|
||||
Preconditions.checkNotNull(restoreComponent);
|
||||
Preconditions.checkState(!isActive());
|
||||
|
||||
this.restoreComponent = restoreComponent;
|
||||
this.backupComponent = backupComponent;
|
||||
public void prepareBackup(int numberOfPackages) {
|
||||
backupComponent.prepareBackup(numberOfPackages);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
backupComponent = new StubBackupComponent();
|
||||
restoreComponent = new StubRestoreComponent();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return !(backupComponent instanceof StubBackupComponent || restoreComponent instanceof StubRestoreComponent);
|
||||
public void prepareRestore(String password, Uri fileUri) {
|
||||
restoreComponent.prepareRestore(password, fileUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -58,6 +52,24 @@ public class ConfigurableBackupTransport extends BackupTransport {
|
|||
return this.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) {
|
||||
// TODO re-include key-value (incremental)
|
||||
// affected apps:
|
||||
// * com.android.documentsui
|
||||
// * android
|
||||
// * com.android.nfc
|
||||
// * com.android.calendar
|
||||
// * com.android.providers.settings
|
||||
// * com.android.cellbroadcastreceiver
|
||||
// * com.android.calllogbackup
|
||||
// * com.android.providers.blockednumber
|
||||
// * com.android.providers.userdictionary
|
||||
if (isFullBackup) return true;
|
||||
Log.i(TAG, "Excluding key-value backup of " + targetPackage.packageName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestBackupTime() {
|
||||
return backupComponent.requestBackupTime();
|
||||
|
@ -78,6 +90,12 @@ public class ConfigurableBackupTransport extends BackupTransport {
|
|||
return backupComponent.currentDestinationString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags) {
|
||||
// TODO handle flags
|
||||
return performBackup(packageInfo, inFd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
|
||||
return backupComponent.performIncrementalBackup(targetPackage, fileDescriptor);
|
||||
|
@ -88,6 +106,12 @@ public class ConfigurableBackupTransport extends BackupTransport {
|
|||
return backupComponent.checkFullBackupSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket, int flags) {
|
||||
// TODO handle flags
|
||||
return performFullBackup(targetPackage, socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
|
||||
return backupComponent.performFullBackup(targetPackage, fileDescriptor);
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
package com.stevesoltys.backup.transport;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class ConfigurableBackupTransportService extends Service {
|
||||
|
||||
private static final int FOREGROUND_ID = 43594;
|
||||
private static final String TAG = ConfigurableBackupTransportService.class.getName();
|
||||
|
||||
private static ConfigurableBackupTransport backupTransport = null;
|
||||
|
||||
public static ConfigurableBackupTransport getBackupTransport() {
|
||||
public static ConfigurableBackupTransport getBackupTransport(Context context) {
|
||||
|
||||
if (backupTransport == null) {
|
||||
backupTransport = new ConfigurableBackupTransport();
|
||||
backupTransport = new ConfigurableBackupTransport(context);
|
||||
}
|
||||
|
||||
return backupTransport;
|
||||
|
@ -26,11 +27,17 @@ public class ConfigurableBackupTransportService extends Service {
|
|||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
startForeground(FOREGROUND_ID, new Notification.Builder(this).build());
|
||||
Log.d(TAG, "Service created.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return getBackupTransport().getBinder();
|
||||
return getBackupTransport(getApplicationContext()).getBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.d(TAG, "Service destroyed.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import android.os.ParcelFileDescriptor;
|
|||
*/
|
||||
public interface BackupComponent {
|
||||
|
||||
void prepareBackup(int numberOfPackages);
|
||||
|
||||
String currentDestinationString();
|
||||
|
||||
String dataManagementLabel();
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.backup.transport.component;
|
|||
import android.app.backup.RestoreDescription;
|
||||
import android.app.backup.RestoreSet;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
/**
|
||||
|
@ -10,6 +11,8 @@ import android.os.ParcelFileDescriptor;
|
|||
*/
|
||||
public interface RestoreComponent {
|
||||
|
||||
void prepareRestore(String password, Uri fileUri);
|
||||
|
||||
int startRestore(long token, PackageInfo[] packages);
|
||||
|
||||
RestoreDescription nextRestorePackage();
|
||||
|
|
|
@ -1,28 +1,47 @@
|
|||
package com.stevesoltys.backup.transport.component.provider;
|
||||
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import com.stevesoltys.backup.security.CipherUtil;
|
||||
import com.stevesoltys.backup.security.KeyGenerator;
|
||||
import com.stevesoltys.backup.settings.SettingsManager;
|
||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static android.app.backup.BackupTransport.*;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import static android.app.backup.BackupTransport.TRANSPORT_ERROR;
|
||||
import static android.app.backup.BackupTransport.TRANSPORT_OK;
|
||||
import static android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED;
|
||||
import static android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
|
||||
import static android.provider.DocumentsContract.buildDocumentUriUsingTree;
|
||||
import static android.provider.DocumentsContract.createDocument;
|
||||
import static android.provider.DocumentsContract.getTreeDocumentId;
|
||||
import static com.stevesoltys.backup.activity.MainActivityController.DOCUMENT_MIME_TYPE;
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_BACKUP_QUOTA;
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_FULL_BACKUP_DIRECTORY;
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_INCREMENTAL_BACKUP_DIRECTORY;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
|
@ -32,18 +51,22 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
|
||||
private static final String TAG = ContentProviderBackupComponent.class.getName();
|
||||
|
||||
private static final String DOCUMENT_SUFFIX = "yyyy-MM-dd_HH_mm_ss";
|
||||
|
||||
private static final String DESTINATION_DESCRIPTION = "Backing up to zip file";
|
||||
|
||||
private static final String TRANSPORT_DATA_MANAGEMENT_LABEL = "";
|
||||
|
||||
private static final int INITIAL_BUFFER_SIZE = 512;
|
||||
|
||||
private final ContentProviderBackupConfiguration configuration;
|
||||
private final Context context;
|
||||
|
||||
private int numberOfPackages = 0;
|
||||
|
||||
private ContentProviderBackupState backupState;
|
||||
|
||||
public ContentProviderBackupComponent(ContentProviderBackupConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
public ContentProviderBackupComponent(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -58,7 +81,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
if (size <= 0) {
|
||||
result = TRANSPORT_PACKAGE_REJECTED;
|
||||
|
||||
} else if (size > configuration.getBackupSizeQuota()) {
|
||||
} else if (size > DEFAULT_BACKUP_QUOTA) {
|
||||
result = TRANSPORT_QUOTA_EXCEEDED;
|
||||
}
|
||||
|
||||
|
@ -70,6 +93,11 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
return TRANSPORT_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareBackup(int numberOfPackages) {
|
||||
this.numberOfPackages = numberOfPackages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String currentDestinationString() {
|
||||
return DESTINATION_DESCRIPTION;
|
||||
|
@ -87,7 +115,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
|
||||
@Override
|
||||
public long getBackupQuota(String packageName, boolean fullBackup) {
|
||||
return configuration.getBackupSizeQuota();
|
||||
return DEFAULT_BACKUP_QUOTA;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -115,7 +143,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
Cipher cipher = CipherUtil.startEncrypt(backupState.getSecretKey(), backupState.getSalt());
|
||||
backupState.setCipher(cipher);
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(configuration.getFullBackupDirectory() + backupState.getPackageName());
|
||||
ZipEntry zipEntry = new ZipEntry(DEFAULT_FULL_BACKUP_DIRECTORY + backupState.getPackageName());
|
||||
backupState.getOutputStream().putNextEntry(zipEntry);
|
||||
|
||||
} catch (Exception ex) {
|
||||
|
@ -164,7 +192,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
|
||||
long bytesTransferred = backupState.getBytesTransferred() + numBytes;
|
||||
|
||||
if (bytesTransferred > configuration.getBackupSizeQuota()) {
|
||||
if (bytesTransferred > DEFAULT_BACKUP_QUOTA) {
|
||||
return TRANSPORT_QUOTA_EXCEEDED;
|
||||
}
|
||||
|
||||
|
@ -199,7 +227,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
int dataSize = backupDataInput.getDataSize();
|
||||
|
||||
if (dataSize >= 0) {
|
||||
ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() +
|
||||
ZipEntry zipEntry = new ZipEntry(DEFAULT_INCREMENTAL_BACKUP_DIRECTORY +
|
||||
backupState.getPackageName() + "/" + chunkFileName);
|
||||
outputStream.putNextEntry(zipEntry);
|
||||
|
||||
|
@ -246,14 +274,18 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
backupState.getOutputStream().write(backupState.getSalt());
|
||||
backupState.getOutputStream().closeEntry();
|
||||
|
||||
String password = requireNonNull(configuration.getPassword());
|
||||
String password = requireNonNull(SettingsManager.getBackupPassword(context));
|
||||
backupState.setSecretKey(KeyGenerator.generate(password, backupState.getSalt()));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeOutputStream() throws IOException {
|
||||
ContentResolver contentResolver = configuration.getContext().getContentResolver();
|
||||
ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w");
|
||||
Uri folderUri = SettingsManager.getBackupFolderUri(context);
|
||||
// TODO notify about failure with notification
|
||||
Uri fileUri = createBackupFile(folderUri);
|
||||
|
||||
ParcelFileDescriptor outputFileDescriptor = context.getContentResolver().openFileDescriptor(fileUri, "w");
|
||||
if (outputFileDescriptor == null) throw new IOException();
|
||||
backupState.setOutputFileDescriptor(outputFileDescriptor);
|
||||
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor());
|
||||
|
@ -261,6 +293,25 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
backupState.setOutputStream(zipOutputStream);
|
||||
}
|
||||
|
||||
private Uri createBackupFile(Uri folderUri) throws IOException {
|
||||
Uri documentUri = buildDocumentUriUsingTree(folderUri, getTreeDocumentId(folderUri));
|
||||
try {
|
||||
Uri fileUri = createDocument(context.getContentResolver(), documentUri, DOCUMENT_MIME_TYPE, getBackupFileName());
|
||||
if (fileUri == null) throw new IOException();
|
||||
return fileUri;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
// happens when folder was deleted and thus Uri permission don't exist anymore
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getBackupFileName() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat(DOCUMENT_SUFFIX, Locale.US);
|
||||
String date = dateFormat.format(new Date());
|
||||
return "backup-" + date;
|
||||
}
|
||||
|
||||
private int clearBackupState(boolean closeFile) {
|
||||
|
||||
if (backupState == null) {
|
||||
|
@ -283,7 +334,7 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
|
||||
if (backupState.getPackageIndex() == numberOfPackages || closeFile) {
|
||||
if (outputStream != null) {
|
||||
outputStream.finish();
|
||||
outputStream.close();
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
package com.stevesoltys.backup.transport.component.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class ContentProviderBackupConfiguration {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final Uri uri;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final long backupSizeQuota;
|
||||
|
||||
private final Set<String> packages;
|
||||
|
||||
private final String fullBackupDirectory;
|
||||
|
||||
private final String incrementalBackupDirectory;
|
||||
|
||||
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, String password,
|
||||
long backupSizeQuota, String fullBackupDirectory,
|
||||
String incrementalBackupDirectory) {
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
this.packages = packages;
|
||||
this.password = password;
|
||||
this.backupSizeQuota = backupSizeQuota;
|
||||
this.fullBackupDirectory = fullBackupDirectory;
|
||||
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
||||
}
|
||||
|
||||
public long getBackupSizeQuota() {
|
||||
return backupSizeQuota;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public String getFullBackupDirectory() {
|
||||
return fullBackupDirectory;
|
||||
}
|
||||
|
||||
public String getIncrementalBackupDirectory() {
|
||||
return incrementalBackupDirectory;
|
||||
}
|
||||
|
||||
public int getPackageCount() {
|
||||
return packages.size();
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package com.stevesoltys.backup.transport.component.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class ContentProviderBackupConfigurationBuilder {
|
||||
|
||||
public static final String DEFAULT_FULL_BACKUP_DIRECTORY = "full/";
|
||||
|
||||
public static final String DEFAULT_INCREMENTAL_BACKUP_DIRECTORY = "incr/";
|
||||
|
||||
private Context context;
|
||||
|
||||
private Uri outputUri;
|
||||
|
||||
private Set<String> packages;
|
||||
|
||||
private String password;
|
||||
|
||||
private long backupSizeQuota = Long.MAX_VALUE;
|
||||
|
||||
private String incrementalBackupDirectory = DEFAULT_INCREMENTAL_BACKUP_DIRECTORY;
|
||||
|
||||
private String fullBackupDirectory = DEFAULT_FULL_BACKUP_DIRECTORY;
|
||||
|
||||
public ContentProviderBackupConfiguration build() {
|
||||
Preconditions.checkState(context != null, "Context must be set.");
|
||||
Preconditions.checkState(outputUri != null, "Output URI must be set.");
|
||||
Preconditions.checkState(packages != null, "Package list must be set.");
|
||||
Preconditions.checkState(incrementalBackupDirectory != null, "Incremental backup directory must be set.");
|
||||
Preconditions.checkState(fullBackupDirectory != null, "Full backup directory must be set.");
|
||||
|
||||
return new ContentProviderBackupConfiguration(context, outputUri, packages, password, backupSizeQuota,
|
||||
fullBackupDirectory, incrementalBackupDirectory);
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) {
|
||||
this.backupSizeQuota = backupSizeQuota;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setContext(Context context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setFullBackupDirectory(String fullBackupDirectory) {
|
||||
this.fullBackupDirectory = fullBackupDirectory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setIncrementalBackupDirectory(String incrementalBackupDirectory) {
|
||||
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setOutputUri(Uri outputUri) {
|
||||
this.outputUri = outputUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setPackages(Set<String> packages) {
|
||||
this.packages = packages;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentProviderBackupConfigurationBuilder setPassword(String password) {
|
||||
this.password = password;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,14 @@ package com.stevesoltys.backup.transport.component.provider;
|
|||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
class ContentProviderBackupConstants {
|
||||
public interface ContentProviderBackupConstants {
|
||||
|
||||
String SALT_FILE_PATH = "salt";
|
||||
|
||||
String DEFAULT_FULL_BACKUP_DIRECTORY = "full/";
|
||||
|
||||
String DEFAULT_INCREMENTAL_BACKUP_DIRECTORY = "incr/";
|
||||
|
||||
long DEFAULT_BACKUP_QUOTA = Long.MAX_VALUE;
|
||||
|
||||
static final String SALT_FILE_PATH = "salt";
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.stevesoltys.backup.transport.component.provider;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.app.backup.RestoreDescription;
|
||||
import android.app.backup.RestoreSet;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
@ -38,6 +41,9 @@ import static android.app.backup.BackupTransport.TRANSPORT_OK;
|
|||
import static android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED;
|
||||
import static android.app.backup.RestoreDescription.TYPE_FULL_STREAM;
|
||||
import static android.app.backup.RestoreDescription.TYPE_KEY_VALUE;
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_FULL_BACKUP_DIRECTORY;
|
||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_INCREMENTAL_BACKUP_DIRECTORY;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -50,12 +56,23 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
|
||||
private static final int DEFAULT_BUFFER_SIZE = 2048;
|
||||
|
||||
private ContentProviderBackupConfiguration configuration;
|
||||
@Nullable
|
||||
private String password;
|
||||
@Nullable
|
||||
private Uri fileUri;
|
||||
|
||||
private ContentProviderRestoreState restoreState;
|
||||
|
||||
public ContentProviderRestoreComponent(ContentProviderBackupConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
private final Context context;
|
||||
|
||||
public ContentProviderRestoreComponent(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareRestore(String password, Uri fileUri) {
|
||||
this.password = password;
|
||||
this.fileUri = fileUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,14 +81,16 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
restoreState.setPackages(packages);
|
||||
restoreState.setPackageIndex(-1);
|
||||
|
||||
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
|
||||
String password = requireNonNull(this.password);
|
||||
|
||||
if (!password.isEmpty()) {
|
||||
try {
|
||||
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
|
||||
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
|
||||
seekToEntry(inputStream, ContentProviderBackupConstants.SALT_FILE_PATH);
|
||||
|
||||
restoreState.setSalt(Streams.readFullyNoClose(inputStream));
|
||||
restoreState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt()));
|
||||
restoreState.setSecretKey(KeyGenerator.generate(password, restoreState.getSalt()));
|
||||
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
IoUtils.closeQuietly(inputStream);
|
||||
|
@ -116,11 +135,11 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
restoreState.setPackageIndex(packageIndex);
|
||||
String name = packages[packageIndex].packageName;
|
||||
|
||||
if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) {
|
||||
if (containsPackageFile(DEFAULT_INCREMENTAL_BACKUP_DIRECTORY + name)) {
|
||||
restoreState.setRestoreType(TYPE_KEY_VALUE);
|
||||
return new RestoreDescription(name, restoreState.getRestoreType());
|
||||
|
||||
} else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) {
|
||||
} else if (containsPackageFile(DEFAULT_FULL_BACKUP_DIRECTORY + name)) {
|
||||
restoreState.setRestoreType(TYPE_FULL_STREAM);
|
||||
return new RestoreDescription(name, restoreState.getRestoreType());
|
||||
}
|
||||
|
@ -159,7 +178,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
|
||||
|
||||
Optional<ZipEntry> zipEntryOptional = seekToEntry(inputStream,
|
||||
configuration.getIncrementalBackupDirectory() + packageName);
|
||||
DEFAULT_INCREMENTAL_BACKUP_DIRECTORY + packageName);
|
||||
|
||||
while (zipEntryOptional.isPresent()) {
|
||||
String fileName = new File(zipEntryOptional.get().getName()).getName();
|
||||
|
@ -170,7 +189,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
backupDataOutput.writeEntityData(backupData, backupData.length);
|
||||
inputStream.closeEntry();
|
||||
|
||||
zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName);
|
||||
zipEntryOptional = seekToEntry(inputStream, DEFAULT_INCREMENTAL_BACKUP_DIRECTORY + packageName);
|
||||
}
|
||||
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
|
@ -207,7 +226,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
|
||||
restoreState.setInputStream(inputStream);
|
||||
|
||||
if (!seekToEntry(inputStream, configuration.getFullBackupDirectory() + name).isPresent()) {
|
||||
if (!seekToEntry(inputStream, DEFAULT_FULL_BACKUP_DIRECTORY + name).isPresent()) {
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
IoUtils.closeQuietly(outputFileDescriptor);
|
||||
return TRANSPORT_PACKAGE_REJECTED;
|
||||
|
@ -317,8 +336,8 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
}
|
||||
|
||||
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
|
||||
ContentResolver contentResolver = configuration.getContext().getContentResolver();
|
||||
return contentResolver.openFileDescriptor(configuration.getUri(), "r");
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
return contentResolver.openFileDescriptor(requireNonNull(fileUri), "r");
|
||||
}
|
||||
|
||||
private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) {
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
package com.stevesoltys.backup.transport.component.stub;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class StubBackupComponent implements BackupComponent {
|
||||
|
||||
@Override
|
||||
public void cancelFullBackup() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int checkFullBackupSize(long size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int clearBackupData(PackageInfo packageInfo) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String currentDestinationString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dataManagementLabel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int finishBackup() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBackupQuota(String packageName, boolean fullBackup) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int initializeDevice() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int performIncrementalBackup(PackageInfo targetPackage, ParcelFileDescriptor data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestBackupTime() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestFullBackupTime() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sendBackupData(int numBytes) {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package com.stevesoltys.backup.transport.component.stub;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import com.stevesoltys.backup.transport.component.RestoreComponent;
|
||||
import android.app.backup.RestoreDescription;
|
||||
import android.app.backup.RestoreSet;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class StubRestoreComponent implements RestoreComponent {
|
||||
|
||||
@Override
|
||||
public int startRestore(long token, PackageInfo[] packages) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestoreDescription nextRestorePackage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int abortFullRestore() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentRestoreSet() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishRestore() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestoreSet[] getAvailableRestoreSets() {
|
||||
return new RestoreSet[0];
|
||||
}
|
||||
}
|
|
@ -25,6 +25,15 @@
|
|||
android:layout_marginRight="@dimen/button_horizontal_margin"
|
||||
android:text="@string/restore_backup_button" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/automatic_backups_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:layout_marginLeft="@dimen/button_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/button_horizontal_margin"
|
||||
android:text="Activate automatic backups" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/change_backup_location_button"
|
||||
android:layout_width="match_parent"
|
||||
|
|
Loading…
Reference in a new issue