Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..67eb53c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+## Java
+*.class
+*.war
+*.ear
+hs_err_pid*
+
+## Intellij
+out/
+lib/
+.idea/
+*.ipr
+*.iws
+*.iml
+
+## Eclipse
+.classpath
+.project
+.metadata
+**/bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.externalToolBuilders/
+*.launch
+
+## NetBeans
+**/nbproject/private/
+build/
+nbbuild/
+dist/
+nbdist/
+nbactions.xml
+nb-configuration.xml
+
+## Gradle
+.gradle
+gradle-app.setting
+build/
+
+## OS Specific
+.DS_Store
+
+## Android
+gen/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5964fbe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Steve Soltys
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3d864f9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Backup
+A backup application for the [Android Open Source Project](https://source.android.com/).
+
+## License
+This application is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..092aedb
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,21 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 26
+ buildToolsVersion '26.0.1'
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ targetCompatibility 1.7
+ sourceCompatibility 1.7
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+}
diff --git a/app/src/main/Android.mk b/app/src/main/Android.mk
new file mode 100644
index 0000000..19c79c4
--- /dev/null
+++ b/app/src/main/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := com.stevesoltys.backup.xml
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/sysconfig
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+LOCAL_PACKAGE_NAME := Backup
+LOCAL_MODULE_TAGS := optional
+LOCAL_REQUIRED_MODULES := com.stevesoltys.backup.xml
+LOCAL_PRIVILEGED_MODULE := true
+LOCAL_SRC_FILES := $(call all-java-files-under, java)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+include $(BUILD_PACKAGE)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f8b2fc1
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.stevesoltys.backup">
+
+ <uses-permission android:name="android.permission.BACKUP"/>
+
+ <uses-sdk
+ android:minSdkVersion="26"
+ android:targetSdkVersion="26"/>
+
+ <application
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:allowBackup="false"
+ tools:replace="android:allowBackup">
+
+ <activity android:name="com.stevesoltys.backup.activity.MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
+ android:parentActivityName="com.stevesoltys.backup.activity.MainActivity">
+ </activity>
+
+ <activity android:name="com.stevesoltys.backup.activity.restore.RestoreBackupActivity"
+ android:parentActivityName="com.stevesoltys.backup.activity.MainActivity">
+ </activity>
+
+ <service android:name="com.stevesoltys.backup.transport.ConfigurableBackupTransportService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.backup.TRANSPORT_HOST" />
+ </intent-filter>
+ </service>
+
+ </application>
+</manifest>
diff --git a/app/src/main/com.stevesoltys.backup.xml b/app/src/main/com.stevesoltys.backup.xml
new file mode 100644
index 0000000..bc98e4c
--- /dev/null
+++ b/app/src/main/com.stevesoltys.backup.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<config>
+ <backup-transport-whitelisted-service
+ service="com.stevesoltys.backup/.transport.ConfigurableBackupTransportService"/>
+</config>
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java
new file mode 100644
index 0000000..7ecffa0
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java
@@ -0,0 +1,65 @@
+package com.stevesoltys.backup.activity;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+
+import com.stevesoltys.backup.R;
+
+public class MainActivity extends Activity implements View.OnClickListener {
+
+ public static final int CREATE_DOCUMENT_REQUEST_CODE = 1;
+
+ public static final int LOAD_DOCUMENT_REQUEST_CODE = 2;
+
+ private MainActivityController controller;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ findViewById(R.id.create_backup_button).setOnClickListener(this);
+ findViewById(R.id.restore_backup_button).setOnClickListener(this);
+
+ controller = new MainActivityController();
+ }
+
+ @Override
+ public void onClick(View view) {
+ int viewId = view.getId();
+
+ switch (viewId) {
+
+ case R.id.create_backup_button:
+ controller.showCreateDocumentActivity(this);
+ break;
+
+ case R.id.restore_backup_button:
+ controller.showLoadDocumentActivity(this);
+ break;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent result) {
+
+ if (resultCode != Activity.RESULT_OK) {
+ Log.e(MainActivity.class.getName(), "Error in activity result: " + requestCode);
+ return;
+ }
+
+ switch (requestCode) {
+
+ case CREATE_DOCUMENT_REQUEST_CODE:
+ controller.handleCreateDocumentResult(result, this);
+ break;
+
+ case LOAD_DOCUMENT_REQUEST_CODE:
+ controller.handleLoadDocumentResult(result, this);
+ break;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java
new file mode 100644
index 0000000..5d4806f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java
@@ -0,0 +1,71 @@
+package com.stevesoltys.backup.activity;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.widget.Toast;
+
+import com.stevesoltys.backup.activity.backup.CreateBackupActivity;
+import com.stevesoltys.backup.activity.restore.RestoreBackupActivity;
+
+import static android.content.Intent.ACTION_CREATE_DOCUMENT;
+import static android.content.Intent.ACTION_OPEN_DOCUMENT;
+import static android.content.Intent.CATEGORY_OPENABLE;
+
+/**
+ * @author Steve Soltys
+ */
+class MainActivityController {
+
+ private static final String DOCUMENT_MIME_TYPE = "application/octet-stream";
+
+ void showCreateDocumentActivity(Activity parent) {
+ Intent createDocumentIntent = new Intent(ACTION_CREATE_DOCUMENT);
+ createDocumentIntent.addCategory(CATEGORY_OPENABLE);
+ createDocumentIntent.setType(DOCUMENT_MIME_TYPE);
+
+ try {
+ Intent documentChooser = Intent.createChooser(createDocumentIntent, "Select the backup location");
+ parent.startActivityForResult(documentChooser, MainActivity.CREATE_DOCUMENT_REQUEST_CODE);
+
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(parent, "Please install a file manager.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ void showLoadDocumentActivity(Activity parent) {
+ Intent loadDocumentIntent = new Intent(ACTION_OPEN_DOCUMENT);
+ loadDocumentIntent.addCategory(CATEGORY_OPENABLE);
+ loadDocumentIntent.setType(DOCUMENT_MIME_TYPE);
+
+ try {
+ Intent documentChooser = Intent.createChooser(loadDocumentIntent, "Select the backup location");
+ parent.startActivityForResult(documentChooser, MainActivity.LOAD_DOCUMENT_REQUEST_CODE);
+
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(parent, "Please install a file manager.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ void handleCreateDocumentResult(Intent result, Activity parent) {
+
+ if (result == null) {
+ return;
+ }
+
+ Intent intent = new Intent(parent, CreateBackupActivity.class);
+ intent.setData(result.getData());
+ parent.startActivity(intent);
+ }
+
+ void handleLoadDocumentResult(Intent result, Activity parent) {
+
+ if (result == null) {
+ return;
+ }
+
+ Intent intent = new Intent(parent, RestoreBackupActivity.class);
+ intent.setData(result.getData());
+ parent.startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java
new file mode 100644
index 0000000..1fc5ee9
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupObserver.java
@@ -0,0 +1,88 @@
+package com.stevesoltys.backup.activity.backup;
+
+import android.app.Activity;
+import android.app.backup.BackupProgress;
+import android.widget.PopupWindow;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.stevesoltys.backup.R;
+import com.stevesoltys.backup.session.backup.BackupResult;
+import com.stevesoltys.backup.session.backup.BackupSession;
+import com.stevesoltys.backup.session.backup.BackupSessionObserver;
+import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
+import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
+
+/**
+ * @author Steve Soltys
+ */
+class BackupObserver implements BackupSessionObserver {
+
+ private final Activity context;
+
+ private final PopupWindow popupWindow;
+
+ BackupObserver(Activity context, PopupWindow popupWindow) {
+ this.context = context;
+ this.popupWindow = popupWindow;
+ }
+
+ @Override
+ public void backupPackageStarted(BackupSession backupSession, String packageName, BackupProgress backupProgress) {
+ context.runOnUiThread(() -> {
+
+ TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
+
+ if (textView != null) {
+ textView.setText(packageName);
+ }
+
+ ProgressBar progressBar = popupWindow.getContentView().findViewById(R.id.popup_progress_bar);
+
+ if (progressBar != null) {
+ progressBar.setMax((int) backupProgress.bytesExpected);
+ progressBar.setProgress((int) backupProgress.bytesTransferred);
+ }
+ });
+ }
+
+ @Override
+ public void backupPackageCompleted(BackupSession backupSession, String packageName, BackupResult result) {
+ context.runOnUiThread(() -> {
+
+ TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
+
+ if (textView != null) {
+ textView.setText(packageName);
+ }
+ });
+ }
+
+ @Override
+ public void backupSessionCompleted(BackupSession backupSession, BackupResult backupResult) {
+ ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
+
+ if (backupTransport.getRestoreComponent() == null || backupTransport.getBackupComponent() == null) {
+ return;
+ }
+
+ backupTransport.setBackupComponent(null);
+ backupTransport.setRestoreComponent(null);
+
+ context.runOnUiThread(() -> {
+ if (backupResult == BackupResult.SUCCESS) {
+ Toast.makeText(context, R.string.backup_success, Toast.LENGTH_LONG).show();
+
+ } else if (backupResult == BackupResult.CANCELLED) {
+ Toast.makeText(context, R.string.backup_cancelled, Toast.LENGTH_LONG).show();
+
+ } else {
+ Toast.makeText(context, R.string.backup_failure, Toast.LENGTH_LONG).show();
+ }
+
+ popupWindow.dismiss();
+ context.finish();
+ });
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java
new file mode 100644
index 0000000..aa702df
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/BackupPopupWindowListener.java
@@ -0,0 +1,41 @@
+package com.stevesoltys.backup.activity.backup;
+
+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;
+
+/**
+ * @author Steve Soltys
+ */
+class BackupPopupWindowListener implements Button.OnClickListener {
+
+ private static final String TAG = BackupPopupWindowListener.class.getName();
+
+ private final BackupSession backupSession;
+
+ public BackupPopupWindowListener(BackupSession backupSession) {
+ this.backupSession = backupSession;
+ }
+
+ @Override
+ public void onClick(View view) {
+ int viewId = view.getId();
+
+ switch (viewId) {
+
+ case R.id.popup_cancel_button:
+ try {
+ backupSession.stop(BackupResult.CANCELLED);
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error cancelling backup session: ", e);
+ }
+ break;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java
new file mode 100644
index 0000000..46503f1
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivity.java
@@ -0,0 +1,64 @@
+package com.stevesoltys.backup.activity.backup;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.stevesoltys.backup.R;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class CreateBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
+
+ private CreateBackupActivityController controller;
+
+ private ListView packageListView;
+
+ private List<String> selectedPackageList;
+
+ private Uri contentUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_create_backup);
+ findViewById(R.id.create_confirm_button).setOnClickListener(this);
+
+ packageListView = findViewById(R.id.create_package_list);
+ selectedPackageList = new LinkedList<>();
+ contentUri = getIntent().getData();
+
+ controller = new CreateBackupActivityController();
+ controller.populatePackageList(packageListView, this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ int viewId = view.getId();
+
+ switch (viewId) {
+
+ case R.id.create_confirm_button:
+ controller.backupPackages(selectedPackageList, contentUri, this);
+ break;
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String clickedPackage = (String) packageListView.getItemAtPosition(position);
+
+ if (!selectedPackageList.remove(clickedPackage)) {
+ selectedPackageList.add(clickedPackage);
+ packageListView.setItemChecked(position, true);
+
+ } else {
+ packageListView.setItemChecked(position, false);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java
new file mode 100644
index 0000000..ce4d80f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java
@@ -0,0 +1,112 @@
+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.net.Uri;
+import android.os.RemoteException;
+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 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.provider.ContentProviderBackupConfiguration;
+import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
+import com.stevesoltys.backup.transport.component.provider.backup.ContentProviderBackupComponent;
+import com.stevesoltys.backup.transport.component.provider.restore.ContentProviderRestoreComponent;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author Steve Soltys
+ */
+class CreateBackupActivityController {
+
+ private static final String TAG = CreateBackupActivityController.class.getName();
+
+ private final BackupManagerController backupManager;
+
+ CreateBackupActivityController() {
+ backupManager = new BackupManagerController();
+ }
+
+ void populatePackageList(ListView packageListView, CreateBackupActivity parent) {
+ List<String> eligiblePackageList = new LinkedList<>();
+ try {
+ eligiblePackageList.addAll(backupManager.getEligiblePackages());
+
+ } 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(List<String> selectedPackages, Uri contentUri, Activity parent) {
+ try {
+ String[] selectedPackageArray = selectedPackages.toArray(new String[selectedPackages.size() + 1]);
+ selectedPackageArray[selectedPackageArray.length - 1] = "@pm@";
+
+ ContentProviderBackupConfiguration backupConfiguration = ContentProviderBackupConfigurationBuilder.
+ buildDefaultConfiguration(parent, contentUri, selectedPackageArray.length);
+ boolean success = initializeBackupTransport(backupConfiguration);
+
+ if(!success) {
+ Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ PopupWindow popupWindow = buildPopupWindow(parent);
+ BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
+ BackupSession backupSession = backupManager.backup(backupObserver, selectedPackageArray);
+
+ 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) {
+ ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
+
+ if(backupTransport.getBackupComponent() != null || backupTransport.getRestoreComponent() != null) {
+ return false;
+ }
+
+ backupTransport.setBackupComponent(new ContentProviderBackupComponent(configuration));
+ backupTransport.setRestoreComponent(new ContentProviderRestoreComponent(configuration));
+ return true;
+ }
+
+ private PopupWindow buildPopupWindow(Activity parent) {
+ LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
+ View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
+
+ PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
+ popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
+ popupWindow.setElevation(10);
+ popupWindow.setFocusable(false);
+ popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
+ return popupWindow;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java
new file mode 100644
index 0000000..bbeffb3
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivity.java
@@ -0,0 +1,64 @@
+package com.stevesoltys.backup.activity.restore;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.stevesoltys.backup.R;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class RestoreBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
+
+ private RestoreBackupActivityController controller;
+
+ private ListView packageListView;
+
+ private List<String> selectedPackageList;
+
+ private Uri contentUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_restore_backup);
+ findViewById(R.id.restore_confirm_button).setOnClickListener(this);
+
+ packageListView = findViewById(R.id.restore_package_list);
+ selectedPackageList = new LinkedList<>();
+ contentUri = getIntent().getData();
+
+ controller = new RestoreBackupActivityController();
+ controller.populatePackageList(packageListView, contentUri, this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ int viewId = view.getId();
+
+ switch (viewId) {
+
+ case R.id.restore_confirm_button:
+ controller.restorePackages(selectedPackageList, contentUri, this);
+ break;
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String clickedPackage = (String) packageListView.getItemAtPosition(position);
+
+ if (!selectedPackageList.remove(clickedPackage)) {
+ selectedPackageList.add(clickedPackage);
+ packageListView.setItemChecked(position, true);
+
+ } else {
+ packageListView.setItemChecked(position, false);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java
new file mode 100644
index 0000000..b823914
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreBackupActivityController.java
@@ -0,0 +1,144 @@
+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.net.Uri;
+import android.os.ParcelFileDescriptor;
+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 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.provider.ContentProviderBackupConfiguration;
+import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
+import com.stevesoltys.backup.transport.component.provider.backup.ContentProviderBackupComponent;
+import com.stevesoltys.backup.transport.component.provider.restore.ContentProviderRestoreComponent;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import libcore.io.IoUtils;
+
+import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration.FULL_BACKUP_DIRECTORY;
+
+/**
+ * @author Steve Soltys
+ */
+class RestoreBackupActivityController {
+
+ private static final String TAG = RestoreBackupActivityController.class.getName();
+
+ private final BackupManagerController backupManager;
+
+ RestoreBackupActivityController() {
+ backupManager = new BackupManagerController();
+ }
+
+ void populatePackageList(ListView packageListView, Uri contentUri, RestoreBackupActivity parent) {
+ List<String> eligiblePackageList = new LinkedList<>();
+ try {
+ eligiblePackageList.addAll(getEligiblePackages(contentUri, parent));
+
+ } catch (IOException 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);
+ }
+
+ private List<String> getEligiblePackages(Uri contentUri, Activity context) throws IOException {
+ List<String> results = new LinkedList<>();
+
+ ParcelFileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(contentUri, "r");
+ FileInputStream fileInputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
+ ZipInputStream inputStream = new ZipInputStream(fileInputStream);
+
+ ZipEntry zipEntry;
+ while ((zipEntry = inputStream.getNextEntry()) != null) {
+ String zipEntryPath = zipEntry.getName();
+
+ if (zipEntryPath.startsWith(FULL_BACKUP_DIRECTORY)) {
+ String fileName = new File(zipEntryPath).getName();
+ results.add(fileName);
+ }
+
+ inputStream.closeEntry();
+ }
+
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(fileDescriptor.getFileDescriptor());
+ return results;
+ }
+
+ void restorePackages(List<String> selectedPackages, Uri contentUri, Activity parent) {
+ try {
+ String[] selectedPackageArray = selectedPackages.toArray(new String[selectedPackages.size()]);
+
+ ContentProviderBackupConfiguration backupConfiguration = ContentProviderBackupConfigurationBuilder.
+ buildDefaultConfiguration(parent, contentUri, selectedPackageArray.length);
+ boolean success = initializeBackupTransport(backupConfiguration);
+
+ if(!success) {
+ Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ PopupWindow popupWindow = buildPopupWindow(parent);
+ RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackageArray.length);
+ RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackageArray);
+
+ View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
+
+ if (popupWindowButton != null) {
+ popupWindowButton.setOnClickListener(new RestorePopupWindowListener(restoreSession));
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error while running restore: ", e);
+ }
+ }
+
+ private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
+ ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
+
+ if(backupTransport.getBackupComponent() != null || backupTransport.getRestoreComponent() != null) {
+ return false;
+ }
+
+ backupTransport.setBackupComponent(new ContentProviderBackupComponent(configuration));
+ backupTransport.setRestoreComponent(new ContentProviderRestoreComponent(configuration));
+ return true;
+ }
+
+ private PopupWindow buildPopupWindow(Activity parent) {
+ LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
+ View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
+
+ PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
+ popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
+ popupWindow.setElevation(10);
+ popupWindow.setFocusable(false);
+ popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
+ return popupWindow;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java
new file mode 100644
index 0000000..df536b4
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestoreObserver.java
@@ -0,0 +1,80 @@
+package com.stevesoltys.backup.activity.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;
+import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
+import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
+
+/**
+ * @author Steve Soltys
+ */
+class RestoreObserver implements RestoreSessionObserver {
+
+ private final Activity context;
+
+ private final PopupWindow popupWindow;
+
+ private final int packageCount;
+
+ RestoreObserver(Activity context, PopupWindow popupWindow, int packageCount) {
+ this.context = context;
+ this.popupWindow = popupWindow;
+ this.packageCount = packageCount;
+ }
+
+ @Override
+ public void restoreSessionStarted(int packageCount) {
+ }
+
+ @Override
+ public void restorePackageStarted(int packageIndex, String packageName) {
+ context.runOnUiThread(() -> {
+ ProgressBar progressBar = popupWindow.getContentView().findViewById(R.id.popup_progress_bar);
+
+ if (progressBar != null) {
+ progressBar.setMax(packageCount);
+ progressBar.setProgress(packageIndex);
+ }
+
+ TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
+
+ if (textView != null) {
+ textView.setText(packageName);
+ }
+ });
+ }
+
+ @Override
+ public void restoreSessionCompleted(RestoreResult restoreResult) {
+ ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
+
+ if(backupTransport.getRestoreComponent() == null || backupTransport.getBackupComponent() == null) {
+ return;
+ }
+
+ backupTransport.setBackupComponent(null);
+ backupTransport.setRestoreComponent(null);
+
+ context.runOnUiThread(() -> {
+ if (restoreResult == RestoreResult.SUCCESS) {
+ Toast.makeText(context, R.string.restore_success, Toast.LENGTH_LONG).show();
+
+ } else if (restoreResult == RestoreResult.CANCELLED) {
+ Toast.makeText(context, R.string.restore_cancelled, Toast.LENGTH_LONG).show();
+
+ } else {
+ Toast.makeText(context, R.string.restore_failure, Toast.LENGTH_LONG).show();
+ }
+
+ popupWindow.dismiss();
+ context.finish();
+ });
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java
new file mode 100644
index 0000000..69397df
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/activity/restore/RestorePopupWindowListener.java
@@ -0,0 +1,41 @@
+package com.stevesoltys.backup.activity.restore;
+
+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.restore.RestoreResult;
+import com.stevesoltys.backup.session.restore.RestoreSession;
+
+/**
+ * @author Steve Soltys
+ */
+class RestorePopupWindowListener implements Button.OnClickListener {
+
+ private static final String TAG = RestorePopupWindowListener.class.getName();
+
+ private final RestoreSession restoreSession;
+
+ RestorePopupWindowListener(RestoreSession restoreSession) {
+ this.restoreSession = restoreSession;
+ }
+
+ @Override
+ public void onClick(View view) {
+ int viewId = view.getId();
+
+ switch (viewId) {
+
+ case R.id.popup_cancel_button:
+ try {
+ restoreSession.stop(RestoreResult.CANCELLED);
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error cancelling restore session: ", e);
+ }
+ break;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java b/app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java
new file mode 100644
index 0000000..9f85826
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/BackupManagerController.java
@@ -0,0 +1,72 @@
+package com.stevesoltys.backup.session;
+
+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 java.util.ArrayList;
+import java.util.List;
+
+import static android.os.UserHandle.USER_SYSTEM;
+
+/**
+ * @author Steve Soltys
+ */
+public class BackupManagerController {
+
+ private static final String BACKUP_TRANSPORT = "com.stevesoltys.backup.transport.ConfigurableBackupTransport";
+
+ private final IBackupManager backupManager;
+
+ private final IPackageManager packageManager;
+
+ public BackupManagerController() {
+ backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup"));
+ packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+ }
+
+ public BackupSession backup(BackupSessionObserver observer, String... packages) throws RemoteException {
+
+ if (!BACKUP_TRANSPORT.equals(backupManager.getCurrentTransport())) {
+ backupManager.selectBackupTransport(BACKUP_TRANSPORT);
+ }
+
+ BackupSession backupSession = new BackupSession(backupManager, observer, packages);
+ backupSession.start();
+ return backupSession;
+ }
+
+ public RestoreSession restore(RestoreSessionObserver observer, String... packages) throws RemoteException {
+
+ if (!BACKUP_TRANSPORT.equals(backupManager.getCurrentTransport())) {
+ backupManager.selectBackupTransport(BACKUP_TRANSPORT);
+ }
+
+ RestoreSession restoreSession = new RestoreSession(backupManager, observer, packages);
+ 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;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupResult.java b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupResult.java
new file mode 100644
index 0000000..8e43177
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupResult.java
@@ -0,0 +1,8 @@
+package com.stevesoltys.backup.session.backup;
+
+/**
+ * @author Steve Soltys
+ */
+public enum BackupResult {
+ SUCCESS, FAILURE, CANCELLED
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSession.java b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSession.java
new file mode 100644
index 0000000..8bc95ca
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSession.java
@@ -0,0 +1,63 @@
+package com.stevesoltys.backup.session.backup;
+
+import android.app.backup.BackupManager;
+import android.app.backup.BackupProgress;
+import android.app.backup.IBackupManager;
+import android.app.backup.IBackupObserver;
+import android.os.RemoteException;
+
+import static android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP;
+
+/**
+ * @author Steve Soltys
+ */
+public class BackupSession extends IBackupObserver.Stub {
+
+ private final IBackupManager backupManager;
+
+ private final BackupSessionObserver backupSessionObserver;
+
+ private final String[] packages;
+
+ public BackupSession(IBackupManager backupManager, BackupSessionObserver backupSessionObserver, String... packages) {
+ this.backupManager = backupManager;
+ this.backupSessionObserver = backupSessionObserver;
+ this.packages = packages;
+ }
+
+ public void start() throws RemoteException {
+ backupManager.requestBackup(packages, this, null, FLAG_NON_INCREMENTAL_BACKUP);
+ }
+
+ public void stop(BackupResult result) throws RemoteException {
+ backupManager.cancelBackups();
+ backupSessionObserver.backupSessionCompleted(this, result);
+ }
+
+ @Override
+ public void onUpdate(String currentPackage, BackupProgress backupProgress) throws RemoteException {
+ backupSessionObserver.backupPackageStarted(this, currentPackage, backupProgress);
+ }
+
+ @Override
+ public void onResult(String currentPackage, int status) throws RemoteException {
+ backupSessionObserver.backupPackageCompleted(this, currentPackage, getBackupResult(status));
+ }
+
+ @Override
+ public void backupFinished(int status) throws RemoteException {
+ backupSessionObserver.backupSessionCompleted(this, getBackupResult(status));
+ }
+
+ private BackupResult getBackupResult(int status) {
+ if (status == BackupManager.SUCCESS) {
+ return BackupResult.SUCCESS;
+
+ } else if (status == BackupManager.ERROR_BACKUP_CANCELLED) {
+ return BackupResult.CANCELLED;
+
+ } else {
+ return BackupResult.FAILURE;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSessionObserver.java b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSessionObserver.java
new file mode 100644
index 0000000..f896736
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupSessionObserver.java
@@ -0,0 +1,15 @@
+package com.stevesoltys.backup.session.backup;
+
+import android.app.backup.BackupProgress;
+
+/**
+ * @author Steve Soltys
+ */
+public interface BackupSessionObserver {
+
+ void backupPackageStarted(BackupSession backupSession, String packageName, BackupProgress backupProgress);
+
+ void backupPackageCompleted(BackupSession backupSession, String packageName, BackupResult result);
+
+ void backupSessionCompleted(BackupSession backupSession, BackupResult result);
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreResult.java b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreResult.java
new file mode 100644
index 0000000..65ddf1f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreResult.java
@@ -0,0 +1,8 @@
+package com.stevesoltys.backup.session.restore;
+
+/**
+ * @author Steve Soltys
+ */
+public enum RestoreResult {
+ SUCCESS, CANCELLED, FAILURE
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSession.java b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSession.java
new file mode 100644
index 0000000..c595847
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSession.java
@@ -0,0 +1,95 @@
+package com.stevesoltys.backup.session.restore;
+
+import android.app.backup.BackupManager;
+import android.app.backup.IBackupManager;
+import android.app.backup.IRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.app.backup.RestoreSet;
+import android.os.RemoteException;
+
+/**
+ * @author Steve Soltys
+ */
+public class RestoreSession extends IRestoreObserver.Stub {
+
+ private final IBackupManager backupManager;
+
+ private final RestoreSessionObserver observer;
+
+ private final String[] packages;
+
+ private IRestoreSession restoreSession;
+
+ public RestoreSession(IBackupManager backupManager, RestoreSessionObserver observer, String... packages) {
+ this.backupManager = backupManager;
+ this.observer = observer;
+ this.packages = packages;
+ }
+
+ public void start() throws RemoteException {
+
+ if (restoreSession != null || packages.length == 0) {
+ observer.restoreSessionCompleted(RestoreResult.FAILURE);
+ return;
+ }
+
+ restoreSession = backupManager.beginRestoreSession(null, null);
+
+ if (restoreSession == null) {
+ stop(RestoreResult.FAILURE);
+ return;
+ }
+
+ int result = restoreSession.getAvailableRestoreSets(this, null);
+
+ if (result != BackupManager.SUCCESS) {
+ stop(RestoreResult.FAILURE);
+ }
+ }
+
+ public void stop(RestoreResult restoreResult) throws RemoteException {
+ clearSession();
+ observer.restoreSessionCompleted(restoreResult);
+ }
+
+ private void clearSession() throws RemoteException {
+ if (restoreSession != null) {
+ restoreSession.endRestoreSession();
+ restoreSession = null;
+ }
+ }
+
+ @Override
+ public void restoreSetsAvailable(RestoreSet[] restoreSets) throws RemoteException {
+ if (restoreSets.length > 0) {
+ RestoreSet restoreSet = restoreSets[0];
+ int result = restoreSession.restoreSome(restoreSet.token, this, null, packages);
+
+ if (result != BackupManager.SUCCESS) {
+ stop(RestoreResult.FAILURE);
+ }
+ }
+ }
+
+ @Override
+ public void restoreStarting(int numPackages) throws RemoteException {
+ observer.restoreSessionStarted(numPackages);
+ }
+
+ @Override
+ public void onUpdate(int nowBeingRestored, String currentPackage) throws RemoteException {
+ observer.restorePackageStarted(nowBeingRestored, currentPackage);
+ }
+
+ @Override
+ public void restoreFinished(int result) throws RemoteException {
+ if (result == BackupManager.SUCCESS) {
+ stop(RestoreResult.SUCCESS);
+
+ } else if (result == BackupManager.ERROR_BACKUP_CANCELLED) {
+ stop(RestoreResult.CANCELLED);
+ } else {
+ stop(RestoreResult.FAILURE);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSessionObserver.java b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSessionObserver.java
new file mode 100644
index 0000000..a6a6b95
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/session/restore/RestoreSessionObserver.java
@@ -0,0 +1,13 @@
+package com.stevesoltys.backup.session.restore;
+
+/**
+ * @author Steve Soltys
+ */
+public interface RestoreSessionObserver {
+
+ void restoreSessionStarted(int packageCount);
+
+ void restorePackageStarted(int packageIndex, String packageName);
+
+ void restoreSessionCompleted(RestoreResult restoreResult);
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java
new file mode 100644
index 0000000..7051774
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java
@@ -0,0 +1,160 @@
+package com.stevesoltys.backup.transport;
+
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+
+import com.stevesoltys.backup.transport.component.BackupComponent;
+import com.stevesoltys.backup.transport.component.RestoreComponent;
+
+/**
+ * @author Steve Soltys
+ */
+public class ConfigurableBackupTransport extends BackupTransport {
+
+ private static final String TRANSPORT_DIRECTORY_NAME =
+ "com.stevesoltys.backup.transport.ConfigurableBackupTransport";
+
+ private BackupComponent backupComponent;
+
+ private RestoreComponent restoreComponent;
+
+ public ConfigurableBackupTransport() {
+ backupComponent = null;
+ restoreComponent = null;
+ }
+
+ @Override
+ public String transportDirName() {
+ return TRANSPORT_DIRECTORY_NAME;
+ }
+
+ @Override
+ public String name() {
+ // TODO: Make this class non-static in ConfigurableBackupTransportService and use Context and a ComponentName.
+ return this.getClass().getName();
+ }
+
+ @Override
+ public long requestBackupTime() {
+ return backupComponent.requestBackupTime();
+ }
+
+ @Override
+ public String dataManagementLabel() {
+ return backupComponent.dataManagementLabel();
+ }
+
+ @Override
+ public int initializeDevice() {
+ return backupComponent.initializeDevice();
+ }
+
+ @Override
+ public String currentDestinationString() {
+ return backupComponent.currentDestinationString();
+ }
+
+ @Override
+ public int performBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
+ return backupComponent.performIncrementalBackup(targetPackage, fileDescriptor);
+ }
+
+ @Override
+ public int checkFullBackupSize(long size) {
+ return backupComponent.checkFullBackupSize(size);
+ }
+
+ @Override
+ public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
+ return backupComponent.performFullBackup(targetPackage, fileDescriptor);
+ }
+
+ @Override
+ public int sendBackupData(int numBytes) {
+ return backupComponent.sendBackupData(numBytes);
+ }
+
+ @Override
+ public void cancelFullBackup() {
+ backupComponent.cancelFullBackup();
+ }
+
+ @Override
+ public int finishBackup() {
+ return backupComponent.finishBackup();
+ }
+
+ @Override
+ public long requestFullBackupTime() {
+ return backupComponent.requestFullBackupTime();
+ }
+
+ @Override
+ public long getBackupQuota(String packageName, boolean isFullBackup) {
+ return backupComponent.getBackupQuota(packageName, isFullBackup);
+ }
+
+ @Override
+ public int clearBackupData(PackageInfo packageInfo) {
+ return backupComponent.clearBackupData(packageInfo);
+ }
+
+ @Override
+ public long getCurrentRestoreSet() {
+ return restoreComponent.getCurrentRestoreSet();
+ }
+
+ @Override
+ public int startRestore(long token, PackageInfo[] packages) {
+ return restoreComponent.startRestore(token, packages);
+ }
+
+ @Override
+ public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
+ return restoreComponent.getNextFullRestoreDataChunk(socket);
+ }
+
+ @Override
+ public RestoreSet[] getAvailableRestoreSets() {
+ return restoreComponent.getAvailableRestoreSets();
+ }
+
+ @Override
+ public RestoreDescription nextRestorePackage() {
+ return restoreComponent.nextRestorePackage();
+ }
+
+ @Override
+ public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
+ return restoreComponent.getRestoreData(outputFileDescriptor);
+ }
+
+ @Override
+ public int abortFullRestore() {
+ return restoreComponent.abortFullRestore();
+ }
+
+ @Override
+ public void finishRestore() {
+ restoreComponent.finishRestore();
+ }
+
+ public BackupComponent getBackupComponent() {
+ return backupComponent;
+ }
+
+ public void setBackupComponent(BackupComponent backupComponent) {
+ this.backupComponent = backupComponent;
+ }
+
+ public RestoreComponent getRestoreComponent() {
+ return restoreComponent;
+ }
+
+ public void setRestoreComponent(RestoreComponent restoreComponent) {
+ this.restoreComponent = restoreComponent;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransportService.java b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransportService.java
new file mode 100644
index 0000000..dfa9e7f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransportService.java
@@ -0,0 +1,34 @@
+package com.stevesoltys.backup.transport;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * @author Steve Soltys
+ */
+public class ConfigurableBackupTransportService extends Service {
+
+ // TODO: Make this field non-static and communicate with this service correctly.
+ private static ConfigurableBackupTransport backupTransport;
+
+ public ConfigurableBackupTransportService() {
+ backupTransport = null;
+ }
+
+ @Override
+ public void onCreate() {
+ if (backupTransport == null) {
+ backupTransport = new ConfigurableBackupTransport();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return backupTransport.getBinder();
+ }
+
+ public static ConfigurableBackupTransport getBackupTransport() {
+ return backupTransport;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/BackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/BackupComponent.java
new file mode 100644
index 0000000..1fafbe6
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/BackupComponent.java
@@ -0,0 +1,36 @@
+package com.stevesoltys.backup.transport.component;
+
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * @author Steve Soltys
+ */
+public interface BackupComponent {
+
+ String currentDestinationString();
+
+ String dataManagementLabel();
+
+ int initializeDevice();
+
+ int clearBackupData(PackageInfo packageInfo);
+
+ int finishBackup();
+
+ int performIncrementalBackup(PackageInfo targetPackage, ParcelFileDescriptor data);
+
+ int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor);
+
+ int checkFullBackupSize(long size);
+
+ int sendBackupData(int numBytes);
+
+ void cancelFullBackup();
+
+ long getBackupQuota(String packageName, boolean fullBackup);
+
+ long requestBackupTime();
+
+ long requestFullBackupTime();
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/RestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/RestoreComponent.java
new file mode 100644
index 0000000..2873cf1
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/RestoreComponent.java
@@ -0,0 +1,28 @@
+package com.stevesoltys.backup.transport.component;
+
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * @author Steve Soltys
+ */
+public interface RestoreComponent {
+
+ int startRestore(long token, PackageInfo[] packages);
+
+ RestoreDescription nextRestorePackage();
+
+ int getRestoreData(ParcelFileDescriptor outputFileDescriptor);
+
+ int getNextFullRestoreDataChunk(ParcelFileDescriptor socket);
+
+ int abortFullRestore();
+
+ long getCurrentRestoreSet();
+
+ void finishRestore();
+
+ RestoreSet[] getAvailableRestoreSets();
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java
new file mode 100644
index 0000000..92e30ab
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfiguration.java
@@ -0,0 +1,46 @@
+package com.stevesoltys.backup.transport.component.provider;
+
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * @author Steve Soltys
+ */
+public class ContentProviderBackupConfiguration {
+
+ public static final String FULL_BACKUP_DIRECTORY = "full/";
+
+ public static final String INCREMENTAL_BACKUP_DIRECTORY = "incr/";
+
+ private final Context context;
+
+ private final Uri uri;
+
+ private final long backupSizeQuota;
+
+ private final int packageCount;
+
+ public ContentProviderBackupConfiguration(Context context, Uri uri, long backupSizeQuota, int packageCount) {
+ this.context = context;
+ this.uri = uri;
+ this.backupSizeQuota = backupSizeQuota;
+ this.packageCount = packageCount;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public long getBackupSizeQuota() {
+ return backupSizeQuota;
+ }
+
+ public int getPackageCount() {
+ return packageCount;
+ }
+
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java
new file mode 100644
index 0000000..f7e12b9
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupConfigurationBuilder.java
@@ -0,0 +1,18 @@
+package com.stevesoltys.backup.transport.component.provider;
+
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * @author Steve Soltys
+ */
+public class ContentProviderBackupConfigurationBuilder {
+
+ private static final long DEFAULT_BACKUP_SIZE_QUOTA = Long.MAX_VALUE;
+
+ public static ContentProviderBackupConfiguration buildDefaultConfiguration(Context context, Uri outputUri,
+ int packageCount) {
+ return new ContentProviderBackupConfiguration(context, outputUri, DEFAULT_BACKUP_SIZE_QUOTA, packageCount);
+ }
+
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupComponent.java
new file mode 100644
index 0000000..892a578
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupComponent.java
@@ -0,0 +1,308 @@
+package com.stevesoltys.backup.transport.component.provider.backup;
+
+import android.app.backup.BackupDataInput;
+import android.content.ContentResolver;
+import android.content.pm.PackageInfo;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Base64;
+import android.util.Log;
+
+import com.stevesoltys.backup.transport.component.BackupComponent;
+import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
+
+import java.io.*;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static android.app.backup.BackupTransport.*;
+import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration.FULL_BACKUP_DIRECTORY;
+import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration.INCREMENTAL_BACKUP_DIRECTORY;
+
+/**
+ * TODO: Clean this up. Much of it was taken from the LocalTransport implementation.
+ *
+ * @author Steve Soltys
+ */
+public class ContentProviderBackupComponent implements BackupComponent {
+
+ private static final String TAG = ContentProviderBackupComponent.class.getName();
+
+ private static final String DESTINATION_DESCRIPTION = "Backing up to zip file";
+
+ private static final String TRANSPORT_DATA_MANAGEMENT_LABEL = "";
+
+ private static final int INITIAL_BUFFER_SIZE = 512;
+
+ private final ContentProviderBackupConfiguration configuration;
+
+ private ContentProviderBackupState backupState;
+
+ public ContentProviderBackupComponent(ContentProviderBackupConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public long requestBackupTime() {
+ return 0;
+ }
+
+ @Override
+ public String currentDestinationString() {
+ return DESTINATION_DESCRIPTION;
+ }
+
+ @Override
+ public String dataManagementLabel() {
+ return TRANSPORT_DATA_MANAGEMENT_LABEL;
+ }
+
+ @Override
+ public int initializeDevice() {
+ return TRANSPORT_OK;
+ }
+
+ private void initializeBackupState() throws IOException {
+ if (backupState == null) {
+ backupState = new ContentProviderBackupState();
+ }
+
+ if (backupState.getOutputStream() == null) {
+ initializeOutputStream();
+ }
+ }
+
+ private void initializeOutputStream() throws FileNotFoundException {
+ Uri outputUri = configuration.getUri();
+
+ ContentResolver contentResolver = configuration.getContext().getContentResolver();
+ ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(outputUri, "w");
+ backupState.setOutputFileDescriptor(outputFileDescriptor);
+
+ FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor());
+ ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
+ backupState.setOutputStream(zipOutputStream);
+ }
+
+ @Override
+ public int clearBackupData(PackageInfo packageInfo) {
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public int performIncrementalBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
+ BackupDataInput backupDataInput = new BackupDataInput(data.getFileDescriptor());
+
+ try {
+ initializeBackupState();
+ backupState.setPackageIndex(backupState.getPackageIndex() + 1);
+ backupState.setPackageName(packageInfo.packageName);
+
+ return transferIncrementalBackupData(backupDataInput);
+
+ } catch (Exception ex) {
+ Log.v(TAG, "Error reading backup input: ", ex);
+ return TRANSPORT_ERROR;
+ }
+ }
+
+ private int transferIncrementalBackupData(BackupDataInput backupDataInput)
+ throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
+
+ 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(INCREMENTAL_BACKUP_DIRECTORY + 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;
+ }
+ }
+ }
+
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
+
+ if (backupState != null && backupState.getInputFileDescriptor() != null) {
+ Log.e(TAG, "Attempt to initiate full backup while one is in progress");
+ return TRANSPORT_ERROR;
+ }
+
+ Log.i(TAG, "performFullBackup : " + targetPackage);
+
+ try {
+ initializeBackupState();
+ backupState.setPackageIndex(backupState.getPackageIndex() + 1);
+ backupState.setPackageName(targetPackage.packageName);
+
+ backupState.setInputFileDescriptor(fileDescriptor);
+ backupState.setInputStream(new FileInputStream(fileDescriptor.getFileDescriptor()));
+ backupState.setBuffer(new byte[INITIAL_BUFFER_SIZE]);
+ backupState.setBytesTransferred(0);
+
+ ZipEntry zipEntry = new ZipEntry(FULL_BACKUP_DIRECTORY + backupState.getPackageName());
+ backupState.getOutputStream().putNextEntry(zipEntry);
+
+ } catch (Exception ex) {
+ Log.e(TAG, "Error creating backup file for " + backupState.getPackageName() + ": ", ex);
+ clearBackupState(true);
+
+ return TRANSPORT_ERROR;
+ }
+
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public int checkFullBackupSize(long size) {
+ int result = TRANSPORT_OK;
+
+ if (size <= 0) {
+ result = TRANSPORT_PACKAGE_REJECTED;
+
+ } else if (size > configuration.getBackupSizeQuota()) {
+ result = TRANSPORT_QUOTA_EXCEEDED;
+ }
+
+ if (result != TRANSPORT_OK) {
+ Log.v(TAG, "Declining backup of size " + size);
+ }
+
+ return result;
+ }
+
+ @Override
+ public int sendBackupData(int numBytes) {
+
+ if (backupState == null) {
+ Log.e(TAG, "Attempted sendBackupData before performFullBackup");
+ return TRANSPORT_ERROR;
+ }
+
+ long bytesTransferred = backupState.getBytesTransferred() + numBytes;
+
+ if (bytesTransferred > configuration.getBackupSizeQuota()) {
+ return TRANSPORT_QUOTA_EXCEEDED;
+ }
+
+ byte[] buffer = backupState.getBuffer();
+ if (numBytes > buffer.length) {
+ buffer = new byte[numBytes];
+ }
+
+ InputStream inputStream = backupState.getInputStream();
+ ZipOutputStream outputStream = backupState.getOutputStream();
+
+ try {
+ int bytesRemaining = numBytes;
+
+ while (bytesRemaining > 0) {
+ int bytesRead = inputStream.read(buffer, 0, bytesRemaining);
+
+ if (bytesRead < 0) {
+ Log.e(TAG, "Unexpected EOD; failing backup");
+ return TRANSPORT_ERROR;
+ }
+
+ outputStream.write(buffer, 0, bytesRead);
+ bytesRemaining -= bytesRead;
+ }
+
+ backupState.setBytesTransferred(bytesTransferred);
+
+ } catch (IOException ex) {
+ Log.e(TAG, "Error handling backup data for " + backupState.getPackageName() + ": ", ex);
+ return TRANSPORT_ERROR;
+ }
+
+ Log.v(TAG, " stored " + numBytes + " of data");
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public void cancelFullBackup() {
+ clearBackupState(true);
+ }
+
+ @Override
+ public int finishBackup() {
+ return clearBackupState(false);
+ }
+
+ private int clearBackupState(boolean closeFile) {
+
+ if (backupState == null) {
+ return TRANSPORT_OK;
+ }
+
+ try {
+ if (backupState.getInputFileDescriptor() != null) {
+ backupState.getInputFileDescriptor().close();
+ backupState.setInputFileDescriptor(null);
+ }
+
+ ZipOutputStream outputStream = backupState.getOutputStream();
+
+ if (outputStream != null) {
+ outputStream.closeEntry();
+ }
+
+ if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
+
+ if (outputStream != null) {
+ outputStream.finish();
+ outputStream.close();
+ }
+
+ if (backupState.getOutputFileDescriptor() != null) {
+ backupState.getOutputFileDescriptor().close();
+ }
+
+ 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 fullBackup ? configuration.getBackupSizeQuota() : Long.MAX_VALUE;
+ }
+
+ @Override
+ public long requestFullBackupTime() {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupState.java
new file mode 100644
index 0000000..87cddab
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/backup/ContentProviderBackupState.java
@@ -0,0 +1,92 @@
+package com.stevesoltys.backup.transport.component.provider.backup;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.InputStream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * @author Steve Soltys
+ */
+class ContentProviderBackupState {
+
+ private ParcelFileDescriptor inputFileDescriptor;
+
+ private ParcelFileDescriptor outputFileDescriptor;
+
+ private InputStream inputStream;
+
+ private ZipOutputStream outputStream;
+
+ private long bytesTransferred;
+
+ private byte[] buffer;
+
+ private String packageName;
+
+ private int packageIndex;
+
+ ParcelFileDescriptor getInputFileDescriptor() {
+ return inputFileDescriptor;
+ }
+
+ void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
+ this.inputFileDescriptor = inputFileDescriptor;
+ }
+
+ ParcelFileDescriptor getOutputFileDescriptor() {
+ return outputFileDescriptor;
+ }
+
+ void setOutputFileDescriptor(ParcelFileDescriptor outputFileDescriptor) {
+ this.outputFileDescriptor = outputFileDescriptor;
+ }
+
+ InputStream getInputStream() {
+ return inputStream;
+ }
+
+ void setInputStream(InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ ZipOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ void setOutputStream(ZipOutputStream outputStream) {
+ this.outputStream = outputStream;
+ }
+
+ long getBytesTransferred() {
+ return bytesTransferred;
+ }
+
+ void setBytesTransferred(long bytesTransferred) {
+ this.bytesTransferred = bytesTransferred;
+ }
+
+ String getPackageName() {
+ return packageName;
+ }
+
+ void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ int getPackageIndex() {
+ return packageIndex;
+ }
+
+ void setPackageIndex(int packageIndex) {
+ this.packageIndex = packageIndex;
+ }
+
+ byte[] getBuffer() {
+ return buffer;
+ }
+
+ void setBuffer(byte[] buffer) {
+ this.buffer = buffer;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreComponent.java
new file mode 100644
index 0000000..37ed41a
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreComponent.java
@@ -0,0 +1,346 @@
+package com.stevesoltys.backup.transport.component.provider.restore;
+
+import android.app.backup.BackupDataOutput;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.ContentResolver;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+import android.util.Base64;
+import android.util.Log;
+
+import com.stevesoltys.backup.transport.component.RestoreComponent;
+import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import static android.app.backup.BackupTransport.NO_MORE_DATA;
+import static android.app.backup.BackupTransport.TRANSPORT_ERROR;
+import static android.app.backup.BackupTransport.TRANSPORT_OK;
+import static android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED;
+import static android.app.backup.RestoreDescription.TYPE_FULL_STREAM;
+import static android.app.backup.RestoreDescription.TYPE_KEY_VALUE;
+import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration.FULL_BACKUP_DIRECTORY;
+import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration.INCREMENTAL_BACKUP_DIRECTORY;
+
+/**
+ * TODO: Clean this up. Much of it was taken from the LocalTransport implementation.
+ *
+ * @author Steve Soltys
+ */
+public class ContentProviderRestoreComponent implements RestoreComponent {
+
+ private static final String TAG = ContentProviderRestoreComponent.class.getName();
+
+ private static final int DEFAULT_RESTORE_SET = 1;
+
+ private static final int DEFAULT_BUFFER_SIZE = 2048;
+
+ private ContentProviderBackupConfiguration configuration;
+
+ private ContentProviderRestoreState restoreState;
+
+ public ContentProviderRestoreComponent(ContentProviderBackupConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public int startRestore(long token, PackageInfo[] packages) {
+ Log.i(TAG, "startRestore() " + Arrays.asList(packages));
+
+ restoreState = new ContentProviderRestoreState();
+ restoreState.setPackages(packages);
+ restoreState.setPackageIndex(-1);
+
+ return TRANSPORT_OK;
+ }
+
+ private ParcelFileDescriptor buildFileDescriptor() throws FileNotFoundException {
+ ContentResolver contentResolver = configuration.getContext().getContentResolver();
+
+ return contentResolver.openFileDescriptor(configuration.getUri(), "r");
+ }
+
+ private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) throws FileNotFoundException {
+ FileInputStream fileInputStream = new FileInputStream(inputFileDescriptor.getFileDescriptor());
+ return new ZipInputStream(fileInputStream);
+ }
+
+ private boolean containsPackageFile(String fileName) throws IOException, InvalidKeyException,
+ InvalidAlgorithmParameterException {
+
+ ParcelFileDescriptor inputFileDescriptor = buildFileDescriptor();
+ ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
+
+ ZipEntry zipEntry;
+ while ((zipEntry = inputStream.getNextEntry()) != null) {
+
+ if (zipEntry.getName().startsWith(fileName)) {
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ return true;
+ }
+
+ inputStream.closeEntry();
+ }
+
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ return false;
+ }
+
+ @Override
+ public RestoreDescription nextRestorePackage() {
+ Log.i(TAG, "nextRestorePackage()");
+
+ if (restoreState == null) {
+ throw new IllegalStateException("startRestore not called");
+ }
+
+ int packageIndex = restoreState.getPackageIndex();
+ PackageInfo[] packages = restoreState.getPackages();
+
+ while (++packageIndex < packages.length) {
+ restoreState.setPackageIndex(packageIndex);
+ String name = packages[packageIndex].packageName;
+
+ try {
+ if (containsPackageFile(INCREMENTAL_BACKUP_DIRECTORY + name)) {
+ Log.i(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ " + packageIndex + " = " + name);
+
+ restoreState.setRestoreType(TYPE_KEY_VALUE);
+ return new RestoreDescription(name, restoreState.getRestoreType());
+
+ } else if (containsPackageFile(FULL_BACKUP_DIRECTORY + name)) {
+ Log.i(TAG, " nextRestorePackage(TYPE_FULL_STREAM) @ " + packageIndex + " = " + name);
+
+ restoreState.setRestoreType(TYPE_FULL_STREAM);
+ return new RestoreDescription(name, restoreState.getRestoreType());
+ }
+
+ } catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, " ... package @ " + packageIndex + " = " + name + " error:", e);
+ }
+
+ Log.i(TAG, " ... package @ " + packageIndex + " = " + name + " has no data; skipping");
+ }
+
+ Log.i(TAG, " no more packages to restore");
+ return RestoreDescription.NO_MORE_PACKAGES;
+ }
+
+ @Override
+ public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
+ Log.i(TAG, "getRestoreData() " + outputFileDescriptor.toString());
+
+ if (restoreState == null) {
+ throw new IllegalStateException("startRestore not called");
+
+ } else if (restoreState.getPackageIndex() < 0) {
+ throw new IllegalStateException("nextRestorePackage not called");
+
+ } else if (restoreState.getRestoreType() != TYPE_KEY_VALUE) {
+ throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset");
+ }
+
+ PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
+ BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
+
+ try {
+ return transferIncrementalRestoreData(packageInfo.packageName, backupDataOutput);
+
+ } catch (Exception ex) {
+ Log.e(TAG, "Unable to read backup records: ", ex);
+ return TRANSPORT_ERROR;
+ }
+ }
+
+ private int transferIncrementalRestoreData(String packageName, BackupDataOutput backupDataOutput)
+ throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
+ Log.i(TAG, "transferIncrementalRestoreData() " + packageName);
+
+ ParcelFileDescriptor inputFileDescriptor = buildFileDescriptor();
+ ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
+
+ ZipEntry zipEntry;
+ while ((zipEntry = inputStream.getNextEntry()) != null) {
+
+ if (zipEntry.getName().startsWith(INCREMENTAL_BACKUP_DIRECTORY + packageName)) {
+ String fileName = new File(zipEntry.getName()).getName();
+ String blobKey = new String(Base64.decode(fileName, Base64.DEFAULT));
+
+ byte[] backupData = Streams.readFullyNoClose(inputStream);
+ Log.i(TAG, "Backup data: " + packageName + ": " + backupData.length);
+
+ backupDataOutput.writeEntityHeader(blobKey, backupData.length);
+ backupDataOutput.writeEntityData(backupData, backupData.length);
+ }
+
+ inputStream.closeEntry();
+ }
+
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public int getNextFullRestoreDataChunk(ParcelFileDescriptor fileDescriptor) {
+
+ if (restoreState.getRestoreType() != TYPE_FULL_STREAM) {
+ throw new IllegalStateException("Asked for full restore data for non-stream package");
+ }
+
+ ParcelFileDescriptor inputFileDescriptor = restoreState.getInputFileDescriptor();
+ ZipInputStream inputStream = restoreState.getInputStream();
+
+ if (inputFileDescriptor == null) {
+ String name = restoreState.getPackages()[restoreState.getPackageIndex()].packageName;
+
+ try {
+ inputFileDescriptor = buildFileDescriptor();
+ restoreState.setInputFileDescriptor(inputFileDescriptor);
+
+ inputStream = buildInputStream(inputFileDescriptor);
+ restoreState.setInputStream(inputStream);
+
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "Unable to read archive for " + name, ex);
+
+ if(inputFileDescriptor != null) {
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ }
+
+ return TRANSPORT_ERROR;
+ }
+
+ try {
+
+ ZipEntry zipEntry;
+ while ((zipEntry = inputStream.getNextEntry()) != null) {
+
+ if (zipEntry.getName().equals(FULL_BACKUP_DIRECTORY + name)) {
+ break;
+ }
+
+ inputStream.closeEntry();
+ }
+
+ if (zipEntry == null) {
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ return TRANSPORT_PACKAGE_REJECTED;
+ }
+
+ } catch (IOException ex) {
+ Log.e(TAG, "Unable to read archive for " + name, ex);
+
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ return TRANSPORT_PACKAGE_REJECTED;
+ }
+ }
+
+ if (restoreState.getOutputStream() == null) {
+ restoreState.setOutputStream(new FileOutputStream(fileDescriptor.getFileDescriptor()));
+ }
+
+ OutputStream outputStream = restoreState.getOutputStream();
+
+ byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ int bytesRead = NO_MORE_DATA;
+
+ try {
+ bytesRead = inputStream.read(buffer);
+
+ if (bytesRead < 0) {
+ bytesRead = NO_MORE_DATA;
+
+ } else if (bytesRead == 0) {
+ Log.w(TAG, "read() of archive file returned 0; treating as EOF");
+ bytesRead = NO_MORE_DATA;
+
+ } else {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while streaming restore data: ", e);
+ return TRANSPORT_ERROR;
+
+ } finally {
+ try {
+ if (bytesRead == NO_MORE_DATA) {
+ IoUtils.closeQuietly(inputFileDescriptor.getFileDescriptor());
+ IoUtils.closeQuietly(inputStream);
+ IoUtils.closeQuietly(outputStream);
+
+ fileDescriptor.close();
+
+ restoreState.setInputFileDescriptor(null);
+ restoreState.setInputStream(null);
+ restoreState.setOutputStream(null);
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "Exception while closing socket for restore: ", ex);
+ }
+ }
+
+ return bytesRead;
+ }
+
+ @Override
+ public int abortFullRestore() {
+ resetFullRestoreState();
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public long getCurrentRestoreSet() {
+ return DEFAULT_RESTORE_SET;
+ }
+
+ @Override
+ public void finishRestore() {
+ if (restoreState.getRestoreType() == TYPE_FULL_STREAM) {
+ resetFullRestoreState();
+ }
+
+ restoreState = null;
+ }
+
+ @Override
+ public RestoreSet[] getAvailableRestoreSets() {
+ return new RestoreSet[]{new RestoreSet("Local disk image", "flash", DEFAULT_RESTORE_SET)};
+ }
+
+ private void resetFullRestoreState() {
+
+ if(restoreState == null) {
+ return;
+ }
+
+ if (restoreState.getRestoreType() != TYPE_FULL_STREAM) {
+ throw new IllegalStateException("abortFullRestore() but not currently restoring");
+ }
+
+ IoUtils.closeQuietly(restoreState.getInputFileDescriptor());
+ IoUtils.closeQuietly(restoreState.getInputStream());
+ IoUtils.closeQuietly(restoreState.getOutputStream());
+
+ restoreState = null;
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreState.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreState.java
new file mode 100644
index 0000000..13b0181
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/restore/ContentProviderRestoreState.java
@@ -0,0 +1,73 @@
+package com.stevesoltys.backup.transport.component.provider.restore;
+
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+
+import java.io.OutputStream;
+import java.util.zip.ZipInputStream;
+
+/**
+ * @author Steve Soltys
+ */
+class ContentProviderRestoreState {
+
+ private ParcelFileDescriptor inputFileDescriptor;
+
+ private PackageInfo[] packages;
+
+ private int packageIndex;
+
+ private int restoreType;
+
+ private ZipInputStream inputStream;
+
+ private OutputStream outputStream;
+
+ PackageInfo[] getPackages() {
+ return packages;
+ }
+
+ 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;
+ }
+
+ OutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ void setOutputStream(OutputStream outputStream) {
+ this.outputStream = outputStream;
+ }
+
+ ZipInputStream getInputStream() {
+ return inputStream;
+ }
+
+ void setInputStream(ZipInputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ ParcelFileDescriptor getInputFileDescriptor() {
+ return inputFileDescriptor;
+ }
+
+ void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
+ this.inputFileDescriptor = inputFileDescriptor;
+ }
+}
diff --git a/app/src/main/res/layout/activity_create_backup.xml b/app/src/main/res/layout/activity_create_backup.xml
new file mode 100644
index 0000000..f7f693e
--- /dev/null
+++ b/app/src/main/res/layout/activity_create_backup.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/activity_create_backup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:weightSum="1"
+ tools:context="com.stevesoltys.backup.activity.restore.RestoreBackupActivity">
+
+ <ListView
+ android:id="@+id/create_package_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="0.99" />
+
+ <Button
+ android:id="@+id/create_confirm_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/button_vertical_margin"
+ android:layout_marginLeft="@dimen/button_horizontal_margin"
+ android:layout_marginRight="@dimen/button_horizontal_margin"
+ android:text="@string/create_backup_button" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..fda8dd0
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/activity_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:weightSum="1"
+ tools:context="com.stevesoltys.backup.activity.MainActivity">
+
+ <Button
+ android:id="@+id/create_backup_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/button_horizontal_margin"
+ android:layout_marginRight="@dimen/button_horizontal_margin"
+ android:layout_marginTop="@dimen/button_vertical_margin"
+ android:text="@string/create_backup_button"/>
+
+ <Button
+ android:id="@+id/restore_backup_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/button_horizontal_margin"
+ android:layout_marginRight="@dimen/button_horizontal_margin"
+ android:layout_marginTop="@dimen/button_vertical_margin"
+ android:text="@string/restore_backup_button"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_restore_backup.xml b/app/src/main/res/layout/activity_restore_backup.xml
new file mode 100644
index 0000000..8e2aebe
--- /dev/null
+++ b/app/src/main/res/layout/activity_restore_backup.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/activity_restore_backup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:weightSum="1"
+ tools:context="com.stevesoltys.backup.activity.restore.RestoreBackupActivity">
+
+ <ListView
+ android:id="@+id/restore_package_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="0.99"/>
+
+ <Button
+ android:id="@+id/restore_confirm_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/button_horizontal_margin"
+ android:layout_marginRight="@dimen/button_horizontal_margin"
+ android:layout_marginTop="@dimen/button_vertical_margin"
+ android:text="@string/restore_button"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/checked_list_item.xml b/app/src/main/res/layout/checked_list_item.xml
new file mode 100644
index 0000000..2ab02a2
--- /dev/null
+++ b/app/src/main/res/layout/checked_list_item.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:gravity="center_vertical"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" />
diff --git a/app/src/main/res/layout/progress_popup_window.xml b/app/src/main/res/layout/progress_popup_window.xml
new file mode 100644
index 0000000..805b038
--- /dev/null
+++ b/app/src/main/res/layout/progress_popup_window.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/popup_layout"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/popup_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"/>
+
+ <ProgressBar
+ android:id="@+id/popup_progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/progress_bar_horizontal_margin"
+ android:layout_marginRight="@dimen/progress_bar_horizontal_margin"/>
+
+ <Button
+ android:id="@+id/popup_cancel_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/button_horizontal_margin"
+ android:layout_marginRight="@dimen/button_horizontal_margin"
+ android:layout_marginTop="@dimen/button_vertical_margin"
+ android:text="@string/popup_cancel"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..1f795be
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string-array name="packages">
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ <item>package</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..6c7e956
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,13 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+
+ <dimen name="button_horizontal_margin">5dp</dimen>
+ <dimen name="button_vertical_margin">7dp</dimen>
+
+ <dimen name="progress_bar_horizontal_margin">5dp</dimen>
+
+ <dimen name="popup_width">300dp</dimen>
+ <dimen name="popup_height">300dp</dimen>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6f81ef9
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<resources>
+ <string name="app_name">Backup</string>
+
+ <string name="create_backup_button">Create backup</string>
+ <string name="restore_backup_button">Restore backup</string>
+
+ <string name="backup_button">Backup packages</string>
+ <string name="backup_success">Backup success</string>
+ <string name="backup_failure">Backup failure</string>
+ <string name="backup_cancelled">Backup cancelled</string>
+ <string name="backup_in_progress">Backup already in progress</string>
+
+ <string name="restore_button">Restore packages</string>
+ <string name="restore_success">Restore success</string>
+ <string name="restore_failure">Restore failure</string>
+ <string name="restore_cancelled">Restore cancelled</string>
+ <string name="restore_in_progress">Restore already in progress</string>
+
+ <string name="popup_cancel">Cancel</string>
+
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..9785e0c
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+
+</resources>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..c2eea8e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.3'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'