From b182e743e8c5551d1710cfb02d5f3d76252a2294 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 15 Feb 2019 02:46:24 -0500 Subject: [PATCH 01/10] Add support for encrypted backups 1. Add prompt for entering password during backup and restore. 2. Use PBKDF2 to generate a secret key that is used to encrypt backups. 3. Store salt in backup zip file. 4. Fetch salt from backup zip file during restore and use it to decrypt restoration data. --- .../activity/backup/CreateBackupActivity.java | 2 +- .../CreateBackupActivityController.java | 72 ++++++- .../restore/RestoreBackupActivity.java | 2 +- .../RestoreBackupActivityController.java | 32 ++- .../backup/security/KeyGenerator.java | 44 ++++ .../ContentProviderBackupComponent.java | 202 ++++++++++-------- .../ContentProviderBackupConfiguration.java | 32 +-- ...entProviderBackupConfigurationBuilder.java | 33 +-- .../ContentProviderBackupConstants.java | 11 + .../provider/ContentProviderBackupState.java | 80 ++++--- .../ContentProviderRestoreComponent.java | 49 ++++- .../provider/ContentProviderRestoreState.java | 58 ++--- 12 files changed, 420 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java create mode 100644 app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java index f9411f65..382469b9 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java @@ -32,7 +32,7 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen switch (viewId) { case R.id.create_confirm_button: - controller.backupPackages(selectedPackageList, contentUri, this); + controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); break; } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index 20a20236..57de2aaf 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -1,20 +1,19 @@ package com.stevesoltys.backup.activity.backup; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.RemoteException; +import android.text.InputType; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.PopupWindow; -import android.widget.Toast; +import android.widget.*; import com.stevesoltys.backup.R; import com.stevesoltys.backup.session.BackupManagerController; import com.stevesoltys.backup.session.backup.BackupSession; @@ -27,7 +26,10 @@ import com.stevesoltys.backup.transport.component.provider.ContentProviderBackup import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder; import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent; -import java.util.*; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; /** * @author Steve Soltys @@ -61,12 +63,64 @@ class CreateBackupActivityController { packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); } - void backupPackages(Set selectedPackages, Uri contentUri, Activity parent) { + void showEnterPasswordAlert(Set selectedPackages, Uri contentUri, Activity parent) { + final EditText passwordTextView = new EditText(parent); + passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + new AlertDialog.Builder(parent) + .setMessage("Please enter a password.\n" + + "You'll need this to restore your backup, so write it down!") + .setView(passwordTextView) + + .setPositiveButton("Set password", (dialog, button) -> + showConfirmPasswordAlert(selectedPackages, contentUri, parent, + passwordTextView.getText().toString())) + + .setNegativeButton("Cancel", (dialog, button) -> dialog.cancel()) + .show(); + } + + private void showConfirmPasswordAlert(Set selectedPackages, Uri contentUri, Activity parent, + String originalPassword) { + final EditText passwordTextView = new EditText(parent); + passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + new AlertDialog.Builder(parent) + .setMessage("Please confirm your password.") + .setView(passwordTextView) + + .setPositiveButton("Confirm", (dialog, button) -> { + String password = passwordTextView.getText().toString(); + + if (originalPassword.equals(password)) { + backupPackages(selectedPackages, contentUri, parent, password); + + } else { + new AlertDialog.Builder(parent) + .setMessage("Passwords do not match, please try again.") + .setPositiveButton("Ok", (dialog2, button2) -> dialog2.dismiss()) + .show(); + + dialog.cancel(); + } + }) + + .setNegativeButton("Cancel", (dialog, button) -> dialog.cancel()) + .show(); + } + + private void backupPackages(Set selectedPackages, Uri contentUri, Activity parent, + String selectedPassword) { try { selectedPackages.add("@pm@"); ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder() - .setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build(); + .setContext(parent) + .setOutputUri(contentUri) + .setPackages(selectedPackages) + .setPassword(selectedPassword) + .build(); + boolean success = initializeBackupTransport(backupConfiguration); if (!success) { @@ -74,7 +128,7 @@ class CreateBackupActivityController { return; } - PopupWindow popupWindow = buildPopupWindow(parent); + PopupWindow popupWindow = buildStatusPopupWindow(parent); BackupObserver backupObserver = new BackupObserver(parent, popupWindow); BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages); @@ -102,7 +156,7 @@ class CreateBackupActivityController { return true; } - private PopupWindow buildPopupWindow(Activity parent) { + private PopupWindow buildStatusPopupWindow(Activity parent) { LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java index 3cf2b003..aa53437d 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java @@ -32,7 +32,7 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe switch (viewId) { case R.id.restore_confirm_button: - controller.restorePackages(selectedPackageList, contentUri, this); + controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); break; } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java index d53838b1..3d0e8d01 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java @@ -1,21 +1,20 @@ package com.stevesoltys.backup.activity.restore; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.text.InputType; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.PopupWindow; -import android.widget.Toast; +import android.widget.*; import com.stevesoltys.backup.R; import com.stevesoltys.backup.session.BackupManagerController; import com.stevesoltys.backup.session.restore.RestoreSession; @@ -89,10 +88,31 @@ class RestoreBackupActivityController { return results; } - void restorePackages(Set selectedPackages, Uri contentUri, Activity parent) { + void showEnterPasswordAlert(Set selectedPackages, Uri contentUri, Activity parent) { + final EditText passwordTextView = new EditText(parent); + passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + new AlertDialog.Builder(parent) + .setMessage("Please enter a password.\n" + + "If you didn't enter one while creating the backup, you can leave this blank.") + .setView(passwordTextView) + + .setPositiveButton("Confirm", (dialog, button) -> + restorePackages(selectedPackages, contentUri, parent, passwordTextView.getText().toString())) + .setNegativeButton("Cancel", (dialog, button) -> dialog.cancel()) + .show(); + } + + + private void restorePackages(Set selectedPackages, Uri contentUri, Activity parent, String password) { try { ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder(). - setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build(); + setContext(parent) + .setOutputUri(contentUri) + .setPackages(selectedPackages) + .setPassword(password) + .build(); + boolean success = initializeBackupTransport(backupConfiguration); if(!success) { diff --git a/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java new file mode 100644 index 00000000..eb1b81a1 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java @@ -0,0 +1,44 @@ +package com.stevesoltys.backup.security; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +/** + * A utility class which can be used for generating an AES secret key using PBKDF2. + * + * @author Steve Soltys + */ +public class KeyGenerator { + + /** + * The number of iterations for key generation. + */ + private static final int ITERATIONS = 1000; + + /** + * The generated key length. + */ + private static final int KEY_LENGTH = 256; + + /** + * Generates an AES secret key using PBKDF2. + * + * @param password The password. + * @param salt The salt. + * @return The generated key. + */ + public static SecretKey generate(String password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); + + SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); + return new SecretKeySpec(secretKey.getEncoded(), "AES"); + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java index 931f2ede..56ecae6d 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java @@ -6,15 +6,20 @@ import android.content.pm.PackageInfo; import android.os.ParcelFileDescriptor; import android.util.Base64; import android.util.Log; +import com.stevesoltys.backup.security.KeyGenerator; import com.stevesoltys.backup.transport.component.BackupComponent; - +import libcore.io.IoUtils; import org.apache.commons.io.IOUtils; -import libcore.io.IoUtils; - -import java.io.*; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -41,11 +46,6 @@ public class ContentProviderBackupComponent implements BackupComponent { this.configuration = configuration; } - @Override - public long requestBackupTime() { - return 0; - } - @Override public String currentDestinationString() { return DESTINATION_DESCRIPTION; @@ -67,33 +67,8 @@ public class ContentProviderBackupComponent implements BackupComponent { } @Override - public long getBackupQuota(String packageName, boolean fullBackup) { - return configuration.getBackupSizeQuota(); - } - - @Override - public long requestFullBackupTime() { - return 0; - } - - private void initializeBackupState() throws IOException { - if (backupState == null) { - backupState = new ContentProviderBackupState(); - } - - if (backupState.getOutputStream() == null) { - initializeOutputStream(); - } - } - - private void initializeOutputStream() throws FileNotFoundException { - ContentResolver contentResolver = configuration.getContext().getContentResolver(); - ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w"); - backupState.setOutputFileDescriptor(outputFileDescriptor); - - FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor()); - ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); - backupState.setOutputStream(zipOutputStream); + public int finishBackup() { + return clearBackupState(false); } @Override @@ -113,44 +88,55 @@ public class ContentProviderBackupComponent implements BackupComponent { } } - private int transferIncrementalBackupData(BackupDataInput backupDataInput) - throws IOException, InvalidAlgorithmParameterException, InvalidKeyException { + private int clearBackupState(boolean closeFile) { - ZipOutputStream outputStream = backupState.getOutputStream(); + if (backupState == null) { + return TRANSPORT_OK; + } - int bufferSize = INITIAL_BUFFER_SIZE; - byte[] buffer = new byte[bufferSize]; + try { + IoUtils.closeQuietly(backupState.getInputFileDescriptor()); + backupState.setInputFileDescriptor(null); - while (backupDataInput.readNextHeader()) { - String chunkFileName = Base64.encodeToString(backupDataInput.getKey().getBytes(), Base64.DEFAULT); - int dataSize = backupDataInput.getDataSize(); + ZipOutputStream outputStream = backupState.getOutputStream(); - if (dataSize >= 0) { - ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() + - backupState.getPackageName() + "/" + chunkFileName); - outputStream.putNextEntry(zipEntry); - - if (dataSize > bufferSize) { - bufferSize = dataSize; - buffer = new byte[bufferSize]; - } - - backupDataInput.readEntityData(buffer, 0, dataSize); - - try { - outputStream.write(buffer, 0, dataSize); - - } catch (Exception ex) { - Log.e(TAG, "Error performing incremental backup for " + backupState.getPackageName() + ": ", ex); - clearBackupState(true); - return TRANSPORT_ERROR; - } + if (outputStream != null) { + outputStream.closeEntry(); } + + if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) { + if (outputStream != null) { + outputStream.finish(); + outputStream.close(); + } + + IoUtils.closeQuietly(backupState.getOutputFileDescriptor()); + backupState = null; + } + + } catch (IOException ex) { + Log.e(TAG, "Error cancelling full backup: ", ex); + return TRANSPORT_ERROR; } return TRANSPORT_OK; } + @Override + public long getBackupQuota(String packageName, boolean fullBackup) { + return configuration.getBackupSizeQuota(); + } + + @Override + public long requestBackupTime() { + return 0; + } + + @Override + public long requestFullBackupTime() { + return 0; + } + @Override public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) { @@ -227,40 +213,74 @@ public class ContentProviderBackupComponent implements BackupComponent { clearBackupState(false); } - @Override - public int finishBackup() { - return clearBackupState(false); - } - - private int clearBackupState(boolean closeFile) { - + private void initializeBackupState() throws IOException { if (backupState == null) { - return TRANSPORT_OK; + backupState = new ContentProviderBackupState(); } - try { - IoUtils.closeQuietly(backupState.getInputFileDescriptor()); - backupState.setInputFileDescriptor(null); + if (backupState.getOutputStream() == null) { + initializeOutputStream(); + } + } - ZipOutputStream outputStream = backupState.getOutputStream(); + private void initializeOutputStream() throws IOException { + ContentResolver contentResolver = configuration.getContext().getContentResolver(); + ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w"); + backupState.setOutputFileDescriptor(outputFileDescriptor); - if (outputStream != null) { - outputStream.closeEntry(); - } + FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); + backupState.setOutputStream(zipOutputStream); - if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) { - if (outputStream != null) { - outputStream.finish(); - outputStream.close(); + ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH); + zipOutputStream.putNextEntry(saltZipEntry); + zipOutputStream.write(backupState.getSalt()); + zipOutputStream.closeEntry(); + } + + private int transferIncrementalBackupData(BackupDataInput backupDataInput) throws IOException { + ZipOutputStream outputStream = backupState.getOutputStream(); + + int bufferSize = INITIAL_BUFFER_SIZE; + byte[] buffer = new byte[bufferSize]; + + while (backupDataInput.readNextHeader()) { + String chunkFileName = Base64.encodeToString(backupDataInput.getKey().getBytes(), Base64.DEFAULT); + int dataSize = backupDataInput.getDataSize(); + + if (dataSize >= 0) { + ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() + + backupState.getPackageName() + "/" + chunkFileName); + outputStream.putNextEntry(zipEntry); + + if (dataSize > bufferSize) { + bufferSize = dataSize; + buffer = new byte[bufferSize]; } - IoUtils.closeQuietly(backupState.getOutputFileDescriptor()); - backupState = null; - } + backupDataInput.readEntityData(buffer, 0, dataSize); - } catch (IOException ex) { - Log.e(TAG, "Error cancelling full backup: ", ex); - return TRANSPORT_ERROR; + try { + if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) { + SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()); + + Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(backupState.getSalt())); + + byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize); + byte[] encryptedBuffer = cipher.doFinal(payload); + outputStream.write(encryptedBuffer); + + } else { + outputStream.write(buffer, 0, dataSize); + } + + } catch (Exception ex) { + Log.e(TAG, "Error performing incremental backup for " + backupState.getPackageName() + ": ", ex); + clearBackupState(true); + return TRANSPORT_ERROR; + } + } } return TRANSPORT_OK; diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java index 444162d4..bf90d5e5 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java @@ -14,6 +14,8 @@ public class ContentProviderBackupConfiguration { private final Uri uri; + private final String password; + private final long backupSizeQuota; private final Set packages; @@ -22,30 +24,24 @@ public class ContentProviderBackupConfiguration { private final String incrementalBackupDirectory; - ContentProviderBackupConfiguration(Context context, Uri uri, Set packages, long backupSizeQuota, - String fullBackupDirectory, String incrementalBackupDirectory) { + ContentProviderBackupConfiguration(Context context, Uri uri, Set 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 Context getContext() { - return context; - } - - public Uri getUri() { - return uri; - } - public long getBackupSizeQuota() { return backupSizeQuota; } - public int getPackageCount() { - return packages.size(); + public Context getContext() { + return context; } public String getFullBackupDirectory() { @@ -55,4 +51,16 @@ public class ContentProviderBackupConfiguration { public String getIncrementalBackupDirectory() { return incrementalBackupDirectory; } + + public int getPackageCount() { + return packages.size(); + } + + public String getPassword() { + return password; + } + + public Uri getUri() { + return uri; + } } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java index 562087b1..0f63b8c5 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java @@ -21,6 +21,8 @@ public class ContentProviderBackupConfigurationBuilder { private Set packages; + private String password; + private long backupSizeQuota = Long.MAX_VALUE; private String incrementalBackupDirectory = DEFAULT_INCREMENTAL_BACKUP_DIRECTORY; @@ -34,15 +36,30 @@ public class ContentProviderBackupConfigurationBuilder { 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, backupSizeQuota, + 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; @@ -53,18 +70,8 @@ public class ContentProviderBackupConfigurationBuilder { return this; } - public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) { - this.backupSizeQuota = backupSizeQuota; - return this; - } - - public ContentProviderBackupConfigurationBuilder setIncrementalBackupDirectory(String incrementalBackupDirectory) { - this.incrementalBackupDirectory = incrementalBackupDirectory; - return this; - } - - public ContentProviderBackupConfigurationBuilder setFullBackupDirectory(String fullBackupDirectory) { - this.fullBackupDirectory = fullBackupDirectory; + public ContentProviderBackupConfigurationBuilder setPassword(String password) { + this.password = password; return this; } } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java new file mode 100644 index 00000000..9ec4235d --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java @@ -0,0 +1,11 @@ +package com.stevesoltys.backup.transport.component.provider; + +/** + * @author Steve Soltys + */ +public class ContentProviderBackupConstants { + + static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding"; + + static final String SALT_FILE_PATH = "salt"; +} diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java index 3211550d..02a14adb 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java @@ -3,6 +3,7 @@ package com.stevesoltys.backup.transport.component.provider; import android.os.ParcelFileDescriptor; import java.io.InputStream; +import java.security.SecureRandom; import java.util.zip.ZipOutputStream; /** @@ -10,6 +11,8 @@ import java.util.zip.ZipOutputStream; */ class ContentProviderBackupState { + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private ParcelFileDescriptor inputFileDescriptor; private ParcelFileDescriptor outputFileDescriptor; @@ -24,36 +27,11 @@ class ContentProviderBackupState { private int packageIndex; - ParcelFileDescriptor getInputFileDescriptor() { - return inputFileDescriptor; - } + private byte[] salt; - void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) { - this.inputFileDescriptor = inputFileDescriptor; - } - - ParcelFileDescriptor getOutputFileDescriptor() { - return outputFileDescriptor; - } - - void setOutputFileDescriptor(ParcelFileDescriptor outputFileDescriptor) { - this.outputFileDescriptor = outputFileDescriptor; - } - - InputStream getInputStream() { - return inputStream; - } - - void setInputStream(InputStream inputStream) { - this.inputStream = inputStream; - } - - ZipOutputStream getOutputStream() { - return outputStream; - } - - void setOutputStream(ZipOutputStream outputStream) { - this.outputStream = outputStream; + public ContentProviderBackupState() { + salt = new byte[16]; + SECURE_RANDOM.nextBytes(salt); } long getBytesTransferred() { @@ -64,12 +42,36 @@ class ContentProviderBackupState { this.bytesTransferred = bytesTransferred; } - String getPackageName() { - return packageName; + ParcelFileDescriptor getInputFileDescriptor() { + return inputFileDescriptor; } - void setPackageName(String packageName) { - this.packageName = packageName; + void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) { + this.inputFileDescriptor = inputFileDescriptor; + } + + InputStream getInputStream() { + return inputStream; + } + + void setInputStream(InputStream inputStream) { + this.inputStream = inputStream; + } + + ParcelFileDescriptor getOutputFileDescriptor() { + return outputFileDescriptor; + } + + void setOutputFileDescriptor(ParcelFileDescriptor outputFileDescriptor) { + this.outputFileDescriptor = outputFileDescriptor; + } + + ZipOutputStream getOutputStream() { + return outputStream; + } + + void setOutputStream(ZipOutputStream outputStream) { + this.outputStream = outputStream; } int getPackageIndex() { @@ -79,4 +81,16 @@ class ContentProviderBackupState { void setPackageIndex(int packageIndex) { this.packageIndex = packageIndex; } + + String getPackageName() { + return packageName; + } + + void setPackageName(String packageName) { + this.packageName = packageName; + } + + byte[] getSalt() { + return salt; + } } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java index a6c95a20..98f06909 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java @@ -9,13 +9,15 @@ import android.os.ParcelFileDescriptor; import android.util.Base64; import android.util.Log; import com.android.internal.util.Preconditions; +import com.stevesoltys.backup.security.KeyGenerator; import com.stevesoltys.backup.transport.component.RestoreComponent; import libcore.io.IoUtils; import libcore.io.Streams; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; import java.io.*; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -48,6 +50,23 @@ public class ContentProviderRestoreComponent implements RestoreComponent { restoreState = new ContentProviderRestoreState(); restoreState.setPackages(packages); restoreState.setPackageIndex(-1); + + if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) { + try { + ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor(); + ZipInputStream inputStream = buildInputStream(inputFileDescriptor); + seekToEntry(inputStream, ContentProviderBackupConstants.SALT_FILE_PATH); + + restoreState.setSalt(Streams.readFullyNoClose(inputStream)); + + IoUtils.closeQuietly(inputFileDescriptor); + IoUtils.closeQuietly(inputStream); + + } catch (IOException ex) { + Log.e(TAG, "Salt not found", ex); + } + } + return TRANSPORT_OK; } @@ -72,15 +91,14 @@ public class ContentProviderRestoreComponent implements RestoreComponent { return new RestoreDescription(name, restoreState.getRestoreType()); } - } catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException ex) { + } catch (IOException ex) { Log.e(TAG, "Error choosing package " + name + " at index " + packageIndex + "failed selection:", ex); } } return RestoreDescription.NO_MORE_PACKAGES; } - private boolean containsPackageFile(String fileName) throws IOException, InvalidKeyException, - InvalidAlgorithmParameterException { + private boolean containsPackageFile(String fileName) throws IOException { ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor(); ZipInputStream inputStream = buildInputStream(inputFileDescriptor); @@ -132,7 +150,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent { } private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor) - throws IOException, InvalidAlgorithmParameterException, InvalidKeyException { + throws Exception { ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor(); ZipInputStream inputStream = buildInputStream(inputFileDescriptor); @@ -140,11 +158,12 @@ public class ContentProviderRestoreComponent implements RestoreComponent { Optional zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName); + while (zipEntryOptional.isPresent()) { String fileName = new File(zipEntryOptional.get().getName()).getName(); String blobKey = new String(Base64.decode(fileName, Base64.DEFAULT)); - byte[] backupData = Streams.readFullyNoClose(inputStream); + byte[] backupData = readBackupData(inputStream); backupDataOutput.writeEntityHeader(blobKey, backupData.length); backupDataOutput.writeEntityData(backupData, backupData.length); inputStream.closeEntry(); @@ -157,6 +176,22 @@ public class ContentProviderRestoreComponent implements RestoreComponent { return TRANSPORT_OK; } + private byte[] readBackupData(ZipInputStream inputStream) throws Exception { + byte[] backupData = Streams.readFullyNoClose(inputStream); + + if (configuration.getPassword() != null && !configuration.getPassword().isEmpty() && + restoreState.getSalt() != null) { + + SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt()); + + Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(restoreState.getSalt())); + backupData = cipher.doFinal(backupData); + } + + return backupData; + } + @Override public int getNextFullRestoreDataChunk(ParcelFileDescriptor outputFileDescriptor) { Preconditions.checkState(restoreState.getRestoreType() == TYPE_FULL_STREAM, diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java index 05b49f1a..cca36ddf 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java @@ -20,28 +20,14 @@ class ContentProviderRestoreState { private ZipInputStream inputStream; - PackageInfo[] getPackages() { - return packages; + private byte[] salt; + + ParcelFileDescriptor getInputFileDescriptor() { + return inputFileDescriptor; } - void setPackages(PackageInfo[] packages) { - this.packages = packages; - } - - int getPackageIndex() { - return packageIndex; - } - - void setPackageIndex(int packageIndex) { - this.packageIndex = packageIndex; - } - - int getRestoreType() { - return restoreType; - } - - void setRestoreType(int restoreType) { - this.restoreType = restoreType; + void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) { + this.inputFileDescriptor = inputFileDescriptor; } ZipInputStream getInputStream() { @@ -52,11 +38,35 @@ class ContentProviderRestoreState { this.inputStream = inputStream; } - ParcelFileDescriptor getInputFileDescriptor() { - return inputFileDescriptor; + int getPackageIndex() { + return packageIndex; } - void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) { - this.inputFileDescriptor = inputFileDescriptor; + void setPackageIndex(int packageIndex) { + this.packageIndex = packageIndex; + } + + PackageInfo[] getPackages() { + return packages; + } + + void setPackages(PackageInfo[] packages) { + this.packages = packages; + } + + int getRestoreType() { + return restoreType; + } + + void setRestoreType(int restoreType) { + this.restoreType = restoreType; + } + + byte[] getSalt() { + return salt; + } + + void setSalt(byte[] salt) { + this.salt = salt; } } From 9b979b3693a9c396dd76ab7fa4178e009c8b0875 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Thu, 21 Feb 2019 21:51:46 -0500 Subject: [PATCH 02/10] Move cipher logic out of backup and restore components --- .../backup/security/CipherUtil.java | 57 +++++++++++++++++++ .../ContentProviderBackupComponent.java | 16 ++---- .../ContentProviderBackupConstants.java | 4 +- .../ContentProviderRestoreComponent.java | 12 ++-- 4 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java diff --git a/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java b/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java new file mode 100644 index 00000000..66c8089d --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java @@ -0,0 +1,57 @@ +package com.stevesoltys.backup.security; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +/** + * A utility class for encrypting and decrypting data using a {@link Cipher}. + * + * @author Steve Soltys + */ +public class CipherUtil { + + /** + * The cipher algorithm. + */ + public static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding"; + + /** + * Encrypts the given payload using a key generated from the provided password and salt. + * + * @param payload The payload. + * @param password The password. + * @param salt The salt. + */ + public static byte[] encrypt(byte[] payload, String password, byte[] salt) throws NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException, + InvalidAlgorithmParameterException, InvalidKeyException { + + SecretKey secretKey = KeyGenerator.generate(password, salt); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(salt)); + + return cipher.doFinal(payload); + } + + /** + * Decrypts the given payload using a key generated from the provided password and salt. + * + * @param payload The payload. + * @param password The password. + * @param salt The salt. + */ + public static byte[] decrypt(byte[] payload, String password, byte[] salt) throws NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException, + InvalidAlgorithmParameterException, InvalidKeyException { + + SecretKey secretKey = KeyGenerator.generate(password, salt); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(salt)); + + return cipher.doFinal(payload); + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java index 56ecae6d..6c037663 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java @@ -6,19 +6,15 @@ import android.content.pm.PackageInfo; import android.os.ParcelFileDescriptor; import android.util.Base64; import android.util.Log; -import com.stevesoltys.backup.security.KeyGenerator; +import com.stevesoltys.backup.security.CipherUtil; 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 javax.crypto.spec.IvParameterSpec; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -262,14 +258,12 @@ public class ContentProviderBackupComponent implements BackupComponent { try { if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) { - SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()); - - Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(backupState.getSalt())); byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize); - byte[] encryptedBuffer = cipher.doFinal(payload); - outputStream.write(encryptedBuffer); + String password = configuration.getPassword(); + byte[] salt = backupState.getSalt(); + + outputStream.write(CipherUtil.encrypt(payload, password, salt)); } else { outputStream.write(buffer, 0, dataSize); diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java index 9ec4235d..b87e30a8 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConstants.java @@ -3,9 +3,7 @@ package com.stevesoltys.backup.transport.component.provider; /** * @author Steve Soltys */ -public class ContentProviderBackupConstants { - - static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding"; +class ContentProviderBackupConstants { static final String SALT_FILE_PATH = "salt"; } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java index 98f06909..eb7841c5 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java @@ -9,6 +9,7 @@ import android.os.ParcelFileDescriptor; import android.util.Base64; import android.util.Log; import com.android.internal.util.Preconditions; +import com.stevesoltys.backup.security.CipherUtil; import com.stevesoltys.backup.security.KeyGenerator; import com.stevesoltys.backup.transport.component.RestoreComponent; import libcore.io.IoUtils; @@ -179,14 +180,11 @@ public class ContentProviderRestoreComponent implements RestoreComponent { private byte[] readBackupData(ZipInputStream inputStream) throws Exception { byte[] backupData = Streams.readFullyNoClose(inputStream); - if (configuration.getPassword() != null && !configuration.getPassword().isEmpty() && - restoreState.getSalt() != null) { + String password = configuration.getPassword(); + byte[] salt = restoreState.getSalt(); - SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt()); - - Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(restoreState.getSalt())); - backupData = cipher.doFinal(backupData); + if (password != null && !password.isEmpty() && salt != null) { + backupData = CipherUtil.decrypt(backupData, password, salt); } return backupData; From 023750be6e245877f9c08578efda89a778319b1a Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Thu, 21 Feb 2019 22:41:54 -0500 Subject: [PATCH 03/10] Add loading popup when fetching packages for backup or restore --- .../activity/backup/CreateBackupActivity.java | 3 +- .../CreateBackupActivityController.java | 64 +++++++++++------- .../restore/RestoreBackupActivity.java | 3 +- .../RestoreBackupActivityController.java | 65 ++++++++++++------- app/src/main/res/values/strings.xml | 2 + 5 files changed, 90 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java index 382469b9..40a6fc00 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java @@ -2,6 +2,7 @@ package com.stevesoltys.backup.activity.backup; import android.app.Activity; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; @@ -49,7 +50,7 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen contentUri = getIntent().getData(); controller = new CreateBackupActivityController(); - controller.populatePackageList(packageListView, this); + AsyncTask.execute(() -> controller.populatePackageList(packageListView, CreateBackupActivity.this)); } @Override diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index 57de2aaf..7389b835 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -14,6 +14,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.*; +import com.google.android.collect.Sets; import com.stevesoltys.backup.R; import com.stevesoltys.backup.session.BackupManagerController; import com.stevesoltys.backup.session.backup.BackupSession; @@ -26,10 +27,10 @@ import com.stevesoltys.backup.transport.component.provider.ContentProviderBackup import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder; import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; /** * @author Steve Soltys @@ -38,7 +39,9 @@ class CreateBackupActivityController { private static final String TAG = CreateBackupActivityController.class.getName(); - private static final Set IGNORED_PACKAGES = Collections.singleton("com.android.providers.downloads.ui"); + private static final Set IGNORED_PACKAGES = Sets.newArraySet( + "com.android.providers.downloads.ui", "com.android.providers.media" + ); private final BackupManagerController backupManager; @@ -47,6 +50,17 @@ class CreateBackupActivityController { } void populatePackageList(ListView packageListView, CreateBackupActivity parent) { + AtomicReference popupWindow = new AtomicReference<>(); + + parent.runOnUiThread(() -> { + popupWindow.set(showLoadingPopupWindow(parent)); + TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view); + textView.setText(R.string.loading_packages); + + View popupWindowButton = popupWindow.get().getContentView().findViewById(R.id.popup_cancel_button); + popupWindowButton.setOnClickListener(view -> parent.finish()); + }); + List eligiblePackageList = new LinkedList<>(); try { @@ -57,10 +71,29 @@ class CreateBackupActivityController { Log.e(TAG, "Error while obtaining package list: ", e); } + parent.runOnUiThread(() -> { + if (popupWindow.get() != null) { + popupWindow.get().dismiss(); + } - packageListView.setOnItemClickListener(parent); - packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList)); - packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + packageListView.setOnItemClickListener(parent); + packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList)); + packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + }); + } + + private PopupWindow showLoadingPopupWindow(Activity parent) { + LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); + View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); + + PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); + popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); + popupWindow.setElevation(10); + popupWindow.setFocusable(false); + popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); + popupWindow.setOutsideTouchable(false); + return popupWindow; } void showEnterPasswordAlert(Set selectedPackages, Uri contentUri, Activity parent) { @@ -68,8 +101,8 @@ class CreateBackupActivityController { passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); new AlertDialog.Builder(parent) - .setMessage("Please enter a password.\n" + - "You'll need this to restore your backup, so write it down!") + .setTitle("Enter a password") + .setMessage("You'll need this to restore your backup, so write it down!") .setView(passwordTextView) .setPositiveButton("Set password", (dialog, button) -> @@ -86,7 +119,7 @@ class CreateBackupActivityController { passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); new AlertDialog.Builder(parent) - .setMessage("Please confirm your password.") + .setTitle("Confirm password") .setView(passwordTextView) .setPositiveButton("Confirm", (dialog, button) -> { @@ -128,7 +161,7 @@ class CreateBackupActivityController { return; } - PopupWindow popupWindow = buildStatusPopupWindow(parent); + PopupWindow popupWindow = showLoadingPopupWindow(parent); BackupObserver backupObserver = new BackupObserver(parent, popupWindow); BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages); @@ -155,17 +188,4 @@ class CreateBackupActivityController { backupTransport.initialize(backupComponent, restoreComponent); return true; } - - private PopupWindow buildStatusPopupWindow(Activity parent) { - LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); - View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); - - PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); - popupWindow.setElevation(10); - popupWindow.setFocusable(false); - popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); - return popupWindow; - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java index aa53437d..e16f792f 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java @@ -2,6 +2,7 @@ package com.stevesoltys.backup.activity.restore; import android.app.Activity; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; @@ -49,7 +50,7 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe contentUri = getIntent().getData(); controller = new RestoreBackupActivityController(); - controller.populatePackageList(packageListView, contentUri, this); + AsyncTask.execute(() -> controller.populatePackageList(packageListView, contentUri, this)); } @Override diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java index 3d0e8d01..223c0879 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java @@ -22,9 +22,9 @@ 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.ContentProviderBackupConfigurationBuilder; -import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent; import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent; import libcore.io.IoUtils; @@ -34,6 +34,7 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -51,7 +52,19 @@ class RestoreBackupActivityController { } void populatePackageList(ListView packageListView, Uri contentUri, RestoreBackupActivity parent) { + AtomicReference popupWindow = new AtomicReference<>(); + + parent.runOnUiThread(() -> { + popupWindow.set(showLoadingPopupWindow(parent)); + TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view); + textView.setText(R.string.loading_backup); + + View popupWindowButton = popupWindow.get().getContentView().findViewById(R.id.popup_cancel_button); + popupWindowButton.setOnClickListener(view -> parent.finish()); + }); + List eligiblePackageList = new LinkedList<>(); + try { eligiblePackageList.addAll(getEligiblePackages(contentUri, parent)); @@ -59,9 +72,29 @@ class RestoreBackupActivityController { Log.e(TAG, "Error while obtaining package list: ", e); } - packageListView.setOnItemClickListener(parent); - packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList)); - packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + parent.runOnUiThread(() -> { + if (popupWindow.get() != null) { + popupWindow.get().dismiss(); + } + + packageListView.setOnItemClickListener(parent); + packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList)); + packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + }); + } + + private PopupWindow showLoadingPopupWindow(Activity parent) { + LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); + View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); + + PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); + popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); + popupWindow.setElevation(10); + popupWindow.setFocusable(false); + popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); + popupWindow.setOutsideTouchable(false); + return popupWindow; } private List getEligiblePackages(Uri contentUri, Activity context) throws IOException { @@ -93,8 +126,8 @@ class RestoreBackupActivityController { passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); new AlertDialog.Builder(parent) - .setMessage("Please enter a password.\n" + - "If you didn't enter one while creating the backup, you can leave this blank.") + .setTitle("Enter a password") + .setMessage("If you didn't enter one while creating the backup, you can leave this blank.") .setView(passwordTextView) .setPositiveButton("Confirm", (dialog, button) -> @@ -103,7 +136,6 @@ class RestoreBackupActivityController { .show(); } - private void restorePackages(Set selectedPackages, Uri contentUri, Activity parent, String password) { try { ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder(). @@ -115,12 +147,12 @@ class RestoreBackupActivityController { boolean success = initializeBackupTransport(backupConfiguration); - if(!success) { + if (!success) { Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show(); return; } - PopupWindow popupWindow = buildPopupWindow(parent); + PopupWindow popupWindow = showLoadingPopupWindow(parent); RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size()); RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackages); @@ -136,7 +168,7 @@ class RestoreBackupActivityController { private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) { ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport(); - if(backupTransport.isActive()) { + if (backupTransport.isActive()) { return false; } @@ -145,17 +177,4 @@ class RestoreBackupActivityController { backupTransport.initialize(backupComponent, restoreComponent); return true; } - - private PopupWindow buildPopupWindow(Activity parent) { - LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); - View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); - - PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); - popupWindow.setElevation(10); - popupWindow.setFocusable(false); - popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); - return popupWindow; - } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e512a59..3be75a51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,5 +19,7 @@ Cancel Select all + Loading backup… + Loading packages… From 0fd4c7833d0a94e672f97efbd8538ecef4289229 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Thu, 21 Feb 2019 22:42:16 -0500 Subject: [PATCH 04/10] Reduce number of key generation iterations to 100 --- .../main/java/com/stevesoltys/backup/security/KeyGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java index eb1b81a1..b9658835 100644 --- a/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java +++ b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java @@ -18,7 +18,7 @@ public class KeyGenerator { /** * The number of iterations for key generation. */ - private static final int ITERATIONS = 1000; + private static final int ITERATIONS = 100; /** * The generated key length. From b0465f7aae466786a0bd05308ff5d54959e23a43 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Thu, 21 Feb 2019 23:50:50 -0500 Subject: [PATCH 05/10] Add 'com.stevesoltys.backup' to ignored packages --- .../backup/activity/backup/CreateBackupActivityController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index 7389b835..9c21f0d4 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -40,7 +40,8 @@ class CreateBackupActivityController { private static final String TAG = CreateBackupActivityController.class.getName(); private static final Set IGNORED_PACKAGES = Sets.newArraySet( - "com.android.providers.downloads.ui", "com.android.providers.media" + "com.android.providers.downloads.ui", "com.android.providers.downloads", "com.android.providers.media", + "com.stevesoltys.backup" ); private final BackupManagerController backupManager; From 1519580a363ae46ae1db4b7c6a00d879e8098d1e Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 22 Feb 2019 01:00:08 -0500 Subject: [PATCH 06/10] Reduce number of key generation iterations to 25 --- .../main/java/com/stevesoltys/backup/security/KeyGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java index b9658835..8001af4b 100644 --- a/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java +++ b/app/src/main/java/com/stevesoltys/backup/security/KeyGenerator.java @@ -18,7 +18,7 @@ public class KeyGenerator { /** * The number of iterations for key generation. */ - private static final int ITERATIONS = 100; + private static final int ITERATIONS = 25; /** * The generated key length. From bd0c41c2d3f1e4f45f41fb6dbe2775c4e9733905 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 22 Feb 2019 01:02:06 -0500 Subject: [PATCH 07/10] Stop regenerating secret key for each package --- .../backup/security/CipherUtil.java | 33 +++--- .../ContentProviderBackupComponent.java | 26 ++-- .../provider/ContentProviderBackupState.java | 11 ++ .../ContentProviderRestoreComponent.java | 111 ++++++++++-------- .../provider/ContentProviderRestoreState.java | 23 ++++ 5 files changed, 125 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java b/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java index 66c8089d..93e76189 100644 --- a/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java +++ b/app/src/main/java/com/stevesoltys/backup/security/CipherUtil.java @@ -5,7 +5,6 @@ import javax.crypto.spec.IvParameterSpec; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; /** * A utility class for encrypting and decrypting data using a {@link Cipher}. @@ -19,39 +18,35 @@ public class CipherUtil { */ public static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding"; - /** - * Encrypts the given payload using a key generated from the provided password and salt. + /**. + * Encrypts the given payload using the provided secret key. * * @param payload The payload. - * @param password The password. - * @param salt The salt. + * @param secretKey The secret key. + * @param iv The initialization vector. */ - public static byte[] encrypt(byte[] payload, String password, byte[] salt) throws NoSuchPaddingException, - NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException, + public static byte[] encrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException, + NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException { - SecretKey secretKey = KeyGenerator.generate(password, salt); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(salt)); - + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); return cipher.doFinal(payload); } /** - * Decrypts the given payload using a key generated from the provided password and salt. + * Decrypts the given payload using the provided secret key. * - * @param payload The payload. - * @param password The password. - * @param salt The salt. + * @param payload The payload. + * @param secretKey The secret key. + * @param iv The initialization vector. */ - public static byte[] decrypt(byte[] payload, String password, byte[] salt) throws NoSuchPaddingException, - NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException, + public static byte[] decrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException, + NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException { - SecretKey secretKey = KeyGenerator.generate(password, salt); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(salt)); - + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); return cipher.doFinal(payload); } } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java index 6c037663..b070f1e2 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java @@ -7,10 +7,12 @@ 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.transport.component.BackupComponent; import libcore.io.IoUtils; import org.apache.commons.io.IOUtils; +import javax.crypto.SecretKey; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -209,13 +211,23 @@ public class ContentProviderBackupComponent implements BackupComponent { clearBackupState(false); } - private void initializeBackupState() throws IOException { + private void initializeBackupState() throws Exception { if (backupState == null) { backupState = new ContentProviderBackupState(); } if (backupState.getOutputStream() == null) { initializeOutputStream(); + + ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH); + backupState.getOutputStream().putNextEntry(saltZipEntry); + backupState.getOutputStream().write(backupState.getSalt()); + backupState.getOutputStream().closeEntry(); + + + if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) { + backupState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), backupState.getSalt())); + } } } @@ -227,11 +239,6 @@ public class ContentProviderBackupComponent implements BackupComponent { FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor()); ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); backupState.setOutputStream(zipOutputStream); - - ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH); - zipOutputStream.putNextEntry(saltZipEntry); - zipOutputStream.write(backupState.getSalt()); - zipOutputStream.closeEntry(); } private int transferIncrementalBackupData(BackupDataInput backupDataInput) throws IOException { @@ -257,13 +264,12 @@ public class ContentProviderBackupComponent implements BackupComponent { backupDataInput.readEntityData(buffer, 0, dataSize); try { - if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) { - + if (backupState.getSecretKey() != null) { byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize); - String password = configuration.getPassword(); + SecretKey secretKey = backupState.getSecretKey(); byte[] salt = backupState.getSalt(); - outputStream.write(CipherUtil.encrypt(payload, password, salt)); + outputStream.write(CipherUtil.encrypt(payload, secretKey, salt)); } else { outputStream.write(buffer, 0, dataSize); diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java index 02a14adb..a2c18baa 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupState.java @@ -2,6 +2,7 @@ package com.stevesoltys.backup.transport.component.provider; import android.os.ParcelFileDescriptor; +import javax.crypto.SecretKey; import java.io.InputStream; import java.security.SecureRandom; import java.util.zip.ZipOutputStream; @@ -29,6 +30,8 @@ class ContentProviderBackupState { private byte[] salt; + private SecretKey secretKey; + public ContentProviderBackupState() { salt = new byte[16]; SECURE_RANDOM.nextBytes(salt); @@ -93,4 +96,12 @@ class ContentProviderBackupState { byte[] getSalt() { return salt; } + + public SecretKey getSecretKey() { + return secretKey; + } + + public void setSecretKey(SecretKey secretKey) { + this.secretKey = secretKey; + } } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java index eb7841c5..eaf0d00f 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java @@ -15,10 +15,10 @@ import com.stevesoltys.backup.transport.component.RestoreComponent; import libcore.io.IoUtils; import libcore.io.Streams; -import javax.crypto.Cipher; import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; import java.io.*; +import java.util.LinkedList; +import java.util.List; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -59,15 +59,37 @@ public class ContentProviderRestoreComponent implements RestoreComponent { seekToEntry(inputStream, ContentProviderBackupConstants.SALT_FILE_PATH); restoreState.setSalt(Streams.readFullyNoClose(inputStream)); + restoreState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt())); IoUtils.closeQuietly(inputFileDescriptor); IoUtils.closeQuietly(inputStream); - } catch (IOException ex) { + } catch (Exception ex) { Log.e(TAG, "Salt not found", ex); } } + try { + List zipEntries = new LinkedList<>(); + + ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor(); + ZipInputStream inputStream = buildInputStream(inputFileDescriptor); + + ZipEntry zipEntry; + while ((zipEntry = inputStream.getNextEntry()) != null) { + zipEntries.add(zipEntry); + inputStream.closeEntry(); + } + + IoUtils.closeQuietly(inputFileDescriptor); + IoUtils.closeQuietly(inputStream); + + restoreState.setZipEntries(zipEntries); + + } catch (Exception ex) { + Log.e(TAG, "Salt not found", ex); + } + return TRANSPORT_OK; } @@ -82,54 +104,21 @@ public class ContentProviderRestoreComponent implements RestoreComponent { restoreState.setPackageIndex(packageIndex); String name = packages[packageIndex].packageName; - try { - if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) { - restoreState.setRestoreType(TYPE_KEY_VALUE); - return new RestoreDescription(name, restoreState.getRestoreType()); + if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) { + restoreState.setRestoreType(TYPE_KEY_VALUE); + return new RestoreDescription(name, restoreState.getRestoreType()); - } else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) { - restoreState.setRestoreType(TYPE_FULL_STREAM); - return new RestoreDescription(name, restoreState.getRestoreType()); - } - - } catch (IOException ex) { - Log.e(TAG, "Error choosing package " + name + " at index " + packageIndex + "failed selection:", ex); + } else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) { + restoreState.setRestoreType(TYPE_FULL_STREAM); + return new RestoreDescription(name, restoreState.getRestoreType()); } } return RestoreDescription.NO_MORE_PACKAGES; } - private boolean containsPackageFile(String fileName) throws IOException { - ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor(); - ZipInputStream inputStream = buildInputStream(inputFileDescriptor); - - Optional zipEntry = seekToEntry(inputStream, fileName); - IoUtils.closeQuietly(inputFileDescriptor); - IoUtils.closeQuietly(inputStream); - return zipEntry.isPresent(); - } - - private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException { - ContentResolver contentResolver = configuration.getContext().getContentResolver(); - return contentResolver.openFileDescriptor(configuration.getUri(), "r"); - } - - private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) throws FileNotFoundException { - FileInputStream fileInputStream = new FileInputStream(inputFileDescriptor.getFileDescriptor()); - return new ZipInputStream(fileInputStream); - } - - private Optional seekToEntry(ZipInputStream inputStream, String entryPath) throws IOException { - ZipEntry zipEntry; - while ((zipEntry = inputStream.getNextEntry()) != null) { - - if (zipEntry.getName().startsWith(entryPath)) { - return Optional.of(zipEntry); - } - inputStream.closeEntry(); - } - - return Optional.empty(); + private boolean containsPackageFile(String fileName) { + return restoreState.getZipEntries().stream() + .anyMatch(zipEntry -> zipEntry.getName().startsWith(fileName)); } @Override @@ -177,14 +166,36 @@ public class ContentProviderRestoreComponent implements RestoreComponent { return TRANSPORT_OK; } + private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException { + ContentResolver contentResolver = configuration.getContext().getContentResolver(); + return contentResolver.openFileDescriptor(configuration.getUri(), "r"); + } + + private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) throws FileNotFoundException { + FileInputStream fileInputStream = new FileInputStream(inputFileDescriptor.getFileDescriptor()); + return new ZipInputStream(fileInputStream); + } + + private Optional seekToEntry(ZipInputStream inputStream, String entryPath) throws IOException { + ZipEntry zipEntry; + while ((zipEntry = inputStream.getNextEntry()) != null) { + + if (zipEntry.getName().startsWith(entryPath)) { + return Optional.of(zipEntry); + } + inputStream.closeEntry(); + } + + return Optional.empty(); + } + private byte[] readBackupData(ZipInputStream inputStream) throws Exception { byte[] backupData = Streams.readFullyNoClose(inputStream); + SecretKey secretKey = restoreState.getSecretKey(); + byte[] initializationVector = restoreState.getSalt(); - String password = configuration.getPassword(); - byte[] salt = restoreState.getSalt(); - - if (password != null && !password.isEmpty() && salt != null) { - backupData = CipherUtil.decrypt(backupData, password, salt); + if (secretKey != null) { + backupData = CipherUtil.decrypt(backupData, secretKey, initializationVector); } return backupData; diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java index cca36ddf..80dc3c6d 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreState.java @@ -3,6 +3,9 @@ package com.stevesoltys.backup.transport.component.provider; import android.content.pm.PackageInfo; import android.os.ParcelFileDescriptor; +import javax.crypto.SecretKey; +import java.util.List; +import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** @@ -22,6 +25,10 @@ class ContentProviderRestoreState { private byte[] salt; + private SecretKey secretKey; + + private List zipEntries; + ParcelFileDescriptor getInputFileDescriptor() { return inputFileDescriptor; } @@ -69,4 +76,20 @@ class ContentProviderRestoreState { void setSalt(byte[] salt) { this.salt = salt; } + + public SecretKey getSecretKey() { + return secretKey; + } + + public void setSecretKey(SecretKey secretKey) { + this.secretKey = secretKey; + } + + public List getZipEntries() { + return zipEntries; + } + + public void setZipEntries(List zipEntries) { + this.zipEntries = zipEntries; + } } From 03c92efc244ac0256186dae48e900058ad716001 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 22 Feb 2019 01:05:55 -0500 Subject: [PATCH 08/10] Add initialization message to loading popup window --- .../activity/backup/CreateBackupActivityController.java | 6 +++--- .../backup/activity/restore/RestoreObserver.java | 7 +++++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index 9c21f0d4..80a72ac7 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -167,10 +167,10 @@ class CreateBackupActivityController { BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages); View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button); + popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession)); - if (popupWindowButton != null) { - popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession)); - } + TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view); + textView.setText(R.string.initializing); } catch (Exception e) { Log.e(TAG, "Error while running backup: ", e); diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java index 9fbe0bc8..7b7d4809 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java @@ -5,7 +5,6 @@ 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; @@ -31,6 +30,10 @@ class RestoreObserver implements RestoreSessionObserver { @Override public void restoreSessionStarted(int packageCount) { + context.runOnUiThread(() -> { + TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view); + textView.setText(R.string.initializing); + }); } @Override @@ -55,7 +58,7 @@ class RestoreObserver implements RestoreSessionObserver { public void restoreSessionCompleted(RestoreResult restoreResult) { ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport(); - if(!backupTransport.isActive()) { + if (!backupTransport.isActive()) { return; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3be75a51..211cd6da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,5 +21,6 @@ Select all Loading backup… Loading packages… + Initializing… From 0b5cc1a798b86d2d1aad04b53dd15c60ca7f6c43 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 22 Feb 2019 01:11:05 -0500 Subject: [PATCH 09/10] Fix logging in content provider restore component --- .../component/provider/ContentProviderRestoreComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java index eaf0d00f..e405ec4b 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderRestoreComponent.java @@ -87,7 +87,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent { restoreState.setZipEntries(zipEntries); } catch (Exception ex) { - Log.e(TAG, "Salt not found", ex); + Log.e(TAG, "Error while caching zip entries", ex); } return TRANSPORT_OK; From 828f257426471dccef321b0d59820b74a87df39b Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Fri, 1 Mar 2019 23:34:04 -0500 Subject: [PATCH 10/10] Clean up package and class structure significantly 1. Add service layer for backup / restore operations. 2. Reduce amount of duplicate code in activity classes. --- .../backup/activity/PackageListActivity.java | 50 ++++++++++ .../backup/activity/PopupWindowUtil.java | 32 +++++++ .../backup/BackupPopupWindowListener.java | 18 ++-- .../activity/backup/CreateBackupActivity.java | 54 +---------- .../CreateBackupActivityController.java | 92 ++----------------- .../restore/RestoreBackupActivity.java | 54 +---------- .../RestoreBackupActivityController.java | 86 ++--------------- .../restore/RestorePopupWindowListener.java | 4 +- .../backup/service/PackageService.java | 42 +++++++++ .../TransportService.java} | 51 +++++----- .../backup/BackupObserver.java | 9 +- .../backup/service/backup/BackupService.java | 67 ++++++++++++++ .../restore/RestoreObserver.java | 2 +- .../service/restore/RestoreService.java | 59 ++++++++++++ 14 files changed, 313 insertions(+), 307 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/activity/PackageListActivity.java create mode 100644 app/src/main/java/com/stevesoltys/backup/activity/PopupWindowUtil.java create mode 100644 app/src/main/java/com/stevesoltys/backup/service/PackageService.java rename app/src/main/java/com/stevesoltys/backup/{session/BackupManagerController.java => service/TransportService.java} (59%) rename app/src/main/java/com/stevesoltys/backup/{activity => service}/backup/BackupObserver.java (92%) create mode 100644 app/src/main/java/com/stevesoltys/backup/service/backup/BackupService.java rename app/src/main/java/com/stevesoltys/backup/{activity => service}/restore/RestoreObserver.java (98%) create mode 100644 app/src/main/java/com/stevesoltys/backup/service/restore/RestoreService.java diff --git a/app/src/main/java/com/stevesoltys/backup/activity/PackageListActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/PackageListActivity.java new file mode 100644 index 00000000..fd488e17 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/activity/PackageListActivity.java @@ -0,0 +1,50 @@ +package com.stevesoltys.backup.activity; + +import android.app.Activity; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; +import com.stevesoltys.backup.R; + +import java.util.Set; +import java.util.stream.IntStream; + +/** + * @author Steve Soltys + */ +public abstract class PackageListActivity extends Activity implements AdapterView.OnItemClickListener { + + protected ListView packageListView; + + protected Set selectedPackageList; + + public void onItemClick(AdapterView parent, View view, int position, long id) { + String clickedPackage = (String) packageListView.getItemAtPosition(position); + + if (!selectedPackageList.remove(clickedPackage)) { + selectedPackageList.add(clickedPackage); + packageListView.setItemChecked(position, true); + + } else { + packageListView.setItemChecked(position, false); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + if (item.getItemId() == R.id.action_select_all) { + + IntStream.range(0, packageListView.getCount()) + .forEach(position -> { + selectedPackageList.add((String) packageListView.getItemAtPosition(position)); + packageListView.setItemChecked(position, true); + }); + + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/activity/PopupWindowUtil.java b/app/src/main/java/com/stevesoltys/backup/activity/PopupWindowUtil.java new file mode 100644 index 00000000..b4b6bdc6 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/activity/PopupWindowUtil.java @@ -0,0 +1,32 @@ +package com.stevesoltys.backup.activity; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupWindow; +import com.stevesoltys.backup.R; + +/** + * @author Steve Soltys + */ +public class PopupWindowUtil { + + public static PopupWindow showLoadingPopupWindow(Activity parent) { + LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); + View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); + + PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); + popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); + popupWindow.setElevation(10); + popupWindow.setFocusable(false); + popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); + popupWindow.setOutsideTouchable(false); + return popupWindow; + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java index aa702dfd..14ea5be8 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java @@ -4,7 +4,6 @@ import android.os.RemoteException; import android.util.Log; import android.view.View; import android.widget.Button; - import com.stevesoltys.backup.R; import com.stevesoltys.backup.session.backup.BackupResult; import com.stevesoltys.backup.session.backup.BackupSession; @@ -12,7 +11,7 @@ import com.stevesoltys.backup.session.backup.BackupSession; /** * @author Steve Soltys */ -class BackupPopupWindowListener implements Button.OnClickListener { +public class BackupPopupWindowListener implements Button.OnClickListener { private static final String TAG = BackupPopupWindowListener.class.getName(); @@ -26,16 +25,13 @@ class BackupPopupWindowListener implements Button.OnClickListener { public void onClick(View view) { int viewId = view.getId(); - switch (viewId) { + if (viewId == R.id.popup_cancel_button) { + try { + backupSession.stop(BackupResult.CANCELLED); - case R.id.popup_cancel_button: - try { - backupSession.stop(BackupResult.CANCELLED); - - } catch (RemoteException e) { - Log.e(TAG, "Error cancelling backup session: ", e); - } - break; + } catch (RemoteException e) { + Log.e(TAG, "Error cancelling backup session: ", e); + } } } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java index 40a6fc00..c3c1bb0a 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java @@ -1,40 +1,28 @@ package com.stevesoltys.backup.activity.backup; -import android.app.Activity; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; import com.stevesoltys.backup.R; +import com.stevesoltys.backup.activity.PackageListActivity; import java.util.HashSet; -import java.util.Set; -import java.util.stream.IntStream; -public class CreateBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener { +public class CreateBackupActivity extends PackageListActivity implements View.OnClickListener { private CreateBackupActivityController controller; - private ListView packageListView; - - private Set selectedPackageList; - private Uri contentUri; @Override public void onClick(View view) { int viewId = view.getId(); - switch (viewId) { - - case R.id.create_confirm_button: - controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); - break; + if (viewId == R.id.create_confirm_button) { + controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); } } @@ -59,38 +47,4 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen inflater.inflate(R.menu.backup_menu, menu); return true; } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()) { - - case R.id.action_select_all: - - IntStream.range(0, packageListView.getCount()) - .forEach(position -> { - selectedPackageList.add((String) packageListView.getItemAtPosition(position)); - packageListView.setItemChecked(position, true); - }); - - return true; - - default: - return super.onOptionsItemSelected(item); - - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - String clickedPackage = (String) packageListView.getItemAtPosition(position); - - if (!selectedPackageList.remove(clickedPackage)) { - selectedPackageList.add(clickedPackage); - packageListView.setItemChecked(position, true); - - } else { - packageListView.setItemChecked(position, false); - } - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index 80a72ac7..1ebc4e4c 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -2,30 +2,17 @@ package com.stevesoltys.backup.activity.backup; import android.app.Activity; import android.app.AlertDialog; -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.RemoteException; import android.text.InputType; import android.util.Log; -import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.*; import com.google.android.collect.Sets; import com.stevesoltys.backup.R; -import com.stevesoltys.backup.session.BackupManagerController; -import com.stevesoltys.backup.session.backup.BackupSession; -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.ContentProviderBackupConfigurationBuilder; -import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent; +import com.stevesoltys.backup.activity.PopupWindowUtil; +import com.stevesoltys.backup.service.backup.BackupService; +import com.stevesoltys.backup.service.PackageService; import java.util.LinkedList; import java.util.List; @@ -44,17 +31,15 @@ class CreateBackupActivityController { "com.stevesoltys.backup" ); - private final BackupManagerController backupManager; + private final BackupService backupService = new BackupService(); - CreateBackupActivityController() { - backupManager = new BackupManagerController(); - } + private final PackageService packageService = new PackageService(); void populatePackageList(ListView packageListView, CreateBackupActivity parent) { AtomicReference popupWindow = new AtomicReference<>(); parent.runOnUiThread(() -> { - popupWindow.set(showLoadingPopupWindow(parent)); + popupWindow.set(PopupWindowUtil.showLoadingPopupWindow(parent)); TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view); textView.setText(R.string.loading_packages); @@ -65,7 +50,7 @@ class CreateBackupActivityController { List eligiblePackageList = new LinkedList<>(); try { - eligiblePackageList.addAll(backupManager.getEligiblePackages()); + eligiblePackageList.addAll(packageService.getEligiblePackages()); eligiblePackageList.removeAll(IGNORED_PACKAGES); } catch (RemoteException e) { @@ -83,20 +68,6 @@ class CreateBackupActivityController { }); } - private PopupWindow showLoadingPopupWindow(Activity parent) { - LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); - View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); - - PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); - popupWindow.setElevation(10); - popupWindow.setFocusable(false); - popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); - popupWindow.setOutsideTouchable(false); - return popupWindow; - } - void showEnterPasswordAlert(Set selectedPackages, Uri contentUri, Activity parent) { final EditText passwordTextView = new EditText(parent); passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); @@ -127,7 +98,7 @@ class CreateBackupActivityController { String password = passwordTextView.getText().toString(); if (originalPassword.equals(password)) { - backupPackages(selectedPackages, contentUri, parent, password); + backupService.backupPackageData(selectedPackages, contentUri, parent, password); } else { new AlertDialog.Builder(parent) @@ -142,51 +113,4 @@ class CreateBackupActivityController { .setNegativeButton("Cancel", (dialog, button) -> dialog.cancel()) .show(); } - - private void backupPackages(Set selectedPackages, Uri contentUri, Activity parent, - String selectedPassword) { - try { - selectedPackages.add("@pm@"); - - ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder() - .setContext(parent) - .setOutputUri(contentUri) - .setPackages(selectedPackages) - .setPassword(selectedPassword) - .build(); - - boolean success = initializeBackupTransport(backupConfiguration); - - if (!success) { - Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show(); - return; - } - - PopupWindow popupWindow = showLoadingPopupWindow(parent); - BackupObserver backupObserver = new BackupObserver(parent, popupWindow); - BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages); - - View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button); - popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession)); - - TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view); - textView.setText(R.string.initializing); - - } catch (Exception e) { - Log.e(TAG, "Error while running backup: ", e); - } - } - - private 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; - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java index e16f792f..4d206f57 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java @@ -1,40 +1,28 @@ package com.stevesoltys.backup.activity.restore; -import android.app.Activity; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; import com.stevesoltys.backup.R; +import com.stevesoltys.backup.activity.PackageListActivity; import java.util.HashSet; -import java.util.Set; -import java.util.stream.IntStream; -public class RestoreBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener { +public class RestoreBackupActivity extends PackageListActivity implements View.OnClickListener { private RestoreBackupActivityController controller; - private ListView packageListView; - - private Set selectedPackageList; - private Uri contentUri; @Override public void onClick(View view) { int viewId = view.getId(); - switch (viewId) { - - case R.id.restore_confirm_button: - controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); - break; + if (viewId == R.id.restore_confirm_button) { + controller.showEnterPasswordAlert(selectedPackageList, contentUri, this); } } @@ -59,38 +47,4 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe inflater.inflate(R.menu.backup_menu, menu); return true; } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()) { - - case R.id.action_select_all: - - IntStream.range(0, packageListView.getCount()) - .forEach(position -> { - selectedPackageList.add((String) packageListView.getItemAtPosition(position)); - packageListView.setItemChecked(position, true); - }); - - return true; - - default: - return super.onOptionsItemSelected(item); - - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - String clickedPackage = (String) packageListView.getItemAtPosition(position); - - if (!selectedPackageList.remove(clickedPackage)) { - selectedPackageList.add(clickedPackage); - packageListView.setItemChecked(position, true); - - } else { - packageListView.setItemChecked(position, false); - } - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java index 223c0879..6370d7e7 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java @@ -2,30 +2,16 @@ package com.stevesoltys.backup.activity.restore; import android.app.Activity; import android.app.AlertDialog; -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.os.RemoteException; import android.text.InputType; import android.util.Log; -import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.*; import com.stevesoltys.backup.R; -import com.stevesoltys.backup.session.BackupManagerController; -import com.stevesoltys.backup.session.restore.RestoreSession; -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.activity.PopupWindowUtil; +import com.stevesoltys.backup.service.restore.RestoreService; import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder; -import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent; import libcore.io.IoUtils; import java.io.File; @@ -45,17 +31,13 @@ class RestoreBackupActivityController { private static final String TAG = RestoreBackupActivityController.class.getName(); - private final BackupManagerController backupManager; - - RestoreBackupActivityController() { - backupManager = new BackupManagerController(); - } + private final RestoreService restoreService = new RestoreService(); void populatePackageList(ListView packageListView, Uri contentUri, RestoreBackupActivity parent) { AtomicReference popupWindow = new AtomicReference<>(); parent.runOnUiThread(() -> { - popupWindow.set(showLoadingPopupWindow(parent)); + popupWindow.set(PopupWindowUtil.showLoadingPopupWindow(parent)); TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view); textView.setText(R.string.loading_backup); @@ -83,20 +65,6 @@ class RestoreBackupActivityController { }); } - private PopupWindow showLoadingPopupWindow(Activity parent) { - LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout); - View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup); - - PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); - popupWindow.setElevation(10); - popupWindow.setFocusable(false); - popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0); - popupWindow.setOutsideTouchable(false); - return popupWindow; - } - private List getEligiblePackages(Uri contentUri, Activity context) throws IOException { List results = new LinkedList<>(); @@ -131,50 +99,10 @@ class RestoreBackupActivityController { .setView(passwordTextView) .setPositiveButton("Confirm", (dialog, button) -> - restorePackages(selectedPackages, contentUri, parent, passwordTextView.getText().toString())) + restoreService.restorePackages(selectedPackages, contentUri, parent, + passwordTextView.getText().toString())) + .setNegativeButton("Cancel", (dialog, button) -> dialog.cancel()) .show(); } - - private void restorePackages(Set selectedPackages, Uri contentUri, Activity parent, String password) { - try { - ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder(). - setContext(parent) - .setOutputUri(contentUri) - .setPackages(selectedPackages) - .setPassword(password) - .build(); - - boolean success = initializeBackupTransport(backupConfiguration); - - if (!success) { - Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show(); - return; - } - - PopupWindow popupWindow = showLoadingPopupWindow(parent); - RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size()); - RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackages); - - View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button); - if (popupWindowButton != null) { - popupWindowButton.setOnClickListener(new RestorePopupWindowListener(restoreSession)); - } - } catch (RemoteException e) { - Log.e(TAG, "Error while running restore: ", e); - } - } - - private 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; - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java index 69397dfc..7e6c04f9 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java @@ -12,13 +12,13 @@ import com.stevesoltys.backup.session.restore.RestoreSession; /** * @author Steve Soltys */ -class RestorePopupWindowListener implements Button.OnClickListener { +public class RestorePopupWindowListener implements Button.OnClickListener { private static final String TAG = RestorePopupWindowListener.class.getName(); private final RestoreSession restoreSession; - RestorePopupWindowListener(RestoreSession restoreSession) { + public RestorePopupWindowListener(RestoreSession restoreSession) { this.restoreSession = restoreSession; } diff --git a/app/src/main/java/com/stevesoltys/backup/service/PackageService.java b/app/src/main/java/com/stevesoltys/backup/service/PackageService.java new file mode 100644 index 00000000..e64f958d --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/service/PackageService.java @@ -0,0 +1,42 @@ +package com.stevesoltys.backup.service; + +import android.app.backup.IBackupManager; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Steve Soltys + */ +public class PackageService { + + private final IBackupManager backupManager; + + private final IPackageManager packageManager; + + public PackageService() { + backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup")); + packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + } + + public List getEligiblePackages() throws RemoteException { + List results = new ArrayList<>(); + List packages = packageManager.getInstalledPackages(0, UserHandle.USER_SYSTEM).getList(); + + if (packages != null) { + for (PackageInfo packageInfo : packages) { + + if (backupManager.isAppEligibleForBackup(packageInfo.packageName)) { + results.add(packageInfo.packageName); + } + } + } + + return results; + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java b/app/src/main/java/com/stevesoltys/backup/service/TransportService.java similarity index 59% rename from app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java rename to app/src/main/java/com/stevesoltys/backup/service/TransportService.java index edc884d9..934005b2 100644 --- a/app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java +++ b/app/src/main/java/com/stevesoltys/backup/service/TransportService.java @@ -1,35 +1,46 @@ -package com.stevesoltys.backup.session; +package com.stevesoltys.backup.service; import android.app.backup.IBackupManager; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; 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.ArrayList; -import java.util.List; import java.util.Set; -import static android.os.UserHandle.USER_SYSTEM; - /** * @author Steve Soltys */ -public class BackupManagerController { +public class TransportService { private static final String BACKUP_TRANSPORT = "com.stevesoltys.backup.transport.ConfigurableBackupTransport"; private final IBackupManager backupManager; - private final IPackageManager packageManager; - - public BackupManagerController() { + public TransportService() { backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup")); - packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + } + + 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 packages) throws RemoteException { @@ -61,20 +72,4 @@ public class BackupManagerController { restoreSession.start(); return restoreSession; } - - public List getEligiblePackages() throws RemoteException { - List results = new ArrayList<>(); - List packages = packageManager.getInstalledPackages(0, USER_SYSTEM).getList(); - - if (packages != null) { - for (PackageInfo packageInfo : packages) { - - if (backupManager.isAppEligibleForBackup(packageInfo.packageName)) { - results.add(packageInfo.packageName); - } - } - } - - return results; - } } diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupObserver.java similarity index 92% rename from app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java rename to app/src/main/java/com/stevesoltys/backup/service/backup/BackupObserver.java index 1fa12e1a..abd2d075 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java +++ b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupObserver.java @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.activity.backup; +package com.stevesoltys.backup.service.backup; import android.app.Activity; import android.app.backup.BackupProgress; @@ -13,6 +13,8 @@ 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 */ @@ -22,9 +24,12 @@ class BackupObserver implements BackupSessionObserver { private final PopupWindow popupWindow; - BackupObserver(Activity context, PopupWindow popupWindow) { + private final URI contentUri; + + BackupObserver(Activity context, PopupWindow popupWindow, URI contentUri) { this.context = context; this.popupWindow = popupWindow; + this.contentUri = contentUri; } @Override diff --git a/app/src/main/java/com/stevesoltys/backup/service/backup/BackupService.java b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupService.java new file mode 100644 index 00000000..dc5604e7 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupService.java @@ -0,0 +1,67 @@ +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.backup.BackupPopupWindowListener; +import com.stevesoltys.backup.activity.PopupWindowUtil; +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 java.net.URI; +import java.util.Set; + +/** + * @author Steve Soltys + */ +public class BackupService { + + private static final String TAG = BackupService.class.getName(); + + private final TransportService transportService = new TransportService(); + + public void backupPackageData(Set selectedPackages, Uri contentUri, Activity parent, + String selectedPassword) { + 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())); + BackupSession backupSession = transportService.backup(backupObserver, selectedPackages); + + View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button); + popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession)); + + TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view); + textView.setText(R.string.initializing); + + } catch (Exception e) { + Log.e(TAG, "Error while running backup: ", e); + } + } + + public void backupPackages() { + + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java b/app/src/main/java/com/stevesoltys/backup/service/restore/RestoreObserver.java similarity index 98% rename from app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java rename to app/src/main/java/com/stevesoltys/backup/service/restore/RestoreObserver.java index 7b7d4809..69caa690 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java +++ b/app/src/main/java/com/stevesoltys/backup/service/restore/RestoreObserver.java @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.activity.restore; +package com.stevesoltys.backup.service.restore; import android.app.Activity; import android.widget.PopupWindow; diff --git a/app/src/main/java/com/stevesoltys/backup/service/restore/RestoreService.java b/app/src/main/java/com/stevesoltys/backup/service/restore/RestoreService.java new file mode 100644 index 00000000..06e15c0b --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/service/restore/RestoreService.java @@ -0,0 +1,59 @@ +package com.stevesoltys.backup.service.restore; + +import android.app.Activity; +import android.net.Uri; +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 java.util.Set; + +/** + * @author Steve Soltys + */ +public class RestoreService { + + private static final String TAG = RestoreService.class.getName(); + + private final TransportService transportService = new TransportService(); + + public void restorePackages(Set selectedPackages, Uri contentUri, Activity parent, String password) { + 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 popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button); + + if (popupWindowButton != null) { + popupWindowButton.setOnClickListener(new RestorePopupWindowListener(restoreSession)); + } + + } catch (RemoteException e) { + Log.e(TAG, "Error while running restore: ", e); + } + } +}