1
0
Fork 0

Initial commit

This commit is contained in:
Steve Soltys 2017-09-20 22:42:15 -04:00
commit 2497a94e4c
52 changed files with 2567 additions and 0 deletions

49
.gitignore vendored Normal file
View file

@ -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/

21
LICENSE Normal file
View file

@ -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.

5
README.md Normal file
View file

@ -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).

21
app/build.gradle Normal file
View file

@ -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')
}

18
app/src/main/Android.mk Normal file
View file

@ -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)

View file

@ -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>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<backup-transport-whitelisted-service
service="com.stevesoltys.backup/.transport.ConfigurableBackupTransportService"/>
</config>

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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();
});
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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();
});
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
package com.stevesoltys.backup.session.backup;
/**
* @author Steve Soltys
*/
public enum BackupResult {
SUCCESS, FAILURE, CANCELLED
}

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -0,0 +1,8 @@
package com.stevesoltys.backup.session.restore;
/**
* @author Steve Soltys
*/
public enum RestoreResult {
SUCCESS, CANCELLED, FAILURE
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View file

@ -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>

Binary file not shown.

After

(image error) Size: 3.3 KiB

Binary file not shown.

After

(image error) Size: 2.2 KiB

Binary file not shown.

After

(image error) Size: 4.7 KiB

Binary file not shown.

After

(image error) Size: 7.5 KiB

Binary file not shown.

After

(image error) Size: 10 KiB

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

23
build.gradle Normal file
View file

@ -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
}

1
settings.gradle Normal file
View file

@ -0,0 +1 @@
include ':app'