From 87b25aa4ec9ad0102c431f62788eb4eca5e522db Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 9 Jul 2019 09:52:03 +0200 Subject: [PATCH] Re-implement manual backup run and show notification during manual backups --- .../java/com/stevesoltys/backup/Backup.java | 12 -- .../java/com/stevesoltys/backup/Backup.kt | 22 ++++ .../backup/NotificationBackupObserver.kt | 106 ++++++++++++++++++ .../activity/MainActivityController.java | 2 +- .../service/backup/BackupJobService.java | 61 ---------- .../backup/service/backup/BackupJobService.kt | 63 +++++++++++ .../backup/session/backup/BackupMonitor.java | 20 ---- .../backup/session/backup/BackupMonitor.kt | 20 ++++ .../backup/settings/SettingsFragment.kt | 24 ++-- .../backup/settings/SettingsViewModel.kt | 9 +- app/src/main/res/values/strings.xml | 6 + 11 files changed, 233 insertions(+), 112 deletions(-) delete mode 100644 app/src/main/java/com/stevesoltys/backup/Backup.java create mode 100644 app/src/main/java/com/stevesoltys/backup/Backup.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt delete mode 100644 app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.java create mode 100644 app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.kt delete mode 100644 app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.java create mode 100644 app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.kt diff --git a/app/src/main/java/com/stevesoltys/backup/Backup.java b/app/src/main/java/com/stevesoltys/backup/Backup.java deleted file mode 100644 index b4b0474c..00000000 --- a/app/src/main/java/com/stevesoltys/backup/Backup.java +++ /dev/null @@ -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; - -} diff --git a/app/src/main/java/com/stevesoltys/backup/Backup.kt b/app/src/main/java/com/stevesoltys/backup/Backup.kt new file mode 100644 index 00000000..707bd642 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/Backup.kt @@ -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)) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt new file mode 100644 index 00000000..d414cb5d --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt @@ -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) + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java index 76af03e4..8fb2aeb2 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java @@ -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; diff --git a/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.java b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.java deleted file mode 100644 index cd0d2d78..00000000 --- a/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.java +++ /dev/null @@ -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; - } - -} diff --git a/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.kt b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.kt new file mode 100644 index 00000000..00fbbd64 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/service/backup/BackupJobService.kt @@ -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") + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.java b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.java deleted file mode 100644 index a8d264f0..00000000 --- a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.java +++ /dev/null @@ -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, "?")); - } - -} diff --git a/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.kt b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.kt new file mode 100644 index 00000000..6660d94a --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/session/backup/BackupMonitor.kt @@ -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, "?")) + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt index 6e1d6135..ccc0bb35 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt @@ -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 -> { diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt index bb782954..dd20a14a 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -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() + private val locationWasSet = MutableLiveEvent() /** * 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() + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44b2e553..2c8e82e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,4 +57,10 @@ Wrong word. Did you mean %1$s or %2$s? We are so sorry! An unexpected error occurred. + + Backup Notification + Backup running + Backup complete + Backup failed +