Allow the user to schedule full background backups
This commit is contained in:
parent
e079aaff1b
commit
540147470d
7 changed files with 167 additions and 3 deletions
app/src/main
|
@ -14,6 +14,8 @@
|
||||||
android:name="android.permission.BACKUP"
|
android:name="android.permission.BACKUP"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Backup"
|
android:name=".Backup"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
@ -48,5 +50,10 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.backup.BackupJobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.backup;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,6 +10,8 @@ import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||||
*/
|
*/
|
||||||
public class Backup extends Application {
|
public class Backup extends Application {
|
||||||
|
|
||||||
|
public static final int JOB_ID_BACKGROUND_BACKUP = 1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.stevesoltys.backup.R;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
|
import static com.stevesoltys.backup.settings.SettingsManager.areBackupsScheduled;
|
||||||
|
|
||||||
public class MainActivity extends Activity implements View.OnClickListener {
|
public class MainActivity extends Activity implements View.OnClickListener {
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
||||||
public static final int LOAD_DOCUMENT_REQUEST_CODE = 3;
|
public static final int LOAD_DOCUMENT_REQUEST_CODE = 3;
|
||||||
|
|
||||||
private MainActivityController controller;
|
private MainActivityController controller;
|
||||||
|
private Button automaticBackupsButton;
|
||||||
private Button changeLocationButton;
|
private Button changeLocationButton;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -33,6 +35,10 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
||||||
findViewById(R.id.create_backup_button).setOnClickListener(this);
|
findViewById(R.id.create_backup_button).setOnClickListener(this);
|
||||||
findViewById(R.id.restore_backup_button).setOnClickListener(this);
|
findViewById(R.id.restore_backup_button).setOnClickListener(this);
|
||||||
|
|
||||||
|
automaticBackupsButton = findViewById(R.id.automatic_backups_button);
|
||||||
|
automaticBackupsButton.setOnClickListener(this);
|
||||||
|
if (areBackupsScheduled(this)) automaticBackupsButton.setVisibility(GONE);
|
||||||
|
|
||||||
changeLocationButton = findViewById(R.id.change_backup_location_button);
|
changeLocationButton = findViewById(R.id.change_backup_location_button);
|
||||||
changeLocationButton.setOnClickListener(this);
|
changeLocationButton.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
@ -61,6 +67,12 @@ public class MainActivity extends Activity implements View.OnClickListener {
|
||||||
controller.showLoadDocumentActivity(this);
|
controller.showLoadDocumentActivity(this);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case R.id.automatic_backups_button:
|
||||||
|
if (controller.onAutomaticBackupsButtonClicked(this)) {
|
||||||
|
automaticBackupsButton.setVisibility(GONE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case R.id.change_backup_location_button:
|
case R.id.change_backup_location_button:
|
||||||
controller.onChangeBackupLocationButtonClicked(this);
|
controller.onChangeBackupLocationButtonClicked(this);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package com.stevesoltys.backup.activity;
|
package com.stevesoltys.backup.activity;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.job.JobInfo;
|
||||||
|
import android.app.job.JobScheduler;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -10,12 +13,14 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import com.stevesoltys.backup.activity.backup.CreateBackupActivity;
|
import com.stevesoltys.backup.activity.backup.CreateBackupActivity;
|
||||||
import com.stevesoltys.backup.activity.restore.RestoreBackupActivity;
|
import com.stevesoltys.backup.activity.restore.RestoreBackupActivity;
|
||||||
|
import com.stevesoltys.backup.service.backup.BackupJobService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static android.app.job.JobInfo.NETWORK_TYPE_UNMETERED;
|
||||||
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
|
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
|
||||||
import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE;
|
import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE;
|
||||||
import static android.content.Intent.CATEGORY_OPENABLE;
|
import static android.content.Intent.CATEGORY_OPENABLE;
|
||||||
|
@ -25,16 +30,21 @@ import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||||
import static android.provider.DocumentsContract.buildDocumentUriUsingTree;
|
import static android.provider.DocumentsContract.buildDocumentUriUsingTree;
|
||||||
import static android.provider.DocumentsContract.createDocument;
|
import static android.provider.DocumentsContract.createDocument;
|
||||||
import static android.provider.DocumentsContract.getTreeDocumentId;
|
import static android.provider.DocumentsContract.getTreeDocumentId;
|
||||||
|
import static com.stevesoltys.backup.Backup.JOB_ID_BACKGROUND_BACKUP;
|
||||||
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE;
|
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE;
|
||||||
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE;
|
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE;
|
||||||
import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri;
|
import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri;
|
||||||
|
import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword;
|
||||||
import static com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri;
|
import static com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri;
|
||||||
|
import static com.stevesoltys.backup.settings.SettingsManager.setBackupsScheduled;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
* @author Torsten Grote
|
* @author Torsten Grote
|
||||||
*/
|
*/
|
||||||
class MainActivityController {
|
public class MainActivityController {
|
||||||
|
|
||||||
private static final String TAG = MainActivityController.class.getName();
|
private static final String TAG = MainActivityController.class.getName();
|
||||||
|
|
||||||
|
@ -90,6 +100,27 @@ class MainActivityController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean onAutomaticBackupsButtonClicked(Activity parent) {
|
||||||
|
if (getBackupFolderUri(parent) == null || getBackupPassword(parent) == null) {
|
||||||
|
Toast.makeText(parent, "Please make at least one manual backup first.", Toast.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final ComponentName serviceName = new ComponentName(parent, BackupJobService.class);
|
||||||
|
JobInfo job = new JobInfo.Builder(JOB_ID_BACKGROUND_BACKUP, serviceName)
|
||||||
|
.setRequiredNetworkType(NETWORK_TYPE_UNMETERED)
|
||||||
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.setRequiresStorageNotLow(true) // TODO warn the user instead
|
||||||
|
.setPeriodic(DAYS.toMillis(1))
|
||||||
|
.setRequiresCharging(true)
|
||||||
|
.setPersisted(true)
|
||||||
|
.build();
|
||||||
|
JobScheduler scheduler = requireNonNull(parent.getSystemService(JobScheduler.class));
|
||||||
|
scheduler.schedule(job);
|
||||||
|
setBackupsScheduled(parent);
|
||||||
|
Toast.makeText(parent, "Backups will run automatically now", Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void onChangeBackupLocationButtonClicked(Activity parent) {
|
void onChangeBackupLocationButtonClicked(Activity parent) {
|
||||||
showChooseFolderActivity(parent, false);
|
showChooseFolderActivity(parent, false);
|
||||||
}
|
}
|
||||||
|
@ -143,7 +174,7 @@ class MainActivityController {
|
||||||
parent.startActivity(intent);
|
parent.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Uri createBackupFile(ContentResolver contentResolver, Uri folderUri) throws IOException {
|
public static Uri createBackupFile(ContentResolver contentResolver, Uri folderUri) throws IOException {
|
||||||
Uri documentUri = buildDocumentUriUsingTree(folderUri, getTreeDocumentId(folderUri));
|
Uri documentUri = buildDocumentUriUsingTree(folderUri, getTreeDocumentId(folderUri));
|
||||||
try {
|
try {
|
||||||
Uri fileUri = createDocument(contentResolver, documentUri, DOCUMENT_MIME_TYPE, getBackupFileName());
|
Uri fileUri = createDocument(contentResolver, documentUri, DOCUMENT_MIME_TYPE, getBackupFileName());
|
||||||
|
@ -156,7 +187,7 @@ class MainActivityController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getBackupFileName() {
|
private static String getBackupFileName() {
|
||||||
SimpleDateFormat dateFormat = new SimpleDateFormat(DOCUMENT_SUFFIX, Locale.US);
|
SimpleDateFormat dateFormat = new SimpleDateFormat(DOCUMENT_SUFFIX, Locale.US);
|
||||||
String date = dateFormat.format(new Date());
|
String date = dateFormat.format(new Date());
|
||||||
return "backup-" + date;
|
return "backup-" + date;
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.stevesoltys.backup.service.backup;
|
||||||
|
|
||||||
|
import android.app.backup.BackupManager;
|
||||||
|
import android.app.backup.IBackupManager;
|
||||||
|
import android.app.job.JobParameters;
|
||||||
|
import android.app.job.JobService;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.collect.Sets;
|
||||||
|
import com.stevesoltys.backup.service.PackageService;
|
||||||
|
import com.stevesoltys.backup.service.TransportService;
|
||||||
|
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
|
||||||
|
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP;
|
||||||
|
import static android.os.ServiceManager.getService;
|
||||||
|
import static com.stevesoltys.backup.activity.MainActivityController.createBackupFile;
|
||||||
|
import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri;
|
||||||
|
import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword;
|
||||||
|
|
||||||
|
public class BackupJobService extends JobService {
|
||||||
|
|
||||||
|
private final static String TAG = BackupJobService.class.getName();
|
||||||
|
|
||||||
|
private static final Set<String> IGNORED_PACKAGES = Sets.newArraySet(
|
||||||
|
"com.android.providers.downloads.ui", "com.android.providers.downloads", "com.android.providers.media",
|
||||||
|
"com.android.providers.calendar", "com.android.providers.contacts", "com.stevesoltys.backup"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final IBackupManager backupManager;
|
||||||
|
private final TransportService transportService = new TransportService();
|
||||||
|
|
||||||
|
public BackupJobService() {
|
||||||
|
backupManager = IBackupManager.Stub.asInterface(getService("backup"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartJob(JobParameters params) {
|
||||||
|
Log.i(TAG, "Triggering full backup");
|
||||||
|
try {
|
||||||
|
LinkedList<String> packages = new LinkedList<>(new PackageService().getEligiblePackages());
|
||||||
|
packages.removeAll(IGNORED_PACKAGES);
|
||||||
|
Uri fileUri = createBackupFile(getContentResolver(), getBackupFolderUri(this));
|
||||||
|
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
|
||||||
|
.setContext(this)
|
||||||
|
.setPackages(new HashSet<>(packages))
|
||||||
|
.setOutputUri(fileUri)
|
||||||
|
.setPassword(getBackupPassword(this))
|
||||||
|
.build();
|
||||||
|
transportService.initializeBackupTransport(backupConfiguration);
|
||||||
|
|
||||||
|
// TODO use an observer to know when backups fail
|
||||||
|
String[] packageArray = packages.toArray(new String[packages.size()]);
|
||||||
|
int result = backupManager.requestBackup(packageArray, null, null, FLAG_NON_INCREMENTAL_BACKUP);
|
||||||
|
if (result == BackupManager.SUCCESS) {
|
||||||
|
Log.i(TAG, "Backup succeeded ");
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Backup failed: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO show notification on backup error
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Error creating backup file: ", e);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Error during backup: ", e);
|
||||||
|
} finally {
|
||||||
|
jobFinished(params, false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStopJob(JobParameters params) {
|
||||||
|
try {
|
||||||
|
backupManager.cancelBackups();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Error cancelling backup: ", e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ public class SettingsManager {
|
||||||
|
|
||||||
private static final String PREF_KEY_BACKUP_URI = "backupUri";
|
private static final String PREF_KEY_BACKUP_URI = "backupUri";
|
||||||
private static final String PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword";
|
private static final String PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword";
|
||||||
|
private static final String PREF_KEY_BACKUPS_SCHEDULED = "backupsScheduled";
|
||||||
|
|
||||||
public static void setBackupFolderUri(Context context, Uri uri) {
|
public static void setBackupFolderUri(Context context, Uri uri) {
|
||||||
getDefaultSharedPreferences(context)
|
getDefaultSharedPreferences(context)
|
||||||
|
@ -41,4 +42,16 @@ public class SettingsManager {
|
||||||
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null);
|
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setBackupsScheduled(Context context) {
|
||||||
|
getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(PREF_KEY_BACKUPS_SCHEDULED, true)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Boolean areBackupsScheduled(Context context) {
|
||||||
|
return getDefaultSharedPreferences(context).getBoolean(PREF_KEY_BACKUPS_SCHEDULED, false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,15 @@
|
||||||
android:layout_marginRight="@dimen/button_horizontal_margin"
|
android:layout_marginRight="@dimen/button_horizontal_margin"
|
||||||
android:text="@string/restore_backup_button" />
|
android:text="@string/restore_backup_button" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/automatic_backups_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
android:layout_marginLeft="@dimen/button_horizontal_margin"
|
||||||
|
android:layout_marginRight="@dimen/button_horizontal_margin"
|
||||||
|
android:text="Activate automatic backups" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/change_backup_location_button"
|
android:id="@+id/change_backup_location_button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
Loading…
Add table
Reference in a new issue