Merge pull request #17 from stevesoltys/feature/encryption
Add support for encrypted backups
This commit is contained in:
commit
93f878f574
24 changed files with 895 additions and 525 deletions
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +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<String> selectedPackageList;
|
||||
|
||||
private Uri contentUri;
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int viewId = view.getId();
|
||||
|
||||
switch (viewId) {
|
||||
|
||||
case R.id.create_confirm_button:
|
||||
controller.backupPackages(selectedPackageList, contentUri, this);
|
||||
break;
|
||||
if (viewId == R.id.create_confirm_button) {
|
||||
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +38,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
|
||||
|
@ -58,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
package com.stevesoltys.backup.activity.backup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.app.AlertDialog;
|
||||
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.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.*;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -36,82 +26,91 @@ class CreateBackupActivityController {
|
|||
|
||||
private static final String TAG = CreateBackupActivityController.class.getName();
|
||||
|
||||
private static final Set<String> IGNORED_PACKAGES = Collections.singleton("com.android.providers.downloads.ui");
|
||||
private static final Set<String> IGNORED_PACKAGES = Sets.newArraySet(
|
||||
"com.android.providers.downloads.ui", "com.android.providers.downloads", "com.android.providers.media",
|
||||
"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> popupWindow = new AtomicReference<>();
|
||||
|
||||
parent.runOnUiThread(() -> {
|
||||
popupWindow.set(PopupWindowUtil.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<String> eligiblePackageList = new LinkedList<>();
|
||||
|
||||
try {
|
||||
eligiblePackageList.addAll(backupManager.getEligiblePackages());
|
||||
eligiblePackageList.addAll(packageService.getEligiblePackages());
|
||||
eligiblePackageList.removeAll(IGNORED_PACKAGES);
|
||||
|
||||
} catch (RemoteException e) {
|
||||
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);
|
||||
}
|
||||
|
||||
void backupPackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
try {
|
||||
selectedPackages.add("@pm@");
|
||||
|
||||
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
|
||||
.setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
|
||||
boolean success = initializeBackupTransport(backupConfiguration);
|
||||
|
||||
if (!success) {
|
||||
Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
parent.runOnUiThread(() -> {
|
||||
if (popupWindow.get() != null) {
|
||||
popupWindow.get().dismiss();
|
||||
}
|
||||
|
||||
PopupWindow popupWindow = buildPopupWindow(parent);
|
||||
BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
|
||||
BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages);
|
||||
|
||||
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
|
||||
|
||||
if (popupWindowButton != null) {
|
||||
popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error while running backup: ", e);
|
||||
}
|
||||
packageListView.setOnItemClickListener(parent);
|
||||
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
|
||||
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
|
||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
||||
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
final EditText passwordTextView = new EditText(parent);
|
||||
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
if (backupTransport.isActive()) {
|
||||
return false;
|
||||
}
|
||||
new AlertDialog.Builder(parent)
|
||||
.setTitle("Enter a password")
|
||||
.setMessage("You'll need this to restore your backup, so write it down!")
|
||||
.setView(passwordTextView)
|
||||
|
||||
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
|
||||
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
|
||||
backupTransport.initialize(backupComponent, restoreComponent);
|
||||
return true;
|
||||
.setPositiveButton("Set password", (dialog, button) ->
|
||||
showConfirmPasswordAlert(selectedPackages, contentUri, parent,
|
||||
passwordTextView.getText().toString()))
|
||||
|
||||
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
|
||||
.show();
|
||||
}
|
||||
|
||||
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);
|
||||
private void showConfirmPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent,
|
||||
String originalPassword) {
|
||||
final EditText passwordTextView = new EditText(parent);
|
||||
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
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;
|
||||
new AlertDialog.Builder(parent)
|
||||
.setTitle("Confirm password")
|
||||
.setView(passwordTextView)
|
||||
|
||||
.setPositiveButton("Confirm", (dialog, button) -> {
|
||||
String password = passwordTextView.getText().toString();
|
||||
|
||||
if (originalPassword.equals(password)) {
|
||||
backupService.backupPackageData(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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +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<String> selectedPackageList;
|
||||
|
||||
private Uri contentUri;
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int viewId = view.getId();
|
||||
|
||||
switch (viewId) {
|
||||
|
||||
case R.id.restore_confirm_button:
|
||||
controller.restorePackages(selectedPackageList, contentUri, this);
|
||||
break;
|
||||
if (viewId == R.id.restore_confirm_button) {
|
||||
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +38,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
|
||||
|
@ -58,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,17 @@
|
|||
package com.stevesoltys.backup.activity.restore;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.app.AlertDialog;
|
||||
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;
|
||||
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.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.ContentProviderBackupComponent;
|
||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -35,6 +20,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;
|
||||
|
||||
|
@ -45,14 +31,22 @@ 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> popupWindow = new AtomicReference<>();
|
||||
|
||||
parent.runOnUiThread(() -> {
|
||||
popupWindow.set(PopupWindowUtil.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<String> eligiblePackageList = new LinkedList<>();
|
||||
|
||||
try {
|
||||
eligiblePackageList.addAll(getEligiblePackages(contentUri, parent));
|
||||
|
||||
|
@ -60,9 +54,15 @@ 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 List<String> getEligiblePackages(Uri contentUri, Activity context) throws IOException {
|
||||
|
@ -89,53 +89,20 @@ class RestoreBackupActivityController {
|
|||
return results;
|
||||
}
|
||||
|
||||
void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
try {
|
||||
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
|
||||
setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
|
||||
boolean success = initializeBackupTransport(backupConfiguration);
|
||||
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||
final EditText passwordTextView = new EditText(parent);
|
||||
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
if(!success) {
|
||||
Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
new AlertDialog.Builder(parent)
|
||||
.setTitle("Enter a password")
|
||||
.setMessage("If you didn't enter one while creating the backup, you can leave this blank.")
|
||||
.setView(passwordTextView)
|
||||
|
||||
PopupWindow popupWindow = buildPopupWindow(parent);
|
||||
RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size());
|
||||
RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackages);
|
||||
.setPositiveButton("Confirm", (dialog, button) ->
|
||||
restoreService.restorePackages(selectedPackages, contentUri, parent,
|
||||
passwordTextView.getText().toString()))
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
* 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 the provided secret key.
|
||||
*
|
||||
* @param payload The payload.
|
||||
* @param secretKey The secret key.
|
||||
* @param iv The initialization vector.
|
||||
*/
|
||||
public static byte[] encrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
|
||||
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given payload using the provided secret key.
|
||||
*
|
||||
* @param payload The payload.
|
||||
* @param secretKey The secret key.
|
||||
* @param iv The initialization vector.
|
||||
*/
|
||||
public static byte[] decrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
|
||||
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(payload);
|
||||
}
|
||||
}
|
|
@ -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 = 25;
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
|
@ -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<String> getEligiblePackages() throws RemoteException {
|
||||
List<String> results = new ArrayList<>();
|
||||
List<PackageInfo> 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;
|
||||
}
|
||||
}
|
|
@ -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<String> packages) throws RemoteException {
|
||||
|
@ -61,20 +72,4 @@ public class BackupManagerController {
|
|||
restoreSession.start();
|
||||
return restoreSession;
|
||||
}
|
||||
|
||||
public List<String> getEligiblePackages() throws RemoteException {
|
||||
List<String> results = new ArrayList<>();
|
||||
List<PackageInfo> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<String> 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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
package com.stevesoltys.backup.activity.restore;
|
||||
package com.stevesoltys.backup.service.restore;
|
||||
|
||||
import android.app.Activity;
|
||||
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;
|
||||
}
|
||||
|
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,15 +6,18 @@ import android.content.pm.PackageInfo;
|
|||
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 libcore.io.IoUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
|
@ -41,11 +44,6 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestBackupTime() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String currentDestinationString() {
|
||||
return DESTINATION_DESCRIPTION;
|
||||
|
@ -67,33 +65,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 +86,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 +211,76 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
|||
clearBackupState(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int finishBackup() {
|
||||
return clearBackupState(false);
|
||||
}
|
||||
|
||||
private int clearBackupState(boolean closeFile) {
|
||||
|
||||
private void initializeBackupState() throws Exception {
|
||||
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();
|
||||
ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH);
|
||||
backupState.getOutputStream().putNextEntry(saltZipEntry);
|
||||
backupState.getOutputStream().write(backupState.getSalt());
|
||||
backupState.getOutputStream().closeEntry();
|
||||
|
||||
if (outputStream != null) {
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
|
||||
backupState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
|
||||
if (outputStream != null) {
|
||||
outputStream.finish();
|
||||
outputStream.close();
|
||||
private void initializeOutputStream() throws IOException {
|
||||
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);
|
||||
}
|
||||
|
||||
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 (backupState.getSecretKey() != null) {
|
||||
byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize);
|
||||
SecretKey secretKey = backupState.getSecretKey();
|
||||
byte[] salt = backupState.getSalt();
|
||||
|
||||
outputStream.write(CipherUtil.encrypt(payload, secretKey, salt));
|
||||
|
||||
} 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;
|
||||
|
|
|
@ -14,6 +14,8 @@ public class ContentProviderBackupConfiguration {
|
|||
|
||||
private final Uri uri;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final long backupSizeQuota;
|
||||
|
||||
private final Set<String> packages;
|
||||
|
@ -22,30 +24,24 @@ public class ContentProviderBackupConfiguration {
|
|||
|
||||
private final String incrementalBackupDirectory;
|
||||
|
||||
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, long backupSizeQuota,
|
||||
String fullBackupDirectory, String incrementalBackupDirectory) {
|
||||
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, String password,
|
||||
long backupSizeQuota, String fullBackupDirectory,
|
||||
String incrementalBackupDirectory) {
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
this.packages = packages;
|
||||
this.password = password;
|
||||
this.backupSizeQuota = backupSizeQuota;
|
||||
this.fullBackupDirectory = fullBackupDirectory;
|
||||
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ public class ContentProviderBackupConfigurationBuilder {
|
|||
|
||||
private Set<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.stevesoltys.backup.transport.component.provider;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
class ContentProviderBackupConstants {
|
||||
|
||||
static final String SALT_FILE_PATH = "salt";
|
||||
}
|
|
@ -2,7 +2,9 @@ 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;
|
||||
|
||||
/**
|
||||
|
@ -10,6 +12,8 @@ import java.util.zip.ZipOutputStream;
|
|||
*/
|
||||
class ContentProviderBackupState {
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
private ParcelFileDescriptor inputFileDescriptor;
|
||||
|
||||
private ParcelFileDescriptor outputFileDescriptor;
|
||||
|
@ -24,36 +28,13 @@ class ContentProviderBackupState {
|
|||
|
||||
private int packageIndex;
|
||||
|
||||
ParcelFileDescriptor getInputFileDescriptor() {
|
||||
return inputFileDescriptor;
|
||||
}
|
||||
private byte[] salt;
|
||||
|
||||
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
|
||||
this.inputFileDescriptor = inputFileDescriptor;
|
||||
}
|
||||
private SecretKey secretKey;
|
||||
|
||||
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 +45,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 +84,24 @@ class ContentProviderBackupState {
|
|||
void setPackageIndex(int packageIndex) {
|
||||
this.packageIndex = packageIndex;
|
||||
}
|
||||
|
||||
String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public SecretKey getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,16 @@ 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;
|
||||
import libcore.io.Streams;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.*;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
@ -48,6 +51,45 @@ 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));
|
||||
restoreState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt()));
|
||||
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
IoUtils.closeQuietly(inputStream);
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Salt not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
List<ZipEntry> 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, "Error while caching zip entries", ex);
|
||||
}
|
||||
|
||||
return TRANSPORT_OK;
|
||||
}
|
||||
|
||||
|
@ -62,32 +104,66 @@ 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 | InvalidKeyException | InvalidAlgorithmParameterException 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, InvalidKeyException,
|
||||
InvalidAlgorithmParameterException {
|
||||
private boolean containsPackageFile(String fileName) {
|
||||
return restoreState.getZipEntries().stream()
|
||||
.anyMatch(zipEntry -> zipEntry.getName().startsWith(fileName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
|
||||
Preconditions.checkState(restoreState != null, "startRestore() not called");
|
||||
Preconditions.checkState(restoreState.getPackageIndex() >= 0, "nextRestorePackage() not called");
|
||||
Preconditions.checkState(restoreState.getRestoreType() == TYPE_KEY_VALUE,
|
||||
"getRestoreData() for non-key/value dataset");
|
||||
|
||||
PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
|
||||
|
||||
try {
|
||||
return transferIncrementalRestoreData(packageInfo.packageName, outputFileDescriptor);
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Unable to read backup records: ", ex);
|
||||
return TRANSPORT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor)
|
||||
throws Exception {
|
||||
|
||||
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
|
||||
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
|
||||
BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
|
||||
|
||||
Optional<ZipEntry> 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 = readBackupData(inputStream);
|
||||
backupDataOutput.writeEntityHeader(blobKey, backupData.length);
|
||||
backupDataOutput.writeEntityData(backupData, backupData.length);
|
||||
inputStream.closeEntry();
|
||||
|
||||
zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName);
|
||||
}
|
||||
|
||||
Optional<ZipEntry> zipEntry = seekToEntry(inputStream, fileName);
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
IoUtils.closeQuietly(inputStream);
|
||||
return zipEntry.isPresent();
|
||||
IoUtils.closeQuietly(outputFileDescriptor);
|
||||
return TRANSPORT_OK;
|
||||
}
|
||||
|
||||
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
|
||||
|
@ -113,48 +189,16 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
|
||||
Preconditions.checkState(restoreState != null, "startRestore() not called");
|
||||
Preconditions.checkState(restoreState.getPackageIndex() >= 0, "nextRestorePackage() not called");
|
||||
Preconditions.checkState(restoreState.getRestoreType() == TYPE_KEY_VALUE,
|
||||
"getRestoreData() for non-key/value dataset");
|
||||
private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
|
||||
byte[] backupData = Streams.readFullyNoClose(inputStream);
|
||||
SecretKey secretKey = restoreState.getSecretKey();
|
||||
byte[] initializationVector = restoreState.getSalt();
|
||||
|
||||
PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
|
||||
|
||||
try {
|
||||
return transferIncrementalRestoreData(packageInfo.packageName, outputFileDescriptor);
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Unable to read backup records: ", ex);
|
||||
return TRANSPORT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor)
|
||||
throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
|
||||
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
|
||||
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
|
||||
BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
|
||||
|
||||
Optional<ZipEntry> 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);
|
||||
backupDataOutput.writeEntityHeader(blobKey, backupData.length);
|
||||
backupDataOutput.writeEntityData(backupData, backupData.length);
|
||||
inputStream.closeEntry();
|
||||
|
||||
zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName);
|
||||
if (secretKey != null) {
|
||||
backupData = CipherUtil.decrypt(backupData, secretKey, initializationVector);
|
||||
}
|
||||
|
||||
IoUtils.closeQuietly(inputFileDescriptor);
|
||||
IoUtils.closeQuietly(outputFileDescriptor);
|
||||
return TRANSPORT_OK;
|
||||
return backupData;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -20,28 +23,18 @@ class ContentProviderRestoreState {
|
|||
|
||||
private ZipInputStream inputStream;
|
||||
|
||||
PackageInfo[] getPackages() {
|
||||
return packages;
|
||||
private byte[] salt;
|
||||
|
||||
private SecretKey secretKey;
|
||||
|
||||
private List<ZipEntry> zipEntries;
|
||||
|
||||
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 +45,51 @@ 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;
|
||||
}
|
||||
|
||||
public SecretKey getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public List<ZipEntry> getZipEntries() {
|
||||
return zipEntries;
|
||||
}
|
||||
|
||||
public void setZipEntries(List<ZipEntry> zipEntries) {
|
||||
this.zipEntries = zipEntries;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,5 +19,8 @@
|
|||
<string name="popup_cancel">Cancel</string>
|
||||
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="loading_backup">Loading backup…</string>
|
||||
<string name="loading_packages">Loading packages…</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue