Re-implement manual backup run and show notification during manual backups
This commit is contained in:
parent
4c79d41963
commit
87b25aa4ec
11 changed files with 233 additions and 112 deletions
|
@ -1,12 +0,0 @@
|
|||
package com.stevesoltys.backup;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
public class Backup extends Application {
|
||||
|
||||
public static final int JOB_ID_BACKGROUND_BACKUP = 1;
|
||||
|
||||
}
|
22
app/src/main/java/com/stevesoltys/backup/Backup.kt
Normal file
22
app/src/main/java/com/stevesoltys/backup/Backup.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
package com.stevesoltys.backup
|
||||
|
||||
import android.app.Application
|
||||
import android.app.backup.IBackupManager
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.os.ServiceManager.getService
|
||||
|
||||
const val JOB_ID_BACKGROUND_BACKUP = 1
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
* @author Torsten Grote
|
||||
*/
|
||||
class Backup : Application() {
|
||||
|
||||
companion object {
|
||||
val backupManager: IBackupManager by lazy {
|
||||
IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package com.stevesoltys.backup
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.NotificationManager.IMPORTANCE_MIN
|
||||
import android.app.backup.BackupManager
|
||||
import android.app.backup.BackupProgress
|
||||
import android.app.backup.IBackupObserver
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.Log.INFO
|
||||
import android.util.Log.isLoggable
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport
|
||||
|
||||
private const val CHANNEL_ID = "NotificationBackupObserver"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
|
||||
private val TAG = NotificationBackupObserver::class.java.name
|
||||
|
||||
class NotificationBackupObserver(
|
||||
private val context: Context,
|
||||
private val userInitiated: Boolean) : IBackupObserver.Stub() {
|
||||
|
||||
private val pm = context.packageManager
|
||||
private val nm = context.getSystemService(NotificationManager::class.java).apply {
|
||||
val title = context.getString(R.string.notification_channel_title)
|
||||
val channel = NotificationChannel(CHANNEL_ID, title, IMPORTANCE_MIN).apply {
|
||||
enableVibration(false)
|
||||
}
|
||||
createNotificationChannel(channel)
|
||||
}
|
||||
private val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
|
||||
setSmallIcon(R.drawable.ic_cloud_upload)
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
}
|
||||
|
||||
/**
|
||||
* This method could be called several times for packages with full data backup.
|
||||
* It will tell how much of backup data is already saved and how much is expected.
|
||||
*
|
||||
* @param currentBackupPackage The name of the package that now being backed up.
|
||||
* @param backupProgress Current progress of backup for the package.
|
||||
*/
|
||||
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||
val transferred = backupProgress.bytesTransferred
|
||||
val expected = backupProgress.bytesExpected
|
||||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Update. Target: $currentBackupPackage, $transferred/$expected")
|
||||
}
|
||||
val notification = notificationBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_title))
|
||||
setContentText(getAppName(currentBackupPackage))
|
||||
setProgress(expected.toInt(), transferred.toInt(), false)
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup of one package or initialization of one transport has completed. This
|
||||
* method will be called at most one time for each package or transport, and might not
|
||||
* be not called if the operation fails before backupFinished(); for example, if the
|
||||
* requested package/transport does not exist.
|
||||
*
|
||||
* @param target The name of the package that was backed up, or of the transport
|
||||
* that was initialized
|
||||
* @param status Zero on success; a nonzero error code if the backup operation failed.
|
||||
*/
|
||||
override fun onResult(target: String, status: Int) {
|
||||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Completed. Target: $target, status: $status")
|
||||
}
|
||||
val title = context.getString(
|
||||
if (status == 0) R.string.notification_backup_result_complete
|
||||
else R.string.notification_backup_result_error
|
||||
)
|
||||
val notification = notificationBuilder.apply {
|
||||
setContentTitle(title)
|
||||
setContentText(getAppName(target))
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* The backup process has completed. This method will always be called,
|
||||
* even if no individual package backup operations were attempted.
|
||||
*
|
||||
* @param status Zero on success; a nonzero error code if the backup operation
|
||||
* as a whole failed.
|
||||
*/
|
||||
override fun backupFinished(status: Int) {
|
||||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Backup finished. Status: $status")
|
||||
}
|
||||
if (status == BackupManager.SUCCESS) getBackupTransport(context).backupFinished()
|
||||
nm.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
private fun getAppName(packageId: String): CharSequence {
|
||||
val appInfo = pm.getApplicationInfo(packageId, 0)
|
||||
return pm.getApplicationLabel(appInfo)
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ import static android.content.Intent.CATEGORY_OPENABLE;
|
|||
import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
|
||||
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
import static com.stevesoltys.backup.Backup.JOB_ID_BACKGROUND_BACKUP;
|
||||
import static com.stevesoltys.backup.BackupKt.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_REQUEST_CODE;
|
||||
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupFolderUri;
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
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.content.Intent;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.stevesoltys.backup.service.PackageService;
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
||||
|
||||
import static android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP;
|
||||
import static android.os.ServiceManager.getService;
|
||||
|
||||
public class BackupJobService extends JobService {
|
||||
|
||||
private final static String TAG = BackupJobService.class.getName();
|
||||
|
||||
private final IBackupManager backupManager;
|
||||
private final PackageService packageService = new PackageService();
|
||||
|
||||
public BackupJobService() {
|
||||
backupManager = IBackupManager.Stub.asInterface(getService("backup"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.i(TAG, "Triggering full backup");
|
||||
startService(new Intent(this, ConfigurableBackupTransportService.class));
|
||||
try {
|
||||
String[] packages = packageService.getEligiblePackages();
|
||||
// TODO use an observer to know when backups fail
|
||||
int result = backupManager.requestBackup(packages, 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 (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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package com.stevesoltys.backup.service.backup
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP
|
||||
import android.app.backup.BackupTransport.FLAG_USER_INITIATED
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.content.Context
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.NotificationBackupObserver
|
||||
import com.stevesoltys.backup.service.PackageService
|
||||
import com.stevesoltys.backup.session.backup.BackupMonitor
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
|
||||
|
||||
private val TAG = BackupJobService::class.java.name
|
||||
|
||||
// TODO might not be needed, if the OS really schedules backups on its own
|
||||
class BackupJobService : JobService() {
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
Log.i(TAG, "Triggering full backup")
|
||||
try {
|
||||
requestFullBackup(this)
|
||||
} finally {
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
try {
|
||||
Backup.backupManager.cancelBackups()
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error cancelling backup: ", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun requestFullBackup(context: Context) {
|
||||
context.startService(Intent(context, ConfigurableBackupTransportService::class.java))
|
||||
val observer = NotificationBackupObserver(context, true)
|
||||
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
||||
val packages = PackageService().eligiblePackages
|
||||
val result = try {
|
||||
Backup.backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
||||
} catch (e: RemoteException) {
|
||||
// TODO show notification on backup error
|
||||
Log.e(TAG, "Error during backup: ", e)
|
||||
}
|
||||
if (result == BackupManager.SUCCESS) {
|
||||
Log.i(TAG, "Backup succeeded ")
|
||||
} else {
|
||||
Log.e(TAG, "Backup failed: $result")
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package com.stevesoltys.backup.session.backup;
|
||||
|
||||
import android.app.backup.IBackupManagerMonitor;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY;
|
||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID;
|
||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
|
||||
|
||||
class BackupMonitor extends IBackupManagerMonitor.Stub {
|
||||
|
||||
@Override
|
||||
public void onEvent(Bundle bundle) {
|
||||
Log.d("BackupMonitor", "ID: " + bundle.getInt(EXTRA_LOG_EVENT_ID));
|
||||
Log.d("BackupMonitor", "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1));
|
||||
Log.d("BackupMonitor", "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.stevesoltys.backup.session.backup
|
||||
|
||||
import android.app.backup.BackupManagerMonitor.*
|
||||
import android.app.backup.IBackupManagerMonitor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Log.DEBUG
|
||||
|
||||
private val TAG = BackupMonitor::class.java.name
|
||||
|
||||
class BackupMonitor : IBackupManagerMonitor.Stub() {
|
||||
|
||||
override fun onEvent(bundle: Bundle) {
|
||||
if (!Log.isLoggable(TAG, DEBUG)) return
|
||||
Log.d(TAG, "ID: " + bundle.getInt(EXTRA_LOG_EVENT_ID))
|
||||
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
|
||||
Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +1,27 @@
|
|||
package com.stevesoltys.backup.settings
|
||||
|
||||
import android.app.backup.IBackupManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
|
||||
import com.stevesoltys.backup.R
|
||||
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.os.ServiceManager.getService
|
||||
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
|
||||
private val TAG = SettingsFragment::class.java.name
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var backupManager: IBackupManager
|
||||
private val backupManager = Backup.backupManager
|
||||
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
|
@ -38,8 +32,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE))
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||
|
||||
backup = findPreference("backup") as TwoStatePreference
|
||||
|
@ -103,7 +95,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
|
||||
item.itemId == R.id.action_backup -> {
|
||||
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
|
||||
viewModel.backupNow()
|
||||
true
|
||||
}
|
||||
item.itemId == R.id.action_restore -> {
|
||||
|
|
|
@ -8,12 +8,15 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import com.stevesoltys.backup.LiveEvent
|
||||
import com.stevesoltys.backup.MutableLiveEvent
|
||||
import com.stevesoltys.backup.security.KeyManager
|
||||
import com.stevesoltys.backup.service.backup.requestFullBackup
|
||||
|
||||
private val TAG = SettingsViewModel::class.java.name
|
||||
|
||||
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val app = application
|
||||
|
||||
private val mLocationWasSet = MutableLiveEvent<Boolean>()
|
||||
private val locationWasSet = MutableLiveEvent<Boolean>()
|
||||
/**
|
||||
* Will be set to true if this is the initial location.
|
||||
* It will be false if an existing location was changed.
|
||||
|
@ -41,7 +44,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||
setBackupFolderUri(app, folderUri)
|
||||
|
||||
// notify the UI that the location has been set
|
||||
mLocationWasSet.setEvent(wasEmptyBefore)
|
||||
locationWasSet.setEvent(wasEmptyBefore)
|
||||
}
|
||||
|
||||
fun backupNow() = Thread { requestFullBackup(app) }.start()
|
||||
|
||||
}
|
||||
|
|
|
@ -57,4 +57,10 @@
|
|||
<string name="recovery_code_error_invalid_word">Wrong word. Did you mean %1$s or %2$s?</string>
|
||||
<string name="recovery_code_error_checksum_word">We are so sorry! An unexpected error occurred.</string>
|
||||
|
||||
<!-- Notification -->
|
||||
<string name="notification_channel_title">Backup Notification</string>
|
||||
<string name="notification_title">Backup running</string>
|
||||
<string name="notification_backup_result_complete">Backup complete</string>
|
||||
<string name="notification_backup_result_error">Backup failed</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue