Refactor transport components to eliminate need to initialize and reset transport

This commit is contained in:
Torsten Grote 2019-06-11 20:53:44 -03:00
parent b17a55ac57
commit 92ce6c1a5c
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
20 changed files with 175 additions and 506 deletions

View file

@ -8,7 +8,6 @@ import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import com.stevesoltys.backup.activity.backup.CreateBackupActivity;
@ -16,11 +15,6 @@ 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;
@ -28,9 +22,6 @@ 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;
@ -47,27 +38,16 @@ import static java.util.concurrent.TimeUnit.DAYS;
*/
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 {
// ensure that backup service is started
parent.startService(new Intent(parent, ConfigurableBackupTransportService.class));
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);
}
}
@ -148,22 +128,11 @@ public class MainActivityController {
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);
}
@ -178,23 +147,4 @@ public class MainActivityController {
parent.startActivity(intent);
}
public static 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 static String getBackupFileName() {
SimpleDateFormat dateFormat = new SimpleDateFormat(DOCUMENT_SUFFIX, Locale.US);
String date = dateFormat.format(new Date());
return "backup-" + date;
}
}

View file

@ -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));

View file

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

View file

@ -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.ConfigurableBackupTransport.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);
}

View file

@ -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())) {

View file

@ -5,27 +5,20 @@ import android.app.backup.IBackupManager;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import com.google.android.collect.Sets;
import com.stevesoltys.backup.service.PackageService;
import com.stevesoltys.backup.service.TransportService;
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import java.io.IOException;
import java.util.HashSet;
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.activity.MainActivityController.createBackupFile;
import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri;
import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword;
import static com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport;
public class BackupJobService extends JobService {
@ -37,7 +30,6 @@ public class BackupJobService extends JobService {
);
private final IBackupManager backupManager;
private final TransportService transportService = new TransportService();
public BackupJobService() {
backupManager = IBackupManager.Stub.asInterface(getService("backup"));
@ -50,17 +42,10 @@ public class BackupJobService extends JobService {
try {
LinkedList<String> packages = new LinkedList<>(new PackageService().getEligiblePackages());
packages.removeAll(IGNORED_PACKAGES);
Uri fileUri = createBackupFile(getContentResolver(), getBackupFolderUri(this));
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
.setContext(this)
.setPackages(new HashSet<>(packages))
.setOutputUri(fileUri)
.setPassword(getBackupPassword(this))
.build();
transportService.initializeBackupTransport(backupConfiguration);
// 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 ");
@ -69,8 +54,6 @@ public class BackupJobService extends JobService {
}
// TODO show notification on backup error
} catch (IOException e) {
Log.e(TAG, "Error creating backup file: ", e);
} catch (RemoteException e) {
Log.e(TAG, "Error during backup: ", e);
} finally {

View file

@ -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();

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -3,14 +3,15 @@ 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 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 +21,27 @@ public class ConfigurableBackupTransport extends BackupTransport {
private static final String TRANSPORT_DIRECTORY_NAME =
"com.stevesoltys.backup.transport.ConfigurableBackupTransport";
private BackupComponent backupComponent;
public static final String DEFAULT_FULL_BACKUP_DIRECTORY = "full/";
private RestoreComponent restoreComponent;
public static final String DEFAULT_INCREMENTAL_BACKUP_DIRECTORY = "incr/";
ConfigurableBackupTransport() {
backupComponent = new StubBackupComponent();
restoreComponent = new StubRestoreComponent();
public static final long DEFAULT_BACKUP_QUOTA = Long.MAX_VALUE;
private final BackupComponent backupComponent;
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

View file

@ -1,6 +1,7 @@
package com.stevesoltys.backup.transport;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
@ -14,10 +15,10 @@ public class ConfigurableBackupTransportService extends Service {
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;
@ -31,7 +32,7 @@ public class ConfigurableBackupTransportService extends Service {
@Override
public IBinder onBind(Intent intent) {
return getBackupTransport().getBinder();
return getBackupTransport(getApplicationContext()).getBinder();
}
@Override

View file

@ -8,6 +8,8 @@ import android.os.ParcelFileDescriptor;
*/
public interface BackupComponent {
void prepareBackup(int numberOfPackages);
String currentDestinationString();
String dataManagementLabel();

View file

@ -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();

View file

@ -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.ConfigurableBackupTransport.DEFAULT_BACKUP_QUOTA;
import static com.stevesoltys.backup.transport.ConfigurableBackupTransport.DEFAULT_FULL_BACKUP_DIRECTORY;
import static com.stevesoltys.backup.transport.ConfigurableBackupTransport.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();

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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.ConfigurableBackupTransport.DEFAULT_FULL_BACKUP_DIRECTORY;
import static com.stevesoltys.backup.transport.ConfigurableBackupTransport.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) {

View file

@ -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;
}
}

View file

@ -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];
}
}