diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a4351e60..18ca14fa8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -177,13 +177,19 @@ android:exported="false" android:label="BackupJobService" android:permission="android.permission.BIND_JOB_SERVICE" /> - + + + - + ().apply { @@ -120,6 +122,9 @@ internal class AppDataRestoreManager( mRestoreBackupResult.postValue( RestoreBackupResult(context.getString(R.string.restore_set_error)) ) + } else { + // don't use startForeground(), because we may stop it sooner than the system likes + context.startService(foregroundServiceIntent) } } @@ -208,6 +213,8 @@ internal class AppDataRestoreManager( mRestoreProgress.postValue(list) mRestoreBackupResult.postValue(result) + + context.stopService(foregroundServiceIntent) } fun closeSession() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreService.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreService.kt new file mode 100644 index 000000000..017b154eb --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreService.kt @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.restore + +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST +import android.os.IBinder +import android.util.Log +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_RESTORE +import org.koin.android.ext.android.inject + +class RestoreService : Service() { + + companion object { + private const val TAG = "RestoreService" + } + + private val nm: BackupNotificationManager by inject() + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i(TAG, "onStartCommand $intent $flags $startId") + + startForeground( + NOTIFICATION_ID_RESTORE, + nm.getRestoreNotification(), + FOREGROUND_SERVICE_TYPE_MANIFEST, + ) + return START_STICKY_COMPATIBILITY + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onDestroy() { + Log.i(TAG, "onDestroy") + super.onDestroy() + nm.cancelRestoreNotification() + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt index dc3af8756..48aa99f9d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt @@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.restore.install import android.app.backup.IBackupManager import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_SIGNATURES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES @@ -20,6 +21,7 @@ import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.restore.RestorableBackup +import com.stevesoltys.seedvault.restore.RestoreService import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS @@ -85,14 +87,18 @@ internal class ApkRestore( return } mInstallResult.value = InstallResult(packages) + val i = Intent(context, RestoreService::class.java) val autoRestore = backupStateManager.isAutoRestoreEnabled try { + // don't use startForeground(), because we may stop it sooner than the system likes + context.startService(i) // disable auto-restore before installing apps, if it was enabled before if (autoRestore) backupManager.setAutoRestore(false) reInstallApps(backup, packages.asIterable().reversed()) } finally { // re-enable auto-restore, if it was enabled before if (autoRestore) backupManager.setAutoRestore(true) + context.stopService(i) } mInstallResult.update { it.copy(isFinished = true) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index 89f006708..0d4fccef0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -38,14 +38,16 @@ import kotlin.math.min private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_SUCCESS = "NotificationBackupSuccess" private const val CHANNEL_ID_ERROR = "NotificationError" +private const val CHANNEL_ID_RESTORE = "NotificationRestore" private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError" internal const val NOTIFICATION_ID_OBSERVER = 1 private const val NOTIFICATION_ID_SUCCESS = 2 private const val NOTIFICATION_ID_ERROR = 3 private const val NOTIFICATION_ID_SPACE_ERROR = 4 -private const val NOTIFICATION_ID_RESTORE_ERROR = 5 -private const val NOTIFICATION_ID_BACKGROUND = 6 -private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 7 +internal const val NOTIFICATION_ID_RESTORE = 5 +private const val NOTIFICATION_ID_RESTORE_ERROR = 6 +private const val NOTIFICATION_ID_BACKGROUND = 7 +private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 8 private val TAG = BackupNotificationManager::class.java.simpleName @@ -55,6 +57,7 @@ internal class BackupNotificationManager(private val context: Context) { createNotificationChannel(getObserverChannel()) createNotificationChannel(getSuccessChannel()) createNotificationChannel(getErrorChannel()) + createNotificationChannel(getRestoreChannel()) createNotificationChannel(getRestoreErrorChannel()) } @@ -77,6 +80,11 @@ internal class BackupNotificationManager(private val context: Context) { return NotificationChannel(CHANNEL_ID_ERROR, title, IMPORTANCE_DEFAULT) } + private fun getRestoreChannel(): NotificationChannel { + val title = context.getString(R.string.notification_restore_error_channel_title) + return NotificationChannel(CHANNEL_ID_RESTORE, title, IMPORTANCE_LOW) + } + private fun getRestoreErrorChannel(): NotificationChannel { val title = context.getString(R.string.notification_restore_error_channel_title) return NotificationChannel(CHANNEL_ID_RESTORE_ERROR, title, IMPORTANCE_HIGH) @@ -235,6 +243,18 @@ internal class BackupNotificationManager(private val context: Context) { nm.notify(NOTIFICATION_ID_SPACE_ERROR, notification) } + fun getRestoreNotification() = Notification.Builder(context, CHANNEL_ID_RESTORE).apply { + setSmallIcon(R.drawable.ic_cloud_restore) + setContentTitle(context.getString(R.string.notification_restore_title)) + setOngoing(true) + setShowWhen(false) + setWhen(System.currentTimeMillis()) + }.build() + + fun cancelRestoreNotification() { + nm.cancel(NOTIFICATION_ID_RESTORE) + } + @SuppressLint("RestrictedApi") fun onRemovableStorageNotAvailableForRestore(packageName: String, storageName: String) { val appName = try { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04de1dcf3..a8230c154 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,6 +169,9 @@ Insufficient backup space Your backup location is running out of space. Free up space, so backups can run. + Restore notification + Restore running + Auto restore flash drive error Could not restore data for %1$s Plug in your %1$s before installing the app to restore its data from backup. diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index 562766b7b..5fae19666 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -6,6 +6,7 @@ package com.stevesoltys.seedvault.restore.install import android.app.backup.IBackupManager +import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException @@ -127,6 +128,13 @@ internal class ApkBackupRestoreTest : TransportTest() { writeBytes(splitBytes) }.absolutePath) + // related to starting/stopping service + every { strictContext.packageName } returns "org.foo.bar" + every { + strictContext.startService(any()) + } returns ComponentName(strictContext, "org.foo.bar.Class") + every { strictContext.stopService(any()) } returns true + every { settingsManager.isBackupEnabled(any()) } returns true every { settingsManager.backupApks() } returns true every { sigInfo.hasMultipleSigners() } returns false diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt index 8d2548c43..201824f6c 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt @@ -6,6 +6,7 @@ package com.stevesoltys.seedvault.restore.install import android.app.backup.IBackupManager +import android.content.ComponentName import android.content.Context import android.content.pm.ApplicationInfo.FLAG_INSTALLED import android.content.pm.ApplicationInfo.FLAG_SYSTEM @@ -107,6 +108,13 @@ internal class ApkRestoreTest : TransportTest() { packageInfo.signingInfo = mockk(relaxed = true) every { storagePluginManager.appPlugin } returns storagePlugin + + // related to starting/stopping service + every { strictContext.packageName } returns "org.foo.bar" + every { + strictContext.startService(any()) + } returns ComponentName(strictContext, "org.foo.bar.Class") + every { strictContext.stopService(any()) } returns true } @Test