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'