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.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
import com.stevesoltys.backup.session.backup.BackupResult;
|
import com.stevesoltys.backup.session.backup.BackupResult;
|
||||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
import com.stevesoltys.backup.session.backup.BackupSession;
|
||||||
|
@ -12,7 +11,7 @@ import com.stevesoltys.backup.session.backup.BackupSession;
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
*/
|
*/
|
||||||
class BackupPopupWindowListener implements Button.OnClickListener {
|
public class BackupPopupWindowListener implements Button.OnClickListener {
|
||||||
|
|
||||||
private static final String TAG = BackupPopupWindowListener.class.getName();
|
private static final String TAG = BackupPopupWindowListener.class.getName();
|
||||||
|
|
||||||
|
@ -26,16 +25,13 @@ class BackupPopupWindowListener implements Button.OnClickListener {
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
int viewId = view.getId();
|
int viewId = view.getId();
|
||||||
|
|
||||||
switch (viewId) {
|
if (viewId == R.id.popup_cancel_button) {
|
||||||
|
try {
|
||||||
|
backupSession.stop(BackupResult.CANCELLED);
|
||||||
|
|
||||||
case R.id.popup_cancel_button:
|
} catch (RemoteException e) {
|
||||||
try {
|
Log.e(TAG, "Error cancelling backup session: ", e);
|
||||||
backupSession.stop(BackupResult.CANCELLED);
|
}
|
||||||
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "Error cancelling backup session: ", e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,28 @@
|
||||||
package com.stevesoltys.backup.activity.backup;
|
package com.stevesoltys.backup.activity.backup;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
|
import com.stevesoltys.backup.activity.PackageListActivity;
|
||||||
|
|
||||||
import java.util.HashSet;
|
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 CreateBackupActivityController controller;
|
||||||
|
|
||||||
private ListView packageListView;
|
|
||||||
|
|
||||||
private Set<String> selectedPackageList;
|
|
||||||
|
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
int viewId = view.getId();
|
int viewId = view.getId();
|
||||||
|
|
||||||
switch (viewId) {
|
if (viewId == R.id.create_confirm_button) {
|
||||||
|
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
|
||||||
case R.id.create_confirm_button:
|
|
||||||
controller.backupPackages(selectedPackageList, contentUri, this);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +38,7 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen
|
||||||
contentUri = getIntent().getData();
|
contentUri = getIntent().getData();
|
||||||
|
|
||||||
controller = new CreateBackupActivityController();
|
controller = new CreateBackupActivityController();
|
||||||
controller.populatePackageList(packageListView, this);
|
AsyncTask.execute(() -> controller.populatePackageList(packageListView, CreateBackupActivity.this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -58,38 +47,4 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen
|
||||||
inflater.inflate(R.menu.backup_menu, menu);
|
inflater.inflate(R.menu.backup_menu, menu);
|
||||||
return true;
|
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;
|
package com.stevesoltys.backup.activity.backup;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.app.AlertDialog;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.widget.*;
|
||||||
import android.widget.ArrayAdapter;
|
import com.google.android.collect.Sets;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
import com.stevesoltys.backup.session.BackupManagerController;
|
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
import com.stevesoltys.backup.service.backup.BackupService;
|
||||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
|
import com.stevesoltys.backup.service.PackageService;
|
||||||
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 java.util.*;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
|
@ -36,82 +26,91 @@ class CreateBackupActivityController {
|
||||||
|
|
||||||
private static final String TAG = CreateBackupActivityController.class.getName();
|
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() {
|
private final PackageService packageService = new PackageService();
|
||||||
backupManager = new BackupManagerController();
|
|
||||||
}
|
|
||||||
|
|
||||||
void populatePackageList(ListView packageListView, CreateBackupActivity parent) {
|
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<>();
|
List<String> eligiblePackageList = new LinkedList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eligiblePackageList.addAll(backupManager.getEligiblePackages());
|
eligiblePackageList.addAll(packageService.getEligiblePackages());
|
||||||
eligiblePackageList.removeAll(IGNORED_PACKAGES);
|
eligiblePackageList.removeAll(IGNORED_PACKAGES);
|
||||||
|
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "Error while obtaining package list: ", e);
|
Log.e(TAG, "Error while obtaining package list: ", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parent.runOnUiThread(() -> {
|
||||||
packageListView.setOnItemClickListener(parent);
|
if (popupWindow.get() != null) {
|
||||||
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
|
popupWindow.get().dismiss();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupWindow popupWindow = buildPopupWindow(parent);
|
packageListView.setOnItemClickListener(parent);
|
||||||
BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
|
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
|
||||||
BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages);
|
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||||
|
});
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
|
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
final EditText passwordTextView = new EditText(parent);
|
||||||
|
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||||
|
|
||||||
if (backupTransport.isActive()) {
|
new AlertDialog.Builder(parent)
|
||||||
return false;
|
.setTitle("Enter a password")
|
||||||
}
|
.setMessage("You'll need this to restore your backup, so write it down!")
|
||||||
|
.setView(passwordTextView)
|
||||||
|
|
||||||
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
|
.setPositiveButton("Set password", (dialog, button) ->
|
||||||
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
|
showConfirmPasswordAlert(selectedPackages, contentUri, parent,
|
||||||
backupTransport.initialize(backupComponent, restoreComponent);
|
passwordTextView.getText().toString()))
|
||||||
return true;
|
|
||||||
|
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private PopupWindow buildPopupWindow(Activity parent) {
|
private void showConfirmPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent,
|
||||||
LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
String originalPassword) {
|
||||||
ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
|
final EditText passwordTextView = new EditText(parent);
|
||||||
View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
|
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||||
|
|
||||||
PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
|
new AlertDialog.Builder(parent)
|
||||||
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
|
.setTitle("Confirm password")
|
||||||
popupWindow.setElevation(10);
|
.setView(passwordTextView)
|
||||||
popupWindow.setFocusable(false);
|
|
||||||
popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
|
.setPositiveButton("Confirm", (dialog, button) -> {
|
||||||
return popupWindow;
|
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;
|
package com.stevesoltys.backup.activity.restore;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
|
import com.stevesoltys.backup.activity.PackageListActivity;
|
||||||
|
|
||||||
import java.util.HashSet;
|
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 RestoreBackupActivityController controller;
|
||||||
|
|
||||||
private ListView packageListView;
|
|
||||||
|
|
||||||
private Set<String> selectedPackageList;
|
|
||||||
|
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
int viewId = view.getId();
|
int viewId = view.getId();
|
||||||
|
|
||||||
switch (viewId) {
|
if (viewId == R.id.restore_confirm_button) {
|
||||||
|
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
|
||||||
case R.id.restore_confirm_button:
|
|
||||||
controller.restorePackages(selectedPackageList, contentUri, this);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +38,7 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe
|
||||||
contentUri = getIntent().getData();
|
contentUri = getIntent().getData();
|
||||||
|
|
||||||
controller = new RestoreBackupActivityController();
|
controller = new RestoreBackupActivityController();
|
||||||
controller.populatePackageList(packageListView, contentUri, this);
|
AsyncTask.execute(() -> controller.populatePackageList(packageListView, contentUri, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -58,38 +47,4 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe
|
||||||
inflater.inflate(R.menu.backup_menu, menu);
|
inflater.inflate(R.menu.backup_menu, menu);
|
||||||
return true;
|
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;
|
package com.stevesoltys.backup.activity.restore;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.app.AlertDialog;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.RemoteException;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.widget.*;
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
import com.stevesoltys.backup.session.BackupManagerController;
|
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||||
import com.stevesoltys.backup.session.restore.RestoreSession;
|
import com.stevesoltys.backup.service.restore.RestoreService;
|
||||||
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.transport.component.provider.ContentProviderBackupConfigurationBuilder;
|
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 libcore.io.IoUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -35,6 +20,7 @@ import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
@ -45,14 +31,22 @@ class RestoreBackupActivityController {
|
||||||
|
|
||||||
private static final String TAG = RestoreBackupActivityController.class.getName();
|
private static final String TAG = RestoreBackupActivityController.class.getName();
|
||||||
|
|
||||||
private final BackupManagerController backupManager;
|
private final RestoreService restoreService = new RestoreService();
|
||||||
|
|
||||||
RestoreBackupActivityController() {
|
|
||||||
backupManager = new BackupManagerController();
|
|
||||||
}
|
|
||||||
|
|
||||||
void populatePackageList(ListView packageListView, Uri contentUri, RestoreBackupActivity parent) {
|
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<>();
|
List<String> eligiblePackageList = new LinkedList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eligiblePackageList.addAll(getEligiblePackages(contentUri, parent));
|
eligiblePackageList.addAll(getEligiblePackages(contentUri, parent));
|
||||||
|
|
||||||
|
@ -60,9 +54,15 @@ class RestoreBackupActivityController {
|
||||||
Log.e(TAG, "Error while obtaining package list: ", e);
|
Log.e(TAG, "Error while obtaining package list: ", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
packageListView.setOnItemClickListener(parent);
|
parent.runOnUiThread(() -> {
|
||||||
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
|
if (popupWindow.get() != null) {
|
||||||
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
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 {
|
private List<String> getEligiblePackages(Uri contentUri, Activity context) throws IOException {
|
||||||
|
@ -89,53 +89,20 @@ class RestoreBackupActivityController {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
|
||||||
try {
|
final EditText passwordTextView = new EditText(parent);
|
||||||
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
|
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||||
setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
|
|
||||||
boolean success = initializeBackupTransport(backupConfiguration);
|
|
||||||
|
|
||||||
if(!success) {
|
new AlertDialog.Builder(parent)
|
||||||
Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
|
.setTitle("Enter a password")
|
||||||
return;
|
.setMessage("If you didn't enter one while creating the backup, you can leave this blank.")
|
||||||
}
|
.setView(passwordTextView)
|
||||||
|
|
||||||
PopupWindow popupWindow = buildPopupWindow(parent);
|
.setPositiveButton("Confirm", (dialog, button) ->
|
||||||
RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size());
|
restoreService.restorePackages(selectedPackages, contentUri, parent,
|
||||||
RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackages);
|
passwordTextView.getText().toString()))
|
||||||
|
|
||||||
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
|
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
|
||||||
if (popupWindowButton != null) {
|
.show();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import com.stevesoltys.backup.session.restore.RestoreSession;
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
*/
|
*/
|
||||||
class RestorePopupWindowListener implements Button.OnClickListener {
|
public class RestorePopupWindowListener implements Button.OnClickListener {
|
||||||
|
|
||||||
private static final String TAG = RestorePopupWindowListener.class.getName();
|
private static final String TAG = RestorePopupWindowListener.class.getName();
|
||||||
|
|
||||||
private final RestoreSession restoreSession;
|
private final RestoreSession restoreSession;
|
||||||
|
|
||||||
RestorePopupWindowListener(RestoreSession restoreSession) {
|
public RestorePopupWindowListener(RestoreSession restoreSession) {
|
||||||
this.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.app.backup.IBackupManager;
|
||||||
import android.content.pm.IPackageManager;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
import com.stevesoltys.backup.session.backup.BackupSession;
|
import com.stevesoltys.backup.session.backup.BackupSession;
|
||||||
import com.stevesoltys.backup.session.backup.BackupSessionObserver;
|
import com.stevesoltys.backup.session.backup.BackupSessionObserver;
|
||||||
import com.stevesoltys.backup.session.restore.RestoreSession;
|
import com.stevesoltys.backup.session.restore.RestoreSession;
|
||||||
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
|
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 java.util.Set;
|
||||||
|
|
||||||
import static android.os.UserHandle.USER_SYSTEM;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
*/
|
*/
|
||||||
public class BackupManagerController {
|
public class TransportService {
|
||||||
|
|
||||||
private static final String BACKUP_TRANSPORT = "com.stevesoltys.backup.transport.ConfigurableBackupTransport";
|
private static final String BACKUP_TRANSPORT = "com.stevesoltys.backup.transport.ConfigurableBackupTransport";
|
||||||
|
|
||||||
private final IBackupManager backupManager;
|
private final IBackupManager backupManager;
|
||||||
|
|
||||||
private final IPackageManager packageManager;
|
public TransportService() {
|
||||||
|
|
||||||
public BackupManagerController() {
|
|
||||||
backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup"));
|
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 {
|
public BackupSession backup(BackupSessionObserver observer, Set<String> packages) throws RemoteException {
|
||||||
|
@ -61,20 +72,4 @@ public class BackupManagerController {
|
||||||
restoreSession.start();
|
restoreSession.start();
|
||||||
return restoreSession;
|
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.Activity;
|
||||||
import android.app.backup.BackupProgress;
|
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.ConfigurableBackupTransport;
|
||||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
*/
|
*/
|
||||||
|
@ -22,9 +24,12 @@ class BackupObserver implements BackupSessionObserver {
|
||||||
|
|
||||||
private final PopupWindow popupWindow;
|
private final PopupWindow popupWindow;
|
||||||
|
|
||||||
BackupObserver(Activity context, PopupWindow popupWindow) {
|
private final URI contentUri;
|
||||||
|
|
||||||
|
BackupObserver(Activity context, PopupWindow popupWindow, URI contentUri) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.popupWindow = popupWindow;
|
this.popupWindow = popupWindow;
|
||||||
|
this.contentUri = contentUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.app.Activity;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.stevesoltys.backup.R;
|
import com.stevesoltys.backup.R;
|
||||||
import com.stevesoltys.backup.session.restore.RestoreResult;
|
import com.stevesoltys.backup.session.restore.RestoreResult;
|
||||||
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
|
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
|
||||||
|
@ -31,6 +30,10 @@ class RestoreObserver implements RestoreSessionObserver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void restoreSessionStarted(int packageCount) {
|
public void restoreSessionStarted(int packageCount) {
|
||||||
|
context.runOnUiThread(() -> {
|
||||||
|
TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
|
||||||
|
textView.setText(R.string.initializing);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -55,7 +58,7 @@ class RestoreObserver implements RestoreSessionObserver {
|
||||||
public void restoreSessionCompleted(RestoreResult restoreResult) {
|
public void restoreSessionCompleted(RestoreResult restoreResult) {
|
||||||
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
|
||||||
|
|
||||||
if(!backupTransport.isActive()) {
|
if (!backupTransport.isActive()) {
|
||||||
return;
|
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.os.ParcelFileDescriptor;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import com.stevesoltys.backup.security.CipherUtil;
|
||||||
|
import com.stevesoltys.backup.security.KeyGenerator;
|
||||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||||
|
import libcore.io.IoUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
import libcore.io.IoUtils;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.*;
|
import java.io.FileOutputStream;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.io.IOException;
|
||||||
import java.security.InvalidKeyException;
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
@ -41,11 +44,6 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long requestBackupTime() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String currentDestinationString() {
|
public String currentDestinationString() {
|
||||||
return DESTINATION_DESCRIPTION;
|
return DESTINATION_DESCRIPTION;
|
||||||
|
@ -67,33 +65,8 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getBackupQuota(String packageName, boolean fullBackup) {
|
public int finishBackup() {
|
||||||
return configuration.getBackupSizeQuota();
|
return clearBackupState(false);
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -113,44 +86,55 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int transferIncrementalBackupData(BackupDataInput backupDataInput)
|
private int clearBackupState(boolean closeFile) {
|
||||||
throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
|
|
||||||
|
|
||||||
ZipOutputStream outputStream = backupState.getOutputStream();
|
if (backupState == null) {
|
||||||
|
return TRANSPORT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int bufferSize = INITIAL_BUFFER_SIZE;
|
try {
|
||||||
byte[] buffer = new byte[bufferSize];
|
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
|
||||||
|
backupState.setInputFileDescriptor(null);
|
||||||
|
|
||||||
while (backupDataInput.readNextHeader()) {
|
ZipOutputStream outputStream = backupState.getOutputStream();
|
||||||
String chunkFileName = Base64.encodeToString(backupDataInput.getKey().getBytes(), Base64.DEFAULT);
|
|
||||||
int dataSize = backupDataInput.getDataSize();
|
|
||||||
|
|
||||||
if (dataSize >= 0) {
|
if (outputStream != null) {
|
||||||
ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() +
|
outputStream.closeEntry();
|
||||||
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 (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;
|
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
|
@Override
|
||||||
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
|
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
|
||||||
|
|
||||||
|
@ -227,40 +211,76 @@ public class ContentProviderBackupComponent implements BackupComponent {
|
||||||
clearBackupState(false);
|
clearBackupState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void initializeBackupState() throws Exception {
|
||||||
public int finishBackup() {
|
|
||||||
return clearBackupState(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int clearBackupState(boolean closeFile) {
|
|
||||||
|
|
||||||
if (backupState == null) {
|
if (backupState == null) {
|
||||||
return TRANSPORT_OK;
|
backupState = new ContentProviderBackupState();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (backupState.getOutputStream() == null) {
|
||||||
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
|
initializeOutputStream();
|
||||||
backupState.setInputFileDescriptor(null);
|
|
||||||
|
|
||||||
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) {
|
private void initializeOutputStream() throws IOException {
|
||||||
if (outputStream != null) {
|
ContentResolver contentResolver = configuration.getContext().getContentResolver();
|
||||||
outputStream.finish();
|
ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w");
|
||||||
outputStream.close();
|
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());
|
backupDataInput.readEntityData(buffer, 0, dataSize);
|
||||||
backupState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException ex) {
|
try {
|
||||||
Log.e(TAG, "Error cancelling full backup: ", ex);
|
if (backupState.getSecretKey() != null) {
|
||||||
return TRANSPORT_ERROR;
|
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;
|
return TRANSPORT_OK;
|
||||||
|
|
|
@ -14,6 +14,8 @@ public class ContentProviderBackupConfiguration {
|
||||||
|
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
private final long backupSizeQuota;
|
private final long backupSizeQuota;
|
||||||
|
|
||||||
private final Set<String> packages;
|
private final Set<String> packages;
|
||||||
|
@ -22,30 +24,24 @@ public class ContentProviderBackupConfiguration {
|
||||||
|
|
||||||
private final String incrementalBackupDirectory;
|
private final String incrementalBackupDirectory;
|
||||||
|
|
||||||
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, long backupSizeQuota,
|
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, String password,
|
||||||
String fullBackupDirectory, String incrementalBackupDirectory) {
|
long backupSizeQuota, String fullBackupDirectory,
|
||||||
|
String incrementalBackupDirectory) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.packages = packages;
|
this.packages = packages;
|
||||||
|
this.password = password;
|
||||||
this.backupSizeQuota = backupSizeQuota;
|
this.backupSizeQuota = backupSizeQuota;
|
||||||
this.fullBackupDirectory = fullBackupDirectory;
|
this.fullBackupDirectory = fullBackupDirectory;
|
||||||
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getUri() {
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getBackupSizeQuota() {
|
public long getBackupSizeQuota() {
|
||||||
return backupSizeQuota;
|
return backupSizeQuota;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPackageCount() {
|
public Context getContext() {
|
||||||
return packages.size();
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFullBackupDirectory() {
|
public String getFullBackupDirectory() {
|
||||||
|
@ -55,4 +51,16 @@ public class ContentProviderBackupConfiguration {
|
||||||
public String getIncrementalBackupDirectory() {
|
public String getIncrementalBackupDirectory() {
|
||||||
return incrementalBackupDirectory;
|
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 Set<String> packages;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
private long backupSizeQuota = Long.MAX_VALUE;
|
private long backupSizeQuota = Long.MAX_VALUE;
|
||||||
|
|
||||||
private String incrementalBackupDirectory = DEFAULT_INCREMENTAL_BACKUP_DIRECTORY;
|
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(incrementalBackupDirectory != null, "Incremental backup directory must be set.");
|
||||||
Preconditions.checkState(fullBackupDirectory != null, "Full 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);
|
fullBackupDirectory, incrementalBackupDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) {
|
||||||
|
this.backupSizeQuota = backupSizeQuota;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ContentProviderBackupConfigurationBuilder setContext(Context context) {
|
public ContentProviderBackupConfigurationBuilder setContext(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
return this;
|
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) {
|
public ContentProviderBackupConfigurationBuilder setOutputUri(Uri outputUri) {
|
||||||
this.outputUri = outputUri;
|
this.outputUri = outputUri;
|
||||||
return this;
|
return this;
|
||||||
|
@ -53,18 +70,8 @@ public class ContentProviderBackupConfigurationBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) {
|
public ContentProviderBackupConfigurationBuilder setPassword(String password) {
|
||||||
this.backupSizeQuota = backupSizeQuota;
|
this.password = password;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentProviderBackupConfigurationBuilder setIncrementalBackupDirectory(String incrementalBackupDirectory) {
|
|
||||||
this.incrementalBackupDirectory = incrementalBackupDirectory;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentProviderBackupConfigurationBuilder setFullBackupDirectory(String fullBackupDirectory) {
|
|
||||||
this.fullBackupDirectory = fullBackupDirectory;
|
|
||||||
return this;
|
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 android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +12,8 @@ import java.util.zip.ZipOutputStream;
|
||||||
*/
|
*/
|
||||||
class ContentProviderBackupState {
|
class ContentProviderBackupState {
|
||||||
|
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
private ParcelFileDescriptor inputFileDescriptor;
|
private ParcelFileDescriptor inputFileDescriptor;
|
||||||
|
|
||||||
private ParcelFileDescriptor outputFileDescriptor;
|
private ParcelFileDescriptor outputFileDescriptor;
|
||||||
|
@ -24,36 +28,13 @@ class ContentProviderBackupState {
|
||||||
|
|
||||||
private int packageIndex;
|
private int packageIndex;
|
||||||
|
|
||||||
ParcelFileDescriptor getInputFileDescriptor() {
|
private byte[] salt;
|
||||||
return inputFileDescriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
|
private SecretKey secretKey;
|
||||||
this.inputFileDescriptor = inputFileDescriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParcelFileDescriptor getOutputFileDescriptor() {
|
public ContentProviderBackupState() {
|
||||||
return outputFileDescriptor;
|
salt = new byte[16];
|
||||||
}
|
SECURE_RANDOM.nextBytes(salt);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
long getBytesTransferred() {
|
long getBytesTransferred() {
|
||||||
|
@ -64,12 +45,36 @@ class ContentProviderBackupState {
|
||||||
this.bytesTransferred = bytesTransferred;
|
this.bytesTransferred = bytesTransferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPackageName() {
|
ParcelFileDescriptor getInputFileDescriptor() {
|
||||||
return packageName;
|
return inputFileDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPackageName(String packageName) {
|
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
|
||||||
this.packageName = packageName;
|
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() {
|
int getPackageIndex() {
|
||||||
|
@ -79,4 +84,24 @@ class ContentProviderBackupState {
|
||||||
void setPackageIndex(int packageIndex) {
|
void setPackageIndex(int packageIndex) {
|
||||||
this.packageIndex = 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.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.android.internal.util.Preconditions;
|
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 com.stevesoltys.backup.transport.component.RestoreComponent;
|
||||||
import libcore.io.IoUtils;
|
import libcore.io.IoUtils;
|
||||||
import libcore.io.Streams;
|
import libcore.io.Streams;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.util.LinkedList;
|
||||||
import java.security.InvalidKeyException;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
@ -48,6 +51,45 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
||||||
restoreState = new ContentProviderRestoreState();
|
restoreState = new ContentProviderRestoreState();
|
||||||
restoreState.setPackages(packages);
|
restoreState.setPackages(packages);
|
||||||
restoreState.setPackageIndex(-1);
|
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;
|
return TRANSPORT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,32 +104,66 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
||||||
restoreState.setPackageIndex(packageIndex);
|
restoreState.setPackageIndex(packageIndex);
|
||||||
String name = packages[packageIndex].packageName;
|
String name = packages[packageIndex].packageName;
|
||||||
|
|
||||||
try {
|
if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) {
|
||||||
if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) {
|
restoreState.setRestoreType(TYPE_KEY_VALUE);
|
||||||
restoreState.setRestoreType(TYPE_KEY_VALUE);
|
return new RestoreDescription(name, restoreState.getRestoreType());
|
||||||
return new RestoreDescription(name, restoreState.getRestoreType());
|
|
||||||
|
|
||||||
} else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) {
|
} else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) {
|
||||||
restoreState.setRestoreType(TYPE_FULL_STREAM);
|
restoreState.setRestoreType(TYPE_FULL_STREAM);
|
||||||
return new RestoreDescription(name, restoreState.getRestoreType());
|
return new RestoreDescription(name, restoreState.getRestoreType());
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
|
|
||||||
Log.e(TAG, "Error choosing package " + name + " at index " + packageIndex + "failed selection:", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RestoreDescription.NO_MORE_PACKAGES;
|
return RestoreDescription.NO_MORE_PACKAGES;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean containsPackageFile(String fileName) throws IOException, InvalidKeyException,
|
private boolean containsPackageFile(String fileName) {
|
||||||
InvalidAlgorithmParameterException {
|
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();
|
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
|
||||||
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
|
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(inputFileDescriptor);
|
||||||
IoUtils.closeQuietly(inputStream);
|
IoUtils.closeQuietly(outputFileDescriptor);
|
||||||
return zipEntry.isPresent();
|
return TRANSPORT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
|
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
|
||||||
|
@ -113,48 +189,16 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
|
||||||
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
|
byte[] backupData = Streams.readFullyNoClose(inputStream);
|
||||||
Preconditions.checkState(restoreState != null, "startRestore() not called");
|
SecretKey secretKey = restoreState.getSecretKey();
|
||||||
Preconditions.checkState(restoreState.getPackageIndex() >= 0, "nextRestorePackage() not called");
|
byte[] initializationVector = restoreState.getSalt();
|
||||||
Preconditions.checkState(restoreState.getRestoreType() == TYPE_KEY_VALUE,
|
|
||||||
"getRestoreData() for non-key/value dataset");
|
|
||||||
|
|
||||||
PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
|
if (secretKey != null) {
|
||||||
|
backupData = CipherUtil.decrypt(backupData, secretKey, initializationVector);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IoUtils.closeQuietly(inputFileDescriptor);
|
return backupData;
|
||||||
IoUtils.closeQuietly(outputFileDescriptor);
|
|
||||||
return TRANSPORT_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -3,6 +3,9 @@ package com.stevesoltys.backup.transport.component.provider;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,28 +23,18 @@ class ContentProviderRestoreState {
|
||||||
|
|
||||||
private ZipInputStream inputStream;
|
private ZipInputStream inputStream;
|
||||||
|
|
||||||
PackageInfo[] getPackages() {
|
private byte[] salt;
|
||||||
return packages;
|
|
||||||
|
private SecretKey secretKey;
|
||||||
|
|
||||||
|
private List<ZipEntry> zipEntries;
|
||||||
|
|
||||||
|
ParcelFileDescriptor getInputFileDescriptor() {
|
||||||
|
return inputFileDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPackages(PackageInfo[] packages) {
|
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
|
||||||
this.packages = packages;
|
this.inputFileDescriptor = inputFileDescriptor;
|
||||||
}
|
|
||||||
|
|
||||||
int getPackageIndex() {
|
|
||||||
return packageIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPackageIndex(int packageIndex) {
|
|
||||||
this.packageIndex = packageIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getRestoreType() {
|
|
||||||
return restoreType;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setRestoreType(int restoreType) {
|
|
||||||
this.restoreType = restoreType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ZipInputStream getInputStream() {
|
ZipInputStream getInputStream() {
|
||||||
|
@ -52,11 +45,51 @@ class ContentProviderRestoreState {
|
||||||
this.inputStream = inputStream;
|
this.inputStream = inputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParcelFileDescriptor getInputFileDescriptor() {
|
int getPackageIndex() {
|
||||||
return inputFileDescriptor;
|
return packageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
|
void setPackageIndex(int packageIndex) {
|
||||||
this.inputFileDescriptor = inputFileDescriptor;
|
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="popup_cancel">Cancel</string>
|
||||||
|
|
||||||
<string name="select_all">Select all</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>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue